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