From 6044d5ae78ea2b3cf78a8a63a6d0da95de3a6db5 Mon Sep 17 00:00:00 2001 From: Nurmuhammet Allanov Date: Wed, 9 Jul 2025 10:41:15 +0500 Subject: [PATCH] card pins ready --- src/navigation/MenuNavigator.js | 6 + src/screens/Card/CardPinOrderDetailsScreen.js | 145 +++++++++++++++++ src/screens/Card/CardPinOrdersScreen.js | 123 +++++++++++++++ src/screens/Card/CreateCardPinOrderScreen.js | 149 ++++++++++++++++++ src/screens/Loan/CreateLoanOrderScreen.js | 13 +- src/screens/Main/MenuScreen.js | 2 + src/services/apiService.js | 40 +++++ src/services/authService.js | 29 ++++ 8 files changed, 504 insertions(+), 3 deletions(-) create mode 100644 src/screens/Card/CardPinOrderDetailsScreen.js create mode 100644 src/screens/Card/CardPinOrdersScreen.js create mode 100644 src/screens/Card/CreateCardPinOrderScreen.js diff --git a/src/navigation/MenuNavigator.js b/src/navigation/MenuNavigator.js index 2b9c3f9..238f968 100644 --- a/src/navigation/MenuNavigator.js +++ b/src/navigation/MenuNavigator.js @@ -18,6 +18,9 @@ import CardBalanceOrderDetailsScreen from '../screens/Card/CardBalanceOrderDetai import CardRequisiteOrdersScreen from '../screens/Card/CardRequisiteOrdersScreen'; import CreateCardRequisiteOrderScreen from '../screens/Card/CreateCardRequisiteOrderScreen'; import CardRequisiteOrderDetailsScreen from '../screens/Card/CardRequisiteOrderDetailsScreen'; +import CardPinOrdersScreen from '../screens/Card/CardPinOrdersScreen'; +import CreateCardPinOrderScreen from '../screens/Card/CreateCardPinOrderScreen'; +import CardPinOrderDetailsScreen from '../screens/Card/CardPinOrderDetailsScreen'; const Stack = createStackNavigator(); @@ -41,6 +44,9 @@ const MenuNavigator = () => ( + + + ); diff --git a/src/screens/Card/CardPinOrderDetailsScreen.js b/src/screens/Card/CardPinOrderDetailsScreen.js new file mode 100644 index 0000000..15a10e6 --- /dev/null +++ b/src/screens/Card/CardPinOrderDetailsScreen.js @@ -0,0 +1,145 @@ +import React, { useEffect, useState } from 'react'; +import { View, Text, StyleSheet, ActivityIndicator, TouchableOpacity, Alert, ScrollView, SafeAreaView, Image, Linking } from 'react-native'; +import { Ionicons } from '@expo/vector-icons'; +import { useNavigation, useRoute } from '@react-navigation/native'; +import { StatusBar } from 'expo-status-bar'; +import apiService from '../../services/apiService'; +import { COLORS } from '../../constants/colors'; +import { useBaseEnums } from '../../contexts/BaseEnumsContext'; + +const DetailRow = ({ label, value, showBorder=true }) => ( + + {label} + {String(value)} + +); + +const CardPinOrderDetailsScreen = () => { + const navigation = useNavigation(); + const { params } = useRoute(); + const orderId = params?.orderId; + const { getLabel, getBranches } = useBaseEnums(); + + const [loading,setLoading]=useState(true); + const [order,setOrder]=useState(null); + const [branchName,setBranchName]=useState(''); + + const fetchDetails=async()=>{ + setLoading(true); + const res = await apiService.getCardPinOrder(orderId); + if(res.success){ + setOrder(res.data); + if(res.data?.region && res.data?.branch_id){ + (async()=>{ try{ const brs = await getBranches(res.data.region); const found=brs.find(b=>String(b.id)===String(res.data.branch_id)); if(found) setBranchName(found.name);}catch(e){} })(); + } + } else { Alert.alert('Ýalňyşlyk', res.error || 'Ýalňyşlyk'); } + setLoading(false); + }; + useEffect(()=>{fetchDetails();},[]); + + const handleDelete=()=>{ Alert.alert('Tassykla','Pozmakçy my?',[{text:'Goýbolsun',style:'cancel'},{text:'Poz',style:'destructive',onPress:async()=>{ + const r=await apiService.deleteCardPinOrder(orderId); + if(r.success){ Alert.alert('Pozuldy', r.message || 'Pozuldy', [{text:'OK',onPress:()=>navigation.goBack()}]); } else { Alert.alert('Ýalňyşlyk', r.error || 'Ýalňyşlyk'); } + }}]); }; + + if(loading){ return ; } + if(!order){ return Maglumat ýok; } + + return ( + + + navigation.goBack()}> + + + + + Kart pin bukjalar + + {/* Card info */} + + + {order.card_type_id && } + {order.card_number && } + + + {/* Location */} + {(order.region || branchName) && ( + <> + Lokasiýa + + {order.region && } + {(branchName || order.branch_id) && } + + + )} + + {/* Personal */} + Şahsy maglumatlar + + {order.customer_name && } + {order.customer_surname && } + {order.customer_patronic_name && } + {order.born_at && } + {order.phone && } + + + {/* Passport */} + Pasport + + {(order.passport_serie || order.passport_id) && } + + {order.passport_one && ( + Linking.openURL(order.passport_one)} style={styles.imageWrapper}> + + 1-nji sah. + + )} + {order.passport_two && ( + Linking.openURL(order.passport_two)} style={styles.imageWrapper}> + + 2-3 sah. + + )} + {order.passport_three && ( + Linking.openURL(order.passport_three)} style={styles.imageWrapper}> + + 8-9 sah. + + )} + {order.passport_four && ( + Linking.openURL(order.passport_four)} style={styles.imageWrapper}> + + 32 sah. + + )} + + + + + Poz + + + + ); +}; + +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}, + sectionTitle:{fontWeight:'700',fontSize:18,marginTop:24,marginBottom:12,color:COLORS.textPrimary}, + imageGrid:{flexDirection:'row',flexWrap:'wrap',justifyContent:'space-between',marginTop:12}, + imageWrapper:{width:'48%',marginBottom:16}, + passportImage:{width:'100%',aspectRatio:4/3,borderRadius:8,backgroundColor:'#f0f0f0'}, + imageLabel:{marginTop:4,fontSize:12,color:COLORS.textSecondary,textAlign:'center'}, + deleteBtn:{backgroundColor:COLORS.error,paddingVertical:16,borderRadius:8,alignItems:'center',marginTop:16}, + deleteText:{color:COLORS.white,fontSize:16,fontWeight:'600'}, +}); + +export default CardPinOrderDetailsScreen; \ No newline at end of file diff --git a/src/screens/Card/CardPinOrdersScreen.js b/src/screens/Card/CardPinOrdersScreen.js new file mode 100644 index 0000000..eb7e353 --- /dev/null +++ b/src/screens/Card/CardPinOrdersScreen.js @@ -0,0 +1,123 @@ +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 { StatusBar } from 'expo-status-bar'; +import apiService from '../../services/apiService'; +import { COLORS } from '../../constants/colors'; +import { useBaseEnums } from '../../contexts/BaseEnumsContext'; + +const CARD_BG = '#F1F9F1'; +const CIRCLE_BG = '#A2E4A4'; + +const CardPinOrdersScreen = () => { + const navigation = useNavigation(); + const [orders, setOrders] = useState([]); + const { getLabel } = useBaseEnums(); + const [loading, setLoading] = useState(true); + const [refreshing, setRefreshing] = useState(false); + + const fetchOrders = async () => { + try { + const res = await apiService.getCardPinOrders(); + 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('CardPinOrderDetails', { orderId: item.id }); + }; + + const renderItem = ({ item }) => { + const masked = item.card_number ? `${item.card_number.slice(0,6)}******${item.card_number.slice(-4)}` : ''; + const created = item.created_at ? new Date(item.created_at).toLocaleDateString() : ''; + const typeLabel = getLabel('card_types', item.card_type_id); + const passportLine = item.passport_serie && item.passport_id ? `${item.passport_serie} ${item.passport_id}` : ''; + + return ( + handleItemPress(item)}> + + {item.id} + + + {masked} + {typeLabel && {typeLabel}} + {passportLine !== '' && {passportLine}} + {created} + + + ); + }; + + if (loading) { + return ( + + + + ); + } + + return ( + + + + navigation.goBack()} style={{ paddingRight: 12 }}> + + + Kart pin bukjalar + + item.id?.toString()} + renderItem={renderItem} + contentContainerStyle={orders.length === 0 && styles.emptyContainer} + refreshControl={} + ListEmptyComponent={Heniz sargyt ýok} + /> + + navigation.navigate('CreateCardPinOrder')}> + + + + ); +}; + +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 }, + typeText:{color:COLORS.textSecondary,fontSize:14}, + passportText:{color:COLORS.textSecondary,fontSize:12}, +}); + +export default CardPinOrdersScreen; \ No newline at end of file diff --git a/src/screens/Card/CreateCardPinOrderScreen.js b/src/screens/Card/CreateCardPinOrderScreen.js new file mode 100644 index 0000000..3d2a0c5 --- /dev/null +++ b/src/screens/Card/CreateCardPinOrderScreen.js @@ -0,0 +1,149 @@ +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 { StatusBar } from 'expo-status-bar'; +import apiService from '../../services/apiService'; +import { COLORS } from '../../constants/colors'; +import Input from '../../components/Input'; +import SelectInput from '../../components/SelectInput'; +import { useAuth } from '../../contexts/AuthContext'; +import DateInput from '../../components/DateInput'; +import ImageInput from '../../components/ImageInput'; +import { useBaseEnums } from '../../contexts/BaseEnumsContext'; + +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 formatCardNumber = (val) => { + const digits = String(val).replace(/[^0-9]/g,'').slice(0,16); + return (digits.match(/.{1,4}/g)||[]).join(' '); +}; + +const CreateCardPinOrderScreen = () => { + const navigation = useNavigation(); + const { user } = useAuth(); + const { getOptions, getBranches } = useBaseEnums(); + + const [passportSerie,setPassportSerie]=useState(''); + const [passportId,setPassportId]=useState(''); + const [cardNumber,setCardNumber]=useState(''); + const [cardType,setCardType]=useState(''); + const [region,setRegion]=useState(''); + const [branchId,setBranchId]=useState(''); + const [customerName,setCustomerName]=useState(''); + const [customerSurname,setCustomerSurname]=useState(''); + const [customerPatro,setCustomerPatro]=useState(''); + const [bornAt,setBornAt]=useState(''); + const [phone,setPhone]=useState(''); + const [passportOne,setPassportOne]=useState(null); + const [passportTwo,setPassportTwo]=useState(null); + const [passportThree,setPassportThree]=useState(null); + const [passportFour,setPassportFour]=useState(null); + const [branchOptions,setBranchOptions]=useState([]); + const [loading,setLoading]=useState(false); + + const cardTypeOptions = getOptions('card_types'); + const regionOptions = getOptions('regions'); + + useEffect(()=>{ + if(user){ + if(user.passport_serie) setPassportSerie(user.passport_serie); + if(user.passport_id) setPassportId(String(user.passport_id)); + if(user.card_number) setCardNumber(formatCardNumber(user.card_number)); + if(user.region) setRegion(user.region); + if(user.phone) setPhone(String(user.phone).slice(-8)); + if(user.name){ const parts=user.name.split(' '); setCustomerName(parts[0]); setCustomerSurname(parts[1]||''); } + } + },[user]); + + useEffect(()=>{(async()=>{ + if(region){ const b = await getBranches(region); setBranchOptions(b.map(br=>({label:br.name,value:br.id})));} else {setBranchOptions([]);} })();},[region]); + + const handleCardNumberChange = (v)=> setCardNumber(formatCardNumber(v)); + + const handleSubmit = async()=>{ + if(!cardType||!cardNumber.trim()||!region||!branchId||!customerName||!customerSurname||!bornAt||!phone||!passportSerie||!passportId.trim()||!passportOne||!passportTwo||!passportThree||!passportFour){ + Alert.alert('Ýalňyşlyk','Ähli zerur meýdançalar doldurylmaly'); return; + } + + const payload = new FormData(); + payload.append('card_type_id', cardType); + payload.append('card_number', cardNumber.replace(/\s+/g,'')); + payload.append('region', region); + payload.append('branch_id', branchId); + payload.append('customer_name', customerName); + payload.append('customer_surname', customerSurname); + if(customerPatro) payload.append('customer_patronic_name', customerPatro); + payload.append('born_at', bornAt); + payload.append('phone', parseInt(phone)); + payload.append('passport_serie', passportSerie); + payload.append('passport_id', passportId.trim()); + payload.append('passport_one', { uri: passportOne, name: 'p1.jpg', type:'image/jpeg'}); + payload.append('passport_two', { uri: passportTwo, name: 'p2.jpg', type:'image/jpeg'}); + payload.append('passport_three', { uri: passportThree, name: 'p3.jpg', type:'image/jpeg'}); + payload.append('passport_four', { uri: passportFour, name: 'p4.jpg', type:'image/jpeg'}); + + setLoading(true); + const res = await apiService.createCardPinOrder(payload); + setLoading(false); + if(res.success){ + Alert.alert('Üstünlik', res.message || 'Döredildi', [{ text:'OK', onPress:()=>navigation.goBack() }]); + } else { + Alert.alert('Ýalňyşlyk', res.error || 'Ýalňyşlyk ýüze çykdy'); + } + }; + + return ( + + + + navigation.goBack()}> + + + Täze sargyt + + + + + {/* Location */} + Lokasiýa + + ({label:b.label,value:b.value}))} placeholder="Saýla" /> + + {/* Personal */} + Şahsy maglumatlar + + + + + + + {/* Passport */} + Pasport + ({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}, + sectionTitle:{fontWeight:'700',fontSize:18,marginTop:24,marginBottom:12,color:COLORS.textPrimary}, + submitBtn:{marginTop:32,backgroundColor:COLORS.primary,paddingVertical:16,borderRadius:8,alignItems:'center'}, + submitText:{color:COLORS.white,fontSize:16,fontWeight:'600'}, +}); + +export default CreateCardPinOrderScreen; \ No newline at end of file diff --git a/src/screens/Loan/CreateLoanOrderScreen.js b/src/screens/Loan/CreateLoanOrderScreen.js index 3edf7eb..3674115 100644 --- a/src/screens/Loan/CreateLoanOrderScreen.js +++ b/src/screens/Loan/CreateLoanOrderScreen.js @@ -188,6 +188,11 @@ const CreateLoanOrderScreen = () => { return; } + if (passportId.length != 6) { + Alert.alert('Ýalňyşlyk', 'Passport nomeri 6 san bolmaly'); + return; + } + const rawCardNumber = cardNumber.replace(/[^0-9]/g, ''); if (rawCardNumber.length !== 16) { Alert.alert('Ýalňyşlyk', 'Kart belgisi dogry dolduryň (16 sany rakam)'); @@ -345,7 +350,9 @@ const CreateLoanOrderScreen = () => { - + + + @@ -391,7 +398,7 @@ const CreateLoanOrderScreen = () => { {/* Guarantor passport */} - + {needsSecondGuarantor && ( <> @@ -407,7 +414,7 @@ const CreateLoanOrderScreen = () => { - + )} diff --git a/src/screens/Main/MenuScreen.js b/src/screens/Main/MenuScreen.js index 0667d74..eeeb55b 100644 --- a/src/screens/Main/MenuScreen.js +++ b/src/screens/Main/MenuScreen.js @@ -56,6 +56,8 @@ const MenuScreen = () => { navigation.navigate('CardRequisiteOrders'); } else if (item.id === 7) { navigation.navigate('CardBalanceOrders'); + } else if (item.id === 8) { + navigation.navigate('CardPinOrders'); } else { console.log('Menu item pressed:', item.title); } diff --git a/src/services/apiService.js b/src/services/apiService.js index 6dd18a8..b119a4e 100644 --- a/src/services/apiService.js +++ b/src/services/apiService.js @@ -484,6 +484,46 @@ class ApiService { return { success: false, error: error.message }; } } + + // ================================ + // Card Pin Orders (Kart pin bukjalar) + // ================================ + + async getCardPinOrders() { + try { + const data = await authService.getCardPinOrders(); + return { success: true, data }; + } catch (error) { + return { success: false, error: error.message }; + } + } + + async createCardPinOrder(payload) { + try { + const response = await authService.createCardPinOrder(payload); + return { success: true, message: response.message }; + } catch (error) { + return { success: false, error: error.message }; + } + } + + async getCardPinOrder(orderId) { + try { + const data = await authService.getCardPinOrder(orderId); + return { success: true, data }; + } catch (error) { + return { success: false, error: error.message }; + } + } + + async deleteCardPinOrder(orderId) { + try { + const response = await authService.deleteCardPinOrder(orderId); + return { success: true, message: response.message }; + } catch (error) { + return { success: false, error: error.message }; + } + } } export default new ApiService(); \ No newline at end of file diff --git a/src/services/authService.js b/src/services/authService.js index 191e07d..2dcd04f 100644 --- a/src/services/authService.js +++ b/src/services/authService.js @@ -534,6 +534,35 @@ class AuthService { async downloadCardRequisites(orderId) { return this.makeRequest(`/card-requisites-download/${orderId}`, null, true, 'GET'); } + + // ================================ + // Card Pin Orders (Kart pin bukjalar) + // ================================ + + // LIST + async getCardPinOrders() { + return this.makeRequest('/card-pin-order', null, true, 'GET'); + } + + // CREATE (multipart payload similar to Card Requisite but without month/year) + async createCardPinOrder(payload) { + return this.makeRequest('/card-pin-order', payload, true, 'POST'); + } + + // SHOW + async getCardPinOrder(orderId) { + return this.makeRequest(`/card-pin-order/${orderId}`, null, true, 'GET'); + } + + // UPDATE + async updateCardPinOrder(orderId, payload) { + return this.makeRequest(`/card-pin-order/${orderId}`, payload, true, 'POST'); + } + + // DELETE + async deleteCardPinOrder(orderId) { + return this.makeRequest(`/card-pin-order/${orderId}`, null, true, 'DELETE'); + } } export default new AuthService(); \ No newline at end of file