card remaning done
This commit is contained in:
@@ -12,6 +12,9 @@ import LoanOrderDetailsScreen from '../screens/Loan/LoanOrderDetailsScreen';
|
|||||||
import CardTransactionOrdersScreen from '../screens/Card/CardTransactionOrdersScreen';
|
import CardTransactionOrdersScreen from '../screens/Card/CardTransactionOrdersScreen';
|
||||||
import CreateCardTransactionOrderScreen from '../screens/Card/CreateCardTransactionOrderScreen';
|
import CreateCardTransactionOrderScreen from '../screens/Card/CreateCardTransactionOrderScreen';
|
||||||
import CardTransactionOrderDetailsScreen from '../screens/Card/CardTransactionOrderDetailsScreen';
|
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();
|
const Stack = createStackNavigator();
|
||||||
|
|
||||||
@@ -29,6 +32,9 @@ const MenuNavigator = () => (
|
|||||||
<Stack.Screen name="CardTransactionOrders" component={CardTransactionOrdersScreen} />
|
<Stack.Screen name="CardTransactionOrders" component={CardTransactionOrdersScreen} />
|
||||||
<Stack.Screen name="CreateCardTransactionOrder" component={CreateCardTransactionOrderScreen} />
|
<Stack.Screen name="CreateCardTransactionOrder" component={CreateCardTransactionOrderScreen} />
|
||||||
<Stack.Screen name="CardTransactionOrderDetails" component={CardTransactionOrderDetailsScreen} />
|
<Stack.Screen name="CardTransactionOrderDetails" component={CardTransactionOrderDetailsScreen} />
|
||||||
|
<Stack.Screen name="CardBalanceOrders" component={CardBalanceOrdersScreen} />
|
||||||
|
<Stack.Screen name="CreateCardBalanceOrder" component={CreateCardBalanceOrderScreen} />
|
||||||
|
<Stack.Screen name="CardBalanceOrderDetails" component={CardBalanceOrderDetailsScreen} />
|
||||||
</Stack.Navigator>
|
</Stack.Navigator>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
314
src/screens/Card/CardBalanceOrderDetailsScreen.js
Normal file
314
src/screens/Card/CardBalanceOrderDetailsScreen.js
Normal file
@@ -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 }) => (
|
||||||
|
<View style={[styles.detailRow, showBorder && styles.detailRowBorder]}>
|
||||||
|
<Text style={styles.detailKey}>{label}</Text>
|
||||||
|
<Text style={styles.detailValue}>{String(value)}</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<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}>Kart galyndylary</Text>
|
||||||
|
|
||||||
|
<View style={styles.detailCard}>
|
||||||
|
<DetailRow label="ID" value={order.id} />
|
||||||
|
{order.card_number && <DetailRow label="Kart" value={order.card_number} />}
|
||||||
|
{order.card_month && order.card_year && <DetailRow label="Kartyň möhleti" value={`${order.card_month}/${order.card_year}`} />}
|
||||||
|
{order.passport_serie && order.passport_id && <DetailRow label="Pasport" value={`${order.passport_serie} ${order.passport_id}`} showBorder={false} />}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<TouchableOpacity style={styles.actionBtn} onPress={openViewModal}>
|
||||||
|
<Text style={styles.actionText}>Görmek</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
<TouchableOpacity style={styles.deleteBtn} onPress={handleDelete}>
|
||||||
|
<Text style={styles.deleteText}>Poz</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
{/* View modal */}
|
||||||
|
<Modal visible={modalVisible} transparent animationType="fade" onRequestClose={() => setModalVisible(false)}>
|
||||||
|
<View style={styles.modalBackdrop}>
|
||||||
|
<View style={[styles.modalCard, { maxHeight: '80%' }] }>
|
||||||
|
{loadingCardInfo ? (
|
||||||
|
<ActivityIndicator size="large" color={COLORS.primary} />
|
||||||
|
) : cardInfo ? (
|
||||||
|
<View>
|
||||||
|
<Text style={styles.bankName}>{cardInfo.depName || 'Türkmenistanyň "Türkmenbaşy" paýdarlar täjirçilik banky'}</Text>
|
||||||
|
<Text style={styles.cardType}>{cardInfo.cardName || 'Kart'}</Text>
|
||||||
|
<Text style={styles.cardLabel}>KART BELGISI</Text>
|
||||||
|
|
||||||
|
<Text style={styles.cardNumber}>{`${order.card_number?.slice(0,6)}******${order.card_number?.slice(-4)}`}</Text>
|
||||||
|
|
||||||
|
<View style={{ flexDirection: 'row', marginTop: 24 }}>
|
||||||
|
<View style={{ flex: 1 }}>
|
||||||
|
<Text style={styles.subLabel}>KARTYŇ EÝESINIŇ ADY</Text>
|
||||||
|
<Text style={styles.subValue}>{cardInfo.clientName || '-'}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={{ width: 100 }}>
|
||||||
|
<Text style={styles.subLabel}>MÖHLETI</Text>
|
||||||
|
<Text style={styles.subValue}>{order.card_month}/{order.card_year}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{cardInfo.balance !== undefined && (
|
||||||
|
<View style={styles.balanceBox}>
|
||||||
|
<Text style={styles.balanceLabel}>GALYNDY</Text>
|
||||||
|
<Text style={styles.balanceValue}>{cardInfo.balance} {cardInfo.valCode || 'TMT'}</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<Text>Maglumat ýok</Text>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<TouchableOpacity onPress={() => setModalVisible(false)} style={{ marginTop: 24, alignItems: 'center' }}>
|
||||||
|
<Text style={{ color: COLORS.error, fontWeight: '600' }}>Ýap</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</Modal>
|
||||||
|
</SafeAreaView>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
195
src/screens/Card/CardBalanceOrdersScreen.js
Normal file
195
src/screens/Card/CardBalanceOrdersScreen.js
Normal file
@@ -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 (
|
||||||
|
<TouchableOpacity style={styles.card} onPress={() => handleItemPress(item)}>
|
||||||
|
<View style={styles.circle}>
|
||||||
|
<Text style={styles.circleText}>{item.id}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={styles.cardContent}>
|
||||||
|
<Text style={styles.cardNumber}>{masked}</Text>
|
||||||
|
{passportLine !== '' && <Text style={styles.passportText}>{passportLine}</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}>Kart galyndylary</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('CreateCardBalanceOrder')}
|
||||||
|
>
|
||||||
|
<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',
|
||||||
|
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;
|
||||||
161
src/screens/Card/CreateCardBalanceOrderScreen.js
Normal file
161
src/screens/Card/CreateCardBalanceOrderScreen.js
Normal file
@@ -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 (
|
||||||
|
<SafeAreaView style={styles.container}>
|
||||||
|
<StatusBar style="dark" />
|
||||||
|
<ScrollView contentContainerStyle={{ paddingHorizontal: 24, paddingTop: 40, paddingBottom: 40 }}>
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<SelectInput
|
||||||
|
label="*Passport seriýasy"
|
||||||
|
value={passportSerie}
|
||||||
|
onValueChange={setPassportSerie}
|
||||||
|
options={PASSPORT_SERIES.map((v) => ({ label: v, value: v }))}
|
||||||
|
placeholder="Saýla"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
label="*Passport belgisi"
|
||||||
|
placeholder="123456"
|
||||||
|
value={passportId}
|
||||||
|
onChangeText={setPassportId}
|
||||||
|
keyboardType="numeric"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
label="*Kart belgisi"
|
||||||
|
placeholder="9934 6121 0000 0243"
|
||||||
|
value={cardNumber}
|
||||||
|
onChangeText={handleCardNumberChange}
|
||||||
|
keyboardType="numeric"
|
||||||
|
maxLength={19} // 16 digits + 3 spaces
|
||||||
|
autoCapitalize="none"
|
||||||
|
autoCorrect={false}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SelectInput
|
||||||
|
label="*Aý"
|
||||||
|
value={cardMonth}
|
||||||
|
onValueChange={setCardMonth}
|
||||||
|
options={monthOptions}
|
||||||
|
placeholder="Saýla"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SelectInput
|
||||||
|
label="*Ýyl"
|
||||||
|
value={cardYear}
|
||||||
|
onValueChange={setCardYear}
|
||||||
|
options={yearOptions}
|
||||||
|
placeholder="Saýla"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<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,
|
||||||
|
},
|
||||||
|
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;
|
||||||
@@ -52,6 +52,8 @@ const MenuScreen = () => {
|
|||||||
navigation.navigate('LoanPaidOffLetterOrders');
|
navigation.navigate('LoanPaidOffLetterOrders');
|
||||||
} else if (item.id === 5) {
|
} else if (item.id === 5) {
|
||||||
navigation.navigate('CardTransactionOrders');
|
navigation.navigate('CardTransactionOrders');
|
||||||
|
} else if (item.id === 7) {
|
||||||
|
navigation.navigate('CardBalanceOrders');
|
||||||
} else {
|
} else {
|
||||||
console.log('Menu item pressed:', item.title);
|
console.log('Menu item pressed:', item.title);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -368,6 +368,64 @@ class ApiService {
|
|||||||
return { success: false, error: error.message };
|
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();
|
export default new ApiService();
|
||||||
@@ -411,6 +411,74 @@ class AuthService {
|
|||||||
// POST request – authenticated (token header will be included if available)
|
// POST request – authenticated (token header will be included if available)
|
||||||
return this.makeRequest('/card-balance-quick-check', payload, true, 'POST');
|
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();
|
export default new AuthService();
|
||||||
Reference in New Issue
Block a user