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