diff --git a/src/navigation/MainNavigator.js b/src/navigation/MainNavigator.js index c01cc2e..070c03a 100644 --- a/src/navigation/MainNavigator.js +++ b/src/navigation/MainNavigator.js @@ -4,7 +4,7 @@ import { Ionicons } from '@expo/vector-icons'; import { COLORS } from '../constants/colors'; import HomeScreen from '../screens/Main/HomeScreen'; -import MenuScreen from '../screens/Main/MenuScreen'; +import MenuNavigator from './MenuNavigator'; import ProfileScreen from '../screens/Main/ProfileScreen'; const Tab = createBottomTabNavigator(); @@ -56,7 +56,7 @@ const MainNavigator = () => { /> ( + + + + + + + + +); + +export default MenuNavigator; \ No newline at end of file diff --git a/src/screens/Loan/CreateLoanRemainingOrderScreen.js b/src/screens/Loan/CreateLoanRemainingOrderScreen.js new file mode 100644 index 0000000..fe5e206 --- /dev/null +++ b/src/screens/Loan/CreateLoanRemainingOrderScreen.js @@ -0,0 +1,93 @@ +import React, { useState } from 'react'; +import { View, Text, StyleSheet, TouchableOpacity, ActivityIndicator, Alert, SafeAreaView } from 'react-native'; +import { Ionicons } from '@expo/vector-icons'; +import { useNavigation } from '@react-navigation/native'; +import apiService from '../../services/apiService'; +import { COLORS } from '../../constants/colors'; +import Input from '../../components/Input'; +import { StatusBar } from 'expo-status-bar'; + +const CreateLoanRemainingOrderScreen = () => { + const navigation = useNavigation(); + const [accountNumber, setAccountNumber] = useState(''); + const [loading, setLoading] = useState(false); + + const handleSubmit = async () => { + if (accountNumber.trim().length === 0) { + Alert.alert('Error', 'Account number is required'); + return; + } + setLoading(true); + const res = await apiService.createLoanRemainingOrder(accountNumber.trim()); + setLoading(false); + if (res.success) { + Alert.alert('Success', res.message || 'Order created successfully', [ + { text: 'OK', onPress: () => navigation.goBack() }, + ]); + } else { + Alert.alert('Error', res.error || 'Could not create order'); + } + }; + + return ( + + + navigation.goBack()}> + + + + Täze sargyt + + + + + {loading ? ( + + ) : ( + Ýatda sakla + )} + + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: COLORS.backgroundSecondary, + paddingHorizontal: 24, + paddingTop: 40, + }, + backBtn: { + marginBottom: 24, + }, + title: { + fontSize: 24, + fontWeight: 'bold', + color: COLORS.textPrimary, + marginBottom: 24, + }, + submitBtn: { + marginTop: 32, + backgroundColor: COLORS.primary, + paddingVertical: 16, + borderRadius: 8, + alignItems: 'center', + }, + submitText: { + color: COLORS.white, + fontSize: 16, + fontWeight: '600', + }, +}); + +export default CreateLoanRemainingOrderScreen; \ No newline at end of file diff --git a/src/screens/Loan/LoanRemainingOrderDetailsScreen.js b/src/screens/Loan/LoanRemainingOrderDetailsScreen.js new file mode 100644 index 0000000..0f078ac --- /dev/null +++ b/src/screens/Loan/LoanRemainingOrderDetailsScreen.js @@ -0,0 +1,144 @@ +import React, { useEffect, useState } from 'react'; +import { View, Text, StyleSheet, ActivityIndicator, TouchableOpacity, Alert } from 'react-native'; +import { Ionicons } from '@expo/vector-icons'; +import { useNavigation, useRoute } from '@react-navigation/native'; +import apiService from '../../services/apiService'; +import { COLORS } from '../../constants/colors'; + +const LoanRemainingOrderDetailsScreen = () => { + const navigation = useNavigation(); + const route = useRoute(); + const { orderId } = route.params || {}; + + const [loading, setLoading] = useState(true); + const [order, setOrder] = useState(null); + + const fetchDetails = async () => { + setLoading(true); + const res = await apiService.getLoanRemainingOrder(orderId); + if (res.success) { + setOrder(res.data); + } else { + Alert.alert('Error', res.error || 'Could not fetch details'); + } + setLoading(false); + }; + + useEffect(() => { + fetchDetails(); + }, []); + + const handleDelete = () => { + Alert.alert('Confirm', 'Are you sure you want to delete this order?', [ + { text: 'Cancel', style: 'cancel' }, + { text: 'Delete', style: 'destructive', onPress: deleteOrder }, + ]); + }; + + const deleteOrder = async () => { + const res = await apiService.deleteLoanRemainingOrder(orderId); + if (res.success) { + Alert.alert('Deleted', res.message || 'Order deleted', [ + { text: 'OK', onPress: () => navigation.goBack() }, + ]); + } else { + Alert.alert('Error', res.error || 'Could not delete'); + } + }; + + if (loading) { + return ( + + + + ); + } + + if (!order) { + return ( + + No data + + ); + } + + return ( + + navigation.goBack()}> + + + + Galyndy detallary + + + {Object.entries(order).map(([key, value]) => ( + + {key} + {String(value)} + + ))} + + + + Poz + + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: COLORS.backgroundSecondary, + paddingHorizontal: 24, + paddingTop: 40, + }, + centered: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + }, + backBtn: { + alignSelf: 'flex-end', + marginBottom: 16, + }, + title: { + fontSize: 24, + fontWeight: 'bold', + color: COLORS.textPrimary, + marginBottom: 24, + }, + detailCard: { + backgroundColor: COLORS.white, + borderRadius: 12, + padding: 20, + marginBottom: 32, + }, + detailRow: { + flexDirection: 'row', + justifyContent: 'space-between', + marginBottom: 12, + }, + detailKey: { + fontWeight: '600', + color: COLORS.textSecondary, + }, + detailValue: { + color: COLORS.textPrimary, + maxWidth: '60%', + textAlign: 'right', + }, + deleteBtn: { + backgroundColor: COLORS.error, + paddingVertical: 14, + borderRadius: 8, + alignItems: 'center', + }, + deleteText: { + color: COLORS.white, + fontSize: 16, + fontWeight: '600', + }, +}); + +export default LoanRemainingOrderDetailsScreen; \ No newline at end of file diff --git a/src/screens/Loan/LoanRemainingOrdersScreen.js b/src/screens/Loan/LoanRemainingOrdersScreen.js new file mode 100644 index 0000000..ee12041 --- /dev/null +++ b/src/screens/Loan/LoanRemainingOrdersScreen.js @@ -0,0 +1,309 @@ +import React, { useState, useCallback } from 'react'; +import { View, Text, StyleSheet, FlatList, ActivityIndicator, TouchableOpacity, RefreshControl, Modal, ScrollView, Alert, SafeAreaView, Pressable } from 'react-native'; +import { Ionicons } from '@expo/vector-icons'; +import { useNavigation, useFocusEffect } from '@react-navigation/native'; +import apiService from '../../services/apiService'; +import { COLORS } from '../../constants/colors'; +import { StatusBar } from 'expo-status-bar'; + +const LoanRemainingOrdersScreen = () => { + const navigation = useNavigation(); + const [orders, setOrders] = useState([]); + const [loading, setLoading] = useState(true); + const [refreshing, setRefreshing] = useState(false); + const [modalVisible, setModalVisible] = useState(false); + const [modalData, setModalData] = useState(null); + const [modalLoading, setModalLoading] = useState(false); + + const fetchOrders = async () => { + try { + const res = await apiService.getLoanRemainingOrders(); + if (res.success) { + setOrders(Array.isArray(res.data) ? res.data : []); + } else { + console.warn(res.error); + } + } catch (e) { + console.warn(e.message); + } finally { + setLoading(false); + setRefreshing(false); + } + }; + + useFocusEffect( + useCallback(() => { + fetchOrders(); + }, []) + ); + + const onRefresh = () => { + setRefreshing(true); + fetchOrders(); + }; + + const handleItemPress = async (item) => { + setModalLoading(true); + setModalVisible(true); + + const res = await apiService.getLoanRemainingBalance(item.account_number, item.passport_serie, item.passport_id); + if (res.success) { + setModalData(res.data); + } else { + setModalData(null); + Alert.alert('Info', res.error || 'Not found'); + setModalVisible(false); + } + setModalLoading(false); + }; + + const renderItem = ({ item }) => ( + handleItemPress(item)} + > + + + + + {item.account_number} + + + + ); + + if (loading) { + return ( + + + + ); + } + + return ( + + + {/* Header */} + + navigation.goBack()} style={{ paddingRight: 12 }}> + + + Karzyň galyndysy + + item.id?.toString()} + renderItem={renderItem} + contentContainerStyle={orders.length === 0 && styles.emptyContainer} + refreshControl={} + ListEmptyComponent={No orders yet} + /> + + {/* Floating Action Button */} + navigation.navigate('CreateLoanRemainingOrder')} + > + + + + {/* Result Modal */} + setModalVisible(false)} + > + setModalVisible(false)}> + e.stopPropagation()}> + setModalVisible(false)}> + + + Netije + {modalLoading ? ( + + + + ) : ( + + {modalData && (() => { + const fields = [ + { key: 'branchName', label: 'Şahamça' }, + { key: 'clientName', label: 'Müşderi' }, + { key: 'docNum', label: 'Şertnama belgisi' }, + { key: '__divider1' }, + { key: 'docSum', label: 'Jemi karzyň möçberi' }, + { key: 'balans', label: 'Karz boýunça jemi galyndy' }, + { key: 'percentBalance', label: 'Hasaplama göterim (şu aý üçin)' }, + { key: 'docMonthSum', label: 'Hasaplanan esasy bergi (şu aý üçin)' }, + { key: 'docPayed', label: 'Jemi tölenen möçberi' }, + ]; + + let renderedCount = 0; + const rows = fields.map((f) => { + if (f.key.startsWith('__divider')) { + return ; + } + const val = modalData[f.key]; + if (val === null || val === undefined || val === '') return null; + renderedCount++; + return ( + + {f.label} + {String(val)} + + ); + }); + + if (renderedCount === 0) { + return Object.entries(modalData).map(([k, v]) => ( + + {k} + {String(v)} + + )); + } + return rows; + })()} + + )} + + + + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: COLORS.backgroundSecondary, + }, + centered: { + flex: 1, + alignItems: 'center', + justifyContent: 'center', + }, + card: { + flexDirection: 'row', + alignItems: 'center', + padding: 20, + borderRadius: 10, + backgroundColor: COLORS.white, + marginHorizontal: 24, + marginTop: 16, + shadowColor: '#000', + shadowOffset: { width: 0, height: 1 }, + shadowOpacity: 0.1, + shadowRadius: 3, + elevation: 2, + }, + cardIconWrapper: { + width: 40, + height: 40, + borderRadius: 20, + backgroundColor: COLORS.backgroundSecondary, + alignItems: 'center', + justifyContent: 'center', + marginRight: 16, + }, + cardContent: { + flex: 1, + }, + cardTitle: { + fontSize: 16, + fontWeight: '600', + color: COLORS.textPrimary, + marginBottom: 4, + }, + cardSubtitle: { + fontSize: 14, + color: COLORS.textSecondary, + }, + emptyContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + }, + emptyText: { + fontSize: 16, + color: COLORS.textSecondary, + }, + fab: { + position: 'absolute', + right: 24, + bottom: 40, + width: 56, + height: 56, + borderRadius: 28, + backgroundColor: COLORS.primary, + alignItems: 'center', + justifyContent: 'center', + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.3, + shadowRadius: 4, + elevation: 4, + }, + header: { + flexDirection: 'row', + alignItems: 'center', + padding: 16, + }, + headerTitle: { + fontSize: 18, + fontWeight: 'bold', + color: COLORS.textPrimary, + marginLeft: 12, + }, + modalBackdrop: { + flex: 1, + backgroundColor: 'rgba(0,0,0,0.4)', + justifyContent: 'center', + alignItems: 'center', + paddingHorizontal: 24, + }, + modalCard: { + width: '100%', + backgroundColor: COLORS.white, + borderRadius: 12, + padding: 20, + elevation: 5, + }, + modalCloseSmall: { + position: 'absolute', + top: 8, + right: 8, + }, + modalTitle: { + fontSize: 18, + fontWeight: 'bold', + color: COLORS.textPrimary, + marginBottom: 16, + textAlign: 'center', + }, + divider: { + height: 1, + backgroundColor: COLORS.gray[200], + marginVertical: 12, + }, + itemWrap: { + marginBottom: 16, + }, + itemLabel: { + fontSize: 15, + color: COLORS.textSecondary, + marginBottom: 4, + }, + itemValue: { + fontSize: 16, + fontWeight: '600', + color: COLORS.textPrimary, + }, +}); + +export default LoanRemainingOrdersScreen; \ No newline at end of file diff --git a/src/screens/Main/HomeScreen.js b/src/screens/Main/HomeScreen.js index 15fef25..bedea89 100644 --- a/src/screens/Main/HomeScreen.js +++ b/src/screens/Main/HomeScreen.js @@ -101,6 +101,7 @@ const styles = StyleSheet.create({ paddingTop: 16, paddingBottom: 24, backgroundColor: COLORS.white, + marginBottom: 16, }, greeting: { fontSize: 16, diff --git a/src/screens/Main/MenuScreen.js b/src/screens/Main/MenuScreen.js index d1a20be..e107215 100644 --- a/src/screens/Main/MenuScreen.js +++ b/src/screens/Main/MenuScreen.js @@ -1,4 +1,5 @@ import React from 'react'; +import { useNavigation } from '@react-navigation/native'; import { View, Text, @@ -12,6 +13,8 @@ import { Ionicons } from '@expo/vector-icons'; import { COLORS } from '../../constants/colors'; const MenuScreen = () => { + const navigation = useNavigation(); + const menuSections = [ { title: 'Karz', @@ -41,8 +44,13 @@ const MenuScreen = () => { ]; const handleMenuItemPress = (item) => { - console.log('Menu item pressed:', item.title); - // Handle navigation or action here + if (item.id === 2) { + navigation.navigate('LoanRemainingOrders'); + } else if (item.id === 3) { + navigation.navigate('LoanPaidOffLetterOrders'); + } else { + console.log('Menu item pressed:', item.title); + } }; return ( diff --git a/src/services/apiService.js b/src/services/apiService.js index b29ccbb..148aed5 100644 --- a/src/services/apiService.js +++ b/src/services/apiService.js @@ -93,6 +93,127 @@ class ApiService { return authService.unblockCard(cardId); } + // ================================ + // Loan Remaining Order (Karzyň galyndysy) + // ================================ + + async getLoanRemainingOrders() { + try { + const data = await authService.getLoanRemainingOrders(); + return { success: true, data }; + } catch (error) { + return { success: false, error: error.message }; + } + } + + async createLoanRemainingOrder(accountNumber) { + try { + const response = await authService.createLoanRemainingOrder(accountNumber); + return { success: true, message: response.message }; + } catch (error) { + return { success: false, error: error.message }; + } + } + + async getLoanRemainingOrder(orderId) { + try { + const data = await authService.getLoanRemainingOrder(orderId); + return { success: true, data }; + } catch (error) { + return { success: false, error: error.message }; + } + } + + async updateLoanRemainingOrder(orderId, accountNumber) { + try { + const response = await authService.updateLoanRemainingOrder(orderId, accountNumber); + return { success: true, message: response.message }; + } catch (error) { + return { success: false, error: error.message }; + } + } + + async deleteLoanRemainingOrder(orderId) { + try { + const response = await authService.deleteLoanRemainingOrder(orderId); + return { success: true, message: response.message }; + } catch (error) { + return { success: false, error: error.message }; + } + } + + // Quick loan remaining check + async getLoanRemainingBalance(accountNumber, passportSerie = null, passportId = null) { + try { + let raw = await authService.getLoanRemainingBalance(accountNumber, passportSerie, passportId); + + // Some endpoints return JSON-string (e.g. "{\"branchName\":...}") instead of object + if (typeof raw === 'string') { + try { + raw = JSON.parse(raw); + } catch (_) { + // leave as-is + } + } + + if (raw && raw.errCode && raw.errCode > 0) { + return { success: false, error: raw.message || 'Not found', data: raw }; + } + return { success: true, data: raw }; + } catch (error) { + return { success: false, error: error.message }; + } + } + + // ================================ + // Loan Paid-Off Letter Orders + // ================================ + + async getLoanPaidOffLetterOrders() { + try { + const data = await authService.getLoanPaidOffLetterOrders(); + return { success: true, data }; + } catch (error) { + return { success: false, error: error.message }; + } + } + + async createLoanPaidOffLetterOrder(payload) { + try { + const response = await authService.createLoanPaidOffLetterOrder(payload); + return { success: true, message: response.message }; + } catch (error) { + return { success: false, error: error.message }; + } + } + + async getLoanPaidOffLetterOrder(orderId) { + try { + const data = await authService.getLoanPaidOffLetterOrder(orderId); + return { success: true, data }; + } catch (error) { + return { success: false, error: error.message }; + } + } + + async updateLoanPaidOffLetterOrder(orderId, payload) { + try { + const response = await authService.updateLoanPaidOffLetterOrder(orderId, payload); + return { success: true, message: response.message }; + } catch (error) { + return { success: false, error: error.message }; + } + } + + async deleteLoanPaidOffLetterOrder(orderId) { + try { + const response = await authService.deleteLoanPaidOffLetterOrder(orderId); + return { success: true, message: response.message }; + } catch (error) { + return { success: false, error: error.message }; + } + } + // Utility methods for common operations async getUserDashboardData() { try { diff --git a/src/services/authService.js b/src/services/authService.js index 1c52da6..28a87d5 100644 --- a/src/services/authService.js +++ b/src/services/authService.js @@ -164,6 +164,114 @@ class AuthService { async deleteAccount() { return this.makeRequest('/user/delete-account', null, true, 'DELETE'); } + + // ================================ + // Loan Remaining Order (Karzyň galyndysy) + // ================================ + + // Helper to read cached user data (for passport details) + async getStoredUser() { + try { + const raw = await AsyncStorage.getItem('user_data'); + return raw ? JSON.parse(raw) : null; + } catch (e) { + return null; + } + } + + // LIST orders + async getLoanRemainingOrders() { + return this.makeRequest('/loan-remaining-order', null, true, 'GET'); + } + + // CREATE order (requires only account number – passport details are fetched from user profile) + async createLoanRemainingOrder(accountNumber) { + const user = await this.getStoredUser(); + if (!user?.passport_serie || !user?.passport_id) { + throw new Error('Passport details are missing from profile'); + } + const payload = { + passport_serie: user.passport_serie, + passport_id: user.passport_id, + account_number: accountNumber, + }; + return this.makeRequest('/loan-remaining-order', payload, true, 'POST'); + } + + // SHOW order details + async getLoanRemainingOrder(orderId) { + return this.makeRequest(`/loan-remaining-order/${orderId}`, null, true, 'GET'); + } + + // UPDATE order (only account number can change; passport details stay the same) + async updateLoanRemainingOrder(orderId, accountNumber) { + const user = await this.getStoredUser(); + if (!user?.passport_serie || !user?.passport_id) { + throw new Error('Passport details are missing from profile'); + } + const payload = { + passport_serie: user.passport_serie, + passport_id: user.passport_id, + account_number: accountNumber, + }; + return this.makeRequest(`/loan-remaining-order/${orderId}`, payload, true, 'POST'); + } + + // DELETE order + async deleteLoanRemainingOrder(orderId) { + return this.makeRequest(`/loan-remaining-order/${orderId}`, null, true, 'DELETE'); + } + + // ================================ + // Loan remaining quick check (returns remaining balance info) + // ================================ + + async getLoanRemainingBalance(accountNumber, passportSerie = null, passportId = null) { + let serie = passportSerie; + let pid = passportId; + + if (!serie || !pid) { + const user = await this.getStoredUser(); + serie = user?.passport_serie; + pid = user?.passport_id; + } + + if (!serie || !pid) { + throw new Error('Passport details are missing'); + } + + const query = `passport_serie=${encodeURIComponent(serie)}&passport_id=${encodeURIComponent(pid)}&account_number=${encodeURIComponent(accountNumber)}`; + return this.makeRequest(`/loan-remaining?${query}`, null, true, 'GET'); + } + + // ================================ + // Loan Paid-Off Letter Orders + // ================================ + + // LIST + async getLoanPaidOffLetterOrders() { + return this.makeRequest('/loan-paid-off-letter-orders', null, true, 'GET'); + } + + // CREATE + async createLoanPaidOffLetterOrder(data) { + return this.makeRequest('/loan-paid-off-letter-orders', data, true, 'POST'); + } + + // SHOW + async getLoanPaidOffLetterOrder(orderId) { + return this.makeRequest(`/loan-paid-off-letter-orders/${orderId}`, null, true, 'GET'); + } + + // UPDATE + async updateLoanPaidOffLetterOrder(orderId, data) { + return this.makeRequest(`/loan-paid-off-letter-orders/${orderId}`, data, true, 'POST'); + } + + // DELETE + async deleteLoanPaidOffLetterOrder(orderId) { + return this.makeRequest(`/loan-paid-off-letter-orders/${orderId}`, null, true, 'DELETE'); + } } export default new AuthService(); \ No newline at end of file