From 6d79420f6894e19b30ce02dea5d6cbb15742558e Mon Sep 17 00:00:00 2001 From: Nurmuhammet Allanov Date: Fri, 4 Jul 2025 22:59:42 +0500 Subject: [PATCH] loan order working --- App.js | 7 +- src/contexts/BaseEnumsContext.js | 52 +++++++ src/navigation/MenuNavigator.js | 6 + src/screens/Loan/CreateLoanOrderScreen.js | 155 +++++++++++++++++++++ src/screens/Loan/LoanOrderDetailsScreen.js | 135 ++++++++++++++++++ src/screens/Loan/LoanOrdersScreen.js | 145 +++++++++++++++++++ src/screens/Main/HomeScreen.js | 6 +- src/screens/Main/MenuScreen.js | 4 +- src/services/apiService.js | 49 +++++++ src/services/authService.js | 29 ++++ 10 files changed, 583 insertions(+), 5 deletions(-) create mode 100644 src/contexts/BaseEnumsContext.js create mode 100644 src/screens/Loan/CreateLoanOrderScreen.js create mode 100644 src/screens/Loan/LoanOrderDetailsScreen.js create mode 100644 src/screens/Loan/LoanOrdersScreen.js diff --git a/App.js b/App.js index 7caa51e..a545f31 100644 --- a/App.js +++ b/App.js @@ -1,11 +1,16 @@ import React from 'react'; import { AuthProvider } from './src/contexts/AuthContext'; import RootNavigator from './src/navigation/RootNavigator'; +import { BaseEnumsProvider } from './src/contexts/BaseEnumsContext'; +import { StatusBar } from 'react-native'; export default function App() { return ( - + + + + ); } diff --git a/src/contexts/BaseEnumsContext.js b/src/contexts/BaseEnumsContext.js new file mode 100644 index 0000000..a064860 --- /dev/null +++ b/src/contexts/BaseEnumsContext.js @@ -0,0 +1,52 @@ +import React, { createContext, useContext, useState, useEffect } from 'react'; +import { API_CONFIG } from '../constants/api'; + +const BaseEnumsContext = createContext({ enums: null, refresh: () => {}, getEnums: async () => null }); + +export const BaseEnumsProvider = ({ children }) => { + const [enums, setEnums] = useState(null); + const [lastFetched, setLastFetched] = useState(0); + + const fetchEnums = async () => { + try { + const res = await fetch(`${API_CONFIG.BASE_URL}${API_CONFIG.ENDPOINTS.ENUMS}`); + const json = await res.json(); + setEnums(json); + setLastFetched(Date.now()); + } catch (e) { + console.warn('Failed to fetch base-app-enums', e.message); + } + }; + + // initial fetch and 60s refresh + useEffect(() => { + fetchEnums(); + const id = setInterval(fetchEnums, 60000); + return () => clearInterval(id); + }, []); + + const getEnums = async () => { + if (!enums || Date.now() - lastFetched > 60000) { + await fetchEnums(); + } + return enums; + }; + + const getLabel = (category, key) => { + if (!enums || !enums[category]) return key; + return enums[category][key] ?? key; + }; + + const getOptions = (category) => { + if (!enums || !enums[category]) return []; + return Object.entries(enums[category]).map(([value, label]) => ({ value, label })); + }; + + return ( + + {children} + + ); +}; + +export const useBaseEnums = () => useContext(BaseEnumsContext); \ No newline at end of file diff --git a/src/navigation/MenuNavigator.js b/src/navigation/MenuNavigator.js index fcf2c50..8fd67cd 100644 --- a/src/navigation/MenuNavigator.js +++ b/src/navigation/MenuNavigator.js @@ -6,6 +6,9 @@ import CreateLoanRemainingOrderScreen from '../screens/Loan/CreateLoanRemainingO import LoanPaidOffLetterOrdersScreen from '../screens/Loan/LoanPaidOffLetterOrdersScreen'; import CreateLoanPaidOffLetterOrderScreen from '../screens/Loan/CreateLoanPaidOffLetterOrderScreen'; import LoanPaidOffLetterOrderDetailsScreen from '../screens/Loan/LoanPaidOffLetterOrderDetailsScreen'; +import LoanOrdersScreen from '../screens/Loan/LoanOrdersScreen'; +import CreateLoanOrderScreen from '../screens/Loan/CreateLoanOrderScreen'; +import LoanOrderDetailsScreen from '../screens/Loan/LoanOrderDetailsScreen'; const Stack = createStackNavigator(); @@ -17,6 +20,9 @@ const MenuNavigator = () => ( + + + ); diff --git a/src/screens/Loan/CreateLoanOrderScreen.js b/src/screens/Loan/CreateLoanOrderScreen.js new file mode 100644 index 0000000..16df066 --- /dev/null +++ b/src/screens/Loan/CreateLoanOrderScreen.js @@ -0,0 +1,155 @@ +import React, { useState, useEffect } from 'react'; +import { Text, StyleSheet, TouchableOpacity, ActivityIndicator, Alert, ScrollView, SafeAreaView } from 'react-native'; +import { Ionicons } from '@expo/vector-icons'; +import { useNavigation } from '@react-navigation/native'; +import { COLORS } from '../../constants/colors'; +import Input from '../../components/Input'; +import SelectInput from '../../components/SelectInput'; +import DateInput from '../../components/DateInput'; +import { API_CONFIG } from '../../constants/api'; +import apiService from '../../services/apiService'; +import { useAuth } from '../../contexts/AuthContext'; +import { StatusBar } from 'expo-status-bar'; + +const CreateLoanOrderScreen = () => { + const navigation = useNavigation(); + const { user } = useAuth(); + + const [loanType, setLoanType] = useState(''); + const [loanAmount, setLoanAmount] = useState(''); + const [region, setRegion] = useState(''); + const [branchId, setBranchId] = useState(''); + const [customerName, setCustomerName] = useState(''); + const [customerSurname, setCustomerSurname] = useState(''); + const [customerPatro, setCustomerPatro] = useState(''); + const [passportSerie, setPassportSerie] = useState(''); + const [passportId, setPassportId] = useState(''); + const [bornAt, setBornAt] = useState(''); + const [phone, setPhone] = useState(''); + const [loading, setLoading] = useState(false); + + const [loanTypeOptions, setLoanTypeOptions] = useState([]); + const [regionOptions, setRegionOptions] = useState([]); + const [passportSeriesOptions, setPassportSeriesOptions] = useState([]); + const [branchesByRegion, setBranchesByRegion] = useState({}); + + useEffect(() => { + if (user) { + if (user.passport_serie) setPassportSerie(user.passport_serie); + if (user.passport_id) setPassportId(String(user.passport_id)); + if (user.phone) setPhone(String(user.phone).slice(-8)); + if (user.region) setRegion(user.region); + if (user.name) { + const parts = user.name.split(' '); + setCustomerName(parts[0]); + setCustomerSurname(parts[1] || ''); + } + } + + const fetchEnums = async () => { + try { + const res = await fetch(`${API_CONFIG.BASE_URL}/base-app-enums`); + const enums = await res.json(); + const regions = Object.entries(enums.regions || {}).map(([value, label]) => ({ value, label })); + setRegionOptions(regions); + const pSeries = Object.keys(enums.passport_series || {}).map((key) => ({ value: key, label: key })); + setPassportSeriesOptions(pSeries); + } catch {} + }; + + const fetchLoanTypes = async () => { + try { + const res = await fetch(`${API_CONFIG.BASE_URL}/loan-types`); + const list = await res.json(); + setLoanTypeOptions(list.map((lt) => ({ value: lt.id, label: lt.name }))); + } catch {} + }; + + const fetchBranches = async () => { + try { + const res = await fetch(`${API_CONFIG.BASE_URL}/branches?groupBy=region`); + const json = await res.json(); + setBranchesByRegion(json); + } catch {} + }; + + fetchEnums(); + fetchLoanTypes(); + fetchBranches(); + }, []); + + const branchOptions = region && branchesByRegion[region] ? branchesByRegion[region].map((b) => ({ label: b.name, value: b.id })) : []; + + const handleSubmit = async () => { + if (!loanType || !loanAmount || !region || !branchId || !customerName || !customerSurname || !passportSerie || !passportId || !bornAt || !phone) { + Alert.alert('Error', 'Fill all required fields'); + return; + } + + const payload = { + loan_type: parseInt(loanType), + loan_amount: parseInt(loanAmount), + region, + branch_id: parseInt(branchId), + customer_name: customerName, + customer_surname: customerSurname, + customer_patronic_name: customerPatro || null, + passport_serie: passportSerie, + passport_id: passportId.trim(), + born_at: bornAt, + phone: parseInt(phone), + }; + + setLoading(true); + const res = await apiService.createLoanOrder(payload); + setLoading(false); + if (res.success) { + Alert.alert('Success', res.message || 'Order created', [{ text: 'OK', onPress: () => navigation.goBack() }]); + } else { + Alert.alert('Error', res.error || 'Could not create'); + } + }; + + return ( + + + navigation.goBack()}> + + + + + Täze karz sargyt + + + + + { setRegion(val); setBranchId(''); }} placeholder="Saýla" /> + + + + + + + + + + + + + + {loading ? : Ýatda sakla} + + + + ); +}; + +const styles = StyleSheet.create({ + container: { flex: 1, backgroundColor: COLORS.backgroundSecondary, paddingTop: 40 }, + backBtn: { marginBottom: 24, marginLeft: 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 CreateLoanOrderScreen; \ No newline at end of file diff --git a/src/screens/Loan/LoanOrderDetailsScreen.js b/src/screens/Loan/LoanOrderDetailsScreen.js new file mode 100644 index 0000000..4654df0 --- /dev/null +++ b/src/screens/Loan/LoanOrderDetailsScreen.js @@ -0,0 +1,135 @@ +import React, { useEffect, useState } from 'react'; +import { View, Text, StyleSheet, ActivityIndicator, TouchableOpacity, Alert, ScrollView, SafeAreaView } 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'; +import { StatusBar } from 'expo-status-bar'; + +const formatDate = (dstr) => { + if (!dstr) return '-'; + const d = new Date(dstr); + return isNaN(d) ? dstr : d.toLocaleDateString('tk-TM'); +}; + +const DetailRow = ({ label, value, last }) => ( + + {label} + {String(value)} + +); + +const LoanOrderDetailsScreen = () => { + 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.getLoanOrder(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', 'Delete this order?', [ + { text: 'Cancel', style: 'cancel' }, + { text: 'Delete', style: 'destructive', onPress: deleteOrder }, + ]); + }; + + const deleteOrder = async () => { + const res = await apiService.deleteLoanOrder(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()}> + + + + + Karz sargyt maglumatlary + + + + + + + + + + Müşderi + + + + + + + + {/* Address */} + Salgylary + + + + + + + Poz + + + + ); +}; + +const styles = StyleSheet.create({ + container: { flex: 1, backgroundColor: COLORS.backgroundSecondary }, + centered: { flex: 1, alignItems: 'center', justifyContent: 'center' }, + backBtn: { alignSelf: 'flex-end', marginBottom: 16, marginRight: 24 }, + title: { fontSize: 24, fontWeight: 'bold', color: COLORS.textPrimary, marginBottom: 24 }, + sectionTitle: { fontSize: 18, fontWeight: '700', color: COLORS.textPrimary, marginBottom: 12 }, + detailCard: { backgroundColor: COLORS.white, borderRadius: 12, padding: 20, marginBottom: 32 }, + detailRow: { flexDirection: 'row', justifyContent: 'space-between', paddingVertical: 12 }, + detailRowBorder: { borderBottomWidth: 1, borderBottomColor: COLORS.border }, + 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 LoanOrderDetailsScreen; \ No newline at end of file diff --git a/src/screens/Loan/LoanOrdersScreen.js b/src/screens/Loan/LoanOrdersScreen.js new file mode 100644 index 0000000..50b34ea --- /dev/null +++ b/src/screens/Loan/LoanOrdersScreen.js @@ -0,0 +1,145 @@ +import React, { useState, useCallback } from 'react'; +import { View, Text, StyleSheet, FlatList, ActivityIndicator, TouchableOpacity, RefreshControl, SafeAreaView } 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 CARD_BG = '#F1F9F1'; +const CIRCLE_BG = '#A2E4A4'; + +const LoanOrdersScreen = () => { + const navigation = useNavigation(); + const [orders, setOrders] = useState([]); + const [loading, setLoading] = useState(true); + const [refreshing, setRefreshing] = useState(false); + + const fetchOrders = async () => { + try { + const res = await apiService.getLoanOrders(); + 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 renderItem = ({ item }) => { + const passportLine = `Pasport: ${item.passport_serie} ${item.passport_id}`; + const amountLine = `Karz mukdary:`; + const created = item.created_at ? new Date(item.created_at).toLocaleDateString() : ''; + + return ( + navigation.navigate('LoanOrderDetails', { orderId: item.id })}> + + {item.id} + + + {passportLine} + {amountLine} + {item.loan_amount || '-'} + {created} + + + ); + }; + + if (loading) { + return ( + + + + ); + } + + return ( + + + {/* Header */} + + navigation.goBack()} style={{ paddingRight: 12 }}> + + + Karz sargytlar + + + it.id?.toString()} + renderItem={renderItem} + contentContainerStyle={orders.length === 0 && styles.emptyContainer} + refreshControl={} + ListEmptyComponent={No orders yet} + /> + + navigation.navigate('CreateLoanOrder')}> + + + + ); +}; + +const styles = StyleSheet.create({ + container: { flex: 1, backgroundColor: COLORS.backgroundSecondary }, + centered: { flex: 1, alignItems: 'center', justifyContent: 'center' }, + header: { flexDirection: 'row', alignItems: 'center', padding: 16 }, + headerTitle: { fontSize: 18, fontWeight: 'bold', color: COLORS.textPrimary, marginLeft: 12 }, + card: { + flexDirection: 'row', + backgroundColor: CARD_BG, + marginHorizontal: 24, + marginTop: 16, + borderRadius: 12, + padding: 16, + alignItems: 'center', + }, + circle: { + width: 40, + height: 40, + borderRadius: 20, + backgroundColor: CIRCLE_BG, + alignItems: 'center', + justifyContent: 'center', + marginRight: 16, + }, + circleText: { color: COLORS.white, fontWeight: '600' }, + cardContent: { flex: 1 }, + passportText: { fontWeight: '700', color: COLORS.textPrimary, marginBottom: 4 }, + accountLabel: { color: COLORS.textSecondary, fontSize: 14 }, + accountValue: { color: COLORS.textPrimary, marginBottom: 4 }, + dateText: { color: COLORS.textSecondary, fontSize: 12 }, + 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', + elevation: 4, + }, +}); + +export default LoanOrdersScreen; \ No newline at end of file diff --git a/src/screens/Main/HomeScreen.js b/src/screens/Main/HomeScreen.js index bedea89..03d58be 100644 --- a/src/screens/Main/HomeScreen.js +++ b/src/screens/Main/HomeScreen.js @@ -59,9 +59,9 @@ const HomeScreen = () => { ) : ( - - - + + + )} diff --git a/src/screens/Main/MenuScreen.js b/src/screens/Main/MenuScreen.js index 627f090..be8215e 100644 --- a/src/screens/Main/MenuScreen.js +++ b/src/screens/Main/MenuScreen.js @@ -44,7 +44,9 @@ const MenuScreen = () => { ]; const handleMenuItemPress = (item) => { - if (item.id === 2) { + if (item.id === 1) { + navigation.navigate('LoanOrders'); + } else if (item.id === 2) { navigation.navigate('LoanRemainingOrders'); } else if (item.id === 3) { navigation.navigate('LoanPaidOffLetterOrders'); diff --git a/src/services/apiService.js b/src/services/apiService.js index 148aed5..f0ac419 100644 --- a/src/services/apiService.js +++ b/src/services/apiService.js @@ -238,6 +238,55 @@ class ApiService { }; } } + + // ================================ + // Loan Orders + // ================================ + + async getLoanOrders() { + try { + const data = await authService.getLoanOrders(); + return { success: true, data }; + } catch (error) { + return { success: false, error: error.message }; + } + } + + async createLoanOrder(payload) { + try { + const response = await authService.createLoanOrder(payload); + return { success: true, message: response.message }; + } catch (error) { + return { success: false, error: error.message }; + } + } + + async getLoanOrder(orderId) { + try { + const data = await authService.getLoanOrder(orderId); + return { success: true, data }; + } catch (error) { + return { success: false, error: error.message }; + } + } + + async updateLoanOrder(orderId, payload) { + try { + const response = await authService.updateLoanOrder(orderId, payload); + return { success: true, message: response.message }; + } catch (error) { + return { success: false, error: error.message }; + } + } + + async deleteLoanOrder(orderId) { + try { + const response = await authService.deleteLoanOrder(orderId); + return { success: true, message: response.message }; + } catch (error) { + return { success: false, error: error.message }; + } + } } export default new ApiService(); \ No newline at end of file diff --git a/src/services/authService.js b/src/services/authService.js index 28a87d5..7418cd4 100644 --- a/src/services/authService.js +++ b/src/services/authService.js @@ -272,6 +272,35 @@ class AuthService { async deleteLoanPaidOffLetterOrder(orderId) { return this.makeRequest(`/loan-paid-off-letter-orders/${orderId}`, null, true, 'DELETE'); } + + // ================================ + // Loan Orders (Karz sargytlary) + // ================================ + + // LIST + async getLoanOrders() { + return this.makeRequest('/loan-order', null, true, 'GET'); + } + + // CREATE + async createLoanOrder(data) { + return this.makeRequest('/loan-order', data, true, 'POST'); + } + + // SHOW + async getLoanOrder(orderId) { + return this.makeRequest(`/loan-order/${orderId}`, null, true, 'GET'); + } + + // UPDATE + async updateLoanOrder(orderId, data) { + return this.makeRequest(`/loan-order/${orderId}`, data, true, 'POST'); + } + + // DELETE + async deleteLoanOrder(orderId) { + return this.makeRequest(`/loan-order/${orderId}`, null, true, 'DELETE'); + } } export default new AuthService(); \ No newline at end of file