From 2a67472ae7cec475a9b4d1b22c4eb242364ee14b Mon Sep 17 00:00:00 2001 From: Nurmuhammet Allanov Date: Fri, 4 Jul 2025 18:36:36 +0500 Subject: [PATCH] loan paid off working --- package-lock.json | 50 ++++++ package.json | 3 + src/components/DateInput.js | 152 +++++++++++++++++ src/components/SelectInput.js | 154 ++++++++++++++++++ .../CreateLoanPaidOffLetterOrderScreen.js | 109 ++++++++++--- 5 files changed, 442 insertions(+), 26 deletions(-) create mode 100644 src/components/DateInput.js create mode 100644 src/components/SelectInput.js diff --git a/package-lock.json b/package-lock.json index 5a073bd..dd83136 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,8 @@ "dependencies": { "@expo/vector-icons": "^14.1.0", "@react-native-async-storage/async-storage": "^2.2.0", + "@react-native-community/datetimepicker": "^7.7.0", + "@react-native-picker/picker": "^2.4.12", "@react-navigation/bottom-tabs": "^7.4.2", "@react-navigation/native": "^7.1.14", "@react-navigation/stack": "^7.4.2", @@ -17,6 +19,7 @@ "expo-status-bar": "~2.2.3", "react": "19.0.0", "react-native": "0.79.5", + "react-native-modal-datetime-picker": "^15.0.1", "react-native-safe-area-context": "^5.5.1", "react-native-screens": "^4.11.1", "react-native-svg": "^15.12.0" @@ -2136,6 +2139,26 @@ "react-native": "^0.0.0-0 || >=0.65 <1.0" } }, + "node_modules/@react-native-community/datetimepicker": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@react-native-community/datetimepicker/-/datetimepicker-7.7.0.tgz", + "integrity": "sha512-nYzZy4DQLRFUzKJShWzRleCaebmCJfZ1lIcFmZgMXJoiVuGJNw3OIGHSWmHhPETh3OhP1RO3to882d7WmDIyrA==", + "dependencies": { + "invariant": "^2.2.4" + } + }, + "node_modules/@react-native-picker/picker": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@react-native-picker/picker/-/picker-2.11.1.tgz", + "integrity": "sha512-ThklnkK4fV3yynnIIRBkxxjxR4IFbdMNJVF6tlLdOJ/zEFUEFUEdXY0KmH0iYzMwY8W4/InWsLiA7AkpAbnexA==", + "workspaces": [ + "example" + ], + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, "node_modules/@react-native/assets-registry": { "version": "0.79.5", "resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.79.5.tgz", @@ -6153,6 +6176,21 @@ "node": ">= 6" } }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -6360,6 +6398,18 @@ "react-native": "*" } }, + "node_modules/react-native-modal-datetime-picker": { + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/react-native-modal-datetime-picker/-/react-native-modal-datetime-picker-15.0.1.tgz", + "integrity": "sha512-FmNFeGwYWH6TCUvAr8OX75tu0FSUDxeEOxCorq2PdeIqbRx9wt7y/5oKUCfbWBWM6y+gme4TzrA0xAtufHqgGg==", + "dependencies": { + "prop-types": "^15.7.2" + }, + "peerDependencies": { + "@react-native-community/datetimepicker": ">=6.7.0", + "react-native": ">=0.65.0" + } + }, "node_modules/react-native-safe-area-context": { "version": "5.5.1", "resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.5.1.tgz", diff --git a/package.json b/package.json index aea8bc7..62762c1 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,8 @@ "dependencies": { "@expo/vector-icons": "^14.1.0", "@react-native-async-storage/async-storage": "^2.2.0", + "@react-native-community/datetimepicker": "^7.7.0", + "@react-native-picker/picker": "^2.4.12", "@react-navigation/bottom-tabs": "^7.4.2", "@react-navigation/native": "^7.1.14", "@react-navigation/stack": "^7.4.2", @@ -18,6 +20,7 @@ "expo-status-bar": "~2.2.3", "react": "19.0.0", "react-native": "0.79.5", + "react-native-modal-datetime-picker": "^15.0.1", "react-native-safe-area-context": "^5.5.1", "react-native-screens": "^4.11.1", "react-native-svg": "^15.12.0" diff --git a/src/components/DateInput.js b/src/components/DateInput.js new file mode 100644 index 0000000..720b5bb --- /dev/null +++ b/src/components/DateInput.js @@ -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 ( + + {label && {label}} + + {value || placeholder} + + + + {show && Platform.OS === 'android' && ( + + )} + + {Platform.OS === 'ios' && ( + + + + d && setTempDate(d)} + maximumDate={maximumDate} + minimumDate={minimumDate} + textColor={COLORS.textPrimary} + themeVariant="light" + style={{ width: '100%', backgroundColor: COLORS.white, height: 220 }} + /> + + + Cancel + + + Confirm + + + + + + )} + + ); +}; + +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; \ No newline at end of file diff --git a/src/components/SelectInput.js b/src/components/SelectInput.js new file mode 100644 index 0000000..a185bbf --- /dev/null +++ b/src/components/SelectInput.js @@ -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 ( + + {label && {label}} + + + {selectedLabel()} + + + + + + + + + + String(item.value)} + renderItem={({ item }) => ( + handleSelect(item.value)} + > + {item.label} + + )} + /> + + + + + + + ); +}; + +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; \ No newline at end of file diff --git a/src/screens/Loan/CreateLoanPaidOffLetterOrderScreen.js b/src/screens/Loan/CreateLoanPaidOffLetterOrderScreen.js index 9557add..92777ff 100644 --- a/src/screens/Loan/CreateLoanPaidOffLetterOrderScreen.js +++ b/src/screens/Loan/CreateLoanPaidOffLetterOrderScreen.js @@ -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 = () => { - + Täze güwanama sargyt et {/* Region & Branch */} - { + setRegion(val); + setBranchId(''); + }} + placeholder="Saýla" /> - setBranchId(val)} + placeholder="Saýla" + disabled={branchOptions.length === 0} /> {/* Customer */} @@ -119,12 +178,12 @@ const CreateLoanPaidOffLetterOrderScreen = () => { /> {/* Passport */} - { /> {/* Other personal */} - { value={contractNumber} onChangeText={setContractNumber} /> -