From 61ed8ccda07e6e4e6e2a4589af29b8a14efaa17f Mon Sep 17 00:00:00 2001 From: Nisch1Mv Date: Tue, 9 Dec 2025 16:16:09 +0530 Subject: [PATCH 01/23] doctype.data is now being accessed --- src/api.ts | 44 +++++++++++++++++++++++++++++ src/app/screens/home/FormDetail.tsx | 3 +- 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/src/api.ts b/src/api.ts index c77c3e1..a5970b1 100644 --- a/src/api.ts +++ b/src/api.ts @@ -300,6 +300,13 @@ export async function getAllDoctypesFromLocal(): Promise< } const parsed = parseJson(value); if (parsed?.payload) { + // Validate that payload has the expected structure + if (!parsed.payload.data) { + return; + } + if (!parsed.payload.fields) { + return; + } result[normalizeDoctypeName(parsed.name)] = parsed.payload; } }); @@ -352,6 +359,7 @@ const defaultFetcher = async (name: string): Promise => { const response = await getDoctypeByName({ path: { form_name: name }, }); + const data = (response as any)?.data?.data ?? (response as any)?.data ?? @@ -359,6 +367,42 @@ const defaultFetcher = async (name: string): Promise => { if (!data) { throw new Error(`Doctype response missing data for ${name}`); } + + // Validate that we have fields array + if (!data.fields || !Array.isArray(data.fields)) { + throw new Error(`Invalid doctype structure for ${name}: missing fields array`); + } + + // Check if data object exists, if not try to create it from top-level properties + if (!data.data || typeof data.data !== 'object') { + // Look for doctype metadata at the top level + const doctypeMetadata: any = {}; + const metadataFields = [ + 'name', 'creation', 'modified', 'modified_by', 'owner', 'docStatus', + 'idx', 'issingle', 'istable', 'editable_grid', 'track_changes', 'module', + 'autoname', 'name_case', 'sort_field', 'sort_order', 'readonly', 'in_create', + 'allow_copy', 'allow_rename', 'allow_import', 'hide_toolbar', 'track_seen', + 'max_attachments', 'document_type', 'engine', 'is_submittable', + 'show_name_in_global_search', 'custom', 'beta', 'has_web_view', + 'allow_guest_to_view', 'qick_entry', 'is_tree', 'track_views', + 'all_events_in_timeline', 'allow_auto_repeat', 'show_preview_popup', + 'email_append_to', 'index_web_pages_for_search', 'docType' + ]; + + metadataFields.forEach(field => { + if (data[field] !== undefined) { + doctypeMetadata[field] = data[field]; + } + }); + + // If we found metadata, create the nested structure + if (Object.keys(doctypeMetadata).length > 0) { + data.data = doctypeMetadata; + } else { + throw new Error(`Invalid doctype structure for ${name}: missing data object`); + } + } + return data as DocType; }; diff --git a/src/app/screens/home/FormDetail.tsx b/src/app/screens/home/FormDetail.tsx index a374644..46ff8d0 100644 --- a/src/app/screens/home/FormDetail.tsx +++ b/src/app/screens/home/FormDetail.tsx @@ -190,9 +190,8 @@ const FormDetail: React.FC = ({ navigation }) => { data: formData, schemaHash, status: 'pending' as 'pending' | 'submitted' | 'failed', - is_submittable: doctype.data?.is_submittable ?? 0, + is_submittable: doctype.data.is_submittable }; - setLoading(true); setConfirmModalVisible(false); try { From 19c57c0d6fb2452360f31d6393c9509e516033e7 Mon Sep 17 00:00:00 2001 From: Nisch1Mv Date: Mon, 5 Jan 2026 15:45:34 +0530 Subject: [PATCH 02/23] added the new doctype and adjusted the css beacuse of the long name --- src/app/screens/home/FormsList.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/app/screens/home/FormsList.tsx b/src/app/screens/home/FormsList.tsx index 472f831..ea77756 100644 --- a/src/app/screens/home/FormsList.tsx +++ b/src/app/screens/home/FormsList.tsx @@ -37,6 +37,8 @@ export interface FormItem { const additionalDoctype = [ 'Combating Malnutrition Basic Data', 'PGS Peer Appraisal Basic Data', + 'Nutri Garden Household Nutrition Survey Tool', + 'Data Registers Farmer Transition to NF', 'Testing DocType', ]; @@ -177,10 +179,14 @@ const FormsList = () => { }); }} > - + {item.name} - + {isConnected && (itemState.isDownloading ? ( From 31faea16aa90304f5fbc98eeb8e7eb622e0f057a Mon Sep 17 00:00:00 2001 From: Nisch1Mv Date: Mon, 5 Jan 2026 16:36:31 +0530 Subject: [PATCH 03/23] - enhanced field visibility logic based on dynamic conditions in isFieldEnabled function - removed component level disabiling feild --- src/app/components/SelectDropdown.tsx | 43 ++-------------- src/app/screens/home/FormDetail.tsx | 74 ++++++++++++++++++++++++--- 2 files changed, 71 insertions(+), 46 deletions(-) diff --git a/src/app/components/SelectDropdown.tsx b/src/app/components/SelectDropdown.tsx index cd4bead..7ee3b30 100644 --- a/src/app/components/SelectDropdown.tsx +++ b/src/app/components/SelectDropdown.tsx @@ -1,5 +1,5 @@ import { ChevronDown } from 'lucide-react-native'; -import React, { useEffect } from 'react'; +import React from 'react'; import { ScrollView, Text, TouchableOpacity, View } from 'react-native'; import { useTheme } from '../../context/ThemeContext'; @@ -11,7 +11,6 @@ interface SelectDropdownProps { isOpen: boolean; onToggle: () => void; containerZIndex?: number; - dependsOn?: string; formData?: Record; } @@ -23,8 +22,6 @@ const SelectDropdown: React.FC = ({ isOpen, onToggle, containerZIndex, - dependsOn, - formData, }) => { const { theme } = useTheme(); @@ -55,31 +52,6 @@ const SelectDropdown: React.FC = ({ maxHeight: 250, }; - // 🔥 Compute disabled state - const isDisabled = React.useMemo(() => { - if (!dependsOn) return false; - - if (dependsOn.startsWith('eval:doc.')) { - const regex = /^eval:doc\.([a-zA-Z0-9_]+)\s*==\s*["'](.+)["']$/; - const match = dependsOn.match(regex); - - if (match && formData) { - const [_, fieldName, expectedValue] = match; - return formData[fieldName] !== expectedValue; - } - return true; - } - - return false; - }, [dependsOn, formData]); - - // 🔄 Reset value when it becomes disabled - useEffect(() => { - if (isDisabled && value !== "") { - onValueChange(""); - } - }, [isDisabled, value]); - return ( {/* Dropdown Toggle Button */} @@ -87,17 +59,14 @@ const SelectDropdown: React.FC = ({ className="h-[40px] w-full flex-row items-center justify-between rounded-md border px-3" style={{ borderColor: theme.border, - backgroundColor: isDisabled ? '#373737ff' : theme.background, - opacity: isDisabled ? 0.5 : 1, - }} - onPress={() => { - if (!isDisabled) onToggle(); // prevent opening when disabled + backgroundColor: theme.background, }} + onPress={onToggle} > {value || placeholder} @@ -132,9 +101,7 @@ const SelectDropdown: React.FC = ({ borderBottomColor: optIndex < options.length - 1 ? theme.border : undefined, }} - onPress={() => { - if (!isDisabled) onValueChange(trimmedOption); - }} + onPress={() => onValueChange(trimmedOption)} > = ({ navigation }) => { const isFieldEnabled = useCallback((field: RawField) => { if (!field.depends_on) return true; - if (field.depends_on.startsWith('eval:doc.')) { - const regex = /^eval:doc\.([a-zA-Z0-9_]+)\s*==\s*["'](.+)["']$/; - const match = field.depends_on.match(regex); - if (match) { - const [_, fieldName, expectedValue] = match; - return formData[fieldName] === expectedValue; + if (field.depends_on.startsWith('eval:')) { + try { + // Remove 'eval:' prefix + let expression = field.depends_on.substring(5).trim(); + + // Replace 'doc.' with actual formData values + // First, find all unique field references + const fieldMatches = expression.matchAll(/doc\.([a-zA-Z0-9_]+)/g); + const fieldReplacements: Record = {}; + + for (const match of fieldMatches) { + const fieldName = match[1]; + const fieldValue = formData[fieldName]; + // Store the value to replace later + if (!fieldReplacements[fieldName]) { + fieldReplacements[fieldName] = fieldValue || ''; + } + } + + // Now evaluate by splitting on OR conditions + const orConditions = expression.split('||').map(cond => cond.trim()); + + // Check if any OR condition is true + const result = orConditions.some(condition => { + // Split by AND operator (&&) + const andConditions = condition.split('&&').map(cond => cond.trim()); + + // All AND conditions must be true + return andConditions.every(andCond => { + // Match pattern: doc.fieldname == "value" + const regex = /doc\.([a-zA-Z0-9_]+)\s*==\s*["']([^"']*)["']/; + const match = andCond.match(regex); + + if (match) { + const [_, fieldName, expectedValue] = match; + const actualValue = formData[fieldName]; + const matches = actualValue === expectedValue; + + // Debug logging + console.log('Depends_on check:', { + fieldLabel: field.label, + condition: andCond, + fieldName, + expectedValue, + actualValue, + matches + }); + + return matches; + } + + console.log('Depends_on regex no match:', andCond); + return false; + }); + }); + + console.log('Final result for', field.label, ':', result); + return result; + } catch (error) { + console.error('Error evaluating depends_on:', field.depends_on, error); + return false; } - return false; } + return true; }, [formData]); @@ -385,6 +440,10 @@ const FormDetail: React.FC = ({ navigation }) => { {fields.map((field, index) => { + // Check if field should be visible based on depends_on + const isEnabled = isFieldEnabled(field); + if (!isEnabled) return null; + const isSelectField = field.fieldtype === 'Select' && field.options; const optionsList = @@ -416,7 +475,6 @@ const FormDetail: React.FC = ({ navigation }) => { {isSelectField ? ( From 4607b060d0a2d0585edef705b5139f1b38c7aac7 Mon Sep 17 00:00:00 2001 From: Nisch1Mv Date: Mon, 5 Jan 2026 17:13:58 +0530 Subject: [PATCH 04/23] added CurrencyInput component and integrate it into FormDetail for currency field handling --- src/app/components/fields/CurrencyInput.tsx | 73 +++++++++++++++++++++ src/app/screens/home/FormDetail.tsx | 22 +++++++ 2 files changed, 95 insertions(+) create mode 100644 src/app/components/fields/CurrencyInput.tsx diff --git a/src/app/components/fields/CurrencyInput.tsx b/src/app/components/fields/CurrencyInput.tsx new file mode 100644 index 0000000..e386417 --- /dev/null +++ b/src/app/components/fields/CurrencyInput.tsx @@ -0,0 +1,73 @@ +import React, { useState } from 'react'; +import { TextInput as RNTextInput, Text, TextInputProps, View } from 'react-native'; +import { useTheme } from '../../../context/ThemeContext'; + +interface CurrencyInputProps extends Omit { + className?: string; + value?: string; + onChangeText?: (text: string) => void; +} + +const formatCurrency = (value: string): string => { + // Remove all non-digit characters + const numericValue = value.replace(/[^0-9]/g, ''); + + if (!numericValue) return ''; + + // Add commas for Indian number system (lakhs and crores) + const number = parseInt(numericValue, 10); + return number.toLocaleString('en-IN'); +}; + +const CurrencyInput: React.FC = ({ + className = "h-[40px] w-full rotate-0 rounded-md border pb-2.5 pl-3 pr-3 pt-2.5 opacity-100", + value, + onChangeText, + ...props +}) => { + const { theme } = useTheme(); + const [displayValue, setDisplayValue] = useState(formatCurrency(value || '')); + + const handleTextChange = (text: string) => { + // Remove commas to get raw numeric value + const rawValue = text.replace(/,/g, ''); + + // Format for display + const formatted = formatCurrency(rawValue); + setDisplayValue(formatted); + + // Pass raw numeric value to parent + if (onChangeText) { + onChangeText(rawValue); + } + }; + + // Update display value when prop value changes + React.useEffect(() => { + setDisplayValue(formatCurrency(value || '')); + }, [value]); + + return ( + + + ₹ + + + + ); +}; + +export default CurrencyInput; diff --git a/src/app/screens/home/FormDetail.tsx b/src/app/screens/home/FormDetail.tsx index 7332a8d..f137c78 100644 --- a/src/app/screens/home/FormDetail.tsx +++ b/src/app/screens/home/FormDetail.tsx @@ -31,6 +31,7 @@ import LanguageControl from '../../components/LanguageControl'; import LinkDropdown from '../../components/LinkDropdown'; import SelectDropdown from '../../components/SelectDropdown'; import TableField from '../../components/TableField'; +import CurrencyInput from '../../components/fields/CurrencyInput'; import { enqueue } from '../../pendingQueue'; type FormDetailRouteProp = RouteProp; @@ -169,6 +170,12 @@ const FormDetail: React.FC = ({ navigation }) => { 'Link', 'Date', 'Table', + 'Check', + 'Phone', + 'Currency', + 'Heading', + 'Section Break', + ].includes(field.fieldtype); }); @@ -459,6 +466,11 @@ const FormDetail: React.FC = ({ navigation }) => { const selectedValue = formData[field.fieldname]; const isNumericField = field.fieldtype === 'Int' || field.fieldtype === 'Float'; + const isCurrencyField = field.fieldtype === 'Currency'; + const isPhoneField = field.fieldtype === 'Phone'; + const isCheckField = field.fieldtype === 'Check'; + const isHeading = field.fieldtype === 'Heading'; + const isSectionBreak = field.fieldtype === 'Section Break'; return ( = ({ navigation }) => { } }} /> + ) : isCurrencyField ? ( + + handleChange(field.fieldname, text) + } + /> ) : ( Date: Mon, 5 Jan 2026 17:40:45 +0530 Subject: [PATCH 05/23] added PhoneInput component and integrated it into FormDetail for phone number handling --- src/app/components/fields/PhoneInput.tsx | 259 +++++++++++++++++++++++ src/app/screens/home/FormDetail.tsx | 11 + 2 files changed, 270 insertions(+) create mode 100644 src/app/components/fields/PhoneInput.tsx diff --git a/src/app/components/fields/PhoneInput.tsx b/src/app/components/fields/PhoneInput.tsx new file mode 100644 index 0000000..6899af0 --- /dev/null +++ b/src/app/components/fields/PhoneInput.tsx @@ -0,0 +1,259 @@ +import { ChevronDown } from 'lucide-react-native'; +import React, { useEffect, useState } from 'react'; +import { + Keyboard, + Modal, + Platform, + Pressable, + TextInput as RNTextInput, + ScrollView, + Text, + TextInputProps, + TouchableOpacity, + View +} from 'react-native'; +import { useTheme } from '../../../context/ThemeContext'; + +interface PhoneInputProps extends Omit { + className?: string; + value?: string; + onChangeText?: (text: string) => void; +} + +interface CountryCode { + code: string; + dial: string; + flag: string; + name: string; +} + +const countryCodes: CountryCode[] = [ + { code: 'IN', dial: '+91', flag: '🇮🇳', name: 'India' }, + { code: 'US', dial: '+1', flag: '🇺🇸', name: 'United States' }, + { code: 'GB', dial: '+44', flag: '🇬🇧', name: 'United Kingdom' }, + { code: 'AU', dial: '+61', flag: '🇦🇺', name: 'Australia' }, + { code: 'CA', dial: '+1', flag: '🇨🇦', name: 'Canada' }, + { code: 'AE', dial: '+971', flag: '🇦🇪', name: 'United Arab Emirates' }, + { code: 'SA', dial: '+966', flag: '🇸🇦', name: 'Saudi Arabia' }, + { code: 'SG', dial: '+65', flag: '🇸🇬', name: 'Singapore' }, + { code: 'MY', dial: '+60', flag: '🇲🇾', name: 'Malaysia' }, + { code: 'NZ', dial: '+64', flag: '🇳🇿', name: 'New Zealand' }, + { code: 'DE', dial: '+49', flag: '🇩🇪', name: 'Germany' }, + { code: 'FR', dial: '+33', flag: '🇫🇷', name: 'France' }, + { code: 'IT', dial: '+39', flag: '🇮🇹', name: 'Italy' }, + { code: 'ES', dial: '+34', flag: '🇪🇸', name: 'Spain' }, + { code: 'JP', dial: '+81', flag: '🇯🇵', name: 'Japan' }, + { code: 'CN', dial: '+86', flag: '🇨🇳', name: 'China' }, + { code: 'KR', dial: '+82', flag: '🇰🇷', name: 'South Korea' }, + { code: 'BR', dial: '+55', flag: '🇧🇷', name: 'Brazil' }, + { code: 'MX', dial: '+52', flag: '🇲🇽', name: 'Mexico' }, + { code: 'ZA', dial: '+27', flag: '🇿🇦', name: 'South Africa' }, +]; + +const PhoneInput: React.FC = ({ + className = "h-[40px] w-full rotate-0 rounded-md border pb-2.5 pl-3 pr-3 pt-2.5 opacity-100", + value, + onChangeText, + ...props +}) => { + const { theme } = useTheme(); + const [selectedCountry, setSelectedCountry] = useState(countryCodes[0]); + const [modalVisible, setModalVisible] = useState(false); + const [phoneNumber, setPhoneNumber] = useState(''); + const [searchQuery, setSearchQuery] = useState(''); + const [keyboardHeight, setKeyboardHeight] = useState(0); + + // Track keyboard height + useEffect(() => { + const keyboardDidShowListener = Keyboard.addListener( + Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow', + (e) => { + setKeyboardHeight(e.endCoordinates.height); + } + ); + const keyboardDidHideListener = Keyboard.addListener( + Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide', + () => { + setKeyboardHeight(0); + } + ); + + return () => { + keyboardDidShowListener.remove(); + keyboardDidHideListener.remove(); + }; + }, []); + + // Parse initial value if it exists + React.useEffect(() => { + if (value) { + // Try to extract country code from value + const country = countryCodes.find(c => value.startsWith(c.dial)); + if (country) { + setSelectedCountry(country); + setPhoneNumber(value.substring(country.dial.length).trim()); + } else { + setPhoneNumber(value); + } + } + }, [value]); + + const handlePhoneChange = (text: string) => { + // Remove non-numeric characters + const numeric = text.replace(/[^0-9]/g, ''); + setPhoneNumber(numeric); + + // Combine country code and phone number + const fullNumber = `${selectedCountry.dial} ${numeric}`; + if (onChangeText) { + onChangeText(fullNumber); + } + }; + const handleCountrySelect = (country: CountryCode) => { + setSelectedCountry(country); + setModalVisible(false); + setSearchQuery(''); + + // Update full number with new country code + if (phoneNumber) { + const fullNumber = `${country.dial} ${phoneNumber}`; + if (onChangeText) { + onChangeText(fullNumber); + } + } + }; + + const filteredCountries = countryCodes.filter( + country => + country.code.toLowerCase().includes(searchQuery.toLowerCase()) || + country.name.toLowerCase().includes(searchQuery.toLowerCase()) || + country.dial.includes(searchQuery) + ); + + return ( + + + {/* Country Code Selector */} + setModalVisible(true)} + className="flex-row items-center justify-center gap-0.5 rounded-l-md border px-2" + style={{ + height: 40, + borderColor: theme.border, + backgroundColor: theme.background, + }} + > + {selectedCountry.flag} + + {selectedCountry.dial} + + + + + {/* Phone Number Input */} + + + + {/* Country Code Modal */} + setModalVisible(false)} + > + { + Keyboard.dismiss(); + setModalVisible(false); + }} + > + 0 ? `${100 - (keyboardHeight / 8)}%` : '70%', + paddingBottom: keyboardHeight > 0 ? keyboardHeight : 0 + }} + > + + + Select Country Code + + {/* Search Bar */} + + + + {filteredCountries.length > 0 ? ( + filteredCountries.map((country) => ( + handleCountrySelect(country)} + > + {country.flag} + + + {country.name} + + + {country.code} + + + + {country.dial} + + + )) + ) : ( + + + No countries found + + + )} + + + + + + ); +}; + +export default PhoneInput; diff --git a/src/app/screens/home/FormDetail.tsx b/src/app/screens/home/FormDetail.tsx index f137c78..942f4b2 100644 --- a/src/app/screens/home/FormDetail.tsx +++ b/src/app/screens/home/FormDetail.tsx @@ -32,6 +32,7 @@ import LinkDropdown from '../../components/LinkDropdown'; import SelectDropdown from '../../components/SelectDropdown'; import TableField from '../../components/TableField'; import CurrencyInput from '../../components/fields/CurrencyInput'; +import PhoneInput from '../../components/fields/PhoneInput'; import { enqueue } from '../../pendingQueue'; type FormDetailRouteProp = RouteProp; @@ -574,6 +575,16 @@ const FormDetail: React.FC = ({ navigation }) => { handleChange(field.fieldname, text) } /> + ) : isPhoneField ? ( + + handleChange(field.fieldname, text) + } + /> ) : ( Date: Tue, 6 Jan 2026 11:09:38 +0530 Subject: [PATCH 06/23] added CheckboxInput component and integrated it into FormDetail for checkbox handling --- src/app/components/fields/CheckboxInput.tsx | 51 +++++++++++++++++++++ src/app/screens/home/FormDetail.tsx | 35 ++++++++------ 2 files changed, 73 insertions(+), 13 deletions(-) create mode 100644 src/app/components/fields/CheckboxInput.tsx diff --git a/src/app/components/fields/CheckboxInput.tsx b/src/app/components/fields/CheckboxInput.tsx new file mode 100644 index 0000000..10d0cf0 --- /dev/null +++ b/src/app/components/fields/CheckboxInput.tsx @@ -0,0 +1,51 @@ +import { Check } from 'lucide-react-native'; +import React from 'react'; +import { Text, TouchableOpacity, View } from 'react-native'; +import { useTheme } from '../../../context/ThemeContext'; + +interface CheckboxInputProps { + value?: number | boolean; + onValueChange?: (value: number) => void; + label?: string; +} + +const CheckboxInput: React.FC = ({ + value, + onValueChange, + label, +}) => { + const { theme } = useTheme(); + const isChecked = value === 1 || value === true; + + const handleToggle = () => { + if (onValueChange) { + onValueChange(isChecked ? 0 : 1); + } + }; + + return ( + + + {isChecked && } + + {label && ( + + {label} + + )} + + ); +}; + +export default CheckboxInput; diff --git a/src/app/screens/home/FormDetail.tsx b/src/app/screens/home/FormDetail.tsx index 942f4b2..29cabfe 100644 --- a/src/app/screens/home/FormDetail.tsx +++ b/src/app/screens/home/FormDetail.tsx @@ -31,6 +31,7 @@ import LanguageControl from '../../components/LanguageControl'; import LinkDropdown from '../../components/LinkDropdown'; import SelectDropdown from '../../components/SelectDropdown'; import TableField from '../../components/TableField'; +import CheckboxInput from '../../components/fields/CheckboxInput'; import CurrencyInput from '../../components/fields/CurrencyInput'; import PhoneInput from '../../components/fields/PhoneInput'; import { enqueue } from '../../pendingQueue'; @@ -69,12 +70,12 @@ const FormDetail: React.FC = ({ navigation }) => { try { // Remove 'eval:' prefix let expression = field.depends_on.substring(5).trim(); - + // Replace 'doc.' with actual formData values // First, find all unique field references const fieldMatches = expression.matchAll(/doc\.([a-zA-Z0-9_]+)/g); const fieldReplacements: Record = {}; - + for (const match of fieldMatches) { const fieldName = match[1]; const fieldValue = formData[fieldName]; @@ -83,26 +84,26 @@ const FormDetail: React.FC = ({ navigation }) => { fieldReplacements[fieldName] = fieldValue || ''; } } - + // Now evaluate by splitting on OR conditions const orConditions = expression.split('||').map(cond => cond.trim()); - + // Check if any OR condition is true const result = orConditions.some(condition => { // Split by AND operator (&&) const andConditions = condition.split('&&').map(cond => cond.trim()); - + // All AND conditions must be true return andConditions.every(andCond => { // Match pattern: doc.fieldname == "value" const regex = /doc\.([a-zA-Z0-9_]+)\s*==\s*["']([^"']*)["']/; const match = andCond.match(regex); - + if (match) { const [_, fieldName, expectedValue] = match; const actualValue = formData[fieldName]; const matches = actualValue === expectedValue; - + // Debug logging console.log('Depends_on check:', { fieldLabel: field.label, @@ -112,15 +113,15 @@ const FormDetail: React.FC = ({ navigation }) => { actualValue, matches }); - + return matches; } - + console.log('Depends_on regex no match:', andCond); return false; }); }); - + console.log('Final result for', field.label, ':', result); return result; } catch (error) { @@ -128,7 +129,7 @@ const FormDetail: React.FC = ({ navigation }) => { return false; } } - + return true; }, [formData]); @@ -212,7 +213,7 @@ const FormDetail: React.FC = ({ navigation }) => { const isEnabled = isFieldEnabled(field); const isEmpty = !formData[field.fieldname] || formData[field.fieldname].toString().trim() === ''; - + // Only report as missing if the field is enabled AND empty return isEnabled && isEmpty; }); @@ -253,7 +254,7 @@ const FormDetail: React.FC = ({ navigation }) => { data: formData, schemaHash, status: 'pending' as 'pending' | 'submitted' | 'failed', - is_submittable: doctype.data.is_submittable + is_submittable: doctype.data.is_submittable }; setLoading(true); setConfirmModalVisible(false); @@ -585,6 +586,14 @@ const FormDetail: React.FC = ({ navigation }) => { handleChange(field.fieldname, text) } /> + ) : isCheckField ? ( + + handleChange(field.fieldname, value) + } + label={field.label || t('formDetail.checkboxLabel')} + /> ) : ( Date: Tue, 6 Jan 2026 11:17:47 +0530 Subject: [PATCH 07/23] added HeadingText component and integrated it into FormDetail for improved heading display --- src/app/components/fields/HeadingText.tsx | 22 ++++++++++++++++++++++ src/app/screens/home/FormDetail.tsx | 19 ++++++++++++------- 2 files changed, 34 insertions(+), 7 deletions(-) create mode 100644 src/app/components/fields/HeadingText.tsx diff --git a/src/app/components/fields/HeadingText.tsx b/src/app/components/fields/HeadingText.tsx new file mode 100644 index 0000000..cf912a5 --- /dev/null +++ b/src/app/components/fields/HeadingText.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import { Text } from 'react-native'; +import { useTheme } from '../../../context/ThemeContext'; + +interface HeadingTextProps { + label: string; +} + +const HeadingText: React.FC = ({ label }) => { + const { theme } = useTheme(); + + return ( + + {label} + + ); +}; + +export default HeadingText; diff --git a/src/app/screens/home/FormDetail.tsx b/src/app/screens/home/FormDetail.tsx index 29cabfe..b9946bb 100644 --- a/src/app/screens/home/FormDetail.tsx +++ b/src/app/screens/home/FormDetail.tsx @@ -33,6 +33,7 @@ import SelectDropdown from '../../components/SelectDropdown'; import TableField from '../../components/TableField'; import CheckboxInput from '../../components/fields/CheckboxInput'; import CurrencyInput from '../../components/fields/CurrencyInput'; +import HeadingText from '../../components/fields/HeadingText'; import PhoneInput from '../../components/fields/PhoneInput'; import { enqueue } from '../../pendingQueue'; @@ -480,13 +481,17 @@ const FormDetail: React.FC = ({ navigation }) => { className="mb-4" style={{ zIndex: 1000 - index }} > - - {field.label || field.fieldname} - - {isSelectField ? ( + {!isHeading && ( + + {field.label || field.fieldname} + + )} + {isHeading ? ( + + ) : isSelectField ? ( Date: Tue, 6 Jan 2026 11:32:08 +0530 Subject: [PATCH 08/23] added SectionBreak component and integrated it into FormDetail , changes are needed to be done to handle a few cases --- src/app/components/fields/SectionBreak.tsx | 30 ++++++++++++++++++++++ src/app/screens/home/FormDetail.tsx | 7 +++-- 2 files changed, 35 insertions(+), 2 deletions(-) create mode 100644 src/app/components/fields/SectionBreak.tsx diff --git a/src/app/components/fields/SectionBreak.tsx b/src/app/components/fields/SectionBreak.tsx new file mode 100644 index 0000000..8f0cb39 --- /dev/null +++ b/src/app/components/fields/SectionBreak.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import { Text, View } from 'react-native'; +import { useTheme } from '../../../context/ThemeContext'; + +interface SectionBreakProps { + label?: string; +} + +const SectionBreak: React.FC = ({ label }) => { + const { theme } = useTheme(); + + return ( + + + {label && ( + + {label} + + )} + + ); +}; + +export default SectionBreak; diff --git a/src/app/screens/home/FormDetail.tsx b/src/app/screens/home/FormDetail.tsx index b9946bb..44879e8 100644 --- a/src/app/screens/home/FormDetail.tsx +++ b/src/app/screens/home/FormDetail.tsx @@ -35,6 +35,7 @@ import CheckboxInput from '../../components/fields/CheckboxInput'; import CurrencyInput from '../../components/fields/CurrencyInput'; import HeadingText from '../../components/fields/HeadingText'; import PhoneInput from '../../components/fields/PhoneInput'; +import SectionBreak from '../../components/fields/SectionBreak'; import { enqueue } from '../../pendingQueue'; type FormDetailRouteProp = RouteProp; @@ -481,7 +482,7 @@ const FormDetail: React.FC = ({ navigation }) => { className="mb-4" style={{ zIndex: 1000 - index }} > - {!isHeading && ( + {!isHeading && !isSectionBreak && ( = ({ navigation }) => { {field.label || field.fieldname} )} - {isHeading ? ( + {isSectionBreak ? ( + + ) : isHeading ? ( ) : isSelectField ? ( Date: Tue, 6 Jan 2026 11:42:44 +0530 Subject: [PATCH 09/23] only field.label will be passsed into label --- src/app/screens/home/FormDetail.tsx | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/app/screens/home/FormDetail.tsx b/src/app/screens/home/FormDetail.tsx index 44879e8..77cd2da 100644 --- a/src/app/screens/home/FormDetail.tsx +++ b/src/app/screens/home/FormDetail.tsx @@ -487,13 +487,13 @@ const FormDetail: React.FC = ({ navigation }) => { className="font-sans text-sm font-medium leading-5 tracking-normal" style={{ color: theme.text }} > - {field.label || field.fieldname} + {field.label} )} {isSectionBreak ? ( - + ) : isHeading ? ( - + ) : isSelectField ? ( = ({ navigation }) => { handleChange(field.fieldname, value) } placeholder={t('formDetail.selectPlaceholder', { - label: field.label || field.fieldname, + label: field.label, })} isOpen={isOpen} onToggle={() => toggleDropdown(field.fieldname)} @@ -517,7 +517,7 @@ const FormDetail: React.FC = ({ navigation }) => { handleChange(field.fieldname, value) } placeholder={t('formDetail.selectPlaceholder', { - label: field.label || field.fieldname, + label: field.label, })} isOpen={isOpen} onToggle={() => toggleDropdown(field.fieldname)} @@ -530,7 +530,7 @@ const FormDetail: React.FC = ({ navigation }) => { handleChange(field.fieldname, value) } placeholder={t('formDetail.selectPlaceholder', { - label: field.label || field.fieldname, + label: field.label, })} /> ) : isTableField ? ( @@ -540,14 +540,14 @@ const FormDetail: React.FC = ({ navigation }) => { (navigation as any).navigate('TableRowEditor', { fieldname: field.fieldname, tableDoctype: (field.options as string) || '', - title: field.label || field.fieldname, + title: field.label, }) } onEditRow={rowIndex => (navigation as any).navigate('TableRowEditor', { fieldname: field.fieldname, tableDoctype: (field.options as string) || '', - title: field.label || field.fieldname, + title: field.label, index: rowIndex, initialRow: Array.isArray(selectedValue) && @@ -577,7 +577,7 @@ const FormDetail: React.FC = ({ navigation }) => { ) : isCurrencyField ? ( @@ -587,7 +587,7 @@ const FormDetail: React.FC = ({ navigation }) => { ) : isPhoneField ? ( @@ -611,7 +611,7 @@ const FormDetail: React.FC = ({ navigation }) => { color: theme.text, }} placeholder={t('formDetail.enterPlaceholder', { - label: field.label || field.fieldname, + label: field.label, })} placeholderTextColor={theme.subtext} value={formData[field.fieldname] || ''} From 267129e8233b0192356ef5facdda5f680977da0f Mon Sep 17 00:00:00 2001 From: Nisch1Mv Date: Tue, 6 Jan 2026 11:52:35 +0530 Subject: [PATCH 10/23] refactor SectionBreak component to simplify label rendering logic --- src/app/components/fields/SectionBreak.tsx | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/app/components/fields/SectionBreak.tsx b/src/app/components/fields/SectionBreak.tsx index 8f0cb39..9abd658 100644 --- a/src/app/components/fields/SectionBreak.tsx +++ b/src/app/components/fields/SectionBreak.tsx @@ -9,20 +9,22 @@ interface SectionBreakProps { const SectionBreak: React.FC = ({ label }) => { const { theme } = useTheme(); + if (!label || label.trim() === '') { + return null; + } + return ( - {label && ( - - {label} - - )} + + {label} + ); }; From a13135b828242962539320e425bee935a9f72ba1 Mon Sep 17 00:00:00 2001 From: Nisch1Mv Date: Tue, 6 Jan 2026 12:03:06 +0530 Subject: [PATCH 11/23] the label for checkboxes is not needed , as it included in the componet itself --- src/app/screens/home/FormDetail.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/screens/home/FormDetail.tsx b/src/app/screens/home/FormDetail.tsx index 77cd2da..9ceb4a2 100644 --- a/src/app/screens/home/FormDetail.tsx +++ b/src/app/screens/home/FormDetail.tsx @@ -482,7 +482,7 @@ const FormDetail: React.FC = ({ navigation }) => { className="mb-4" style={{ zIndex: 1000 - index }} > - {!isHeading && !isSectionBreak && ( + {!isHeading && !isSectionBreak && !isCheckField && ( Date: Tue, 6 Jan 2026 12:11:43 +0530 Subject: [PATCH 12/23] refactor CheckboxInput and SectionBreak components for improved styling and rendering --- src/app/components/fields/CheckboxInput.tsx | 2 +- src/app/components/fields/SectionBreak.tsx | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/app/components/fields/CheckboxInput.tsx b/src/app/components/fields/CheckboxInput.tsx index 10d0cf0..7d8886c 100644 --- a/src/app/components/fields/CheckboxInput.tsx +++ b/src/app/components/fields/CheckboxInput.tsx @@ -40,7 +40,7 @@ const CheckboxInput: React.FC = ({ {isChecked && } {label && ( - + {label} )} diff --git a/src/app/components/fields/SectionBreak.tsx b/src/app/components/fields/SectionBreak.tsx index 9abd658..baef566 100644 --- a/src/app/components/fields/SectionBreak.tsx +++ b/src/app/components/fields/SectionBreak.tsx @@ -10,7 +10,14 @@ const SectionBreak: React.FC = ({ label }) => { const { theme } = useTheme(); if (!label || label.trim() === '') { - return null; + return ( + + + + ); } return ( From d78eae716cf5384c77cf39a038729661dee0aafe Mon Sep 17 00:00:00 2001 From: Nisch1Mv Date: Tue, 6 Jan 2026 12:15:17 +0530 Subject: [PATCH 13/23] added a calender icon for data picker --- src/app/components/DatePicker.tsx | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/app/components/DatePicker.tsx b/src/app/components/DatePicker.tsx index 429e5b7..4c9fd7c 100644 --- a/src/app/components/DatePicker.tsx +++ b/src/app/components/DatePicker.tsx @@ -1,18 +1,19 @@ +import DateTimePicker, { + DateTimePickerAndroid, + type DateTimePickerEvent, +} from '@react-native-community/datetimepicker'; +import { Calendar } from 'lucide-react-native'; import React, { useState } from 'react'; +import { useTranslation } from 'react-i18next'; import { - View, - Text, - TouchableOpacity, Modal, Platform, StyleSheet, + Text, + TouchableOpacity, + View, } from 'react-native'; -import DateTimePicker, { - DateTimePickerAndroid, - type DateTimePickerEvent, -} from '@react-native-community/datetimepicker'; import { useTheme } from '../../context/ThemeContext'; -import { useTranslation } from 'react-i18next'; interface DatePickerProps { value?: string; @@ -129,6 +130,7 @@ const DatePicker: React.FC = ({ > {value ? formatDisplayDate(value) : placeholder} + {Platform.OS === 'ios' && datePickerVisible && ( From a656e5142983a0a7cd188fb70ea1f9f0bcf7306b Mon Sep 17 00:00:00 2001 From: Nisch1Mv Date: Tue, 6 Jan 2026 12:33:47 +0530 Subject: [PATCH 14/23] enhance input handling for numeric fields to restrict input types and format on blur --- src/app/screens/home/FormDetail.tsx | 39 ++++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/src/app/screens/home/FormDetail.tsx b/src/app/screens/home/FormDetail.tsx index 9ceb4a2..b76fbc1 100644 --- a/src/app/screens/home/FormDetail.tsx +++ b/src/app/screens/home/FormDetail.tsx @@ -616,9 +616,42 @@ const FormDetail: React.FC = ({ navigation }) => { placeholderTextColor={theme.subtext} value={formData[field.fieldname] || ''} keyboardType={isNumericField ? 'numeric' : 'default'} - onChangeText={text => - handleChange(field.fieldname, text) - } + onChangeText={text => { + if (field.fieldtype === 'Int') { + // Only allow integers (no decimals or other characters) + const integerOnly = text.replace(/[^0-9-]/g, ''); + // Ensure minus sign only appears at the start + const validInteger = integerOnly.replace(/(?!^)-/g, ''); + handleChange(field.fieldname, validInteger); + } else if (field.fieldtype === 'Float') { + // Allow floats with up to 3 decimal places + // Remove non-numeric characters except decimal point and minus + let cleaned = text.replace(/[^0-9.-]/g, ''); + // Ensure minus sign only at start + cleaned = cleaned.replace(/(?!^)-/g, ''); + // Ensure only one decimal point + const parts = cleaned.split('.'); + if (parts.length > 2) { + cleaned = parts[0] + '.' + parts.slice(1).join(''); + } + // Limit to 3 decimal places + if (parts.length === 2 && parts[1].length > 3) { + cleaned = parts[0] + '.' + parts[1].substring(0, 3); + } + handleChange(field.fieldname, cleaned); + } else { + handleChange(field.fieldname, text); + } + }} + onBlur={() => { + if (field.fieldtype === 'Float' && formData[field.fieldname]) { + const value = formData[field.fieldname]; + const numValue = parseFloat(value); + if (!isNaN(numValue)) { + handleChange(field.fieldname, numValue.toFixed(3)); + } + } + }} /> )} From 64dc2d9b0e8af6f2be6ca29205db91066dee2861 Mon Sep 17 00:00:00 2001 From: Nisch1Mv Date: Tue, 6 Jan 2026 12:36:16 +0530 Subject: [PATCH 15/23] add validation functions for integer and float inputs, and format float to fixed decimal places --- src/app/screens/home/FormDetail.tsx | 36 ++++++++--------------- src/utils/fieldValidation.ts | 44 +++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 25 deletions(-) create mode 100644 src/utils/fieldValidation.ts diff --git a/src/app/screens/home/FormDetail.tsx b/src/app/screens/home/FormDetail.tsx index b76fbc1..dc2d44b 100644 --- a/src/app/screens/home/FormDetail.tsx +++ b/src/app/screens/home/FormDetail.tsx @@ -26,6 +26,11 @@ import { useNetwork } from '../../../context/NetworkProvider'; import { useTheme } from '../../../context/ThemeContext'; import generateSchemaHash from '../../../helper/hashFunction'; import { RawField } from '../../../types'; +import { + formatFloatToFixed, + validateFloatInput, + validateIntegerInput, +} from '../../../utils/fieldValidation'; import DatePicker from '../../components/DatePicker'; import LanguageControl from '../../components/LanguageControl'; import LinkDropdown from '../../components/LinkDropdown'; @@ -618,38 +623,19 @@ const FormDetail: React.FC = ({ navigation }) => { keyboardType={isNumericField ? 'numeric' : 'default'} onChangeText={text => { if (field.fieldtype === 'Int') { - // Only allow integers (no decimals or other characters) - const integerOnly = text.replace(/[^0-9-]/g, ''); - // Ensure minus sign only appears at the start - const validInteger = integerOnly.replace(/(?!^)-/g, ''); - handleChange(field.fieldname, validInteger); + handleChange(field.fieldname, validateIntegerInput(text)); } else if (field.fieldtype === 'Float') { - // Allow floats with up to 3 decimal places - // Remove non-numeric characters except decimal point and minus - let cleaned = text.replace(/[^0-9.-]/g, ''); - // Ensure minus sign only at start - cleaned = cleaned.replace(/(?!^)-/g, ''); - // Ensure only one decimal point - const parts = cleaned.split('.'); - if (parts.length > 2) { - cleaned = parts[0] + '.' + parts.slice(1).join(''); - } - // Limit to 3 decimal places - if (parts.length === 2 && parts[1].length > 3) { - cleaned = parts[0] + '.' + parts[1].substring(0, 3); - } - handleChange(field.fieldname, cleaned); + handleChange(field.fieldname, validateFloatInput(text)); } else { handleChange(field.fieldname, text); } }} onBlur={() => { if (field.fieldtype === 'Float' && formData[field.fieldname]) { - const value = formData[field.fieldname]; - const numValue = parseFloat(value); - if (!isNaN(numValue)) { - handleChange(field.fieldname, numValue.toFixed(3)); - } + handleChange( + field.fieldname, + formatFloatToFixed(formData[field.fieldname]) + ); } }} /> diff --git a/src/utils/fieldValidation.ts b/src/utils/fieldValidation.ts new file mode 100644 index 0000000..6f5d80b --- /dev/null +++ b/src/utils/fieldValidation.ts @@ -0,0 +1,44 @@ +/** + * Validates and formats integer input + * Removes decimals and non-numeric characters, allows negative numbers + */ +export const validateIntegerInput = (text: string): string => { + // Only allow integers (no decimals or other characters) + const integerOnly = text.replace(/[^0-9-]/g, ''); + // Ensure minus sign only appears at the start + const validInteger = integerOnly.replace(/(?!^)-/g, ''); + return validInteger; +}; + +/** + * Validates and formats float input during typing + * Allows up to 3 decimal places, handles negative numbers + */ +export const validateFloatInput = (text: string): string => { + // Remove non-numeric characters except decimal point and minus + let cleaned = text.replace(/[^0-9.-]/g, ''); + // Ensure minus sign only at start + cleaned = cleaned.replace(/(?!^)-/g, ''); + // Ensure only one decimal point + const parts = cleaned.split('.'); + if (parts.length > 2) { + cleaned = parts[0] + '.' + parts.slice(1).join(''); + } + // Limit to 3 decimal places + if (parts.length === 2 && parts[1].length > 3) { + cleaned = parts[0] + '.' + parts[1].substring(0, 3); + } + return cleaned; +}; + +/** + * Formats float value to exactly 3 decimal places + * Called when user finishes editing (onBlur) + */ +export const formatFloatToFixed = (value: string): string => { + const numValue = parseFloat(value); + if (!isNaN(numValue)) { + return numValue.toFixed(3); + } + return value; +}; From 637f08f43a6c6b3661109b1fac5ca278ab0d39c1 Mon Sep 17 00:00:00 2001 From: Nisch1Mv Date: Tue, 6 Jan 2026 13:41:27 +0530 Subject: [PATCH 16/23] add required field handling to form validation and display --- src/api.ts | 1 + src/app/screens/home/FormDetail.tsx | 23 ++++++++++++++--------- src/types.ts | 1 + 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/api.ts b/src/api.ts index a5970b1..8d744f9 100644 --- a/src/api.ts +++ b/src/api.ts @@ -638,6 +638,7 @@ export function extractFields(docType: DocType): RawField[] { print_hide: field.print_hide, report_hide: field.report_hide, depends_on: field.depends_on, + reqd: field.reqd, })); } diff --git a/src/app/screens/home/FormDetail.tsx b/src/app/screens/home/FormDetail.tsx index dc2d44b..16c058a 100644 --- a/src/app/screens/home/FormDetail.tsx +++ b/src/app/screens/home/FormDetail.tsx @@ -215,14 +215,21 @@ const FormDetail: React.FC = ({ navigation }) => { return; } - // Only check fields that are enabled (not disabled by depends_on) + // Only check fields that are enabled (not disabled by depends_on) AND required const missingFields = fields.filter(field => { + // Skip display-only fields (Section Break, Heading) + const isDisplayOnly = field.fieldtype === 'Section Break' || field.fieldtype === 'Heading'; + if (isDisplayOnly) { + return false; + } + const isEnabled = isFieldEnabled(field); + const isRequired = field.reqd === 1; const isEmpty = !formData[field.fieldname] || formData[field.fieldname].toString().trim() === ''; - // Only report as missing if the field is enabled AND empty - return isEnabled && isEmpty; + // Only report as missing if the field is enabled AND required AND empty + return isEnabled && isRequired && isEmpty; }); if (missingFields.length > 0) { @@ -236,10 +243,6 @@ const FormDetail: React.FC = ({ navigation }) => { return; } - if (Object.keys(formData).length === 0) { - Alert.alert(t('common.error'), t('formDetail.noData')); - return; - } setConfirmModalVisible(true); }; @@ -480,6 +483,7 @@ const FormDetail: React.FC = ({ navigation }) => { const isCheckField = field.fieldtype === 'Check'; const isHeading = field.fieldtype === 'Heading'; const isSectionBreak = field.fieldtype === 'Section Break'; + const isRequired = field.reqd === 1; return ( = ({ navigation }) => { style={{ color: theme.text }} > {field.label} + {isRequired && *} )} {isSectionBreak ? ( - + ) : isHeading ? ( - + ) : isSelectField ? ( Date: Tue, 6 Jan 2026 14:13:25 +0530 Subject: [PATCH 17/23] refactor Forms component layout for improved styling and spacing --- src/app/screens/files/Forms.tsx | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/app/screens/files/Forms.tsx b/src/app/screens/files/Forms.tsx index 0b722d3..9bfc021 100644 --- a/src/app/screens/files/Forms.tsx +++ b/src/app/screens/files/Forms.tsx @@ -753,13 +753,14 @@ function Forms() { return ( { console.log( 'Navigating to PreviewForm with formId:', @@ -784,16 +785,19 @@ function Forms() { handleSubmitSingleForm(formData)} > - {t('formsScreen.submitForm')} - ); From 43e1bd05570bb56343e64edc12fc1ab142b9cfa0 Mon Sep 17 00:00:00 2001 From: Nisch1Mv Date: Wed, 7 Jan 2026 11:23:13 +0530 Subject: [PATCH 18/23] refactor hash function for improved readability and structure --- src/helper/hashFunction.ts | 72 +++++++++++++++++++++++++++++++------- 1 file changed, 60 insertions(+), 12 deletions(-) diff --git a/src/helper/hashFunction.ts b/src/helper/hashFunction.ts index bf37a94..35e2f20 100644 --- a/src/helper/hashFunction.ts +++ b/src/helper/hashFunction.ts @@ -1,22 +1,70 @@ -import SHA256 from 'crypto-js/sha256'; -import encHex from 'crypto-js/enc-hex'; import { RawField } from '@/types'; +import encHex from 'crypto-js/enc-hex'; +import SHA256 from 'crypto-js/sha256'; + +const LAYOUT_FIELD_TYPES = new Set([ + 'Section Break', + 'Column Break', + 'HTML', +]); + +function normalizeFieldname( + name: string, + allNames: string[] +): string { + const base = name.replace(/\d+$/, ''); + if (base !== name && allNames.filter(n => n === base).length === 1) { + return base; + } + return name; +} + +function normalizeOptions( + fieldtype: string, + options: unknown +): string { + if (!options) return ''; -function generateSchemaHash(fields: RawField[]): string { - const simplifiedFields = fields.map(f => ({ - fieldname: f.fieldname, - fieldtype: f.fieldtype, - options: f.options || '', - })); + const str = String(options).trim(); - const sorted = simplifiedFields.sort((a, b) => - a.fieldname.localeCompare(b.fieldname) + if (fieldtype === 'Select') { + return str + .split('\n') + .map(s => s.trim()) + .filter(Boolean) + .join('\n'); + } + + return str; +} + +function generateSchemaHash( + fields: RawField[] +):string { + + const allNames = fields.map(f => f.fieldname || ''); + + const simplified = fields + .filter(f => !LAYOUT_FIELD_TYPES.has(f.fieldtype)) + .map(f => ({ + fieldname: normalizeFieldname(f.fieldname, allNames), + fieldtype: f.fieldtype, + options: normalizeOptions(f.fieldtype, f.options), + })); + + simplified.sort((a, b) => + a.fieldname.localeCompare(b.fieldname) || + a.fieldtype.localeCompare(b.fieldtype) || + a.options.localeCompare(b.options) ); - const concatStr = sorted + const concatStr = simplified .map(f => `${f.fieldname}:${f.fieldtype}:${f.options}`) .join('|'); - return SHA256(concatStr).toString(encHex); + const hash = SHA256(concatStr).toString(encHex); + + return hash; } + export default generateSchemaHash; From 9bc82bc609ea4676e04c26a8239019db9a22c383 Mon Sep 17 00:00:00 2001 From: Nisch1Mv Date: Thu, 8 Jan 2026 16:36:03 +0530 Subject: [PATCH 19/23] refactor dropdown Pushes content down when open --- src/app/components/LinkDropdown.tsx | 55 +++++++++++---------------- src/app/components/SelectDropdown.tsx | 37 ++++++++---------- 2 files changed, 39 insertions(+), 53 deletions(-) diff --git a/src/app/components/LinkDropdown.tsx b/src/app/components/LinkDropdown.tsx index 535f1dd..80645b5 100644 --- a/src/app/components/LinkDropdown.tsx +++ b/src/app/components/LinkDropdown.tsx @@ -1,17 +1,17 @@ +import { ChevronDown } from 'lucide-react-native'; import React, { useEffect, useMemo, useRef, useState } from 'react'; import { - View, - Text, - TouchableOpacity, - ScrollView, ActivityIndicator, + ScrollView, + Text, TextInput, + TouchableOpacity, + View, } from 'react-native'; -import { ChevronDown } from 'lucide-react-native'; -import { useTheme } from '../../context/ThemeContext'; +import { getLinkOptionsFromLocal, saveLinkOptionsToLocal } from '../../api'; import { useNetwork } from '../../context/NetworkProvider'; +import { useTheme } from '../../context/ThemeContext'; import { getLinkOptions } from '../../lib/hey-api/client/sdk.gen'; -import { getLinkOptionsFromLocal, saveLinkOptionsToLocal } from '../../api'; type LinkDropdownProps = { doctype: string; // linked doctype to fetch options for @@ -41,27 +41,9 @@ const LinkDropdown: React.FC = ({ const hasLoadedRef = useRef(false); const containerStyle = { - position: 'relative' as const, zIndex: containerZIndex, }; - const dropdownStyle = { - position: 'absolute' as const, - top: 45, - left: 0, - right: 0, - zIndex: 2000, - backgroundColor: theme.dropdownBg, - borderWidth: 1.5, - borderColor: theme.border, - borderRadius: 8, - shadowColor: theme.shadow, - shadowOffset: { width: 0, height: 4 }, - shadowOpacity: 0.25, - shadowRadius: 8, - elevation: 20, - }; - const filteredOptions = useMemo(() => { if (!searchTerm.trim()) { return allOptions; @@ -75,13 +57,8 @@ const LinkDropdown: React.FC = ({ [filteredOptions] ); - const dropdownMaxHeight = Math.min( - Math.max(displayOptions.length, 4) * 48 + 56, - 480 - ); - const scrollViewStyle = { - maxHeight: dropdownMaxHeight - 56, + maxHeight: 250, }; const normalizedDoctype = useMemo(() => (doctype || '').trim(), [doctype]); @@ -282,7 +259,21 @@ const LinkDropdown: React.FC = ({ {isOpen && ( - + {loading ? ( diff --git a/src/app/components/SelectDropdown.tsx b/src/app/components/SelectDropdown.tsx index 7ee3b30..181960a 100644 --- a/src/app/components/SelectDropdown.tsx +++ b/src/app/components/SelectDropdown.tsx @@ -26,28 +26,9 @@ const SelectDropdown: React.FC = ({ const { theme } = useTheme(); const containerStyle = { - position: 'relative' as const, zIndex: containerZIndex, }; - const dropdownStyle = { - position: 'absolute' as const, - top: 45, - left: 0, - right: 0, - zIndex: 2000, - backgroundColor: theme.dropdownBg, - borderWidth: 1.5, - borderColor: theme.border, - borderRadius: 8, - shadowColor: theme.shadow, - shadowOffset: { width: 0, height: 4 }, - shadowOpacity: 0.25, - shadowRadius: 8, - elevation: 20, - maxHeight: 250, - }; - const scrollViewStyle = { maxHeight: 250, }; @@ -81,9 +62,23 @@ const SelectDropdown: React.FC = ({ /> - {/* Dropdown Options - Always render structure, only open if isOpen */} + {/* Pushes content down when open */} {isOpen && ( - + {options.length > 0 ? ( options.map((option: string, optIndex: number) => { From 56c97fbb2fd36738a37ba382da6ab8c7aa1c2513 Mon Sep 17 00:00:00 2001 From: Nisch1Mv Date: Thu, 8 Jan 2026 16:40:37 +0530 Subject: [PATCH 20/23] refactor dropdown components for improved styling and spacing --- src/app/components/LinkDropdown.tsx | 28 +++++++++++++++------------ src/app/components/SelectDropdown.tsx | 25 +++++++++++++----------- 2 files changed, 30 insertions(+), 23 deletions(-) diff --git a/src/app/components/LinkDropdown.tsx b/src/app/components/LinkDropdown.tsx index 80645b5..9238cbf 100644 --- a/src/app/components/LinkDropdown.tsx +++ b/src/app/components/LinkDropdown.tsx @@ -234,7 +234,7 @@ const LinkDropdown: React.FC = ({ return ( = ({ {value || placeholder} = ({ {isOpen && ( {loading ? ( @@ -289,13 +290,14 @@ const LinkDropdown: React.FC = ({ ) : ( <> - + setSearchTerm(text)} @@ -314,7 +316,7 @@ const LinkDropdown: React.FC = ({ return ( = ({ optIndex < displayOptions.length - 1 ? theme.border : undefined, + borderBottomWidth: optIndex < displayOptions.length - 1 ? 0.5 : 0, }} onPress={() => { onValueChange(trimmedOption); @@ -332,6 +335,7 @@ const LinkDropdown: React.FC = ({ style={{ color: theme.text, fontWeight, + fontSize: 15, }} > {trimmedOption} diff --git a/src/app/components/SelectDropdown.tsx b/src/app/components/SelectDropdown.tsx index 181960a..4c1ea22 100644 --- a/src/app/components/SelectDropdown.tsx +++ b/src/app/components/SelectDropdown.tsx @@ -37,7 +37,7 @@ const SelectDropdown: React.FC = ({ {/* Dropdown Toggle Button */} = ({ = ({ {isOpen && ( @@ -88,20 +89,22 @@ const SelectDropdown: React.FC = ({ return ( onValueChange(trimmedOption)} > {trimmedOption} From 0355f8b795c04033dab8d13706ff566e7f4ae15d Mon Sep 17 00:00:00 2001 From: Nisch1Mv Date: Tue, 24 Feb 2026 12:36:01 +0530 Subject: [PATCH 21/23] phone number is now sedning corectly with the country code --- src/app/components/fields/PhoneInput.tsx | 321 +++++--------- src/app/screens/files/PreviewForm.tsx | 508 ++++++++++------------- 2 files changed, 310 insertions(+), 519 deletions(-) diff --git a/src/app/components/fields/PhoneInput.tsx b/src/app/components/fields/PhoneInput.tsx index 6899af0..818bbfe 100644 --- a/src/app/components/fields/PhoneInput.tsx +++ b/src/app/components/fields/PhoneInput.tsx @@ -1,16 +1,8 @@ -import { ChevronDown } from 'lucide-react-native'; -import React, { useEffect, useState } from 'react'; +import React, { useState } from 'react'; import { - Keyboard, - Modal, - Platform, - Pressable, - TextInput as RNTextInput, - ScrollView, - Text, - TextInputProps, - TouchableOpacity, - View + TextInput as RNTextInput, + TextInputProps, + View } from 'react-native'; import { useTheme } from '../../../context/ThemeContext'; @@ -20,35 +12,29 @@ interface PhoneInputProps extends Omit void; } -interface CountryCode { - code: string; - dial: string; - flag: string; - name: string; -} -const countryCodes: CountryCode[] = [ - { code: 'IN', dial: '+91', flag: '🇮🇳', name: 'India' }, - { code: 'US', dial: '+1', flag: '🇺🇸', name: 'United States' }, - { code: 'GB', dial: '+44', flag: '🇬🇧', name: 'United Kingdom' }, - { code: 'AU', dial: '+61', flag: '🇦🇺', name: 'Australia' }, - { code: 'CA', dial: '+1', flag: '🇨🇦', name: 'Canada' }, - { code: 'AE', dial: '+971', flag: '🇦🇪', name: 'United Arab Emirates' }, - { code: 'SA', dial: '+966', flag: '🇸🇦', name: 'Saudi Arabia' }, - { code: 'SG', dial: '+65', flag: '🇸🇬', name: 'Singapore' }, - { code: 'MY', dial: '+60', flag: '🇲🇾', name: 'Malaysia' }, - { code: 'NZ', dial: '+64', flag: '🇳🇿', name: 'New Zealand' }, - { code: 'DE', dial: '+49', flag: '🇩🇪', name: 'Germany' }, - { code: 'FR', dial: '+33', flag: '🇫🇷', name: 'France' }, - { code: 'IT', dial: '+39', flag: '🇮🇹', name: 'Italy' }, - { code: 'ES', dial: '+34', flag: '🇪🇸', name: 'Spain' }, - { code: 'JP', dial: '+81', flag: '🇯🇵', name: 'Japan' }, - { code: 'CN', dial: '+86', flag: '🇨🇳', name: 'China' }, - { code: 'KR', dial: '+82', flag: '🇰🇷', name: 'South Korea' }, - { code: 'BR', dial: '+55', flag: '🇧🇷', name: 'Brazil' }, - { code: 'MX', dial: '+52', flag: '🇲🇽', name: 'Mexico' }, - { code: 'ZA', dial: '+27', flag: '🇿🇦', name: 'South Africa' }, -]; +const parsePhoneValue = (input?: string) => { + // If value is already in +{countrycode}-{phonenumber} format, split it + if (input && input.startsWith('+') && input.includes('-')) { + const [country, number] = input.split('-'); + return { + countryCode: country.replace('+', ''), + phoneNumber: number || '', + }; + } + // If only 10 digits, treat as Indian number + if (input && /^[0-9]{10}$/.test(input)) { + return { + countryCode: '91', + phoneNumber: input, + }; + } + // Fallback + return { + countryCode: '91', + phoneNumber: '', + }; +}; const PhoneInput: React.FC = ({ className = "h-[40px] w-full rotate-0 rounded-md border pb-2.5 pl-3 pr-3 pt-2.5 opacity-100", @@ -57,201 +43,90 @@ const PhoneInput: React.FC = ({ ...props }) => { const { theme } = useTheme(); - const [selectedCountry, setSelectedCountry] = useState(countryCodes[0]); - const [modalVisible, setModalVisible] = useState(false); - const [phoneNumber, setPhoneNumber] = useState(''); - const [searchQuery, setSearchQuery] = useState(''); - const [keyboardHeight, setKeyboardHeight] = useState(0); + // Parse value into country code and phone number + const parsed = parsePhoneValue(value); + const [countryCode, setCountryCode] = useState(parsed.countryCode); + const [phoneNumber, setPhoneNumber] = useState(parsed.phoneNumber); - // Track keyboard height - useEffect(() => { - const keyboardDidShowListener = Keyboard.addListener( - Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow', - (e) => { - setKeyboardHeight(e.endCoordinates.height); - } - ); - const keyboardDidHideListener = Keyboard.addListener( - Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide', - () => { - setKeyboardHeight(0); - } - ); + React.useEffect(() => { + const parsed = parsePhoneValue(value); + setCountryCode(parsed.countryCode); + setPhoneNumber(parsed.phoneNumber); + }, [value]); - return () => { - keyboardDidShowListener.remove(); - keyboardDidHideListener.remove(); - }; - }, []); - // Parse initial value if it exists - React.useEffect(() => { - if (value) { - // Try to extract country code from value - const country = countryCodes.find(c => value.startsWith(c.dial)); - if (country) { - setSelectedCountry(country); - setPhoneNumber(value.substring(country.dial.length).trim()); - } else { - setPhoneNumber(value); - } + // Helper to emit value only if both country code and phone number are present and valid + const emitPhoneValue = (cc: string, pn: string) => { + if (cc && pn && pn.length === 10) { + onChangeText && onChangeText(`+${cc}-${pn}`); + } else { + onChangeText && onChangeText(''); } - }, [value]); + }; const handlePhoneChange = (text: string) => { - // Remove non-numeric characters - const numeric = text.replace(/[^0-9]/g, ''); - setPhoneNumber(numeric); - - // Combine country code and phone number - const fullNumber = `${selectedCountry.dial} ${numeric}`; - if (onChangeText) { - onChangeText(fullNumber); + let numeric = text.replace(/[^0-9]/g, ''); + if (numeric.length > 10) { + numeric = numeric.slice(0, 10); } + setPhoneNumber(numeric); + emitPhoneValue(countryCode, numeric); }; - const handleCountrySelect = (country: CountryCode) => { - setSelectedCountry(country); - setModalVisible(false); - setSearchQuery(''); - - // Update full number with new country code - if (phoneNumber) { - const fullNumber = `${country.dial} ${phoneNumber}`; - if (onChangeText) { - onChangeText(fullNumber); - } + + const handleCountryCodeChange = (text: string) => { + let numeric = text.replace(/[^0-9]/g, ''); + if (numeric.length > 4) { + numeric = numeric.slice(0, 4); } + setCountryCode(numeric); + emitPhoneValue(numeric, phoneNumber); }; - const filteredCountries = countryCodes.filter( - country => - country.code.toLowerCase().includes(searchQuery.toLowerCase()) || - country.name.toLowerCase().includes(searchQuery.toLowerCase()) || - country.dial.includes(searchQuery) - ); - return ( - - - {/* Country Code Selector */} - setModalVisible(true)} - className="flex-row items-center justify-center gap-0.5 rounded-l-md border px-2" - style={{ - height: 40, - borderColor: theme.border, - backgroundColor: theme.background, - }} - > - {selectedCountry.flag} - - {selectedCountry.dial} - - - - - {/* Phone Number Input */} - - - - {/* Country Code Modal */} - setModalVisible(false)} - > - { - Keyboard.dismiss(); - setModalVisible(false); - }} - > - 0 ? `${100 - (keyboardHeight / 8)}%` : '70%', - paddingBottom: keyboardHeight > 0 ? keyboardHeight : 0 - }} - > - - - Select Country Code - - {/* Search Bar */} - - - - {filteredCountries.length > 0 ? ( - filteredCountries.map((country) => ( - handleCountrySelect(country)} - > - {country.flag} - - - {country.name} - - - {country.code} - - - - {country.dial} - - - )) - ) : ( - - - No countries found - - - )} - - - - + + { + // Remove + if user types it + handleCountryCodeChange(text.replace('+', '')); + }} + maxLength={5} + /> + ); }; diff --git a/src/app/screens/files/PreviewForm.tsx b/src/app/screens/files/PreviewForm.tsx index 13af6a7..9fe3fb6 100644 --- a/src/app/screens/files/PreviewForm.tsx +++ b/src/app/screens/files/PreviewForm.tsx @@ -1,30 +1,35 @@ +import { FormStackParamList } from '@/app/navigation/FormStackParamList'; +import AsyncStorage from '@react-native-async-storage/async-storage'; +import { RouteProp, useNavigation, useRoute } from '@react-navigation/native'; +import { NativeStackNavigationProp } from '@react-navigation/native-stack'; import { ArrowLeft } from 'lucide-react-native'; import React, { useCallback, useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; import { Modal, - TouchableOpacity, - View, + ScrollView, Text, TextInput, - ScrollView, + TouchableOpacity, + View, } from 'react-native'; import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view'; import { SafeAreaView } from 'react-native-safe-area-context'; +import { extractFields, getDocTypeFromLocal } from '../../../api'; +import { useTheme } from '../../../context/ThemeContext'; +import { usePendingFormsExport } from '../../../hooks/usePendingFormsExport'; +import { RawField, SubmissionItem } from '../../../types'; +import DatePicker from '../../components/DatePicker'; import LanguageControl from '../../components/LanguageControl'; -import SelectDropdown from '../../components/SelectDropdown'; import LinkDropdown from '../../components/LinkDropdown'; -import DatePicker from '../../components/DatePicker'; +import SelectDropdown from '../../components/SelectDropdown'; import TableField from '../../components/TableField'; -import { useTranslation } from 'react-i18next'; -import { RouteProp, useNavigation, useRoute } from '@react-navigation/native'; -import { SubmissionItem, RawField } from '../../../types'; +import CheckboxInput from '../../components/fields/CheckboxInput'; +import CurrencyInput from '../../components/fields/CurrencyInput'; +import HeadingText from '../../components/fields/HeadingText'; +import PhoneInput from '../../components/fields/PhoneInput'; +import SectionBreak from '../../components/fields/SectionBreak'; import { getQueue, removeFromQueue } from '../../pendingQueue'; -import { FormStackParamList } from '@/app/navigation/FormStackParamList'; -import { NativeStackNavigationProp } from '@react-navigation/native-stack'; -import AsyncStorage from '@react-native-async-storage/async-storage'; -import { useTheme } from '../../../context/ThemeContext'; -import { extractFields, getDocTypeFromLocal } from '../../../api'; -import { usePendingFormsExport } from '../../../hooks/usePendingFormsExport'; type PreviewFormRouteProp = RouteProp; type PreviewFormNavigationProp = NativeStackNavigationProp< @@ -373,300 +378,211 @@ function PreviewForm() { 'Link', 'Date', 'Table', + 'Check', + 'Phone', + 'Currency', + 'Heading', + 'Section Break', + ]; + const fieldsToRender = formFields.length > 0 ? formFields - .filter(field => { - // Skip if hidden, print_hide, or report_hide is true (value is 1 or truthy) - if (field.hidden || field.print_hide || field.report_hide) { - return false; - } - return allowedFieldTypes.includes(field.fieldtype || 'Data'); - }) - .map(field => ({ - fieldname: field.fieldname, - label: - field.label || - field.fieldname.charAt(0).toUpperCase() + - field.fieldname.slice(1).replace(/([A-Z])/g, ' $1'), - fieldtype: field.fieldtype || 'Data', - options: field.options, - value: formData[field.fieldname], - })) - : Object.keys(formData).map(key => ({ - fieldname: key, + .filter(field => { + // Skip if hidden, print_hide, or report_hide is true (value is 1 or truthy) + if (field.hidden || field.print_hide || field.report_hide) { + return false; + } + return allowedFieldTypes.includes(field.fieldtype || 'Data'); + }) + .map(field => ({ + fieldname: field.fieldname, label: - key.charAt(0).toUpperCase() + - key.slice(1).replace(/([A-Z])/g, ' $1'), - fieldtype: typeof formData[key] === 'boolean' ? 'Check' : 'Data', - options: undefined, - value: formData[key], - })); + field.label || + field.fieldname.charAt(0).toUpperCase() + + field.fieldname.slice(1).replace(/([A-Z])/g, ' $1'), + fieldtype: field.fieldtype || 'Data', + options: field.options, + value: formData[field.fieldname], + })) + : Object.keys(formData).map(key => ({ + fieldname: key, + label: + key.charAt(0).toUpperCase() + + key.slice(1).replace(/([A-Z])/g, ' $1'), + fieldtype: typeof formData[key] === 'boolean' ? 'Check' : 'Data', + options: undefined, + value: formData[key], + })); // Helper function to render field based on type const renderField = (field: any, index: number = 0) => { - const { fieldname, label, fieldtype, options, value } = field; + const { fieldname, label, fieldtype, options, value, reqd } = field; const isOpen = dropdownStates[fieldname] || false; + const isRequired = reqd === 1; - switch (fieldtype) { - case 'Select': - if (options) { - const optionsList = options - .split('\n') - .filter((opt: string) => opt.trim()); - - return ( - - - {label} - - handleChange(fieldname, val)} - placeholder={t('formDetail.selectPlaceholder', { - label: label, - })} - isOpen={isOpen} - onToggle={() => toggleDropdown(fieldname)} - containerZIndex={1000 - index} - /> - - ); - } - // Fallback to text input if no options - return ( - - - {label} - - handleChange(fieldname, text)} - editable={true} - /> - - ); - - case 'Link': - if (options) { - return ( - - - {label} - - handleChange(fieldname, val)} - placeholder={t('formDetail.selectPlaceholder', { - label: label, - })} - isOpen={isOpen} - onToggle={() => toggleDropdown(fieldname)} - containerZIndex={1000 - index} - /> - - ); - } - // Fallback to text input if no doctype - return ( - - - {label} - - handleChange(fieldname, text)} - editable={true} - /> - - ); - - case 'Date': - return ( - - - {label} - - handleChange(fieldname, val)} - placeholder={t('formDetail.selectPlaceholder', { - label: label, - })} - /> - - ); - - case 'Check': - const checkBoxStyle = { - backgroundColor: value ? theme.buttonBackground : 'transparent', - borderColor: value ? theme.buttonBackground : theme.border, - }; - return ( - - handleChange(fieldname, !value)} - > - - {value && ( - - ✓ - - )} - - - {label} - - - - ); - - case 'Text': - return ( - - - {label} - - handleChange(fieldname, text)} - multiline={true} - textAlignVertical="top" - editable={true} - /> - - ); + // SectionBreak and Heading do not need label above + if (fieldtype === 'Section Break') { + return ; + } + if (fieldtype === 'Heading') { + return ; + } - case 'Table': { - const tableSchema = tableSchemas[fieldname]; - return ( - - - {label} - - - // @ts-ignore - (navigation as any).navigate('TableRowEditor', { - fieldname, - tableDoctype: (options as string) || '', - title: label, - index: rowIndex, - initialRow: - Array.isArray(value) && value[rowIndex] - ? value[rowIndex] - : null, - schema: tableSchema || undefined, - }) + return ( + + {/* Field label and required asterisk */} + {fieldtype !== 'Check' && ( + + {label} + {isRequired && *} + + )} + + {(() => { + switch (fieldtype) { + case 'Select': + if (options) { + const optionsList = options + .split('\n') + .filter((opt: string) => opt.trim()); + return ( + handleChange(fieldname, val)} + placeholder={t('formDetail.selectPlaceholder', { label })} + isOpen={isOpen} + onToggle={() => toggleDropdown(fieldname)} + containerZIndex={1000 - index} + formData={formData} + /> + ); } - onDeleteRow={rowIndex => { - const current = Array.isArray(value) - ? [...(value as any[])] - : []; - if (rowIndex >= 0 && rowIndex < current.length) { - current.splice(rowIndex, 1); - handleChange( - fieldname, - current as unknown as string | boolean - ); - } - }} - /> - - ); - } - - default: - // Default to regular text input - return ( - - - {label} - - handleChange(fieldname, text)} - editable={true} - /> - - ); - } + return null; + case 'Link': + if (options) { + return ( + handleChange(fieldname, val)} + placeholder={t('formDetail.selectPlaceholder', { label })} + isOpen={isOpen} + onToggle={() => toggleDropdown(fieldname)} + containerZIndex={1000 - index} + /> + ); + } + return null; + case 'Date': + return ( + handleChange(fieldname, val)} + placeholder={t('formDetail.selectPlaceholder', { label })} + /> + ); + case 'Table': { + const tableSchema = tableSchemas[fieldname]; + return ( + + (navigation as any).navigate('TableRowEditor', { + fieldname, + tableDoctype: (options as string) || '', + title: label, + index: rowIndex, + initialRow: + Array.isArray(value) && value[rowIndex] + ? value[rowIndex] + : null, + schema: tableSchema || undefined, + }) + } + onDeleteRow={rowIndex => { + const current = Array.isArray(value) + ? [...(value as any[])] + : []; + if (rowIndex >= 0 && rowIndex < current.length) { + current.splice(rowIndex, 1); + handleChange( + fieldname, + current as unknown as string | boolean + ); + } + }} + /> + ); + } + case 'Currency': + return ( + handleChange(fieldname, text)} + /> + ); + case 'Phone': + return ( + handleChange(fieldname, text)} + /> + ); + case 'Check': + return ( + handleChange(fieldname, val)} + label={label} + /> + ); + case 'Text': + return ( + handleChange(fieldname, text)} + multiline={true} + textAlignVertical="top" + editable={true} + /> + ); + default: + return ( + handleChange(fieldname, text)} + editable={true} + /> + ); + } + })()} + + ); }; return ( From c90012a58789fc029f84d4fa74aadf8f340008f0 Mon Sep 17 00:00:00 2001 From: Nisch1Mv Date: Tue, 24 Feb 2026 12:54:59 +0530 Subject: [PATCH 22/23] added flag to phone ninpur ui --- src/app/components/fields/PhoneInput.tsx | 51 +++++++++++------------- 1 file changed, 23 insertions(+), 28 deletions(-) diff --git a/src/app/components/fields/PhoneInput.tsx b/src/app/components/fields/PhoneInput.tsx index 818bbfe..accf16f 100644 --- a/src/app/components/fields/PhoneInput.tsx +++ b/src/app/components/fields/PhoneInput.tsx @@ -82,49 +82,44 @@ const PhoneInput: React.FC = ({ emitPhoneValue(numeric, phoneNumber); }; + // Combine flag, country code, and phone number in a single input box UI return ( { - // Remove + if user types it - handleCountryCodeChange(text.replace('+', '')); - }} - maxLength={5} - /> - { + // Parse input: expect format like '🇮🇳 +91 1234567890' + // Remove flag and split + let cleaned = text.replace(/[^0-9+]/g, ' '); + let parts = cleaned.trim().split(' '); + let cc = '91'; + let pn = ''; + if (parts.length >= 2) { + // Find country code + cc = parts[0].replace('+', '') || '91'; + pn = parts.slice(1).join('').replace(/[^0-9]/g, '').slice(0, 10); + } else if (parts.length === 1) { + pn = parts[0].replace(/[^0-9]/g, '').slice(0, 10); + } + setCountryCode(cc); + setPhoneNumber(pn); + emitPhoneValue(cc, pn); + }} + maxLength={18} {...props} /> From 5c8d89d3558ce0351f5d227026437e45bde4fda5 Mon Sep 17 00:00:00 2001 From: Nisch1Mv Date: Tue, 24 Feb 2026 14:39:18 +0530 Subject: [PATCH 23/23] phone number input updated --- src/app/components/fields/PhoneInput.tsx | 73 +++++++++++++++--------- 1 file changed, 45 insertions(+), 28 deletions(-) diff --git a/src/app/components/fields/PhoneInput.tsx b/src/app/components/fields/PhoneInput.tsx index accf16f..2381ea6 100644 --- a/src/app/components/fields/PhoneInput.tsx +++ b/src/app/components/fields/PhoneInput.tsx @@ -55,13 +55,11 @@ const PhoneInput: React.FC = ({ }, [value]); - // Helper to emit value only if both country code and phone number are present and valid + // Always emit in +{countrycode}-{phonenumber} format, even if incomplete const emitPhoneValue = (cc: string, pn: string) => { - if (cc && pn && pn.length === 10) { - onChangeText && onChangeText(`+${cc}-${pn}`); - } else { - onChangeText && onChangeText(''); - } + // Always emit in +{countrycode}-{phonenumber} format + const formatted = `+${cc || ''}-${pn || ''}`; + onChangeText && onChangeText(formatted); }; const handlePhoneChange = (text: string) => { @@ -82,9 +80,45 @@ const PhoneInput: React.FC = ({ emitPhoneValue(numeric, phoneNumber); }; - // Combine flag, country code, and phone number in a single input box UI return ( + + + { + let numeric = text.replace(/[^0-9]/g, ''); + if (numeric.length > 4) numeric = numeric.slice(0, 4); + setCountryCode(numeric); + emitPhoneValue(numeric, phoneNumber); + }} + maxLength={5} + /> + = ({ color: theme.text, fontSize: 16, }} - placeholder="🇮🇳 +91 1234567890" + placeholder="Phone number" placeholderTextColor={theme.subtext} keyboardType="phone-pad" - value={`🇮🇳 +${countryCode} ${phoneNumber}`} - onChangeText={text => { - // Parse input: expect format like '🇮🇳 +91 1234567890' - // Remove flag and split - let cleaned = text.replace(/[^0-9+]/g, ' '); - let parts = cleaned.trim().split(' '); - let cc = '91'; - let pn = ''; - if (parts.length >= 2) { - // Find country code - cc = parts[0].replace('+', '') || '91'; - pn = parts.slice(1).join('').replace(/[^0-9]/g, '').slice(0, 10); - } else if (parts.length === 1) { - pn = parts[0].replace(/[^0-9]/g, '').slice(0, 10); - } - setCountryCode(cc); - setPhoneNumber(pn); - emitPhoneValue(cc, pn); - }} - maxLength={18} + value={phoneNumber} + onChangeText={handlePhoneChange} + maxLength={10} {...props} />