update logo, limit step navigation (1-6), and add custom toast

This commit is contained in:
Mochammad Adhi Buchori
2025-06-19 16:26:08 +07:00
parent 91e93d4c10
commit 0cd9e1500b
35 changed files with 597 additions and 216 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#2B3A51</color>
<color name="ic_launcher_background">#FFFFFF</color>
</resources>

2
package-lock.json generated
View File

@ -30,7 +30,7 @@
"@babel/core": "^7.25.2",
"@babel/preset-env": "^7.25.3",
"@babel/runtime": "^7.25.0",
"@react-native-community/cli": "15.0.1",
"@react-native-community/cli": "^15.0.1",
"@react-native-community/cli-platform-android": "15.0.1",
"@react-native-community/cli-platform-ios": "15.0.1",
"@react-native/babel-preset": "0.78.0",

View File

@ -32,7 +32,7 @@
"@babel/core": "^7.25.2",
"@babel/preset-env": "^7.25.3",
"@babel/runtime": "^7.25.0",
"@react-native-community/cli": "15.0.1",
"@react-native-community/cli": "^15.0.1",
"@react-native-community/cli-platform-android": "15.0.1",
"@react-native-community/cli-platform-ios": "15.0.1",
"@react-native/babel-preset": "0.78.0",

View File

@ -0,0 +1,53 @@
import React from 'react';
import {StyleSheet, Text, View} from 'react-native';
import {Snackbar} from 'react-native-paper';
import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
import Colors from '../../assets/styles/Colors';
type InfoToastProps = {
visible: boolean;
message: string;
onDismiss: () => void;
duration?: number;
};
const InfoToast = ({
visible,
message,
onDismiss,
duration = 2000,
}: InfoToastProps) => {
return (
<Snackbar
visible={visible}
onDismiss={onDismiss}
duration={duration}
style={styles.snackbar}>
<View style={styles.contentContainer}>
<Icon name="information" size={20} color="white" style={styles.icon} />
<Text style={styles.message}>{message}</Text>
</View>
</Snackbar>
);
};
const styles = StyleSheet.create({
snackbar: {
backgroundColor: Colors.secondary10.color,
borderRadius: 100,
margin: 16,
alignSelf: 'center',
},
contentContainer: {
flexDirection: 'row',
alignItems: 'center',
},
icon: {
marginRight: 8,
},
message: {
color: 'white',
},
});
export default InfoToast;

View File

@ -194,7 +194,10 @@ const TextInputComponent = (props: TextInputComponentProps) => {
)}
</View>
<Dropdown
style={[styles.dropdown, isDisabled && styles.outlineColorDisabledDropdown]}
style={[
styles.dropdown,
isDisabled && styles.outlineColorDisabledDropdown,
]}
placeholderStyle={styles.placeholderDropdownStyle}
selectedTextStyle={styles.selectedTextStyle}
iconStyle={styles.iconStyle}
@ -264,7 +267,10 @@ const TextInputComponent = (props: TextInputComponentProps) => {
</View>
)}
<Dropdown
style={[styles.dropdown, isDisabled && styles.outlineColorDisabledDropdown]}
style={[
styles.dropdown,
isDisabled && styles.outlineColorDisabledDropdown,
]}
selectedTextStyle={styles.selectedTextStyle}
placeholderStyle={styles.placeholderDropdownStyle}
inputSearchStyle={styles.inputSearchStyle}
@ -311,11 +317,15 @@ const TextInputComponent = (props: TextInputComponentProps) => {
<TextInput
mode="outlined"
placeholder={placeholder}
style={[inputStyle, isDisabled && styles.outlineColorDisabledDropdown]}
style={[
inputStyle,
isDisabled && styles.outlineColorDisabledDropdown,
]}
theme={{roundness: 12}}
placeholderTextColor={Colors.primary60.color}
editable={false}
value={formattedDate}
value={value}
onChangeText={onChangeText}
right={
<TextInput.Icon
icon="menu-down"

View File

@ -101,26 +101,6 @@ const passportAppointmentData = [
},
{
id: '6',
applicantName: 'Nabila Khairunisa',
applicantCode: '1038000007773344',
appointmentDate: 'Rabu, 19 April 2025',
appointmentTime: '13:00 - 14:00 WIB',
serviceUnit: 'Kantor Imigrasi Bekasi',
status: 'Menunggu Pembayaran',
submissionDate: 'Senin, 14 April 2025 23:45',
serviceCode: 'EH-MK2YPQ',
applicationDetails: {
nationalIdNumber: '3271234560009120007',
gender: 'Wanita',
applicationType: 'Penggantian Paspor',
replacementReason: 'Hilang',
applicationPurpose: 'Kuliah di Luar Negeri',
passportType: 'PASPOR BIASA ELEKTRONIK 5 TAHUN',
fee: '650.000',
},
},
{
id: '7',
applicantName: 'Ayaka Haishima',
applicantCode: '1038000008885566',
appointmentDate: 'Selasa, 18 April 2025',
@ -139,6 +119,26 @@ const passportAppointmentData = [
fee: '350.000',
},
},
{
id: '7',
applicantName: 'Nabila Khairunisa',
applicantCode: '1038000007773344',
appointmentDate: 'Rabu, 19 April 2025',
appointmentTime: '13:00 - 14:00 WIB',
serviceUnit: 'Kantor Imigrasi Bekasi',
status: 'Menunggu Pembayaran',
submissionDate: 'Senin, 14 April 2025 23:45',
serviceCode: 'EH-MK2YPQ',
applicationDetails: {
nationalIdNumber: '3271234560009120007',
gender: 'Wanita',
applicationType: 'Penggantian Paspor',
replacementReason: 'Hilang',
applicationPurpose: 'Kuliah di Luar Negeri',
passportType: 'PASPOR BIASA ELEKTRONIK 5 TAHUN',
fee: '650.000',
},
},
];
export default passportAppointmentData;

View File

@ -1,27 +1,31 @@
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';
// React & React Native Core
import React, {RefObject, useEffect, useRef, useState} from 'react';
import {BackHandler, StatusBar, Text, ToastAndroid, View} from 'react-native';
// React Navigation
import {useNavigation} from '@react-navigation/native';
import Colors from '../../../assets/styles/Colors';
import RadioButtonOptionComponent from '../../components/RadioButtonOption';
import {RootStackParamList} from '../../navigation/type';
import {NativeStackNavigationProp} from '@react-navigation/native-stack';
// Third-Party Libraries
import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
import {PaperProvider} from 'react-native-paper';
// Components
import StepIndicator from '../../components/StepIndicator';
import RadioButtonOptionComponent from '../../components/RadioButtonOption';
import DialogApplicationPassport from '../../components/dialog/DialogApplicationPassport';
import DialogDontHaveYetPassport from '../../components/dialog/DialogDontHaveYetPassport';
import DialogLostOrDamagedPassport from '../../components/dialog/DialogLostOrDamagedPassport';
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';
import DialogCivilStatusDocumentsInfo from '../../components/dialog/DialogCivilStatusDocumentsInfo';
import DialogSubmitSuccess from '../../components/dialog/DialogSubmitSuccess';
import DialogFinalizationConfirmation from '../../components/dialog/DialogFinalizationConfirmation';
import DialogPassportConditionInfo from '../../components/dialog/DialogPassportConditionInfo';
import DialogPassportTypeInfo from '../../components/dialog/DialogPassportTypeInfo';
import SheetEditData from '../../components/sheet/SheetEditData';
import SheetSearchLocation from '../../components/sheet/SheetSearchLocation';
import SheetSelectDate from '../../components/sheet/SheetSelectDate';
// Options Data
import passportForOptions from '../../data/Options/PassportForOptions';
import Step4ApplicantAdditionalDataSubStep2 from './steps/Step4ApplicantAdditionalData/Step4ApplicantAdditionalDataSubStep2';
import Step4ApplicantAdditionalDataSubStep1 from './steps/Step4ApplicantAdditionalData/Step4ApplicantAdditionalDataSubStep1';
// Steps - Step Screens
import Step1VerifyNikSubStep1 from './steps/Step1VerifyNik/Step1VerifyNikSubStep1';
import Step1VerifyNikSubStep2 from './steps/Step1VerifyNik/Step1VerifyNikSubStep2';
import Step1VerifyNikSubStep3 from './steps/Step1VerifyNik/Step1VerifyNikSubStep3';
@ -36,14 +40,28 @@ import Step2PassportApplicationQuestionnaireSubStep8 from './steps/Step2Passport
import Step2PassportApplicationQuestionnaireSubStep9 from './steps/Step2PassportApplicationQuestionnaire/Step2PassportApplicationQuestionnaireSubStep9';
import Step2PassportApplicationQuestionnaireSubStep10 from './steps/Step2PassportApplicationQuestionnaire/Step2PassportApplicationQuestionnaireSubStep10';
import Step2PassportApplicationQuestionnaireSubStep11 from './steps/Step2PassportApplicationQuestionnaire/Step2PassportApplicationQuestionnaireSubStep11';
import DialogCivilStatusDocumentsInfo from '../../components/dialog/DialogCivilStatusDocumentsInfo';
import DialogSubmitSuccess from '../../components/dialog/DialogSubmitSuccess';
import DialogFinalizationConfirmation from '../../components/dialog/DialogFinalizationConfirmation';
import DialogPassportConditionInfo from '../../components/dialog/DialogPassportConditionInfo';
import DialogPassportTypeInfo from '../../components/dialog/DialogPassportTypeInfo';
import SheetEditData from '../../components/sheet/SheetEditData';
import SheetSearchLocation from '../../components/sheet/SheetSearchLocation';
import SheetSelectDate from '../../components/sheet/SheetSelectDate';
import Step3UploadDocuments from './steps/Step3UploadDocuments/Step3UploadDocuments';
import Step4ApplicantAdditionalDataSubStep1 from './steps/Step4ApplicantAdditionalData/Step4ApplicantAdditionalDataSubStep1';
import Step4ApplicantAdditionalDataSubStep2 from './steps/Step4ApplicantAdditionalData/Step4ApplicantAdditionalDataSubStep2';
import Step5ApplicationTypeAndApplicantData from './steps/Step5ApplicationTypeAndApplicantData/Step5ApplicationTypeAndApplicantData';
import Step6ApplicationTypeAndApplicantData from './steps/Step6ApplicationTypeAndApplicantData/Step6ApplicationTypeAndApplicantData';
import Step7ApplicationFeeDetails from './steps/Step7ApplicationFeeDetails/Step7ApplicationFeeDetails';
// Navigation
import {RootStackParamList} from '../../navigation/type';
// Types
import {
StepValidationStatus,
StepValidationStatusSetter,
} from '../../../types/step';
// Data & Styles
import passportForOptions from '../../data/Options/PassportForOptions';
import Colors from '../../../assets/styles/Colors';
import styles from './styles';
import {changeStep} from '../../utils/stepNavigation';
import InfoToast from '../../components/InfoToast';
type RegularPassportScreenNavigationProp = NativeStackNavigationProp<
RootStackParamList,
@ -51,6 +69,7 @@ type RegularPassportScreenNavigationProp = NativeStackNavigationProp<
>;
type RenderApplicationStepsContentProps = {
navigation: any;
step: number;
subStep: number;
setStep: (step: number) => void;
@ -74,15 +93,16 @@ type RenderApplicationStepsContentProps = {
showSelectDateSheet: () => void;
selectedDestinationCountryOption: string;
setSelectedDestinationCountryOption: (val: string) => void;
setStepValidationStatus: React.Dispatch<
React.SetStateAction<Record<number, 'incomplete' | 'completed' | 'invalid'>>
>;
stepValidationStatus: StepValidationStatus;
setStepValidationStatus: StepValidationStatusSetter;
editedCompletedRef: RefObject<Set<number>>;
};
const RenderApplicationStepsContent = (
props: RenderApplicationStepsContentProps,
) => {
const {
navigation,
step,
subStep,
setStep,
@ -107,6 +127,7 @@ const RenderApplicationStepsContent = (
showSearchLocationSheet,
showSelectDateSheet,
setStepValidationStatus,
editedCompletedRef,
} = props;
if (step === 1) {
@ -118,14 +139,17 @@ const RenderApplicationStepsContent = (
case 3:
return (
<Step1VerifyNikSubStep3
step={step}
setStep={setStep}
setSubStep={setSubStep}
setStepValidationStatus={setStepValidationStatus}
onSubStepValidation={isValid => {
setStepValidationStatus(prev => ({
...prev,
1: isValid ? 'completed' : 'invalid',
}));
}}
editedCompletedRef={editedCompletedRef}
/>
);
default:
@ -138,11 +162,14 @@ const RenderApplicationStepsContent = (
case 1:
return (
<Step2PassportApplicationQuestionnaireSubStep1
step={step}
setStep={setStep}
setSubStep={setSubStep}
setStepValidationStatus={setStepValidationStatus}
selectedPassportOption={selectedPassportOption}
setSelectedPassportOption={setSelectedPassportOption}
showDontHaveYetDialog={showDontHaveYetDialog}
editedCompletedRef={editedCompletedRef}
/>
);
case 2:
@ -209,16 +236,27 @@ const RenderApplicationStepsContent = (
case 10:
return (
<Step2PassportApplicationQuestionnaireSubStep10
step={step}
setStep={setStep}
setSubStep={setSubStep}
setStepValidationStatus={setStepValidationStatus}
selectedDestinationCountryOption={selectedDestinationCountryOption}
onSubStepValidation={isValid => {
setStepValidationStatus(prev => ({
...prev,
2: isValid ? 'completed' : 'invalid',
}));
}}
editedCompletedRef={editedCompletedRef}
/>
);
case 11:
return (
<Step2PassportApplicationQuestionnaireSubStep11
step={step}
setStep={setStep}
setSubStep={setSubStep}
setStepValidationStatus={setStepValidationStatus}
selectedOption={selectedOption}
setSelectedOption={setSelectedOption}
onSubStepValidation={isValid => {
@ -227,6 +265,7 @@ const RenderApplicationStepsContent = (
2: isValid ? 'completed' : 'invalid',
}));
}}
editedCompletedRef={editedCompletedRef}
/>
);
default:
@ -239,17 +278,29 @@ const RenderApplicationStepsContent = (
case 1:
return (
<Step4ApplicantAdditionalDataSubStep1
step={step}
setStep={setStep}
setSubStep={setSubStep}
setStepValidationStatus={setStepValidationStatus}
checkedOption={checkedOption}
setCheckedOption={setCheckedOption}
editedCompletedRef={editedCompletedRef}
/>
);
case 2:
return (
<Step4ApplicantAdditionalDataSubStep2
step={step}
setStep={setStep}
setSubStep={setSubStep}
setStepValidationStatus={setStepValidationStatus}
onSubStepValidation={isValid => {
setStepValidationStatus(prev => ({
...prev,
4: isValid ? 'completed' : 'invalid',
}));
}}
editedCompletedRef={editedCompletedRef}
/>
);
}
@ -258,8 +309,10 @@ const RenderApplicationStepsContent = (
case 3:
return (
<Step3UploadDocuments
step={step}
setStep={setStep}
setSubStep={setSubStep}
setStepValidationStatus={setStepValidationStatus}
selectedPassportOption={selectedPassportOption}
showCivilStatusDocumentsInfoDialog={
showCivilStatusDocumentsInfoDialog
@ -271,14 +324,21 @@ const RenderApplicationStepsContent = (
3: isValid ? 'completed' : 'invalid',
}));
}}
editedCompletedRef={editedCompletedRef}
/>
);
case 5:
return (
<Step5ApplicationTypeAndApplicantData
setStep={setStep}
setSubStep={setSubStep}
showEditDataSheet={showEditDataSheet}
navigation={navigation}
onSubStepValidation={() => {
setStepValidationStatus(prev => ({
...prev,
5: 'completed',
}));
}}
/>
);
case 6:
@ -297,6 +357,12 @@ const RenderApplicationStepsContent = (
<Step7ApplicationFeeDetails
showSubmitSuccessDialog={showSubmitSuccessDialog}
setLastCompletedSteps={setLastCompletedSteps}
onSubStepValidation={() => {
setStepValidationStatus(prev => ({
...prev,
7: 'completed',
}));
}}
/>
);
default:
@ -362,9 +428,12 @@ function RegularPassportScreen() {
const [checkedOption, setCheckedOption] = useState(false);
const [showApplicationStepsContent, setShowApplicationStepsContent] =
useState(false);
const [toastVisible, setToastVisible] = useState(false);
const [toastMessage, setToastMessage] = useState('');
const [step, setStep] = useState(1);
const [subStep, setSubStep] = useState(1);
const [completedSteps, setCompletedSteps] = useState<number[]>(
[...Array(step - 1)].map((_, i) => i + 1),
);
@ -512,6 +581,11 @@ function RegularPassportScreen() {
});
};
const showInfoToast = (msg: string) => {
setToastMessage(msg);
setToastVisible(true);
};
// Render steps or questionnaire
const renderApplicationStepsContent = showApplicationStepsContent ? (
<>
@ -523,67 +597,33 @@ function RegularPassportScreen() {
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 isCurrentStep7 = step === 7;
const isTargetStepIn1to6 = targetStep >= 1 && targetStep <= 6;
const isStep1to4Completed = [1, 2, 3, 4].every(
s => stepValidationStatus[s] === 'completed',
);
const toastMessage = isCurrentStep7
? 'Tak dapat kembali langkah terakhir.'
: !isTargetStepIn1to6
? 'Lengkapi langkah 1 6 dulu'
: null;
if (!isCurrentStepIn5to7 && isTargetStepIn5to7) {
ToastAndroid.show(
'Lengkapi langkah 1 4 dulu',
ToastAndroid.SHORT,
);
if (toastMessage) {
showInfoToast(toastMessage);
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;
changeStep({
currentStep: step,
targetStep: targetStep,
setStep,
setSubStep: () => setSubStep(1),
setStepValidationStatus,
editedCompletedRef,
});
setStep(targetStep);
setSubStep(1);
}}
/>
<RenderApplicationStepsContent
navigation={navigation}
step={step}
subStep={subStep}
setStep={setStep}
@ -613,7 +653,9 @@ function RegularPassportScreen() {
showEditDataSheet={showEditDataSheet}
showSearchLocationSheet={showSearchLocationSheet}
showSelectDateSheet={showSelectDateSheet}
stepValidationStatus={stepValidationStatus}
setStepValidationStatus={setStepValidationStatus}
editedCompletedRef={editedCompletedRef}
/>
</View>
@ -672,7 +714,19 @@ function RegularPassportScreen() {
visible={visibleFinalizationConfirmationDialog}
onClose={hideFinalizationConfirmationDialog}
onContinue={() => {
setStep(7);
setStepValidationStatus(prev => ({
...prev,
6: 'completed',
}));
const canProceedToStep7 = [1, 2, 3, 4, 5, 6].every(
s => stepValidationStatus[s] === 'completed',
);
!canProceedToStep7
? showInfoToast('Lengkapi semua langkah terlebih dahulu.')
: setStep(7);
hideFinalizationConfirmationDialog();
}}
/>
@ -753,6 +807,11 @@ function RegularPassportScreen() {
setShowApplicationStepsContent(true);
}}
/>
<InfoToast
visible={toastVisible}
message={toastMessage}
onDismiss={() => setToastVisible(false)}
/>
</PaperProvider>
</View>
);

View File

@ -1,4 +1,4 @@
import React from 'react';
import React, {RefObject, useState} from 'react';
import {ScrollView, View} from 'react-native';
import {Button} from 'react-native-paper';
import styles from '../styles';
@ -6,23 +6,31 @@ import TextInputComponent from '../../../../components/TextInput';
import genderData from '../../../../data/DropdownData/GenderData';
import civilStatusData from '../../../../data/DropdownData/CivilStatusData';
import Colors from '../../../../../assets/styles/Colors';
import {changeStep} from '../../../../utils/stepNavigation';
import {StepValidationStatusSetter} from '../../../../../types/step';
type Step1VerifyNikSubStep3Props = {
step: number;
setStep: (val: number) => void;
setSubStep: (val: number) => void;
setStepValidationStatus: StepValidationStatusSetter;
onSubStepValidation: (isValid: boolean) => void;
editedCompletedRef: RefObject<Set<number>>;
};
const Step1VerifyNikSubStep3 = ({
step,
setStep,
setSubStep,
setStepValidationStatus,
onSubStepValidation,
editedCompletedRef,
}: 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 [fullName, setFullName] = useState('');
const [nik, setNik] = useState('');
const [birthDate, setBirthDate] = useState('');
const [gender, setGender] = useState('');
const [civilStatus, setCivilStatus] = useState('');
const onNextPress = () => {
const isFormValid =
@ -38,8 +46,14 @@ const Step1VerifyNikSubStep3 = ({
onSubStepValidation(false);
}
setStep(2);
setSubStep(1);
changeStep({
currentStep: step,
targetStep: 2,
setStep,
setSubStep: () => setSubStep(1),
setStepValidationStatus,
editedCompletedRef,
});
};
return (

View File

@ -1,36 +1,52 @@
import React from 'react';
import React, {RefObject, useRef} from 'react';
import {View, Pressable, Text} from 'react-native';
import styles from '../styles';
import RadioButtonOptionComponent from '../../../../components/RadioButtonOption';
import hasHadPassportBeforeOptions from '../../../../data/Options/HasHadPassportBeforeOptions';
import {Button} from 'react-native-paper';
import Colors from '../../../../../assets/styles/Colors';
import {changeStep} from '../../../../utils/stepNavigation';
import {StepValidationStatusSetter} from '../../../../../types/step';
type Step2PassportApplicationQuestionnaireSubStep1Props = {
step: number;
setStep: (step: number) => void;
setSubStep: (subStep: number) => void;
setStepValidationStatus: StepValidationStatusSetter;
selectedPassportOption: string;
setSelectedPassportOption: (value: string) => void;
showDontHaveYetDialog: () => void;
editedCompletedRef: RefObject<Set<number>>;
};
const Step2PassportApplicationQuestionnaireSubStep1 = ({
step,
setStep,
setSubStep,
setStepValidationStatus,
selectedPassportOption,
setSelectedPassportOption,
showDontHaveYetDialog,
editedCompletedRef,
}: Step2PassportApplicationQuestionnaireSubStep1Props) => {
const onBackPress = () => {
changeStep({
currentStep: step,
targetStep: 1,
setStep,
setSubStep: () => setSubStep(3),
setStepValidationStatus,
editedCompletedRef,
});
};
return (
<View style={styles.subStepContainer}>
<Pressable
style={({pressed}) => ({
transform: [{scale: pressed ? 0.99 : 1}],
})}
onPress={() => {
setStep(1);
setSubStep(3);
}}>
onPress={onBackPress}>
<Button
mode="contained"
icon="chevron-left"

View File

@ -1,21 +1,62 @@
import React from 'react';
import React, {RefObject, useState} from 'react';
import {ScrollView, View, Text, Pressable} from 'react-native';
import {Button} from 'react-native-paper';
import styles from '../styles';
import TextInputComponent from '../../../../components/TextInput';
import Colors from '../../../../../assets/styles/Colors';
import familyRelationshipData from '../../../../data/DropdownData/FamilyRelationshipData';
import {changeStep} from '../../../../utils/stepNavigation';
import {StepValidationStatusSetter} from '../../../../../types/step';
type Step2PassportApplicationQuestionnaireSubStep10Props = {
selectedDestinationCountryOption: string;
step: number;
setStep: (step: number) => void;
setSubStep: (step: number) => void;
setStepValidationStatus: StepValidationStatusSetter;
selectedDestinationCountryOption: string;
onSubStepValidation: (isValid: boolean) => void;
editedCompletedRef: RefObject<Set<number>>;
};
const Step2PassportApplicationQuestionnaireSubStep10 = (
props: Step2PassportApplicationQuestionnaireSubStep10Props,
) => {
const {selectedDestinationCountryOption, setStep, setSubStep} = props;
const {
step,
setStep,
setSubStep,
setStepValidationStatus,
selectedDestinationCountryOption,
onSubStepValidation,
editedCompletedRef,
} = props;
const [relativeName, setRelativeName] = useState('');
const [relativePhone, setRelativePhone] = useState('');
const [relativeRelationship, setRelativeRelationship] = useState('');
const onNextPress = () => {
const isFormValid =
relativeName.trim() !== '' &&
relativePhone.trim() !== '' &&
relativeRelationship.trim() !== '';
if (selectedDestinationCountryOption === 'destination_country_not_set') {
if (isFormValid) {
onSubStepValidation(true);
}
changeStep({
currentStep: step,
targetStep: 3,
setStep,
setStepValidationStatus,
editedCompletedRef,
});
} else {
setSubStep(11);
}
};
return (
<ScrollView>
<View style={styles.subStepContainer}>
@ -47,12 +88,16 @@ const Step2PassportApplicationQuestionnaireSubStep10 = (
title="Nama Kerabat"
placeholder="Masukkan Nama Kerabat Anda"
isRequired
value={relativeName}
onChangeText={setRelativeName}
/>
<TextInputComponent
title="Nomor Telepon"
placeholder="Contoh: 08513456789"
isRequired
value={relativePhone}
onChangeText={setRelativePhone}
/>
<TextInputComponent
@ -61,16 +106,14 @@ const Step2PassportApplicationQuestionnaireSubStep10 = (
isRequired
isDropdown
dropdownItemData={familyRelationshipData}
value={relativeRelationship}
onChangeText={setRelativeRelationship}
/>
</View>
<Button
mode="contained"
onPress={() => {
selectedDestinationCountryOption === 'destination_country_not_set'
? setStep(3)
: setSubStep(11);
}}
onPress={onNextPress}
style={styles.subStepButtonContained}
textColor={Colors.neutral100.color}>
Lanjut

View File

@ -1,4 +1,4 @@
import React, {useState} from 'react';
import React, {RefObject, useState} from 'react';
import {ScrollView, View, Text, Pressable} from 'react-native';
import {Button} from 'react-native-paper';
import styles from '../styles';
@ -8,21 +8,29 @@ import destinationFamilyContactOptions from '../../../../data/Options/Destinatio
import familyRelationshipData from '../../../../data/DropdownData/FamilyRelationshipData';
import RadioButtonOptionComponent from '../../../../components/RadioButtonOption';
import Colors from '../../../../../assets/styles/Colors';
import {changeStep} from '../../../../utils/stepNavigation';
import {StepValidationStatusSetter} from '../../../../../types/step';
type Step2PassportApplicationQuestionnaireSubStep11Props = {
step: number;
setStep: (step: number) => void;
setSubStep: (subStep: number) => void;
setStepValidationStatus: StepValidationStatusSetter;
selectedOption: string;
setSelectedOption: (value: string) => void;
onSubStepValidation: (isValid: boolean) => void;
editedCompletedRef: RefObject<Set<number>>;
};
const Step2PassportApplicationQuestionnaireSubStep11 = ({
step,
setStep,
setSubStep,
setStepValidationStatus,
selectedOption,
setSelectedOption,
onSubStepValidation,
editedCompletedRef,
}: Step2PassportApplicationQuestionnaireSubStep11Props) => {
const [relativeName, setRelativeName] = useState('');
const [phoneNumber, setPhoneNumber] = useState('');
@ -41,7 +49,13 @@ const Step2PassportApplicationQuestionnaireSubStep11 = ({
onSubStepValidation(false);
}
setStep(3);
changeStep({
currentStep: step,
targetStep: 3,
setStep,
setStepValidationStatus,
editedCompletedRef,
});
};
return (

View File

@ -1,4 +1,4 @@
import React, {useState} from 'react';
import React, {RefObject, useState} from 'react';
import {ScrollView, View, Text, Pressable} from 'react-native';
import styles from '../styles';
import Colors from '../../../../../assets/styles/Colors';
@ -7,6 +7,8 @@ import genderData from '../../../../data/DropdownData/GenderData';
import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
import {Button} from 'react-native-paper';
import TextInputComponent from '../../../../components/TextInput';
import {changeStep} from '../../../../utils/stepNavigation';
import { StepValidationStatusSetter } from '../../../../../types/step';
interface BackButtonProps {
onPress: () => void;
@ -22,12 +24,15 @@ interface DocumentUploadSectionProps {
}
interface Step3UploadDocumentsProps {
step: number;
setStep: (step: number) => void;
setSubStep: (subStep: number) => void;
setStepValidationStatus: StepValidationStatusSetter;
selectedPassportOption: string;
selectedDestinationCountryOption: string;
showCivilStatusDocumentsInfoDialog: () => void;
onSubStepValidation: (isValid: boolean) => void;
editedCompletedRef: RefObject<Set<number>>;
}
const BackButton = (props: BackButtonProps) => {
@ -149,12 +154,15 @@ const DocumentUploadSection = (props: DocumentUploadSectionProps) => {
const Step3UploadDocuments = (props: Step3UploadDocumentsProps) => {
const {
step,
setStep,
setSubStep,
setStepValidationStatus,
selectedPassportOption,
selectedDestinationCountryOption,
showCivilStatusDocumentsInfoDialog,
onSubStepValidation,
editedCompletedRef,
} = props;
const [isKTPUploaded, setIsKTPUploaded] = useState(false);
@ -162,6 +170,22 @@ const Step3UploadDocuments = (props: Step3UploadDocumentsProps) => {
const [isCivilStatusUploaded, setIsCivilStatusUploaded] = useState(false);
const [isOldPassportUploaded, setIsOldPassportUploaded] = useState(false);
const onBackPress = () => {
changeStep({
currentStep: step,
targetStep: 2,
setStep,
setSubStep: () =>
setSubStep(
selectedDestinationCountryOption === 'destination_country_not_set'
? 10
: 11,
),
setStepValidationStatus,
editedCompletedRef,
});
};
const onNextPress = () => {
const isFormValid =
isKTPUploaded &&
@ -175,23 +199,20 @@ const Step3UploadDocuments = (props: Step3UploadDocumentsProps) => {
onSubStepValidation(false);
}
setStep(4);
setSubStep(1);
changeStep({
currentStep: step,
targetStep: 4,
setStep,
setSubStep: () => setSubStep(1),
setStepValidationStatus,
editedCompletedRef,
});
};
return (
<ScrollView>
<View style={styles.subStepContainer}>
<BackButton
onPress={() => {
setStep(2);
setSubStep(
selectedDestinationCountryOption === 'destination_country_not_set'
? 10
: 11,
);
}}
/>
<BackButton onPress={onBackPress} />
<View style={{marginTop: 12, marginBottom: 16, gap: 4}}>
<Text style={styles.subStepDesc}>

View File

@ -1,4 +1,4 @@
import React from 'react';
import React, {RefObject} from 'react';
import {ScrollView, View, Pressable, Text} from 'react-native';
import {Checkbox, Button} from 'react-native-paper';
import Icon from 'react-native-vector-icons/MaterialIcons';
@ -9,17 +9,40 @@ import postalCodeData from '../../../../data/DropdownData/PostalCodeData';
import districtData from '../../../../data/DropdownData/DistrictData';
import cityData from '../../../../data/DropdownData/CityData';
import provinceData from '../../../../data/DropdownData/ProvinceData';
import {changeStep} from '../../../../utils/stepNavigation';
import { StepValidationStatusSetter } from '../../../../../types/step';
type Step4ApplicantAdditionalDataSubStep1Props = {
step: number;
setStep: (step: number) => void;
setSubStep: (subStep: number) => void;
setStepValidationStatus: StepValidationStatusSetter;
checkedOption: boolean;
setCheckedOption: React.Dispatch<React.SetStateAction<boolean>>;
editedCompletedRef: RefObject<Set<number>>;
};
const Step4ApplicantAdditionalDataSubStep1: React.FC<
Step4ApplicantAdditionalDataSubStep1Props
> = ({setStep, setSubStep, checkedOption, setCheckedOption}) => {
> = ({
step,
setStep,
setSubStep,
setStepValidationStatus,
checkedOption,
setCheckedOption,
editedCompletedRef,
}) => {
const onBackPress = () => {
changeStep({
currentStep: step,
targetStep: 3,
setStep,
setStepValidationStatus,
editedCompletedRef,
});
};
return (
<ScrollView>
<View style={styles.subStepContainer}>
@ -28,9 +51,7 @@ const Step4ApplicantAdditionalDataSubStep1: React.FC<
transform: [{scale: pressed ? 0.99 : 1}],
marginBottom: 12,
})}
onPress={() => {
setStep(3);
}}>
onPress={onBackPress}>
<Button
mode="contained"
icon="chevron-left"

View File

@ -1,6 +1,5 @@
import React from 'react';
import React, {RefObject, useState} from 'react';
import {ScrollView, View, Text, Pressable} from 'react-native';
import Icon from 'react-native-vector-icons/MaterialIcons';
import styles from '../styles';
import TextInputComponent from '../../../../components/TextInput';
import {Button} from 'react-native-paper';
@ -9,14 +8,96 @@ import jobData from '../../../../data/DropdownData/JobData';
import nationalityData from '../../../../data/DropdownData/NationalityData';
import {PassportAppointment} from '../../../../navigation/type';
import {addData, getData} from '../../../../helper/asyncStorageHelper';
import {changeStep} from '../../../../utils/stepNavigation';
import { StepValidationStatusSetter } from '../../../../../types/step';
const Step4ApplicantAdditionalDataSubStep2 = ({
setStep,
setSubStep,
}: {
type Step4ApplicantAdditionalDataSubStep2Props = {
step: number;
setStep: (step: number) => void;
setSubStep: (subStep: number) => void;
}) => {
setStepValidationStatus: StepValidationStatusSetter;
onSubStepValidation: (isValid: boolean) => void;
editedCompletedRef: RefObject<Set<number>>;
};
const Step4ApplicantAdditionalDataSubStep2 = ({
step,
setStep,
setSubStep,
setStepValidationStatus,
onSubStepValidation,
editedCompletedRef,
}: Step4ApplicantAdditionalDataSubStep2Props) => {
const [job, setJob] = useState('');
const [phone, setPhone] = useState('');
const [motherName, setMotherName] = useState('');
const [motherNation, setMotherNation] = useState('');
const [motherAddress, setMotherAddress] = useState('');
const isFormValid =
job.trim() !== '' &&
phone.trim() !== '' &&
motherName.trim() !== '' &&
motherNation.trim() !== '' &&
motherAddress.trim() !== '';
const handleSaveDraft = async () => {
if (isFormValid) {
onSubStepValidation(true);
// Ambil data appointment yang sudah tersimpan
const storedAppointments: PassportAppointment[] =
(await getData('passportAppointments')) || [];
// Ambil ID terakhir dan hitung ID baru
const lastId = storedAppointments.length
? Math.max(...storedAppointments.map(item => Number(item.id)))
: 0;
const nextId = (lastId + 1).toString();
// Buat appointment baru dengan ID yang sudah dihitung
const newAppointment: PassportAppointment = {
id: nextId,
applicantName: 'Salwa Aisyah Adhani',
applicantCode: '1038000008887777',
appointmentDate: 'Selasa, 20 Mei 2025',
appointmentTime: '10:00-11:00 WIB',
serviceUnit: 'Kantor Imigrasi Depok',
status: 'Menunggu Pembayaran',
submissionDate: 'Kamis, 15 Mei 2025 21:30',
serviceCode: 'EH-LP7RNC',
applicationDetails: {
nationalIdNumber: '3271234560009123456',
gender: 'Wanita',
applicationType: 'Penggantian Paspor',
replacementReason: 'Penuh/Halaman Penuh',
applicationPurpose: 'Wisata/Liburan',
passportType: 'PASPOR ELEKTRONIK POLIKARBONAT 5 TAHUN',
fee: '650.000',
},
};
// Simpan appointment baru
await addData<PassportAppointment>(
'passportAppointments',
newAppointment,
);
const updatedAppointments = await getData('passportAppointments');
console.log('Data yang berhasil ditambahkan:', updatedAppointments);
} else {
onSubStepValidation(false);
}
changeStep({
currentStep: step,
targetStep: 5,
setStep,
setStepValidationStatus,
editedCompletedRef,
});
};
return (
<ScrollView>
<View style={styles.subStepContainer}>
@ -37,6 +118,7 @@ const Step4ApplicantAdditionalDataSubStep2 = ({
Kembali
</Button>
</Pressable>
<Text style={styles.subStepDesc}>
Data di bawah ini harus sesuai dengan keterangan pada KTP pemohon.
Data yang bertanda (
@ -58,11 +140,15 @@ const Step4ApplicantAdditionalDataSubStep2 = ({
isRequired
isDropdown
dropdownItemData={jobData}
value={job}
onChangeText={setJob}
/>
<TextInputComponent
title="Nomor Telepon"
placeholder="Contoh: 08513456789"
isRequired
value={phone}
onChangeText={setPhone}
/>
</View>
@ -78,6 +164,8 @@ const Step4ApplicantAdditionalDataSubStep2 = ({
title="Nama Ibu"
placeholder="Masukkan nama lengkap ibu"
isRequired
value={motherName}
onChangeText={setMotherName}
/>
<TextInputComponent
title="Kewarganegaraan Ibu"
@ -85,6 +173,8 @@ const Step4ApplicantAdditionalDataSubStep2 = ({
isRequired
isDropdown
dropdownItemData={nationalityData}
value={motherNation}
onChangeText={setMotherNation}
/>
<TextInputComponent
title="Alamat Ibu"
@ -93,6 +183,8 @@ const Step4ApplicantAdditionalDataSubStep2 = ({
supportText="0/100 karakter"
containerHeight={90}
isMultiline
value={motherAddress}
onChangeText={setMotherAddress}
/>
</View>
@ -152,49 +244,7 @@ const Step4ApplicantAdditionalDataSubStep2 = ({
<Button
mode="contained"
onPress={async () => {
// Ambil data appointment yang sudah tersimpan
const storedAppointments: PassportAppointment[] =
(await getData('passportAppointments')) || [];
// Ambil ID terakhir dan hitung ID baru
const lastId = storedAppointments.length
? Math.max(...storedAppointments.map(item => Number(item.id)))
: 0;
const nextId = (lastId + 1).toString();
// Buat appointment baru dengan ID yang sudah dihitung
const newAppointment: PassportAppointment = {
id: nextId,
applicantName: 'Salwa Aisyah Adhani',
applicantCode: '1038000008887777',
appointmentDate: 'Selasa, 20 Mei 2025',
appointmentTime: '10:00-11:00 WIB',
serviceUnit: 'Kantor Imigrasi Depok',
status: 'Menunggu Pembayaran',
submissionDate: 'Kamis, 15 Mei 2025 21:30',
serviceCode: 'EH-LP7RNC',
applicationDetails: {
nationalIdNumber: '3271234560009123456',
gender: 'Wanita',
applicationType: 'Penggantian Paspor',
replacementReason: 'Penuh/Halaman Penuh',
applicationPurpose: 'Wisata/Liburan',
passportType: 'PASPOR ELEKTRONIK POLIKARBONAT 5 TAHUN',
fee: '650.000',
},
};
// Simpan appointment baru
await addData<PassportAppointment>(
'passportAppointments',
newAppointment,
);
const updatedAppointments = await getData('passportAppointments');
console.log('Data yang berhasil ditambahkan:', updatedAppointments);
setStep(5);
}}
onPress={handleSaveDraft}
style={[styles.subStepButtonContained, {marginBottom: 8}]}
textColor={Colors.neutral100.color}>
Simpan Draft

View File

@ -9,12 +9,15 @@ import {PassportAppointment} from '../../../../navigation/type';
type Step5ApplicationTypeAndApplicantDataProps = {
setStep: (step: number) => void;
setSubStep: (subStep: number) => void;
showEditDataSheet: () => void;
navigation: any;
onSubStepValidation: (isValid: boolean) => void;
};
const Step5Content = (props: Step5ApplicationTypeAndApplicantDataProps) => {
const {setStep, setSubStep, showEditDataSheet} = props;
const Step5ApplicationTypeAndApplicantData = (
props: Step5ApplicationTypeAndApplicantDataProps,
) => {
const {setStep, showEditDataSheet, navigation, onSubStepValidation} = props;
const [lastAppointment, setLastAppointment] = useState<PassportAppointment>();
@ -32,6 +35,18 @@ const Step5Content = (props: Step5ApplicationTypeAndApplicantDataProps) => {
fetchData();
}, []);
const onNextPress = () => {
onSubStepValidation(true);
setStep(6);
};
const onBackHomePress = () => {
navigation.reset({
index: 0,
routes: [{name: 'NavigationRoute'}],
});
};
return (
<View style={styles.subStepContainer}>
<Text style={styles.subStepDesc}>
@ -104,20 +119,17 @@ const Step5Content = (props: Step5ApplicationTypeAndApplicantDataProps) => {
<View style={[styles.subStepButtonContainer, {marginTop: 12}]}>
<Button
mode="contained"
onPress={() => setStep(6)}
onPress={onNextPress}
style={styles.subStepButtonContained}
textColor={Colors.neutral100.color}>
Lanjut
</Button>
<Button
mode="outlined"
onPress={() => {
setStep(4);
setSubStep(2);
}}
onPress={onBackHomePress}
style={styles.subStepButtonOutlined}
textColor={Colors.primary30.color}>
Kembali
Beranda
</Button>
</View>
</View>
@ -137,4 +149,4 @@ const DetailRow = ({
</View>
);
export default Step5Content;
export default Step5ApplicationTypeAndApplicantData;

View File

@ -1,4 +1,4 @@
import React from 'react';
import React, {useState} from 'react';
import {ScrollView, View, Text} from 'react-native';
import {Button, Divider} from 'react-native-paper';
import TextInputComponent from '../../../../components/TextInput';
@ -15,13 +15,16 @@ type Step6ApplicationTypeAndApplicantDataProps = {
showSelectDateSheet: () => void;
};
const Step6ApplicationTypeAndApplicantData = (props: Step6ApplicationTypeAndApplicantDataProps) => {
const Step6ApplicationTypeAndApplicantData = (
props: Step6ApplicationTypeAndApplicantDataProps,
) => {
const {
showFinalizationConfirmationDialog,
showPassportTypeInfoDialog,
showSearchLocationSheet,
showSelectDateSheet,
} = props;
return (
<ScrollView>
<View style={styles.subStepContainer}>

View File

@ -12,10 +12,12 @@ import {getData} from '../../../../helper/asyncStorageHelper';
type Step7ApplicationFeeDetailsProps = {
showSubmitSuccessDialog: () => void;
setLastCompletedSteps: () => void;
onSubStepValidation: (isValid: boolean) => void;
};
const Step7ApplicationFeeDetails = (props: Step7ApplicationFeeDetailsProps) => {
const {showSubmitSuccessDialog, setLastCompletedSteps} = props;
const {showSubmitSuccessDialog, setLastCompletedSteps, onSubStepValidation} =
props;
const [lastAppointment, setLastAppointment] = useState<PassportAppointment>();
useEffect(() => {
@ -32,6 +34,12 @@ const Step7ApplicationFeeDetails = (props: Step7ApplicationFeeDetailsProps) => {
fetchData();
}, []);
const onNextPress = () => {
onSubStepValidation(true);
showSubmitSuccessDialog();
setLastCompletedSteps();
};
return (
<ScrollView>
<View style={[styles.subStepContainer, {paddingBottom: 0}]}>
@ -215,10 +223,7 @@ const Step7ApplicationFeeDetails = (props: Step7ApplicationFeeDetailsProps) => {
mode="contained"
style={styles.subStepButtonContained}
textColor={Colors.neutral100.color}
onPress={() => {
showSubmitSuccessDialog();
setLastCompletedSteps();
}}>
onPress={onNextPress}>
Kembali ke Halaman Utama
</Button>
</View>

View File

@ -0,0 +1,53 @@
import {RefObject} from 'react';
export type StepStatus = 'completed' | 'incomplete' | 'invalid';
interface StepChangeParams {
currentStep: number;
targetStep: number;
setStep: (step: number) => void;
setSubStep?: (sub: number) => void;
setStepValidationStatus: React.Dispatch<
React.SetStateAction<Record<number, StepStatus>>
>;
editedCompletedRef: RefObject<Set<number>>;
}
export function changeStep({
currentStep,
targetStep,
setStep,
setSubStep,
setStepValidationStatus,
editedCompletedRef,
}: StepChangeParams) {
setStepValidationStatus(prev => {
const next = {...prev};
if (currentStep !== targetStep &&
editedCompletedRef.current?.has(currentStep)) {
next[currentStep] = 'completed';
editedCompletedRef.current.delete(currentStep);
}
if (prev[targetStep] === 'completed') {
editedCompletedRef.current?.add(targetStep);
}
next[targetStep] = 'incomplete';
if (targetStep > currentStep) {
for (let s = 1; s < targetStep; s++) {
if (next[s] !== 'completed') next[s] = 'invalid';
}
} else if (targetStep < currentStep) {
for (let s = currentStep; s > targetStep; s--) {
if (next[s] !== 'completed') next[s] = 'invalid';
}
}
return next;
});
setStep(targetStep);
if (setSubStep) setSubStep(1);
}

7
types/step.ts Normal file
View File

@ -0,0 +1,7 @@
export type StepStatus = 'incomplete' | 'completed' | 'invalid';
export type StepValidationStatus = Record<number, StepStatus>;
export type StepValidationStatusSetter = React.Dispatch<
React.SetStateAction<StepValidationStatus>
>;