diff --git a/src/navigation/MenuNavigator.js b/src/navigation/MenuNavigator.js
index 83e78e7..088bbe5 100644
--- a/src/navigation/MenuNavigator.js
+++ b/src/navigation/MenuNavigator.js
@@ -12,6 +12,9 @@ import LoanOrderDetailsScreen from '../screens/Loan/LoanOrderDetailsScreen';
import CardTransactionOrdersScreen from '../screens/Card/CardTransactionOrdersScreen';
import CreateCardTransactionOrderScreen from '../screens/Card/CreateCardTransactionOrderScreen';
import CardTransactionOrderDetailsScreen from '../screens/Card/CardTransactionOrderDetailsScreen';
+import CardBalanceOrdersScreen from '../screens/Card/CardBalanceOrdersScreen';
+import CreateCardBalanceOrderScreen from '../screens/Card/CreateCardBalanceOrderScreen';
+import CardBalanceOrderDetailsScreen from '../screens/Card/CardBalanceOrderDetailsScreen';
const Stack = createStackNavigator();
@@ -29,6 +32,9 @@ const MenuNavigator = () => (
+
+
+
);
diff --git a/src/screens/Card/CardBalanceOrderDetailsScreen.js b/src/screens/Card/CardBalanceOrderDetailsScreen.js
new file mode 100644
index 0000000..3130611
--- /dev/null
+++ b/src/screens/Card/CardBalanceOrderDetailsScreen.js
@@ -0,0 +1,314 @@
+import React, { useEffect, useState } from 'react';
+import { View, Text, StyleSheet, ActivityIndicator, TouchableOpacity, Alert, ScrollView, SafeAreaView, Modal } 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';
+import DateInput from '../../components/DateInput';
+import { Linking } from 'react-native';
+
+const DetailRow = ({ label, value, showBorder = true }) => (
+
+ {label}
+ {String(value)}
+
+);
+
+const CardBalanceOrderDetailsScreen = () => {
+ const navigation = useNavigation();
+ const route = useRoute();
+ const { orderId } = route.params || {};
+
+ const [loading, setLoading] = useState(true);
+ const [order, setOrder] = useState(null);
+ const [modalVisible, setModalVisible] = useState(false);
+ const [loadingCardInfo, setLoadingCardInfo] = useState(false);
+ const [cardInfo, setCardInfo] = useState(null);
+
+ const fetchDetails = async () => {
+ setLoading(true);
+ const res = await apiService.getCardBalanceOrder(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.deleteCardBalanceOrder(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');
+ }
+ };
+
+ const openViewModal = async () => {
+ setModalVisible(true);
+ setLoadingCardInfo(true);
+ // Fetch card info via download endpoint
+ const res = await apiService.downloadCardBalances(orderId);
+ if (res.success && res.data?.status && res.data.data) {
+ setCardInfo(res.data.data);
+ } else {
+ Alert.alert('Ýalňyşlyk', res.error || 'Maglumat tapylmady');
+ }
+ setLoadingCardInfo(false);
+ };
+
+ if (loading) {
+ return (
+
+
+
+ );
+ }
+
+ if (!order) {
+ return (
+
+ No data
+
+ );
+ }
+
+ return (
+
+
+ navigation.goBack()}>
+
+
+
+
+ Kart galyndylary
+
+
+
+ {order.card_number && }
+ {order.card_month && order.card_year && }
+ {order.passport_serie && order.passport_id && }
+
+
+
+ Görmek
+
+
+
+ Poz
+
+
+
+ {/* View modal */}
+ setModalVisible(false)}>
+
+
+ {loadingCardInfo ? (
+
+ ) : cardInfo ? (
+
+ {cardInfo.depName || 'Türkmenistanyň "Türkmenbaşy" paýdarlar täjirçilik banky'}
+ {cardInfo.cardName || 'Kart'}
+ KART BELGISI
+
+ {`${order.card_number?.slice(0,6)}******${order.card_number?.slice(-4)}`}
+
+
+
+ KARTYŇ EÝESINIŇ ADY
+ {cardInfo.clientName || '-'}
+
+
+ MÖHLETI
+ {order.card_month}/{order.card_year}
+
+
+
+ {cardInfo.balance !== undefined && (
+
+ GALYNDY
+ {cardInfo.balance} {cardInfo.valCode || 'TMT'}
+
+ )}
+
+ ) : (
+ Maglumat ýok
+ )}
+
+ setModalVisible(false)} style={{ marginTop: 24, alignItems: 'center' }}>
+ Ýap
+
+
+
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: COLORS.backgroundSecondary,
+ paddingTop: 40,
+ },
+ centered: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ backBtn: {
+ alignSelf: 'flex-end',
+ marginRight: 24,
+ 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',
+ paddingVertical: 12,
+ },
+ detailRowBorder: {
+ borderBottomWidth: 1,
+ borderBottomColor: COLORS.border,
+ },
+ detailKey: {
+ fontWeight: '600',
+ color: COLORS.textSecondary,
+ },
+ detailValue: {
+ color: COLORS.textPrimary,
+ },
+ actionBtn: {
+ backgroundColor: COLORS.primary,
+ paddingVertical: 16,
+ borderRadius: 8,
+ alignItems: 'center',
+ marginBottom: 16,
+ },
+ actionText: {
+ color: COLORS.white,
+ fontSize: 16,
+ fontWeight: '600',
+ },
+ deleteBtn: {
+ backgroundColor: COLORS.error,
+ paddingVertical: 16,
+ borderRadius: 8,
+ alignItems: 'center',
+ },
+ deleteText: {
+ color: COLORS.white,
+ fontSize: 16,
+ fontWeight: '600',
+ },
+ modalBackdrop: {
+ flex: 1,
+ backgroundColor: 'rgba(0,0,0,0.25)',
+ justifyContent: 'center',
+ alignItems: 'center',
+ paddingHorizontal: 24,
+ },
+ modalCard: {
+ backgroundColor: COLORS.white,
+ borderRadius: 12,
+ width: '100%',
+ padding: 24,
+ },
+ modalTitle: {
+ fontSize: 18,
+ fontWeight: 'bold',
+ color: COLORS.textPrimary,
+ marginBottom: 16,
+ },
+ submitBtn: {
+ marginTop: 8,
+ backgroundColor: COLORS.primary,
+ paddingVertical: 14,
+ borderRadius: 8,
+ alignItems: 'center',
+ },
+ submitText: {
+ color: COLORS.white,
+ fontSize: 16,
+ fontWeight: '600',
+ },
+ bankName: {
+ fontSize: 16,
+ fontWeight: '700',
+ color: COLORS.textPrimary,
+ marginBottom: 8,
+ textAlign: 'center',
+ },
+ cardType: {
+ fontSize: 16,
+ fontWeight: '700',
+ color: COLORS.textSecondary,
+ marginBottom: 22,
+ textAlign: 'center',
+ },
+ cardLabel: {
+ fontSize: 12,
+ color: COLORS.textSecondary,
+ marginTop: 4,
+ },
+ cardNumber: {
+ fontSize: 20,
+ fontWeight: '700',
+ marginTop: 4,
+ },
+ subLabel: {
+ fontSize: 12,
+ color: COLORS.textSecondary,
+ },
+ subValue: {
+ fontSize: 14,
+ fontWeight: '600',
+ color: COLORS.textPrimary,
+ marginTop: 4,
+ },
+ balanceBox: {
+ borderWidth: 1,
+ borderColor: COLORS.gray[300],
+ borderRadius: 4,
+ padding: 12,
+ marginTop: 24,
+ },
+ balanceLabel: {
+ fontSize: 12,
+ color: COLORS.textSecondary,
+ marginBottom: 4,
+ },
+ balanceValue: {
+ fontSize: 22,
+ fontWeight: '700',
+ color: COLORS.success || '#2ecc71',
+ },
+});
+
+export default CardBalanceOrderDetailsScreen;
\ No newline at end of file
diff --git a/src/screens/Card/CardBalanceOrdersScreen.js b/src/screens/Card/CardBalanceOrdersScreen.js
new file mode 100644
index 0000000..3e97f68
--- /dev/null
+++ b/src/screens/Card/CardBalanceOrdersScreen.js
@@ -0,0 +1,195 @@
+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 formatCardNumber = (num) => {
+ if (!num) return '';
+ return num.replace(/\s+/g, '').replace(/(.{4})/g, '$1 ').trim();
+};
+
+const CardBalanceOrdersScreen = () => {
+ 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.getCardBalanceOrders();
+ 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 = (item) => {
+ navigation.navigate('CardBalanceOrderDetails', { orderId: item.id });
+ };
+
+ const renderItem = ({ item }) => {
+ const cardNum = item.card_number || '';
+ const masked = cardNum ? `${cardNum.slice(0, 6)}******${cardNum.slice(-4)}` : '';
+ const created = item.created_at ? new Date(item.created_at).toLocaleDateString() : '';
+ const passportLine = item.passport_serie && item.passport_id ? `Pasport: ${item.passport_serie} ${item.passport_id}` : '';
+
+ return (
+ handleItemPress(item)}>
+
+ {item.id}
+
+
+ {masked}
+ {passportLine !== '' && {passportLine}}
+ {created}
+
+
+ );
+ };
+
+ if (loading) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+ {/* Header */}
+
+ navigation.goBack()} style={{ paddingRight: 12 }}>
+
+
+ Kart galyndylary
+
+ item.id?.toString()}
+ renderItem={renderItem}
+ contentContainerStyle={orders.length === 0 && styles.emptyContainer}
+ refreshControl={}
+ ListEmptyComponent={No orders yet}
+ />
+
+ {/* Floating Action Button */}
+ navigation.navigate('CreateCardBalanceOrder')}
+ >
+
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: COLORS.backgroundSecondary,
+ },
+ centered: {
+ flex: 1,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ header: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ paddingHorizontal: 24,
+ paddingVertical: 16,
+ },
+ headerTitle: {
+ fontSize: 20,
+ fontWeight: 'bold',
+ color: COLORS.textPrimary,
+ },
+ 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,
+ },
+ cardNumber: {
+ fontWeight: '700',
+ 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',
+ bottom: 32,
+ right: 32,
+ backgroundColor: COLORS.primary,
+ width: 56,
+ height: 56,
+ borderRadius: 28,
+ alignItems: 'center',
+ justifyContent: 'center',
+ elevation: 4,
+ },
+ passportText: {
+ color: COLORS.textSecondary,
+ fontSize: 14,
+ marginBottom: 2,
+ },
+});
+
+export default CardBalanceOrdersScreen;
\ No newline at end of file
diff --git a/src/screens/Card/CreateCardBalanceOrderScreen.js b/src/screens/Card/CreateCardBalanceOrderScreen.js
new file mode 100644
index 0000000..3c78095
--- /dev/null
+++ b/src/screens/Card/CreateCardBalanceOrderScreen.js
@@ -0,0 +1,161 @@
+import React, { useState, useEffect } from 'react';
+import { View, Text, StyleSheet, TouchableOpacity, ActivityIndicator, Alert, SafeAreaView, ScrollView } 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 SelectInput from '../../components/SelectInput';
+import { StatusBar } from 'expo-status-bar';
+import { useAuth } from '../../contexts/AuthContext';
+
+const monthOptions = Array.from({ length: 12 }).map((_, i) => {
+ const m = String(i + 1).padStart(2, '0');
+ return { label: m, value: m };
+});
+const yearOptions = Array.from({ length: 60 }).map((_, i) => {
+ const y = String(new Date().getFullYear() + i);
+ return { label: y, value: y };
+});
+
+const PASSPORT_SERIES = ['I-AS','I-MR','II-MR','I-AH','II-AH','I-LB','II-LB','I-BN','II-BN','I-DZ','II-DZ'];
+
+const CreateCardBalanceOrderScreen = () => {
+ const navigation = useNavigation();
+ const [cardNumber, setCardNumber] = useState('');
+ const [cardMonth, setCardMonth] = useState('');
+ const [cardYear, setCardYear] = useState('');
+ const [loading, setLoading] = useState(false);
+ const { user } = useAuth();
+ const [passportSerie, setPassportSerie] = useState('');
+ const [passportId, setPassportId] = useState('');
+
+ const handleCardNumberChange = (value) => {
+ // Keep only digits
+ const digits = value.replace(/[^0-9]/g, '').slice(0, 16);
+ // Group into chunks of 4
+ const parts = digits.match(/.{1,4}/g) || [];
+ const formatted = parts.join(' ');
+ setCardNumber(formatted);
+ };
+
+ useEffect(() => {
+ if (user) {
+ if (user.passport_serie) setPassportSerie(user.passport_serie);
+ if (user.passport_id) setPassportId(String(user.passport_id));
+ }
+ }, [user]);
+
+ const handleSubmit = async () => {
+ if (!cardNumber.trim() || !cardMonth || !cardYear || !passportSerie || !passportId.trim()) {
+ Alert.alert('Error', 'All fields are required');
+ return;
+ }
+ setLoading(true);
+ // Remove spaces before sending
+ const rawCard = cardNumber.replace(/\s+/g, '');
+ const res = await apiService.createCardBalanceOrder(rawCard, cardMonth, cardYear, passportSerie, passportId.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
+
+ ({ label: v, value: v }))}
+ placeholder="Saýla"
+ />
+
+
+
+
+
+
+
+
+
+
+ {loading ? (
+
+ ) : (
+ Ýatda sakla
+ )}
+
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: COLORS.backgroundSecondary,
+ },
+ 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 CreateCardBalanceOrderScreen;
\ No newline at end of file
diff --git a/src/screens/Main/MenuScreen.js b/src/screens/Main/MenuScreen.js
index 959e5eb..3fd4549 100644
--- a/src/screens/Main/MenuScreen.js
+++ b/src/screens/Main/MenuScreen.js
@@ -52,6 +52,8 @@ const MenuScreen = () => {
navigation.navigate('LoanPaidOffLetterOrders');
} else if (item.id === 5) {
navigation.navigate('CardTransactionOrders');
+ } else if (item.id === 7) {
+ navigation.navigate('CardBalanceOrders');
} else {
console.log('Menu item pressed:', item.title);
}
diff --git a/src/services/apiService.js b/src/services/apiService.js
index a804892..f77d4cb 100644
--- a/src/services/apiService.js
+++ b/src/services/apiService.js
@@ -368,6 +368,64 @@ class ApiService {
return { success: false, error: error.message };
}
}
+
+ // ================================
+ // Card Balance Orders (Kart galyndylary)
+ // ================================
+
+ async getCardBalanceOrders() {
+ try {
+ const data = await authService.getCardBalanceOrders();
+ return { success: true, data };
+ } catch (error) {
+ return { success: false, error: error.message };
+ }
+ }
+
+ async createCardBalanceOrder(cardNumber, cardMonth, cardYear, passportSerie = null, passportId = null) {
+ try {
+ const response = await authService.createCardBalanceOrder(cardNumber, cardMonth, cardYear, passportSerie, passportId);
+ return { success: true, message: response.message };
+ } catch (error) {
+ return { success: false, error: error.message };
+ }
+ }
+
+ async getCardBalanceOrder(orderId) {
+ try {
+ const data = await authService.getCardBalanceOrder(orderId);
+ return { success: true, data };
+ } catch (error) {
+ return { success: false, error: error.message };
+ }
+ }
+
+ async updateCardBalanceOrder(orderId, cardNumber, cardMonth, cardYear, passportSerie = null, passportId = null) {
+ try {
+ const response = await authService.updateCardBalanceOrder(orderId, cardNumber, cardMonth, cardYear, passportSerie, passportId);
+ return { success: true, message: response.message };
+ } catch (error) {
+ return { success: false, error: error.message };
+ }
+ }
+
+ async deleteCardBalanceOrder(orderId) {
+ try {
+ const response = await authService.deleteCardBalanceOrder(orderId);
+ return { success: true, message: response.message };
+ } catch (error) {
+ return { success: false, error: error.message };
+ }
+ }
+
+ async downloadCardBalances(orderId) {
+ try {
+ const response = await authService.downloadCardBalances(orderId);
+ return { success: true, data: response };
+ } 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 b3b6e60..fdc8220 100644
--- a/src/services/authService.js
+++ b/src/services/authService.js
@@ -411,6 +411,74 @@ class AuthService {
// POST request – authenticated (token header will be included if available)
return this.makeRequest('/card-balance-quick-check', payload, true, 'POST');
}
+
+ // ================================
+ // Card Balance Orders (Kart galyndylary)
+ // ================================
+
+ // LIST
+ async getCardBalanceOrders() {
+ return this.makeRequest('/card-balances', null, true, 'GET');
+ }
+
+ // CREATE
+ async createCardBalanceOrder(cardNumber, cardMonth, cardYear, passportSerie = null, passportId = null) {
+ let serie = passportSerie;
+ let pid = passportId;
+ if (!serie || !pid) {
+ const user = await this.getStoredUser();
+ serie = serie || user?.passport_serie;
+ pid = pid || user?.passport_id;
+ }
+ if (!serie || !pid) {
+ throw new Error('Passport details are missing');
+ }
+ const payload = {
+ passport_serie: serie,
+ passport_id: pid,
+ card_number: cardNumber,
+ card_month: cardMonth,
+ card_year: cardYear,
+ };
+ return this.makeRequest('/card-balances', payload, true, 'POST');
+ }
+
+ // SHOW
+ async getCardBalanceOrder(orderId) {
+ return this.makeRequest(`/card-balances/${orderId}`, null, true, 'GET');
+ }
+
+ // UPDATE
+ async updateCardBalanceOrder(orderId, cardNumber, cardMonth, cardYear, passportSerie = null, passportId = null) {
+ let serie = passportSerie;
+ let pid = passportId;
+ if (!serie || !pid) {
+ const user = await this.getStoredUser();
+ serie = serie || user?.passport_serie;
+ pid = pid || user?.passport_id;
+ }
+ if (!serie || !pid) {
+ throw new Error('Passport details are missing');
+ }
+ const payload = {
+ passport_serie: serie,
+ passport_id: pid,
+ card_number: cardNumber,
+ card_month: cardMonth,
+ card_year: cardYear,
+ };
+ return this.makeRequest(`/card-balances/${orderId}`, payload, true, 'POST');
+ }
+
+ // DELETE
+ async deleteCardBalanceOrder(orderId) {
+ return this.makeRequest(`/card-balances/${orderId}`, null, true, 'DELETE');
+ }
+
+ // DOWNLOAD (returns object with card details)
+ async downloadCardBalances(orderId) {
+ return this.makeRequest(`/card-balances-download/${orderId}`, null, true, 'GET');
+ }
}
export default new AuthService();
\ No newline at end of file