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}
/>