few changes

This commit is contained in:
2025-07-04 17:19:02 +05:00
parent c75dc93474
commit fbf201bcc1
9 changed files with 811 additions and 4 deletions

View File

@@ -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 = () => {
/>
<Tab.Screen
name="Menu"
component={MenuScreen}
component={MenuNavigator}
options={{
tabBarLabel: 'Hyzmatlar',
}}

View File

@@ -0,0 +1,23 @@
import React from 'react';
import { createStackNavigator } from '@react-navigation/stack';
import MenuScreen from '../screens/Main/MenuScreen';
import LoanRemainingOrdersScreen from '../screens/Loan/LoanRemainingOrdersScreen';
import CreateLoanRemainingOrderScreen from '../screens/Loan/CreateLoanRemainingOrderScreen';
import LoanPaidOffLetterOrdersScreen from '../screens/Loan/LoanPaidOffLetterOrdersScreen';
import CreateLoanPaidOffLetterOrderScreen from '../screens/Loan/CreateLoanPaidOffLetterOrderScreen';
import LoanPaidOffLetterOrderDetailsScreen from '../screens/Loan/LoanPaidOffLetterOrderDetailsScreen';
const Stack = createStackNavigator();
const MenuNavigator = () => (
<Stack.Navigator screenOptions={{ headerShown: false }}>
<Stack.Screen name="MenuHome" component={MenuScreen} />
<Stack.Screen name="LoanRemainingOrders" component={LoanRemainingOrdersScreen} />
<Stack.Screen name="CreateLoanRemainingOrder" component={CreateLoanRemainingOrderScreen} />
<Stack.Screen name="LoanPaidOffLetterOrders" component={LoanPaidOffLetterOrdersScreen} />
<Stack.Screen name="CreateLoanPaidOffLetterOrder" component={CreateLoanPaidOffLetterOrderScreen} />
<Stack.Screen name="LoanPaidOffLetterOrderDetails" component={LoanPaidOffLetterOrderDetailsScreen} />
</Stack.Navigator>
);
export default MenuNavigator;

View File

@@ -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 (
<SafeAreaView style={styles.container}>
<StatusBar style="dark" />
<TouchableOpacity style={styles.backBtn} onPress={() => navigation.goBack()}>
<Ionicons name="arrow-back" size={24} color={COLORS.textPrimary} />
</TouchableOpacity>
<Text style={styles.title}>Täze sargyt</Text>
<Input
label="Account number"
placeholder="1420..."
value={accountNumber}
onChangeText={setAccountNumber}
keyboardType="numeric"
autoCapitalize="none"
autoCorrect={false}
returnKeyType="done"
/>
<TouchableOpacity style={styles.submitBtn} onPress={handleSubmit} disabled={loading}>
{loading ? (
<ActivityIndicator color={COLORS.white} />
) : (
<Text style={styles.submitText}>Ýatda sakla</Text>
)}
</TouchableOpacity>
</SafeAreaView>
);
};
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;

View File

@@ -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 (
<View style={styles.centered}>
<ActivityIndicator size="large" color={COLORS.primary} />
</View>
);
}
if (!order) {
return (
<View style={styles.centered}>
<Text>No data</Text>
</View>
);
}
return (
<View style={styles.container}>
<TouchableOpacity style={styles.backBtn} onPress={() => navigation.goBack()}>
<Ionicons name="close" size={28} color={COLORS.textPrimary} />
</TouchableOpacity>
<Text style={styles.title}>Galyndy detallary</Text>
<View style={styles.detailCard}>
{Object.entries(order).map(([key, value]) => (
<View key={key} style={styles.detailRow}>
<Text style={styles.detailKey}>{key}</Text>
<Text style={styles.detailValue}>{String(value)}</Text>
</View>
))}
</View>
<TouchableOpacity style={styles.deleteBtn} onPress={handleDelete}>
<Text style={styles.deleteText}>Poz</Text>
</TouchableOpacity>
</View>
);
};
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;

View File

@@ -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 }) => (
<TouchableOpacity
style={styles.card}
onPress={() => handleItemPress(item)}
>
<View style={styles.cardIconWrapper}>
<Ionicons name="stats-chart" size={24} color={COLORS.primary} />
</View>
<View style={styles.cardContent}>
<Text style={styles.cardTitle}>{item.account_number}</Text>
</View>
<Ionicons name="information-circle" size={20} color={COLORS.gray[400]} />
</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}>Karzyň galyndysy</Text>
</View>
<FlatList
data={orders}
keyExtractor={(item) => item.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>}
/>
{/* Floating Action Button */}
<TouchableOpacity
style={styles.fab}
onPress={() => navigation.navigate('CreateLoanRemainingOrder')}
>
<Ionicons name="add" size={28} color={COLORS.white} />
</TouchableOpacity>
{/* Result Modal */}
<Modal
visible={modalVisible}
transparent
animationType="fade"
onRequestClose={() => setModalVisible(false)}
>
<Pressable style={styles.modalBackdrop} onPress={() => setModalVisible(false)}>
<Pressable style={styles.modalCard} onPress={(e) => e.stopPropagation()}>
<TouchableOpacity style={styles.modalCloseSmall} onPress={() => setModalVisible(false)}>
<Ionicons name="close" size={20} color={COLORS.textPrimary} />
</TouchableOpacity>
<Text style={styles.modalTitle}>Netije</Text>
{modalLoading ? (
<View style={{ paddingVertical: 40, alignItems: 'center' }}>
<ActivityIndicator size="large" color={COLORS.primary} />
</View>
) : (
<ScrollView
style={{ maxHeight: 320, marginTop: 8 }}
contentContainerStyle={{ paddingBottom: 16 }}
showsVerticalScrollIndicator={false}
>
{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 <View key={f.key} style={styles.divider} />;
}
const val = modalData[f.key];
if (val === null || val === undefined || val === '') return null;
renderedCount++;
return (
<View key={f.key} style={styles.itemWrap}>
<Text style={styles.itemLabel}>{f.label}</Text>
<Text style={styles.itemValue}>{String(val)}</Text>
</View>
);
});
if (renderedCount === 0) {
return Object.entries(modalData).map(([k, v]) => (
<View key={k} style={styles.itemWrap}>
<Text style={styles.itemLabel}>{k}</Text>
<Text style={styles.itemValue}>{String(v)}</Text>
</View>
));
}
return rows;
})()}
</ScrollView>
)}
</Pressable>
</Pressable>
</Modal>
</SafeAreaView>
);
};
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;

View File

@@ -101,6 +101,7 @@ const styles = StyleSheet.create({
paddingTop: 16,
paddingBottom: 24,
backgroundColor: COLORS.white,
marginBottom: 16,
},
greeting: {
fontSize: 16,

View File

@@ -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 (

View File

@@ -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 {

View File

@@ -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();