loan paid off working

This commit is contained in:
2025-07-04 18:36:36 +05:00
parent 15ed37aee4
commit 2a67472ae7
5 changed files with 442 additions and 26 deletions

152
src/components/DateInput.js Normal file
View File

@@ -0,0 +1,152 @@
import React, { useState } from 'react';
import { View, Text, StyleSheet, TouchableOpacity, Platform, Modal } from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import { COLORS } from '../constants/colors';
import DateTimePicker from '@react-native-community/datetimepicker';
const formatDate = (date) => {
const day = String(date.getDate()).padStart(2, '0');
const month = String(date.getMonth() + 1).padStart(2, '0');
const year = date.getFullYear();
return `${day}.${month}.${year}`;
};
const parseDate = (str) => {
if (!str) return null;
const [day, month, year] = str.split('.');
if (!day || !month || !year) return null;
return new Date(parseInt(year), parseInt(month) - 1, parseInt(day));
};
const DateInput = ({ label, value, onChange, placeholder = 'Saýla', maximumDate, minimumDate }) => {
const [show, setShow] = useState(false);
const [tempDate, setTempDate] = useState(value ? (typeof value === 'string' ? parseDate(value) : value) : new Date());
const open = () => {
setTempDate(value ? (typeof value === 'string' ? parseDate(value) : value) : new Date());
setShow(true);
};
const close = () => setShow(false);
const onChangeAndroid = (event, selectedDate) => {
close();
if (event.type !== 'dismissed' && selectedDate) {
onChange(formatDate(selectedDate));
}
};
const onConfirmIOS = () => {
onChange(formatDate(tempDate));
close();
};
return (
<View style={styles.container}>
{label && <Text style={styles.label}>{label}</Text>}
<TouchableOpacity style={styles.selectBox} onPress={open} activeOpacity={0.7}>
<Text style={[styles.selectText, !value && { color: COLORS.gray[400] }]}>{value || placeholder}</Text>
<Ionicons name="calendar" size={18} color={COLORS.gray[400]} />
</TouchableOpacity>
{show && Platform.OS === 'android' && (
<DateTimePicker
value={tempDate}
mode="date"
display="calendar"
onChange={onChangeAndroid}
maximumDate={maximumDate}
minimumDate={minimumDate}
/>
)}
{Platform.OS === 'ios' && (
<Modal visible={show} transparent animationType="fade">
<View style={styles.modalOverlay}>
<View style={styles.modalContent}>
<DateTimePicker
value={tempDate}
mode="date"
display="spinner"
onChange={(e, d) => d && setTempDate(d)}
maximumDate={maximumDate}
minimumDate={minimumDate}
textColor={COLORS.textPrimary}
themeVariant="light"
style={{ width: '100%', backgroundColor: COLORS.white, height: 220 }}
/>
<View style={styles.modalButtons}>
<TouchableOpacity style={styles.modalBtn} onPress={close}>
<Text style={styles.modalBtnTextCancel}>Cancel</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.modalBtn} onPress={onConfirmIOS}>
<Text style={styles.modalBtnText}>Confirm</Text>
</TouchableOpacity>
</View>
</View>
</View>
</Modal>
)}
</View>
);
};
const styles = StyleSheet.create({
container: {
marginBottom: 20,
},
label: {
fontSize: 14,
fontWeight: '600',
color: COLORS.textPrimary,
marginBottom: 8,
},
selectBox: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
borderWidth: 1,
borderColor: COLORS.gray[300],
borderRadius: 12,
backgroundColor: COLORS.white,
paddingHorizontal: 16,
minHeight: 52,
},
selectText: {
fontSize: 16,
color: COLORS.textPrimary,
},
modalOverlay: {
flex: 1,
backgroundColor: 'rgba(0,0,0,0.25)',
justifyContent: 'center',
paddingHorizontal: 24,
},
modalContent: {
backgroundColor: COLORS.white,
borderRadius: 12,
overflow: 'hidden',
},
modalButtons: {
flexDirection: 'row',
borderTopWidth: 1,
borderTopColor: COLORS.gray[200],
},
modalBtn: {
flex: 1,
paddingVertical: 14,
alignItems: 'center',
},
modalBtnText: {
color: COLORS.primary,
fontWeight: '600',
fontSize: 16,
},
modalBtnTextCancel: {
color: COLORS.error,
fontWeight: '600',
fontSize: 16,
},
});
export default DateInput;

View File

@@ -0,0 +1,154 @@
import React, { useState } from 'react';
import {
View,
Text,
StyleSheet,
TouchableOpacity,
Modal,
FlatList,
TouchableWithoutFeedback,
} from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import { COLORS } from '../constants/colors';
/**
* SelectInput simple dropdown selector that mimics the look of Input component.
*
* Props:
* - label : string Field label
* - value : any Currently selected value
* - options : Array<{ label: string, value: any }>
* - onValueChange : (val) => void
* - placeholder : string Text when no value selected
* - disabled : boolean
*/
const SelectInput = ({
label,
value,
options = [],
onValueChange,
placeholder = 'Select',
disabled = false,
}) => {
const [modalVisible, setModalVisible] = useState(false);
const selectedLabel = () => {
const found = options.find((o) => o.value === value);
return found ? found.label : placeholder;
};
const openModal = () => {
if (disabled) return;
setModalVisible(true);
};
const closeModal = () => setModalVisible(false);
const handleSelect = (val) => {
onValueChange && onValueChange(val);
closeModal();
};
return (
<View style={styles.container}>
{label && <Text style={styles.label}>{label}</Text>}
<TouchableOpacity
style={[styles.selectBox, disabled && styles.disabled]}
onPress={openModal}
activeOpacity={0.7}
>
<Text
style={[styles.selectText, !value && { color: COLORS.gray[400] }]}
numberOfLines={1}
>
{selectedLabel()}
</Text>
<Ionicons name="chevron-down" size={18} color={COLORS.gray[400]} />
</TouchableOpacity>
<Modal
animationType="slide"
transparent
visible={modalVisible}
onRequestClose={closeModal}
>
<TouchableWithoutFeedback onPress={closeModal}>
<View style={styles.modalOverlay}>
<TouchableWithoutFeedback>
<View style={styles.modalContainer}>
<FlatList
data={options}
keyExtractor={(item) => String(item.value)}
renderItem={({ item }) => (
<TouchableOpacity
style={styles.optionRow}
onPress={() => handleSelect(item.value)}
>
<Text style={styles.optionText}>{item.label}</Text>
</TouchableOpacity>
)}
/>
</View>
</TouchableWithoutFeedback>
</View>
</TouchableWithoutFeedback>
</Modal>
</View>
);
};
const styles = StyleSheet.create({
container: {
marginBottom: 20,
},
label: {
fontSize: 14,
fontWeight: '600',
color: COLORS.textPrimary,
marginBottom: 8,
},
selectBox: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
borderWidth: 1,
borderColor: COLORS.gray[300],
borderRadius: 12,
backgroundColor: COLORS.white,
paddingHorizontal: 16,
minHeight: 52,
},
selectText: {
fontSize: 16,
color: COLORS.textPrimary,
flex: 1,
marginRight: 8,
},
disabled: {
backgroundColor: COLORS.gray[100],
opacity: 0.6,
},
modalOverlay: {
flex: 1,
backgroundColor: 'rgba(0,0,0,0.3)',
justifyContent: 'center',
paddingHorizontal: 24,
},
modalContainer: {
backgroundColor: COLORS.white,
borderRadius: 12,
maxHeight: '70%',
},
optionRow: {
paddingVertical: 16,
paddingHorizontal: 20,
borderBottomWidth: 1,
borderBottomColor: COLORS.gray[200],
},
optionText: {
fontSize: 16,
color: COLORS.textPrimary,
},
});
export default SelectInput;

View File

@@ -1,11 +1,15 @@
import React, { useState } from 'react';
import React, { useState, useEffect } 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 SelectInput from '../../components/SelectInput';
import DateInput from '../../components/DateInput';
import { API_CONFIG } from '../../constants/api';
import { useAuth } from '../../contexts/AuthContext';
import apiService from '../../services/apiService';
import { StatusBar } from 'expo-status-bar';
const CreateLoanPaidOffLetterOrderScreen = () => {
const navigation = useNavigation();
@@ -25,6 +29,57 @@ const CreateLoanPaidOffLetterOrderScreen = () => {
const [loanReason, setLoanReason] = useState('');
const [loading, setLoading] = useState(false);
// Options
const [regionOptions, setRegionOptions] = useState([]);
const [passportSeriesOptions, setPassportSeriesOptions] = useState([]);
const [branchesByRegion, setBranchesByRegion] = useState({});
const { user } = useAuth();
useEffect(() => {
// Prefill user passport if available
if (user) {
if (user.passport_serie) setPassportSerie(user.passport_serie);
if (user.passport_id) setPassportId(String(user.passport_id));
if (user.phone) setPhone(String(user.phone).slice(-8));
if (user.region) setRegion(user.region);
}
const fetchEnums = async () => {
try {
const res = await fetch(`${API_CONFIG.BASE_URL}/base-app-enums`);
const enums = await res.json();
// Regions
const regions = Object.entries(enums.regions || {}).map(([value, label]) => ({ value, label }));
setRegionOptions(regions);
// Passport series
const pSeries = Object.keys(enums.passport_series || {}).map((key) => ({ value: key, label: key }));
setPassportSeriesOptions(pSeries);
} catch (e) {
console.warn('Failed loading enums', e.message);
}
};
const fetchBranches = async () => {
try {
const res = await fetch(`${API_CONFIG.BASE_URL}/branches?groupBy=region`);
const json = await res.json();
setBranchesByRegion(json);
} catch (e) {
console.warn('Failed loading branches', e.message);
}
};
fetchEnums();
fetchBranches();
}, []);
const branchOptions = region && branchesByRegion[region]
? branchesByRegion[region].map((b) => ({ label: b.name, value: b.id }))
: [];
const handleSubmit = async () => {
// Basic validation ensure all required fields are filled
if (
@@ -51,7 +106,7 @@ const CreateLoanPaidOffLetterOrderScreen = () => {
customer_name: customerName,
customer_surname: customerSurname,
passport_serie: passportSerie,
passport_id: parseInt(passportId),
passport_id: passportId.trim(),
born_at: bornAt,
phone: parseInt(phone),
loan_contract_number: contractNumber,
@@ -85,23 +140,27 @@ const CreateLoanPaidOffLetterOrderScreen = () => {
<Ionicons name="arrow-back" size={24} color={COLORS.textPrimary} />
</TouchableOpacity>
<ScrollView contentContainerStyle={{ paddingBottom: 40 }} showsVerticalScrollIndicator={false}>
<ScrollView contentContainerStyle={{ paddingBottom: 40, paddingHorizontal: 24 }} showsVerticalScrollIndicator={false}>
<Text style={styles.title}>Täze güwanama sargyt et</Text>
{/* Region & Branch */}
<Input
label="Region (ag, ak, mr, ... )"
placeholder="mr"
<SelectInput
label="Welaýat"
value={region}
onChangeText={setRegion}
autoCapitalize="none"
options={regionOptions}
onValueChange={(val) => {
setRegion(val);
setBranchId('');
}}
placeholder="Saýla"
/>
<Input
label="Şahamça ID-si"
placeholder="12"
<SelectInput
label="Şahamça"
value={branchId}
onChangeText={setBranchId}
keyboardType="numeric"
options={branchOptions}
onValueChange={(val) => setBranchId(val)}
placeholder="Saýla"
disabled={branchOptions.length === 0}
/>
{/* Customer */}
@@ -119,12 +178,12 @@ const CreateLoanPaidOffLetterOrderScreen = () => {
/>
{/* Passport */}
<Input
<SelectInput
label="Passport seriýasy"
placeholder="I-AS"
value={passportSerie}
onChangeText={setPassportSerie}
autoCapitalize="characters"
options={passportSeriesOptions}
onValueChange={setPassportSerie}
placeholder="Saýla"
/>
<Input
label="Passport nomeri"
@@ -135,11 +194,10 @@ const CreateLoanPaidOffLetterOrderScreen = () => {
/>
{/* Other personal */}
<Input
label="Doglan senesi (DD.MM.YYYY)"
placeholder="10.10.2000"
<DateInput
label="Doglan senesi"
value={bornAt}
onChangeText={setBornAt}
onChange={setBornAt}
/>
<Input
label="Telefon belgi (+9936...)"
@@ -156,11 +214,10 @@ const CreateLoanPaidOffLetterOrderScreen = () => {
value={contractNumber}
onChangeText={setContractNumber}
/>
<Input
<DateInput
label="Karz şertnama senesi"
placeholder="20.04.2022"
value={contractDate}
onChangeText={setContractDate}
onChange={setContractDate}
/>
<Input
label="Karz mukdary"
@@ -192,11 +249,11 @@ const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: COLORS.backgroundSecondary,
paddingHorizontal: 24,
paddingTop: 40,
},
backBtn: {
marginBottom: 24,
marginLeft: 24,
},
title: {
fontSize: 24,