Loan paid off letter

This commit is contained in:
2025-07-04 17:37:17 +05:00
parent fbf201bcc1
commit 15ed37aee4
3 changed files with 514 additions and 0 deletions

View File

@@ -0,0 +1,221 @@
import React, { useState } from 'react';
import { Text, StyleSheet, TouchableOpacity, ActivityIndicator, Alert, ScrollView, SafeAreaView } from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import { useNavigation } from '@react-navigation/native';
import { COLORS } from '../../constants/colors';
import Input from '../../components/Input';
import { StatusBar } from 'expo-status-bar';
import apiService from '../../services/apiService';
const CreateLoanPaidOffLetterOrderScreen = () => {
const navigation = useNavigation();
// Form states (required fields only)
const [region, setRegion] = useState('');
const [branchId, setBranchId] = useState('');
const [customerName, setCustomerName] = useState('');
const [customerSurname, setCustomerSurname] = useState('');
const [passportSerie, setPassportSerie] = useState('');
const [passportId, setPassportId] = useState('');
const [bornAt, setBornAt] = useState('');
const [phone, setPhone] = useState('');
const [contractNumber, setContractNumber] = useState('');
const [contractDate, setContractDate] = useState('');
const [loanAmount, setLoanAmount] = useState('');
const [loanReason, setLoanReason] = useState('');
const [loading, setLoading] = useState(false);
const handleSubmit = async () => {
// Basic validation ensure all required fields are filled
if (
!region ||
!branchId ||
!customerName ||
!customerSurname ||
!passportSerie ||
!passportId ||
!bornAt ||
!phone ||
!contractNumber ||
!contractDate ||
!loanAmount ||
!loanReason
) {
Alert.alert('Error', 'Please fill in all required fields');
return;
}
const payload = {
region,
branch_id: parseInt(branchId),
customer_name: customerName,
customer_surname: customerSurname,
passport_serie: passportSerie,
passport_id: parseInt(passportId),
born_at: bornAt,
phone: parseInt(phone),
loan_contract_number: contractNumber,
loan_contract_date: contractDate,
loan_amount: loanAmount,
loan_reason: loanReason,
};
try {
setLoading(true);
const res = await apiService.createLoanPaidOffLetterOrder(payload);
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');
}
} catch (error) {
setLoading(false);
Alert.alert('Error', error.message || 'Could not create order');
}
};
return (
<SafeAreaView style={styles.container}>
<StatusBar style="dark" />
<TouchableOpacity style={styles.backBtn} onPress={() => navigation.goBack()}>
<Ionicons name="arrow-back" size={24} color={COLORS.textPrimary} />
</TouchableOpacity>
<ScrollView contentContainerStyle={{ paddingBottom: 40 }} showsVerticalScrollIndicator={false}>
<Text style={styles.title}>Täze güwanama sargyt et</Text>
{/* Region & Branch */}
<Input
label="Region (ag, ak, mr, ... )"
placeholder="mr"
value={region}
onChangeText={setRegion}
autoCapitalize="none"
/>
<Input
label="Şahamça ID-si"
placeholder="12"
value={branchId}
onChangeText={setBranchId}
keyboardType="numeric"
/>
{/* Customer */}
<Input
label="Ady"
placeholder="Mahmyt"
value={customerName}
onChangeText={setCustomerName}
/>
<Input
label="Familiýasy"
placeholder="Allaberdiyev"
value={customerSurname}
onChangeText={setCustomerSurname}
/>
{/* Passport */}
<Input
label="Passport seriýasy"
placeholder="I-AS"
value={passportSerie}
onChangeText={setPassportSerie}
autoCapitalize="characters"
/>
<Input
label="Passport nomeri"
placeholder="100999"
value={passportId}
onChangeText={setPassportId}
keyboardType="numeric"
/>
{/* Other personal */}
<Input
label="Doglan senesi (DD.MM.YYYY)"
placeholder="10.10.2000"
value={bornAt}
onChangeText={setBornAt}
/>
<Input
label="Telefon belgi (+9936...)"
placeholder="65999990"
value={phone}
onChangeText={setPhone}
keyboardType="numeric"
/>
{/* Loan contract */}
<Input
label="Karz şertnama nomeri"
placeholder="3242358989234"
value={contractNumber}
onChangeText={setContractNumber}
/>
<Input
label="Karz şertnama senesi"
placeholder="20.04.2022"
value={contractDate}
onChangeText={setContractDate}
/>
<Input
label="Karz mukdary"
placeholder="20000"
value={loanAmount}
onChangeText={setLoanAmount}
keyboardType="numeric"
/>
<Input
label="Karz maksady"
placeholder="Puldan pul gazanmak üçin"
value={loanReason}
onChangeText={setLoanReason}
/>
<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,
paddingHorizontal: 24,
paddingTop: 40,
},
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 CreateLoanPaidOffLetterOrderScreen;

View File

@@ -0,0 +1,148 @@
import React, { useEffect, useState } from 'react';
import { View, Text, StyleSheet, ActivityIndicator, TouchableOpacity, Alert, ScrollView, SafeAreaView } 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';
const LoanPaidOffLetterOrderDetailsScreen = () => {
const navigation = useNavigation();
const route = useRoute();
const { orderId } = route.params || {};
const [loading, setLoading] = useState(true);
const [order, setOrder] = useState(null);
const fetchDetails = async () => {
setLoading(true);
const res = await apiService.getLoanPaidOffLetterOrder(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.deleteLoanPaidOffLetterOrder(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');
}
};
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 }}>
<Text style={styles.title}>Güwanama detallary</Text>
<View style={styles.detailCard}>
{Object.entries(order).map(([key, value]) => (
<View key={key} style={styles.detailRow}>
<Text style={styles.detailKey}>{key}</Text>
<Text style={styles.detailValue}>{String(value)}</Text>
</View>
))}
</View>
<TouchableOpacity style={styles.deleteBtn} onPress={handleDelete}>
<Text style={styles.deleteText}>Poz</Text>
</TouchableOpacity>
</ScrollView>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: COLORS.backgroundSecondary,
paddingHorizontal: 24,
paddingTop: 40,
},
centered: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
backBtn: {
alignSelf: 'flex-end',
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',
marginBottom: 12,
},
detailKey: {
fontWeight: '600',
color: COLORS.textSecondary,
},
detailValue: {
color: COLORS.textPrimary,
maxWidth: '60%',
textAlign: 'right',
},
deleteBtn: {
backgroundColor: COLORS.error,
paddingVertical: 14,
borderRadius: 8,
alignItems: 'center',
},
deleteText: {
color: COLORS.white,
fontSize: 16,
fontWeight: '600',
},
});
export default LoanPaidOffLetterOrderDetailsScreen;

View File

@@ -0,0 +1,145 @@
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 LoanPaidOffLetterOrdersScreen = () => {
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.getLoanPaidOffLetterOrders();
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 renderItem = ({ item }) => {
const passportLine = `Pasport: ${item.passport_serie} ${item.passport_id}`;
const accountLine = `Karz hasaby:`;
const created = item.created_at ? new Date(item.created_at).toLocaleDateString() : '';
return (
<TouchableOpacity style={styles.card} onPress={() => navigation.navigate('LoanPaidOffLetterOrderDetails', { orderId: item.id })}>
<View style={styles.circle}>
<Text style={styles.circleText}>{item.id}</Text>
</View>
<View style={styles.cardContent}>
<Text style={styles.passportText}>{passportLine}</Text>
<Text style={styles.accountLabel}>{accountLine}</Text>
<Text style={styles.accountValue}>{item.loan_contract_number || '-'}</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}>Ýapylandygy barada güwanamalar</Text>
</View>
<FlatList
data={orders}
keyExtractor={(it) => it.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>}
/>
<TouchableOpacity style={styles.fab} onPress={() => navigation.navigate('CreateLoanPaidOffLetterOrder')}>
<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', padding: 16 },
headerTitle: { fontSize: 18, fontWeight: 'bold', color: COLORS.textPrimary, marginLeft: 12 },
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 },
passportText: { fontWeight: '700', color: COLORS.textPrimary, marginBottom: 4 },
accountLabel: { color: COLORS.textSecondary, fontSize: 14 },
accountValue: { 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',
right: 24,
bottom: 40,
width: 56,
height: 56,
borderRadius: 28,
backgroundColor: COLORS.primary,
alignItems: 'center',
justifyContent: 'center',
elevation: 4,
},
});
export default LoanPaidOffLetterOrdersScreen;