From 91e93d4c1090d0d9ac570b3bdc32c4bfcd0ed934 Mon Sep 17 00:00:00 2001 From: Mochammad Adhi Buchori Date: Mon, 16 Jun 2025 22:09:35 +0700 Subject: [PATCH] Update step feature to allow users to jump to any step regardless of completion status --- src/components/StepIndicator.tsx | 108 +++++++++++------ src/components/TextInput.tsx | 23 +++- src/screens/regularPassport/index.tsx | 110 +++++++++++++++++- .../Step1VerifyNik/Step1VerifyNikSubStep3.tsx | 41 ++++++- ...sportApplicationQuestionnaireSubStep11.tsx | 35 +++++- .../Step3UploadDocuments.tsx | 66 +++++++++-- 6 files changed, 325 insertions(+), 58 deletions(-) diff --git a/src/components/StepIndicator.tsx b/src/components/StepIndicator.tsx index d8ea4b4..f8bb352 100644 --- a/src/components/StepIndicator.tsx +++ b/src/components/StepIndicator.tsx @@ -1,25 +1,50 @@ import React from 'react'; -import {StyleSheet, Text, View} from 'react-native'; +import {StyleSheet, Pressable, Text, View} from 'react-native'; import Colors from '../../assets/styles/Colors'; import FontFamily from '../../assets/styles/FontFamily'; +import Icon from 'react-native-vector-icons/MaterialCommunityIcons'; -const StepIndicator = ({currentStep, totalSteps, completedSteps}: any) => { +const StepIndicator = ({ + currentStep, + totalSteps, + completedSteps, + onStepPress, + validationStatus, +}: any) => { return ( {[...Array(totalSteps)].map((_, index) => { const stepNumber = index + 1; const isCompleted = completedSteps.includes(stepNumber); const isCurrent = currentStep === stepNumber; + const stepStatus = validationStatus[stepNumber]; - const backgroundColor = isCompleted + const backgroundColorStyle = + stepStatus === 'completed' + ? Colors.indicatorGreen.color + : stepStatus === 'invalid' + ? Colors.indicatorOrange.color + : Colors.neutral100.color; + + const indicatorLineBackgroundColorStyle = isCompleted ? Colors.secondary30.color : Colors.neutral100.color; - const textColor = isCompleted - ? Colors.neutral100.color - : isCurrent - ? Colors.secondary30.color - : Colors.secondary50.color; + const borderColorStyle = + stepStatus === 'completed' + ? Colors.indicatorGreen.color + : stepStatus === 'invalid' + ? Colors.indicatorOrange.color + : isCurrent + ? Colors.secondary30.color + : Colors.neutral100.color; + + const textColorStyle = + isCompleted || stepStatus === 'invalid' + ? Colors.neutral100.color + : isCurrent + ? Colors.secondary30.color + : Colors.secondary50.color; const textStyle = isCompleted ? FontFamily.notoSansBold @@ -29,36 +54,53 @@ const StepIndicator = ({currentStep, totalSteps, completedSteps}: any) => { return ( - - - - {stepNumber} - + onStepPress?.(stepNumber)} + style={({pressed}) => ({ + transform: [{scale: pressed ? 0.97 : 1}], + })}> + + + {stepStatus === 'completed' ? ( + + ) : stepStatus === 'invalid' ? ( + + ) : ( + + {stepNumber} + + )} + - + + {index < totalSteps - 1 && ( @@ -93,7 +135,7 @@ const styles = StyleSheet.create({ stepIndicatorLine: { flex: 1, height: 2, - } + }, }); export default StepIndicator; diff --git a/src/components/TextInput.tsx b/src/components/TextInput.tsx index 9364f8f..40a5e30 100644 --- a/src/components/TextInput.tsx +++ b/src/components/TextInput.tsx @@ -7,7 +7,7 @@ import Colors from '../../assets/styles/Colors'; import FontFamily from '../../assets/styles/FontFamily'; import DateTimePicker from '@react-native-community/datetimepicker'; import {useState} from 'react'; -import {Dropdown, SelectCountry} from 'react-native-element-dropdown'; +import {Dropdown} from 'react-native-element-dropdown'; type DropdownItem = { label: string; @@ -42,6 +42,8 @@ interface TextInputComponentProps { handleDropdownPressed?: () => void; countryValue?: string | null; setCountryValue?: (country: string) => void; + value?: string; + onChangeText?: (text: string) => void; } const TextInputComponent = (props: TextInputComponentProps) => { @@ -65,6 +67,8 @@ const TextInputComponent = (props: TextInputComponentProps) => { handleDropdownPressed, countryValue, setCountryValue, + value, + onChangeText, } = props; const [secureText, setSecureText] = useState(isPassword); @@ -97,6 +101,7 @@ const TextInputComponent = (props: TextInputComponentProps) => { date.getMonth() + 1, ).padStart(2, '0')}/${date.getFullYear()}`; setFormattedDate(formatted); + onChangeText?.(formatted); } }; @@ -189,7 +194,7 @@ const TextInputComponent = (props: TextInputComponentProps) => { )} { value={dropdownValue} onChange={item => { setDropdownValue(item.value); + onChangeText?.(item.value); }} disable={isDisabled} renderRightIcon={() => } @@ -258,7 +264,7 @@ const TextInputComponent = (props: TextInputComponentProps) => { )} { { } multiline={isMultiline} textColor="#48454E" + value={value} + onChangeText={onChangeText} /> {supportText && {supportText}} @@ -449,6 +457,13 @@ const styles = StyleSheet.create({ borderRadius: 12, borderColor: '#e3e3e5', }, + outlineColorDisabledDropdown: { + height: 58, + backgroundColor: '#F8F9FE', + borderWidth: 1, + borderRadius: 12, + borderColor: '#e3e3e5', + }, imageCountryStyle: { width: 32, height: 20, diff --git a/src/screens/regularPassport/index.tsx b/src/screens/regularPassport/index.tsx index 708f48e..300c82a 100644 --- a/src/screens/regularPassport/index.tsx +++ b/src/screens/regularPassport/index.tsx @@ -1,4 +1,4 @@ -import React, {useEffect, useState} from 'react'; +import React, {useEffect, useRef, useState} from 'react'; import {BackHandler, StatusBar, Text, View} from 'react-native'; import styles from './styles'; import Icon from 'react-native-vector-icons/MaterialCommunityIcons'; @@ -12,11 +12,11 @@ import StepIndicator from '../../components/StepIndicator'; import DialogApplicationPassport from '../../components/dialog/DialogApplicationPassport'; import DialogDontHaveYetPassport from '../../components/dialog/DialogDontHaveYetPassport'; import DialogLostOrDamagedPassport from '../../components/dialog/DialogLostOrDamagedPassport'; -import passportAppointmentData from '../../data/History/PassportAppointmentData'; import Step7ApplicationFeeDetails from './steps/Step7ApplicationFeeDetails/Step7ApplicationFeeDetails'; import Step6ApplicationTypeAndApplicantData from './steps/Step6ApplicationTypeAndApplicantData/Step6ApplicationTypeAndApplicantData'; import Step5ApplicationTypeAndApplicantData from './steps/Step5ApplicationTypeAndApplicantData/Step5ApplicationTypeAndApplicantData'; import Step3UploadDocuments from './steps/Step3UploadDocuments/Step3UploadDocuments'; +import {ToastAndroid} from 'react-native'; // Options Data import passportForOptions from '../../data/Options/PassportForOptions'; @@ -74,6 +74,9 @@ type RenderApplicationStepsContentProps = { showSelectDateSheet: () => void; selectedDestinationCountryOption: string; setSelectedDestinationCountryOption: (val: string) => void; + setStepValidationStatus: React.Dispatch< + React.SetStateAction> + >; }; const RenderApplicationStepsContent = ( @@ -103,6 +106,7 @@ const RenderApplicationStepsContent = ( showEditDataSheet, showSearchLocationSheet, showSelectDateSheet, + setStepValidationStatus, } = props; if (step === 1) { @@ -113,7 +117,16 @@ const RenderApplicationStepsContent = ( return ; case 3: return ( - + { + setStepValidationStatus(prev => ({ + ...prev, + 1: isValid ? 'completed' : 'invalid', + })); + }} + /> ); default: return null; @@ -208,6 +221,12 @@ const RenderApplicationStepsContent = ( setSubStep={setSubStep} selectedOption={selectedOption} setSelectedOption={setSelectedOption} + onSubStepValidation={isValid => { + setStepValidationStatus(prev => ({ + ...prev, + 2: isValid ? 'completed' : 'invalid', + })); + }} /> ); default: @@ -246,6 +265,12 @@ const RenderApplicationStepsContent = ( showCivilStatusDocumentsInfoDialog } selectedDestinationCountryOption={selectedDestinationCountryOption} + onSubStepValidation={isValid => { + setStepValidationStatus(prev => ({ + ...prev, + 3: isValid ? 'completed' : 'invalid', + })); + }} /> ); case 5: @@ -253,7 +278,6 @@ const RenderApplicationStepsContent = ( ); @@ -340,10 +364,24 @@ function RegularPassportScreen() { useState(false); const [step, setStep] = useState(1); const [subStep, setSubStep] = useState(1); + const [completedSteps, setCompletedSteps] = useState( [...Array(step - 1)].map((_, i) => i + 1), ); + // Step status states + const [stepValidationStatus, setStepValidationStatus] = useState<{ + [key: number]: 'completed' | 'incomplete' | 'invalid'; + }>({ + 1: 'incomplete', + 2: 'incomplete', + 3: 'incomplete', + 4: 'incomplete', + 5: 'incomplete', + 6: 'incomplete', + 7: 'incomplete', + }); + // Dialog visibility states const [visible, setVisible] = useState(false); const [visibleDontHaveYetDialog, setVisibleDontHaveYetDialog] = @@ -416,6 +454,8 @@ function RegularPassportScreen() { const showSelectDateSheet = () => setVisibleSelectDateSheet(true); const hideSelectDateSheet = () => setVisibleSelectDateSheet(false); + const editedCompletedRef = useRef>(new Set()); + const stepTitles: {[key: number]: string} = { 1: 'Verifikasi NIK', 2: 'Kuesioner Permohonan Paspor (PERDIM)', @@ -481,7 +521,68 @@ function RegularPassportScreen() { currentStep={step} totalSteps={7} completedSteps={completedSteps} + validationStatus={stepValidationStatus} + onStepPress={(targetStep: number) => { + const isCurrentStepIn5to7 = step >= 5 && step <= 7; + const isTargetStepIn1to4 = targetStep >= 1 && targetStep <= 4; + const isTargetStepIn5to7 = targetStep >= 5 && targetStep <= 7; + + const isStep1to4Completed = [1, 2, 3, 4].every( + s => stepValidationStatus[s] === 'completed', + ); + + if (!isCurrentStepIn5to7 && isTargetStepIn5to7) { + ToastAndroid.show( + 'Lengkapi langkah 1 – 4 dulu', + ToastAndroid.SHORT, + ); + return; + } + + if ( + isCurrentStepIn5to7 && + isStep1to4Completed && + isTargetStepIn1to4 + ) { + ToastAndroid.show( + 'Hanya dapat berpindah di langkah 5 – 7.', + ToastAndroid.SHORT, + ); + return; + } + + setStepValidationStatus(prev => { + const next = {...prev}; + + if (step !== targetStep && editedCompletedRef.current.has(step)) { + next[step] = 'completed'; + editedCompletedRef.current.delete(step); + } + + if (prev[targetStep] === 'completed') { + editedCompletedRef.current.add(targetStep); + } + + next[targetStep] = 'incomplete'; + + if (targetStep > step) { + for (let s = 1; s < targetStep; s++) { + if (next[s] !== 'completed') next[s] = 'invalid'; + } + } else if (targetStep < step) { + for (let s = step; s > targetStep; s--) { + if (next[s] !== 'completed') next[s] = 'invalid'; + } + } + + return next; + }); + + setStep(targetStep); + setSubStep(1); + }} /> + diff --git a/src/screens/regularPassport/steps/Step1VerifyNik/Step1VerifyNikSubStep3.tsx b/src/screens/regularPassport/steps/Step1VerifyNik/Step1VerifyNikSubStep3.tsx index 213d2a2..26cd7ce 100644 --- a/src/screens/regularPassport/steps/Step1VerifyNik/Step1VerifyNikSubStep3.tsx +++ b/src/screens/regularPassport/steps/Step1VerifyNik/Step1VerifyNikSubStep3.tsx @@ -10,12 +10,38 @@ import Colors from '../../../../../assets/styles/Colors'; type Step1VerifyNikSubStep3Props = { setStep: (val: number) => void; setSubStep: (val: number) => void; + onSubStepValidation: (isValid: boolean) => void; }; const Step1VerifyNikSubStep3 = ({ setStep, setSubStep, + onSubStepValidation, }: Step1VerifyNikSubStep3Props) => { + const [fullName, setFullName] = React.useState(''); + const [nik, setNik] = React.useState(''); + const [birthDate, setBirthDate] = React.useState(''); + const [gender, setGender] = React.useState(''); + const [civilStatus, setCivilStatus] = React.useState(''); + + const onNextPress = () => { + const isFormValid = + fullName.trim() !== '' && + nik.trim() !== '' && + birthDate.trim() !== '' && + gender.trim() !== '' && + civilStatus.trim() !== ''; + + if (isFormValid) { + onSubStepValidation(true); + } else { + onSubStepValidation(false); + } + + setStep(2); + setSubStep(1); + }; + return ( @@ -28,11 +54,15 @@ const Step1VerifyNikSubStep3 = ({ title="Nama Lengkap Pemohon" placeholder="Nama Lengkap Anda" isRequired + value={fullName} + onChangeText={setFullName} /> @@ -41,6 +71,8 @@ const Step1VerifyNikSubStep3 = ({ placeholder="DD/MM/YYYY" isRequired isDate + value={birthDate} + onChangeText={setBirthDate} /> @@ -50,6 +82,8 @@ const Step1VerifyNikSubStep3 = ({ isRequired isDropdown dropdownItemData={genderData} + value={gender} + onChangeText={setGender} /> @@ -59,16 +93,15 @@ const Step1VerifyNikSubStep3 = ({ isRequired isDropdown dropdownItemData={civilStatusData} + value={civilStatus} + onChangeText={setCivilStatus} />