loan order working
This commit is contained in:
7
App.js
7
App.js
@@ -1,11 +1,16 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { AuthProvider } from './src/contexts/AuthContext';
|
import { AuthProvider } from './src/contexts/AuthContext';
|
||||||
import RootNavigator from './src/navigation/RootNavigator';
|
import RootNavigator from './src/navigation/RootNavigator';
|
||||||
|
import { BaseEnumsProvider } from './src/contexts/BaseEnumsContext';
|
||||||
|
import { StatusBar } from 'react-native';
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
return (
|
return (
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<RootNavigator />
|
<BaseEnumsProvider>
|
||||||
|
<StatusBar barStyle="dark-content" backgroundColor="#fff"/>
|
||||||
|
<RootNavigator />
|
||||||
|
</BaseEnumsProvider>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
52
src/contexts/BaseEnumsContext.js
Normal file
52
src/contexts/BaseEnumsContext.js
Normal file
@@ -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 (
|
||||||
|
<BaseEnumsContext.Provider value={{ enums, refresh: fetchEnums, getEnums, getLabel, getOptions }}>
|
||||||
|
{children}
|
||||||
|
</BaseEnumsContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useBaseEnums = () => useContext(BaseEnumsContext);
|
||||||
@@ -6,6 +6,9 @@ import CreateLoanRemainingOrderScreen from '../screens/Loan/CreateLoanRemainingO
|
|||||||
import LoanPaidOffLetterOrdersScreen from '../screens/Loan/LoanPaidOffLetterOrdersScreen';
|
import LoanPaidOffLetterOrdersScreen from '../screens/Loan/LoanPaidOffLetterOrdersScreen';
|
||||||
import CreateLoanPaidOffLetterOrderScreen from '../screens/Loan/CreateLoanPaidOffLetterOrderScreen';
|
import CreateLoanPaidOffLetterOrderScreen from '../screens/Loan/CreateLoanPaidOffLetterOrderScreen';
|
||||||
import LoanPaidOffLetterOrderDetailsScreen from '../screens/Loan/LoanPaidOffLetterOrderDetailsScreen';
|
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();
|
const Stack = createStackNavigator();
|
||||||
|
|
||||||
@@ -17,6 +20,9 @@ const MenuNavigator = () => (
|
|||||||
<Stack.Screen name="LoanPaidOffLetterOrders" component={LoanPaidOffLetterOrdersScreen} />
|
<Stack.Screen name="LoanPaidOffLetterOrders" component={LoanPaidOffLetterOrdersScreen} />
|
||||||
<Stack.Screen name="CreateLoanPaidOffLetterOrder" component={CreateLoanPaidOffLetterOrderScreen} />
|
<Stack.Screen name="CreateLoanPaidOffLetterOrder" component={CreateLoanPaidOffLetterOrderScreen} />
|
||||||
<Stack.Screen name="LoanPaidOffLetterOrderDetails" component={LoanPaidOffLetterOrderDetailsScreen} />
|
<Stack.Screen name="LoanPaidOffLetterOrderDetails" component={LoanPaidOffLetterOrderDetailsScreen} />
|
||||||
|
<Stack.Screen name="LoanOrders" component={LoanOrdersScreen} />
|
||||||
|
<Stack.Screen name="CreateLoanOrder" component={CreateLoanOrderScreen} />
|
||||||
|
<Stack.Screen name="LoanOrderDetails" component={LoanOrderDetailsScreen} />
|
||||||
</Stack.Navigator>
|
</Stack.Navigator>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
155
src/screens/Loan/CreateLoanOrderScreen.js
Normal file
155
src/screens/Loan/CreateLoanOrderScreen.js
Normal file
@@ -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 (
|
||||||
|
<SafeAreaView style={styles.container}>
|
||||||
|
<StatusBar style="dark" />
|
||||||
|
<TouchableOpacity style={styles.backBtn} onPress={() => navigation.goBack()}>
|
||||||
|
<Ionicons name="arrow-back" size={24} color={COLORS.textPrimary} />
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
<ScrollView contentContainerStyle={{ paddingBottom: 40, paddingHorizontal: 24 }} showsVerticalScrollIndicator={false}>
|
||||||
|
<Text style={styles.title}>Täze karz sargyt</Text>
|
||||||
|
|
||||||
|
<SelectInput label="Karzyň görnüşi" value={loanType} options={loanTypeOptions} onValueChange={setLoanType} placeholder="Saýla" />
|
||||||
|
<Input label="Karz mukdary" placeholder="20000" value={loanAmount} onChangeText={setLoanAmount} keyboardType="numeric" />
|
||||||
|
|
||||||
|
<SelectInput label="Welaýat" value={region} options={regionOptions} onValueChange={(val) => { setRegion(val); setBranchId(''); }} placeholder="Saýla" />
|
||||||
|
<SelectInput label="Şahamça" value={branchId} options={branchOptions} onValueChange={setBranchId} placeholder="Saýla" disabled={branchOptions.length === 0} />
|
||||||
|
|
||||||
|
<Input label="Ady" placeholder="Mahmyt" value={customerName} onChangeText={setCustomerName} />
|
||||||
|
<Input label="Familiýasy" placeholder="Allaberdiyev" value={customerSurname} onChangeText={setCustomerSurname} />
|
||||||
|
<Input label="Atasynyň ady" placeholder="" value={customerPatro} onChangeText={setCustomerPatro} />
|
||||||
|
|
||||||
|
<SelectInput label="Passport seriýasy" value={passportSerie} options={passportSeriesOptions} onValueChange={setPassportSerie} placeholder="Saýla" />
|
||||||
|
<Input label="Passport nomeri" placeholder="100999" value={passportId} onChangeText={setPassportId} keyboardType="numeric" />
|
||||||
|
|
||||||
|
<DateInput label="Doglan senesi" value={bornAt} onChange={setBornAt} />
|
||||||
|
<Input label="Telefon (+9936...)" value={phone} onChangeText={setPhone} keyboardType="numeric" />
|
||||||
|
|
||||||
|
<TouchableOpacity style={styles.submitBtn} onPress={handleSubmit} disabled={loading}>
|
||||||
|
{loading ? <ActivityIndicator color={COLORS.white} /> : <Text style={styles.submitText}>Ýatda sakla</Text>}
|
||||||
|
</TouchableOpacity>
|
||||||
|
</ScrollView>
|
||||||
|
</SafeAreaView>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
135
src/screens/Loan/LoanOrderDetailsScreen.js
Normal file
135
src/screens/Loan/LoanOrderDetailsScreen.js
Normal file
@@ -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 }) => (
|
||||||
|
<View style={[styles.detailRow, !last && styles.detailRowBorder]}>
|
||||||
|
<Text style={styles.detailKey}>{label}</Text>
|
||||||
|
<Text style={styles.detailValue}>{String(value)}</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<View style={styles.centered}>
|
||||||
|
<ActivityIndicator size="large" color={COLORS.primary} />
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!order) {
|
||||||
|
return (
|
||||||
|
<View style={styles.centered}>
|
||||||
|
<Text>No data</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SafeAreaView style={styles.container}>
|
||||||
|
<StatusBar style="dark" />
|
||||||
|
<TouchableOpacity style={styles.backBtn} onPress={() => navigation.goBack()}>
|
||||||
|
<Ionicons name="close" size={28} color={COLORS.textPrimary} />
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
<ScrollView contentContainerStyle={{ paddingBottom: 40, paddingHorizontal: 24 }}>
|
||||||
|
<Text style={styles.title}>Karz sargyt maglumatlary</Text>
|
||||||
|
|
||||||
|
<View style={styles.detailCard}>
|
||||||
|
<DetailRow label="ID" value={order.id} />
|
||||||
|
<DetailRow label="Karz mukdary" value={order.loan_amount} />
|
||||||
|
<DetailRow label="Loan type" value={order.loan_type} />
|
||||||
|
<DetailRow label="Status" value={order.status ?? '-'} />
|
||||||
|
<DetailRow label="Bellik" value={order.notes ?? '-'} last />
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<Text style={styles.sectionTitle}>Müşderi</Text>
|
||||||
|
<View style={styles.detailCard}>
|
||||||
|
<DetailRow label="FIO" value={`${order.customer_name} ${order.customer_surname} ${order.customer_patronic_name ?? ''}`.trim()} />
|
||||||
|
<DetailRow label="Doglan senesi" value={formatDate(order.born_at)} />
|
||||||
|
<DetailRow label="Telefon" value={order.phone} />
|
||||||
|
<DetailRow label="Passport" value={`${order.passport_serie} ${order.passport_id}`} last />
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Address */}
|
||||||
|
<Text style={styles.sectionTitle}>Salgylary</Text>
|
||||||
|
<View style={styles.detailCard}>
|
||||||
|
<DetailRow label="Passport salgysy" value={order.passport_address} />
|
||||||
|
<DetailRow label="Real salgysy" value={order.real_address} last />
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<TouchableOpacity style={styles.deleteBtn} onPress={handleDelete}>
|
||||||
|
<Text style={styles.deleteText}>Poz</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</ScrollView>
|
||||||
|
</SafeAreaView>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
145
src/screens/Loan/LoanOrdersScreen.js
Normal file
145
src/screens/Loan/LoanOrdersScreen.js
Normal file
@@ -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 (
|
||||||
|
<TouchableOpacity style={styles.card} onPress={() => navigation.navigate('LoanOrderDetails', { orderId: item.id })}>
|
||||||
|
<View style={styles.circle}>
|
||||||
|
<Text style={styles.circleText}>{item.id}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={styles.cardContent}>
|
||||||
|
<Text style={styles.passportText}>{passportLine}</Text>
|
||||||
|
<Text style={styles.accountLabel}>{amountLine}</Text>
|
||||||
|
<Text style={styles.accountValue}>{item.loan_amount || '-'}</Text>
|
||||||
|
<Text style={styles.dateText}>{created}</Text>
|
||||||
|
</View>
|
||||||
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<View style={styles.centered}>
|
||||||
|
<ActivityIndicator size="large" color={COLORS.primary} />
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SafeAreaView style={styles.container}>
|
||||||
|
<StatusBar style="dark" />
|
||||||
|
{/* Header */}
|
||||||
|
<View style={styles.header}>
|
||||||
|
<TouchableOpacity onPress={() => navigation.goBack()} style={{ paddingRight: 12 }}>
|
||||||
|
<Ionicons name="arrow-back" size={24} color={COLORS.textPrimary} />
|
||||||
|
</TouchableOpacity>
|
||||||
|
<Text style={styles.headerTitle}>Karz sargytlar</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<FlatList
|
||||||
|
data={orders}
|
||||||
|
keyExtractor={(it) => it.id?.toString()}
|
||||||
|
renderItem={renderItem}
|
||||||
|
contentContainerStyle={orders.length === 0 && styles.emptyContainer}
|
||||||
|
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />}
|
||||||
|
ListEmptyComponent={<Text style={styles.emptyText}>No orders yet</Text>}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TouchableOpacity style={styles.fab} onPress={() => navigation.navigate('CreateLoanOrder')}>
|
||||||
|
<Ionicons name="add" size={28} color={COLORS.white} />
|
||||||
|
</TouchableOpacity>
|
||||||
|
</SafeAreaView>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
@@ -59,9 +59,9 @@ const HomeScreen = () => {
|
|||||||
<ActivityIndicator color={COLORS.primary} style={{ marginTop: 16 }} />
|
<ActivityIndicator color={COLORS.primary} style={{ marginTop: 16 }} />
|
||||||
) : (
|
) : (
|
||||||
<View style={styles.metricsGrid}>
|
<View style={styles.metricsGrid}>
|
||||||
<MetricCard label="Loan Orders" value={metrics?.loanOrders ?? '-'} />
|
<MetricCard label="Işlenilýän karz sargytlary" value={metrics?.loanOrders ?? '-'} />
|
||||||
<MetricCard label="Accepted" value={metrics?.acceptedLoanOrders ?? '-'} />
|
<MetricCard label="Kanagatlandan karz sargytlary" value={metrics?.acceptedLoanOrders ?? '-'} />
|
||||||
<MetricCard label="Denied" value={metrics?.deniedLoanOrders ?? '-'} />
|
<MetricCard label="Ret edilen karz sargytlary" value={metrics?.deniedLoanOrders ?? '-'} />
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -44,7 +44,9 @@ const MenuScreen = () => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
const handleMenuItemPress = (item) => {
|
const handleMenuItemPress = (item) => {
|
||||||
if (item.id === 2) {
|
if (item.id === 1) {
|
||||||
|
navigation.navigate('LoanOrders');
|
||||||
|
} else if (item.id === 2) {
|
||||||
navigation.navigate('LoanRemainingOrders');
|
navigation.navigate('LoanRemainingOrders');
|
||||||
} else if (item.id === 3) {
|
} else if (item.id === 3) {
|
||||||
navigation.navigate('LoanPaidOffLetterOrders');
|
navigation.navigate('LoanPaidOffLetterOrders');
|
||||||
|
|||||||
@@ -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();
|
export default new ApiService();
|
||||||
@@ -272,6 +272,35 @@ class AuthService {
|
|||||||
async deleteLoanPaidOffLetterOrder(orderId) {
|
async deleteLoanPaidOffLetterOrder(orderId) {
|
||||||
return this.makeRequest(`/loan-paid-off-letter-orders/${orderId}`, null, true, 'DELETE');
|
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();
|
export default new AuthService();
|
||||||
Reference in New Issue
Block a user