loan order details more beautiful

This commit is contained in:
2025-07-08 16:06:22 +05:00
parent af13251758
commit a520a9065f
12 changed files with 1126 additions and 446 deletions

View File

@@ -0,0 +1,253 @@
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 CardTransactionOrderDetailsScreen = () => {
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 [startDate, setStartDate] = useState('');
const [endDate, setEndDate] = useState('');
const [downloading, setDownloading] = useState(false);
const fetchDetails = async () => {
setLoading(true);
const res = await apiService.getCardTransactionOrder(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.deleteCardTransactionOrder(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 openDownloadModal = () => {
setStartDate('');
setEndDate('');
setModalVisible(true);
};
const handleDownload = async () => {
if (!startDate || !endDate) {
Alert.alert('Error', 'Select both dates');
return;
}
setDownloading(true);
const res = await apiService.downloadCardTransactions(orderId, startDate, endDate);
setDownloading(false);
if (res.success && res.data?.url) {
Linking.openURL(res.data.url);
setModalVisible(false);
} else {
Alert.alert('Error', res.error || 'Could not download');
}
};
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 hereketleri</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}`} />}
</View>
<TouchableOpacity style={styles.actionBtn} onPress={openDownloadModal}>
<Text style={styles.actionText}>Ýükle (PDF)</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.deleteBtn} onPress={handleDelete}>
<Text style={styles.deleteText}>Poz</Text>
</TouchableOpacity>
</ScrollView>
{/* Download modal */}
<Modal visible={modalVisible} transparent animationType="fade" onRequestClose={() => setModalVisible(false)}>
<View style={styles.modalBackdrop}>
<View style={styles.modalCard}>
<Text style={styles.modalTitle}>Sene aralygy</Text>
<DateInput label="*Başla" value={startDate} onChange={setStartDate} />
<DateInput label="*Tamamla" value={endDate} onChange={setEndDate} />
<TouchableOpacity style={styles.submitBtn} onPress={handleDownload} disabled={downloading}>
{downloading ? (
<ActivityIndicator color={COLORS.white} />
) : (
<Text style={styles.submitText}>Ýükle</Text>
)}
</TouchableOpacity>
<TouchableOpacity onPress={() => setModalVisible(false)} style={{ marginTop: 16, alignItems: 'center' }}>
<Text style={{ color: COLORS.error }}>Ý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',
},
});
export default CardTransactionOrderDetailsScreen;

View File

@@ -0,0 +1,190 @@
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 CardTransactionOrdersScreen = () => {
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.getCardTransactionOrders();
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('CardTransactionOrderDetails', { 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 hereketleri</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('CreateCardTransactionOrder')}
>
<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 CardTransactionOrdersScreen;

View File

@@ -0,0 +1,150 @@
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';
// Build options for months and years
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 CreateCardTransactionOrderScreen = () => {
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('');
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);
const res = await apiService.createCardTransactionOrder(cardNumber.trim(), 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..."
value={cardNumber}
onChangeText={setCardNumber}
keyboardType="numeric"
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 CreateCardTransactionOrderScreen;