Finalized all of the passport application flow feature and and adding a feature to add data

This commit is contained in:
Mochammad Adhi Buchori
2025-04-26 09:41:08 +07:00
parent ad67df1461
commit 20cd765338
28 changed files with 1241 additions and 266 deletions

110
package-lock.json generated
View File

@ -8,13 +8,16 @@
"name": "mpaspor", "name": "mpaspor",
"version": "0.0.1", "version": "0.0.1",
"dependencies": { "dependencies": {
"@react-native-async-storage/async-storage": "^2.1.2",
"@react-native-community/datetimepicker": "^8.3.0", "@react-native-community/datetimepicker": "^8.3.0",
"@react-navigation/elements": "^2.3.8", "@react-navigation/elements": "^2.3.8",
"@react-navigation/native": "^7.1.6", "@react-navigation/native": "^7.1.6",
"@react-navigation/native-stack": "^7.3.10", "@react-navigation/native-stack": "^7.3.10",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"moment": "^2.30.1",
"react": "19.0.0", "react": "19.0.0",
"react-native": "0.78.0", "react-native": "0.78.0",
"react-native-calendars": "^1.1311.1",
"react-native-element-dropdown": "^2.12.4", "react-native-element-dropdown": "^2.12.4",
"react-native-paper": "^5.13.2", "react-native-paper": "^5.13.2",
"react-native-safe-area-context": "^5.4.0", "react-native-safe-area-context": "^5.4.0",
@ -2609,6 +2612,18 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/@react-native-async-storage/async-storage": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-2.1.2.tgz",
"integrity": "sha512-dvlNq4AlGWC+ehtH12p65+17V0Dx7IecOWl6WanF2ja38O1Dcjjvn7jVzkUHJ5oWkQBlyASurTPlTHgKXyYiow==",
"license": "MIT",
"dependencies": {
"merge-options": "^3.0.4"
},
"peerDependencies": {
"react-native": "^0.0.0-0 || >=0.65 <1.0"
}
},
"node_modules/@react-native-community/cli": { "node_modules/@react-native-community/cli": {
"version": "15.0.1", "version": "15.0.1",
"resolved": "https://registry.npmjs.org/@react-native-community/cli/-/cli-15.0.1.tgz", "resolved": "https://registry.npmjs.org/@react-native-community/cli/-/cli-15.0.1.tgz",
@ -7576,6 +7591,15 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/is-plain-obj": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz",
"integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/is-plain-object": { "node_modules/is-plain-object": {
"version": "2.0.4", "version": "2.0.4",
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz",
@ -9182,6 +9206,18 @@
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
"integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==" "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q=="
}, },
"node_modules/merge-options": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/merge-options/-/merge-options-3.0.4.tgz",
"integrity": "sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ==",
"license": "MIT",
"dependencies": {
"is-plain-obj": "^2.1.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/merge-stream": { "node_modules/merge-stream": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
@ -9678,6 +9714,15 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/moment": {
"version": "2.30.1",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
"integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
"license": "MIT",
"engines": {
"node": "*"
}
},
"node_modules/ms": { "node_modules/ms": {
"version": "2.1.3", "version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@ -10557,6 +10602,38 @@
} }
} }
}, },
"node_modules/react-native-calendars": {
"version": "1.1311.1",
"resolved": "https://registry.npmjs.org/react-native-calendars/-/react-native-calendars-1.1311.1.tgz",
"integrity": "sha512-/he1+4irh/643SOnnWlEfoAsHhKq2v8WFRMbcXr+dGgf5hcl/TeVgUINw3Vb2SsAxYM2dV+mVCaNJTbMjS025Q==",
"license": "MIT",
"dependencies": {
"hoist-non-react-statics": "^3.3.1",
"lodash": "^4.17.15",
"memoize-one": "^5.2.1",
"prop-types": "^15.5.10",
"react-native-safe-area-context": "4.5.0",
"react-native-swipe-gestures": "^1.0.5",
"recyclerlistview": "^4.0.0",
"xdate": "^0.8.0"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"moment": "^2.29.4"
}
},
"node_modules/react-native-calendars/node_modules/react-native-safe-area-context": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-4.5.0.tgz",
"integrity": "sha512-0WORnk9SkREGUg2V7jHZbuN5x4vcxj/1B0QOcXJjdYWrzZHgLcUzYWWIUecUPJh747Mwjt/42RZDOaFn3L8kPQ==",
"license": "MIT",
"peerDependencies": {
"react": "*",
"react-native": "*"
}
},
"node_modules/react-native-element-dropdown": { "node_modules/react-native-element-dropdown": {
"version": "2.12.4", "version": "2.12.4",
"resolved": "https://registry.npmjs.org/react-native-element-dropdown/-/react-native-element-dropdown-2.12.4.tgz", "resolved": "https://registry.npmjs.org/react-native-element-dropdown/-/react-native-element-dropdown-2.12.4.tgz",
@ -10662,6 +10739,12 @@
"react-native-svg": ">=12.0.0" "react-native-svg": ">=12.0.0"
} }
}, },
"node_modules/react-native-swipe-gestures": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/react-native-swipe-gestures/-/react-native-swipe-gestures-1.0.5.tgz",
"integrity": "sha512-Ns7Bn9H/Tyw278+5SQx9oAblDZ7JixyzeOczcBK8dipQk2pD7Djkcfnf1nB/8RErAmMLL9iXgW0QHqiII8AhKw==",
"license": "MIT"
},
"node_modules/react-native-vector-icons": { "node_modules/react-native-vector-icons": {
"version": "10.2.0", "version": "10.2.0",
"resolved": "https://registry.npmjs.org/react-native-vector-icons/-/react-native-vector-icons-10.2.0.tgz", "resolved": "https://registry.npmjs.org/react-native-vector-icons/-/react-native-vector-icons-10.2.0.tgz",
@ -10832,6 +10915,21 @@
"node": ">= 4" "node": ">= 4"
} }
}, },
"node_modules/recyclerlistview": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/recyclerlistview/-/recyclerlistview-4.2.3.tgz",
"integrity": "sha512-STR/wj/FyT8EMsBzzhZ1l2goYirMkIgfV3gYEPxI3Kf3lOnu6f7Dryhyw7/IkQrgX5xtTcDrZMqytvteH9rL3g==",
"license": "Apache-2.0",
"dependencies": {
"lodash.debounce": "4.0.8",
"prop-types": "15.8.1",
"ts-object-utils": "0.0.5"
},
"peerDependencies": {
"react": ">= 15.2.1",
"react-native": ">= 0.30.0"
}
},
"node_modules/reflect.getprototypeof": { "node_modules/reflect.getprototypeof": {
"version": "1.0.10", "version": "1.0.10",
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
@ -12058,6 +12156,12 @@
"typescript": ">=4.2.0" "typescript": ">=4.2.0"
} }
}, },
"node_modules/ts-object-utils": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/ts-object-utils/-/ts-object-utils-0.0.5.tgz",
"integrity": "sha512-iV0GvHqOmilbIKJsfyfJY9/dNHCs969z3so90dQWsO1eMMozvTpnB1MEaUbb3FYtZTGjv5sIy/xmslEz0Rg2TA==",
"license": "ISC"
},
"node_modules/tslib": { "node_modules/tslib": {
"version": "2.8.1", "version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
@ -12569,6 +12673,12 @@
"async-limiter": "~1.0.0" "async-limiter": "~1.0.0"
} }
}, },
"node_modules/xdate": {
"version": "0.8.3",
"resolved": "https://registry.npmjs.org/xdate/-/xdate-0.8.3.tgz",
"integrity": "sha512-1NhJWPJwN+VjbkACT9XHbQK4o6exeSVtS2CxhMPwUE7xQakoEFTlwra9YcqV/uHQVyeEUYoYC46VGDJ+etnIiw==",
"license": "(MIT OR GPL-2.0)"
},
"node_modules/y18n": { "node_modules/y18n": {
"version": "5.0.8", "version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",

View File

@ -10,13 +10,16 @@
"test": "jest" "test": "jest"
}, },
"dependencies": { "dependencies": {
"@react-native-async-storage/async-storage": "^2.1.2",
"@react-native-community/datetimepicker": "^8.3.0", "@react-native-community/datetimepicker": "^8.3.0",
"@react-navigation/elements": "^2.3.8", "@react-navigation/elements": "^2.3.8",
"@react-navigation/native": "^7.1.6", "@react-navigation/native": "^7.1.6",
"@react-navigation/native-stack": "^7.3.10", "@react-navigation/native-stack": "^7.3.10",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"moment": "^2.30.1",
"react": "19.0.0", "react": "19.0.0",
"react-native": "0.78.0", "react-native": "0.78.0",
"react-native-calendars": "^1.1311.1",
"react-native-element-dropdown": "^2.12.4", "react-native-element-dropdown": "^2.12.4",
"react-native-paper": "^5.13.2", "react-native-paper": "^5.13.2",
"react-native-safe-area-context": "^5.4.0", "react-native-safe-area-context": "^5.4.0",

View File

@ -5,15 +5,15 @@ import FontFamily from '../../assets/styles/FontFamily';
import Colors from '../../assets/styles/Colors'; import Colors from '../../assets/styles/Colors';
type PassportAppointmentCardProps = { type PassportAppointmentCardProps = {
applicantName: string; applicantName: string | undefined;
applicantCode: string; applicantCode: string | undefined;
appointmentDate: string; appointmentDate: string | undefined;
appointmentTime: string; appointmentTime: string | undefined;
serviceUnit: string; serviceUnit: string | undefined;
status: string; status: string | undefined;
}; };
const renderStatusContent = (status: string) => { const renderStatusContent = (status: string | undefined) => {
let backgroundColor; let backgroundColor;
let IconComponent; let IconComponent;

View File

@ -38,8 +38,8 @@ interface TextInputComponentProps {
supportText?: string; supportText?: string;
containerHeight?: any; containerHeight?: any;
isMultiline?: boolean; isMultiline?: boolean;
isDropdownSearchLocation?: boolean; isDropdownPressedSheet?: boolean;
handlePressSearchLocation?: () => void; handleDropdownPressed?: () => void;
} }
const TextInputComponent = (props: TextInputComponentProps) => { const TextInputComponent = (props: TextInputComponentProps) => {
@ -59,8 +59,8 @@ const TextInputComponent = (props: TextInputComponentProps) => {
supportText, supportText,
containerHeight, containerHeight,
isMultiline = false, isMultiline = false,
isDropdownSearchLocation = false, isDropdownPressedSheet = false,
handlePressSearchLocation, handleDropdownPressed,
} = props; } = props;
const [secureText, setSecureText] = useState(isPassword); const [secureText, setSecureText] = useState(isPassword);
@ -281,7 +281,7 @@ const TextInputComponent = (props: TextInputComponentProps) => {
); );
} }
if (isDropdownSearchLocation) { if (isDropdownPressedSheet) {
return ( return (
<View> <View>
{title && ( {title && (
@ -291,7 +291,7 @@ const TextInputComponent = (props: TextInputComponentProps) => {
</View> </View>
)} )}
<Pressable <Pressable
onPress={handlePressSearchLocation} onPress={handleDropdownPressed}
style={({pressed}) => ({ style={({pressed}) => ({
transform: [{scale: pressed ? 0.99 : 1}], transform: [{scale: pressed ? 0.99 : 1}],
})}> })}>
@ -303,7 +303,14 @@ const TextInputComponent = (props: TextInputComponentProps) => {
placeholderTextColor={Colors.primary60.color} placeholderTextColor={Colors.primary60.color}
editable={false} editable={false}
value={formattedDate} value={formattedDate}
right={<TextInput.Icon icon="menu-down" color="#48454E" size={20} style={{marginLeft: 24}}/>} right={
<TextInput.Icon
icon="menu-down"
color="#48454E"
size={20}
style={{marginLeft: 24}}
/>
}
multiline={false} multiline={false}
textColor="#48454E" textColor="#48454E"
disabled={isDisabled} disabled={isDisabled}

View File

@ -0,0 +1,79 @@
import React from 'react';
import {View, Text, StyleSheet} from 'react-native';
import {Dialog, Portal, Button} from 'react-native-paper';
import Colors from '../../../assets/styles/Colors';
import FontFamily from '../../../assets/styles/FontFamily';
interface DialogLogoutProps {
visible: boolean;
hideDialog: () => void;
onNavigate: () => void;
}
const DialogLogout = (props: DialogLogoutProps) => {
const {visible, hideDialog, onNavigate} = props;
return (
<Portal>
<Dialog visible={visible} style={styles.container}>
<Dialog.Title style={styles.title}>
Apakah Anda yakin akan menutup akun?
</Dialog.Title>
<View style={styles.content}>
<Button
style={styles.buttonContained}
mode="contained"
textColor={Colors.neutral100.color}
onPress={() => {
hideDialog();
onNavigate();
}}>
Ya, lanjut tutup akun
</Button>
<Button
style={styles.buttonOutlined}
mode="outlined"
textColor={Colors.indicatorRed.color}
onPress={() => {
hideDialog();
}}>
Tidak, jangan tutup akun
</Button>
</View>
</Dialog>
</Portal>
);
};
export default DialogLogout;
const styles = StyleSheet.create({
container: {
backgroundColor: 'white',
elevation: 0,
shadowColor: 'transparent',
borderRadius: 20,
},
title: {
fontSize: 22,
color: Colors.indicatorRed.color,
},
content: {
marginHorizontal: 24,
marginBottom: 24,
gap: 16,
},
message: {
fontSize: 14,
...FontFamily.notoSansRegular,
includeFontPadding: false,
lineHeight: 22,
color: Colors.primary30.color,
},
buttonContained: {
backgroundColor: Colors.indicatorRed.color,
},
buttonOutlined: {
borderColor: Colors.indicatorRed.color,
},
});

View File

@ -0,0 +1,78 @@
import React from 'react';
import {View, Text, StyleSheet} from 'react-native';
import {Dialog, Portal, Button} from 'react-native-paper';
import Colors from '../../../assets/styles/Colors';
import FontFamily from '../../../assets/styles/FontFamily';
interface DialogWarningApplicationProps {
visible: boolean;
hideDialog: () => void;
onNavigate: () => void;
}
const DialogWarningApplication = (props: DialogWarningApplicationProps) => {
const {visible, hideDialog, onNavigate} = props;
return (
<Portal>
<Dialog visible={visible} style={styles.container}>
<Dialog.Title style={styles.title}>Peringatan</Dialog.Title>
<View style={styles.content}>
<Text style={styles.message}>
Silakan melakukan pengisian kuesioner.
</Text>
<Text style={[styles.message, {...FontFamily.notoSansBold}]}>
Pastikan data dan jawaban yang Anda berikan benar.
</Text>
<Text style={styles.message}>
Pemberian keterangan yang tidak benar merupakan pelanggaran
keimigrasian sebagaimana ketentuan Pasal 126 huruf c UU No. 6 tahun
2011 tentang Keimigrasian dan akan mengakibatkan permohonan paspor
Anda ditolak dan pembayaran tidak dapat dikembalikan.
</Text>
<Button
style={styles.button}
mode="contained"
textColor={Colors.neutral100.color}
onPress={() => {
hideDialog();
onNavigate();
}}>
Lanjut
</Button>
</View>
</Dialog>
</Portal>
);
};
export default DialogWarningApplication;
const styles = StyleSheet.create({
container: {
backgroundColor: 'white',
elevation: 0,
shadowColor: 'transparent',
borderRadius: 20,
},
title: {
fontSize: 22,
color: Colors.secondary30.color,
},
content: {
marginHorizontal: 24,
marginBottom: 24,
gap: 16,
},
message: {
fontSize: 14,
...FontFamily.notoSansRegular,
includeFontPadding: false,
lineHeight: 22,
color: Colors.primary30.color,
},
button: {
backgroundColor: Colors.primary30.color,
marginTop: 12,
},
});

View File

@ -1,95 +1,353 @@
import {StyleSheet, Text, View} from 'react-native'; import {
import {Dialog, Portal, Button} from 'react-native-paper'; FlatList,
Modal,
Pressable,
ScrollView,
StyleSheet,
Text,
TouchableOpacity,
View,
} from 'react-native';
import {Portal, Button, Divider, RadioButton} from 'react-native-paper';
import Colors from '../../../assets/styles/Colors'; import Colors from '../../../assets/styles/Colors';
import FontFamily from '../../../assets/styles/FontFamily'; import FontFamily from '../../../assets/styles/FontFamily';
import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
import {useState} from 'react';
import {Calendar} from 'react-native-calendars';
import moment from 'moment';
import markedDatesData from '../../data/Steps/MarkedDatesData';
import arrivalTimesData from '../../data/Steps/ArrivalTimesData';
type Props = { type Props = {
visible: boolean; visible: boolean;
onClose: () => void; onClose: () => void;
onContinue: () => void;
}; };
const LegendItem = ({color, label, border}: any) => (
<View style={styles.legendItemContainer}>
<View
style={[
styles.legendColorBox,
{backgroundColor: color},
border && styles.legendColorBoxBorder,
]}
/>
<Text style={styles.legendLabel}>{label}</Text>
</View>
);
const SheetSelectDate = (props: Props) => { const SheetSelectDate = (props: Props) => {
const {visible, onClose} = props; const {visible, onClose, onContinue} = props;
const [selectedDate, setSelectedDate] = useState('');
const [currentMonth, setCurrentMonth] = useState('2025-05-01');
const [selectedTimeSlot, setSelectedTimeSlot] = useState('');
const markedDates = {
...markedDatesData,
[selectedDate]: {
selected: true,
selectedColor: Colors.indicatorOrange.color,
selectedTextColor: Colors.neutral100.color,
},
};
const renderHeaderCalendar = () => {
const headerText = moment(currentMonth).format('MMMM YYYY');
const handleMonthChange = (amount: number) => {
const updatedMonth = moment(currentMonth)
.add(amount, 'months')
.format('YYYY-MM-DD');
setCurrentMonth(updatedMonth);
};
return (
<View style={styles.calendarHeaderContainer}>
<Text style={styles.calendarHeaderText}>{headerText}</Text>
<View style={styles.calendarNavigation}>
<TouchableOpacity onPress={() => handleMonthChange(-1)}>
<Icon
name="chevron-left"
size={24}
color={Colors.secondary30.color}
/>
</TouchableOpacity>
<TouchableOpacity onPress={() => handleMonthChange(1)}>
<Icon
name="chevron-right"
size={24}
color={Colors.secondary30.color}
/>
</TouchableOpacity>
</View>
</View>
);
};
const renderTimeSlotItem = ({item}: any) => {
return (
<View>
<Text style={styles.timeSlotTitle}>Jam Kedatangan</Text>
<View style={styles.timeSlotRadioGroup}>
<View style={styles.timeSlotItem}>
<RadioButton
value={item.id}
status={
selectedTimeSlot === item.id ? 'checked' : 'unchecked'
}
onPress={() => setSelectedTimeSlot(item.id)}
color={Colors.secondary30.color}
uncheckedColor={Colors.secondary30.color}
/>
<Text style={styles.timeSlotLabel}>{item.arrivalTime}</Text>
</View>
<View style={styles.timeSlotQueueInfo}>
<View style={styles.queueDetail}>
<Text style={styles.queueDetailTitle}>Jumlah Antrian</Text>
<Text style={styles.queueDetailValue}>{item.queueCount}</Text>
</View>
<View style={styles.queueDetail}>
<Text style={styles.queueDetailTitle}>Sisa Kuota</Text>
<Text style={styles.queueDetailValue}>{item.remainingQuota}</Text>
</View>
</View>
</View>
</View>
);
};
return ( return (
<Portal> <Portal>
<Dialog visible={visible} style={styles.dialogContainer}> <Modal visible={visible} transparent animationType="slide">
<Dialog.Title style={styles.dialogTitle}> <Pressable style={styles.modalBackdrop} onPress={onClose} />
Jenis dan Manfaat Paspor <View style={styles.modalContentContainer}>
</Dialog.Title> <ScrollView
<View style={styles.dialogContentContainer}> keyboardShouldPersistTaps="handled"
<Text style={[styles.dialogDesc, {...FontFamily.notoSansBold}]}> showsVerticalScrollIndicator={false}
1. Paspor Biasa contentContainerStyle={styles.modalScrollViewContent}>
</Text> <View style={styles.modalCloseButtonContainer}>
<Text style={styles.dialogDesc}> <Pressable
Biaya pemrosesan lebih murah, tersedia di seluruh lokasi Kanim. style={({pressed}) => [
{'\n'}Biaya pembuatan: Rp350.000 styles.closeButton,
</Text> pressed && styles.closeButtonPressed,
<Text style={[styles.dialogDesc, {...FontFamily.notoSansBold}]}> ]}
2. Paspor Elektronik onPress={onClose}>
</Text> <Icon name="close" size={24} color={Colors.primary30.color} />
<Text style={styles.dialogDesc}> </Pressable>
Terdapat chip pada cover paspor yang menyimpan data pemegang paspor </View>
sehingga meningkatkan fitur keamanan, tersedia di 35 Kanim.{'\n'}
Biaya pembuatan: Rp650.000 <View style={styles.calendarContainer}>
</Text> <Calendar
<Text style={[styles.dialogDesc, {...FontFamily.notoSansBold}]}> key={currentMonth}
3. Paspor Elektronik Polikarbonat theme={{
</Text> textDayFontSize: 14,
<Text style={styles.dialogDesc}> textDayStyle: {
Berbahan polikarbonat (PC) pada halaman data pemegang paspor includeFontPadding: false,
(halaman 2) yang memiliki fitur keamanan tinggi dan lebih kuat, marginTop: 6,
tersedia di Kanim Jakarta Selatan, Kanim Jakarta Barat, dan Kanim ...FontFamily.notoSansRegular,
Soekarno Hatta.{'\n'}Biaya pembuatan: Rp650.000 },
</Text> textDayHeaderFontFamily: 'NotoSans-Medium',
<View> textDayHeaderFontSize: 14,
<Button textSectionTitleColor: Colors.primary30.color,
style={styles.buttonContinue} arrowColor: Colors.secondary30.color,
mode="contained" }}
textColor={Colors.neutral100.color} onDayPress={day => setSelectedDate(day.dateString)}
onPress={() => { markedDates={markedDates}
onClose(); hideArrows={true}
}}> current={currentMonth}
Lanjut onMonthChange={month => {
</Button> const newMonth = `${month.year}-${String(
</View> month.month,
).padStart(2, '0')}-01`;
setCurrentMonth(newMonth);
}}
renderHeader={renderHeaderCalendar}
/>
<Divider style={styles.divider} />
<View style={styles.legendMainContainer}>
<View style={styles.legendColumn}>
<LegendItem
color={Colors.indicatorGreen.color}
label="Kuota tersedia"
/>
<LegendItem
color={Colors.indicatorRed.color}
label="Kuota penuh"
/>
</View>
<View style={styles.legendColumn}>
<LegendItem
color={Colors.indicatorOrange.color}
label="Tanggal terpilih"
/>
<LegendItem
color={Colors.neutral100.color}
border
label="Kuota belum dibuka"
/>
</View>
</View>
<Divider style={styles.divider} />
</View>
<FlatList
data={arrivalTimesData}
keyExtractor={item => item.id}
scrollEnabled={false}
renderItem={renderTimeSlotItem}
/>
<View style={styles.continueButtonWrapper}>
<Button
style={styles.continueButton}
mode="contained"
textColor={Colors.neutral100.color}
onPress={onContinue}>
Simpan
</Button>
</View>
</ScrollView>
</View> </View>
</Dialog> </Modal>
</Portal> </Portal>
); );
}; };
const styles = StyleSheet.create({ const styles = StyleSheet.create({
dialogContainer: { modalBackdrop: {
backgroundColor: 'white', flex: 1,
elevation: 0, backgroundColor: 'rgba(0,0,0,0.3)',
shadowColor: 'transparent',
borderRadius: 20,
}, },
dialogContentContainer: { modalContentContainer: {
marginHorizontal: 24, backgroundColor: 'white',
marginBottom: 24, borderTopLeftRadius: 20,
borderTopRightRadius: 20,
paddingTop: 24,
paddingHorizontal: 16,
position: 'absolute',
bottom: 0,
height: '90%',
width: '100%',
maxHeight: '90%',
},
modalScrollViewContent: {
paddingBottom: 24,
},
modalCloseButtonContainer: {
flexDirection: 'row',
justifyContent: 'flex-end',
},
closeButton: {
transform: [{scale: 1}],
},
closeButtonPressed: {
transform: [{scale: 0.925}],
},
calendarContainer: {
marginTop: 8,
},
calendarHeaderContainer: {
width: '100%',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
marginBottom: 16,
},
calendarHeaderText: {
fontSize: 14,
...FontFamily.notoSansBold,
color: Colors.secondary30.color,
includeFontPadding: false,
},
calendarNavigation: {
flexDirection: 'row',
gap: 12,
},
divider: {
marginVertical: 16,
height: 1,
backgroundColor: Colors.primary70.color,
},
legendMainContainer: {
flexDirection: 'row',
gap: 32,
},
legendColumn: {
gap: 16, gap: 16,
}, },
dialogTitle: { legendItemContainer: {
fontSize: 22, flexDirection: 'row',
color: Colors.secondary30.color, alignItems: 'center',
gap: 4,
}, },
dialogDesc: { legendColorBox: {
fontSize: 14, width: 16,
height: 16,
marginRight: 6,
borderRadius: 8,
},
legendColorBoxBorder: {
borderWidth: 1,
borderColor: '#ccc',
},
legendLabel: {
fontSize: 11,
...FontFamily.notoSansRegular, ...FontFamily.notoSansRegular,
includeFontPadding: false, includeFontPadding: false,
color: Colors.primary30.color, color: Colors.primary30.color,
lineHeight: 22,
}, },
dialogDescRed: { continueButtonWrapper: {
...FontFamily.notoSansBold, marginTop: 16,
color: Colors.indicatorRed.color,
includeFontPadding: false,
}, },
buttonContinue: { continueButton: {
backgroundColor: Colors.primary30.color, backgroundColor: Colors.primary30.color,
marginTop: 12, },
timeSlotTitle: {
color: Colors.primary30.color,
includeFontPadding: false,
fontSize: 12,
...FontFamily.notoSansSemiBold,
},
timeSlotRadioGroup: {
flexDirection: 'row',
justifyContent: 'space-between',
},
timeSlotItem: {
flexDirection: 'row',
gap: 12,
alignItems: 'center',
marginVertical: 8,
},
timeSlotLabel: {
...FontFamily.notoSansMedium,
fontSize: 12,
includeFontPadding: false,
color: Colors.secondary30.color,
marginStart: -8,
},
timeSlotQueueInfo: {
flexDirection: 'row',
gap: 12,
alignItems: 'center',
},
queueDetail: {
gap: 6,
},
queueDetailTitle: {
fontSize: 10.5,
includeFontPadding: false,
color: Colors.primary30.color,
...FontFamily.notoSansBold,
},
queueDetailValue: {
fontSize: 10.5,
includeFontPadding: false,
color: Colors.primary30.color,
...FontFamily.notoSansRegular,
}, },
}); });

View File

@ -0,0 +1,7 @@
const passportTypeData = [
{label: 'Paspor Biasa', value: '1'},
{label: 'Paspor Elektronik', value: '2'},
{label: 'Paspor Elektronik Polikarbonat', value: '3'},
];
export default passportTypeData;

View File

@ -41,17 +41,17 @@ const passportAppointmentData = [
}, },
{ {
id: '3', id: '3',
applicantName: 'Salwa Aisyah Adhani', applicantName: 'Mochammad Adhi Buchori',
applicantCode: '1038000002223344', applicantCode: '1038000002223344',
appointmentDate: 'Senin, 14 April 2025', appointmentDate: 'Senin, 14 April 2025',
appointmentTime: '08:00 - 09:00 WIB', appointmentTime: '08:00 - 09:00 WIB',
serviceUnit: 'Kantor Imigrasi Depok', serviceUnit: 'Kantor Imigrasi Jakarta',
status: 'Menunggu Pembayaran', status: 'Menunggu Pembayaran',
submissionDate: 'Sabtu, 12 April 2025 18:30', submissionDate: 'Sabtu, 12 April 2025 18:30',
serviceCode: 'EH-GT4JWR', serviceCode: 'EH-GT4JWR',
applicationDetails: { applicationDetails: {
nationalIdNumber: '3271234560009120003', nationalIdNumber: '3271234560009120003',
gender: 'Wanita', gender: 'Pria',
applicationType: 'Baru', applicationType: 'Baru',
replacementReason: 'Sekolah di Luar Negeri', replacementReason: 'Sekolah di Luar Negeri',
applicationPurpose: 'Wisata/Liburan', applicationPurpose: 'Wisata/Liburan',
@ -101,26 +101,6 @@ const passportAppointmentData = [
}, },
{ {
id: '6', id: '6',
applicantName: 'Fadlan Ramadhan',
applicantCode: '1038000008885566',
appointmentDate: 'Selasa, 18 April 2025',
appointmentTime: '09:00 - 10:00 WIB',
serviceUnit: 'Kantor Imigrasi Jakarta Barat',
status: 'Sudah Terbayar',
submissionDate: 'Senin, 14 April 2025 20:00',
serviceCode: 'EH-QZ5TVN',
applicationDetails: {
nationalIdNumber: '3271234560009120006',
gender: 'Pria',
applicationType: 'Baru',
replacementReason: 'Hilang',
applicationPurpose: 'Tugas Kantor',
passportType: 'PASPOR BIASA NON ELEKTRONIK',
fee: '350.000',
},
},
{
id: '7',
applicantName: 'Nabila Khairunisa', applicantName: 'Nabila Khairunisa',
applicantCode: '1038000007773344', applicantCode: '1038000007773344',
appointmentDate: 'Rabu, 19 April 2025', appointmentDate: 'Rabu, 19 April 2025',
@ -139,6 +119,26 @@ const passportAppointmentData = [
fee: '650.000', fee: '650.000',
}, },
}, },
{
id: '7',
applicantName: 'Ayaka Haishima',
applicantCode: '1038000008885566',
appointmentDate: 'Selasa, 18 April 2025',
appointmentTime: '09:00 - 10:00 WIB',
serviceUnit: 'Kantor Imigrasi Jakarta Barat',
status: 'Sudah Terbayar',
submissionDate: 'Senin, 14 April 2025 20:00',
serviceCode: 'EH-QZ5TVN',
applicationDetails: {
nationalIdNumber: '3271234560009120006',
gender: 'Wanita',
applicationType: 'Baru',
replacementReason: 'Hilang',
applicationPurpose: 'Kuliah di Luar Negeri',
passportType: 'PASPOR BIASA NON ELEKTRONIK',
fee: '350.000',
},
},
]; ];
export default passportAppointmentData; export default passportAppointmentData;

View File

@ -0,0 +1,64 @@
const arrivalTimesData = [
{
id: '1',
arrivalTime: '09:01-10:00',
queueCount: '4 people',
remainingQuota: '1',
},
{
id: '2',
arrivalTime: '10:01-11:00',
queueCount: '4 people',
remainingQuota: '1',
},
{
id: '3',
arrivalTime: '11:01-12:00',
queueCount: '3 people',
remainingQuota: '2',
},
{
id: '4',
arrivalTime: '13:01-14:00',
queueCount: '1 person',
remainingQuota: '4',
},
{
id: '5',
arrivalTime: '14:01-15:00',
queueCount: '2 people',
remainingQuota: '3',
},
{
id: '6',
arrivalTime: '15:01-16:00',
queueCount: '5 people',
remainingQuota: '0',
},
{
id: '7',
arrivalTime: '16:01-17:00',
queueCount: '3 people',
remainingQuota: '2',
},
{
id: '8',
arrivalTime: '17:01-18:00',
queueCount: '2 people',
remainingQuota: '3',
},
{
id: '9',
arrivalTime: '18:01-19:00',
queueCount: '1 person',
remainingQuota: '4',
},
{
id: '10',
arrivalTime: '19:01-20:00',
queueCount: '0 people',
remainingQuota: '5',
},
];
export default arrivalTimesData;

View File

@ -0,0 +1,129 @@
import Colors from '../../../assets/styles/Colors';
const markedDatesData = {
// Kuota penuh (merah)
'2025-05-02': {
selectedColor: Colors.indicatorRed.color,
disableTouchEvent: true,
selected: true,
textColor: Colors.neutral100.color,
},
'2025-05-03': {
selectedColor: Colors.indicatorRed.color,
disableTouchEvent: true,
selected: true,
textColor: Colors.neutral100.color,
},
'2025-05-04': {
selectedColor: Colors.indicatorRed.color,
disableTouchEvent: true,
selected: true,
textColor: Colors.neutral100.color,
},
'2025-05-08': {
selectedColor: Colors.indicatorRed.color,
disableTouchEvent: true,
selected: true,
textColor: Colors.neutral100.color,
},
'2025-05-09': {
selectedColor: Colors.indicatorRed.color,
disableTouchEvent: true,
selected: true,
textColor: Colors.neutral100.color,
},
'2025-05-10': {
selectedColor: Colors.indicatorRed.color,
disableTouchEvent: true,
selected: true,
textColor: Colors.neutral100.color,
},
'2025-05-11': {
selectedColor: Colors.indicatorRed.color,
disableTouchEvent: true,
selected: true,
textColor: Colors.neutral100.color,
},
'2025-05-17': {
selectedColor: Colors.indicatorRed.color,
disableTouchEvent: true,
selected: true,
textColor: Colors.neutral100.color,
},
'2025-05-18': {
selectedColor: Colors.indicatorRed.color,
disableTouchEvent: true,
selected: true,
textColor: Colors.neutral100.color,
},
'2025-05-24': {
selectedColor: Colors.indicatorRed.color,
disableTouchEvent: true,
selected: true,
textColor: Colors.neutral100.color,
},
'2025-05-25': {
selectedColor: Colors.indicatorRed.color,
disableTouchEvent: true,
selected: true,
textColor: Colors.neutral100.color,
},
'2025-05-30': {
selectedColor: Colors.indicatorRed.color,
disableTouchEvent: true,
selected: true,
textColor: Colors.neutral100.color,
},
'2025-05-31': {
selectedColor: Colors.indicatorRed.color,
disableTouchEvent: true,
selected: true,
textColor: Colors.neutral100.color,
},
// Kuota tersedia (hijau)
'2025-05-05': {
selectedColor: Colors.indicatorGreen.color,
disableTouchEvent: true,
selected: true,
textColor: Colors.neutral100.color,
},
'2025-05-14': {
selectedColor: Colors.indicatorGreen.color,
disableTouchEvent: true,
selected: true,
textColor: Colors.neutral100.color,
},
'2025-05-15': {
selectedColor: Colors.indicatorGreen.color,
disableTouchEvent: true,
selected: true,
textColor: Colors.neutral100.color,
},
'2025-05-16': {
selectedColor: Colors.indicatorGreen.color,
disableTouchEvent: true,
selected: true,
textColor: Colors.neutral100.color,
},
'2025-05-20': {
selectedColor: Colors.indicatorGreen.color,
disableTouchEvent: true,
selected: true,
textColor: Colors.neutral100.color,
},
'2025-05-28': {
selectedColor: Colors.indicatorGreen.color,
disableTouchEvent: true,
selected: true,
textColor: Colors.neutral100.color,
},
'2025-05-29': {
selectedColor: Colors.indicatorGreen.color,
disableTouchEvent: true,
selected: true,
textColor: Colors.neutral100.color,
},
};
export default markedDatesData;

View File

@ -0,0 +1,40 @@
import AsyncStorage from '@react-native-async-storage/async-storage';
// Fungsi untuk menyimpan data
export const storeData = async <T,>(key: string, value: T): Promise<void> => {
try {
await AsyncStorage.setItem(key, JSON.stringify(value));
console.log('Data berhasil disimpan!');
} catch (e) {
console.error('Gagal menyimpan data:', e);
}
};
// Fungsi untuk mengambil data
export const getData = async <T,>(key: string): Promise<T | null> => {
try {
const storedData = await AsyncStorage.getItem(key);
if (storedData !== null) {
return JSON.parse(storedData) as T;
}
return null;
} catch (e) {
console.error('Gagal mengambil data:', e);
return null;
}
};
// Fungsi untuk menambah data
export const addData = async <T,>(key: string, newData: T): Promise<void> => {
try {
const storedData = await AsyncStorage.getItem(key);
if (storedData !== null) {
const parsedData: T[] = JSON.parse(storedData);
parsedData.push(newData);
await AsyncStorage.setItem(key, JSON.stringify(parsedData));
console.log('Data berhasil ditambahkan!');
}
} catch (e) {
console.error('Gagal menambah data:', e);
}
};

View File

@ -54,7 +54,9 @@ function RootStack() {
options={{headerShown: false}} options={{headerShown: false}}
/> />
<Stack.Screen name="Home" options={{headerShown: false}}> <Stack.Screen name="Home" options={{headerShown: false}}>
{() => <HomeScreen showDialog={() => console.log('Show dialog!')} visible />} {() => (
<HomeScreen showDialog={() => {}} visible />
)}
</Stack.Screen> </Stack.Screen>
<Stack.Screen <Stack.Screen
name="History" name="History"
@ -66,11 +68,11 @@ function RootStack() {
component={NotificationScreen} component={NotificationScreen}
options={{headerShown: false}} options={{headerShown: false}}
/> />
<Stack.Screen <Stack.Screen name="Profile" options={{headerShown: false}}>
name="Profile" {() => (
component={ProfileScreen} <ProfileScreen showDialog={() => {}} visible />
options={{headerShown: false}} )}
/> </Stack.Screen>
<Stack.Screen <Stack.Screen
name="EditProfile" name="EditProfile"
component={EditProfileScreen} component={EditProfileScreen}

View File

@ -23,3 +23,26 @@ export type RootStackParamList = {
OtherMethod: undefined; OtherMethod: undefined;
BillingCode: undefined; BillingCode: undefined;
}; };
export interface ApplicationDetails {
nationalIdNumber?: string | undefined;
gender?: string | undefined;
applicationType?: string | undefined;
replacementReason?: string | undefined;
applicationPurpose?: string | undefined;
passportType?: string | undefined;
fee?: string | undefined;
}
export interface PassportAppointment {
id?: string | undefined;
applicantName?: string | undefined;
applicantCode?: string | undefined;
appointmentDate?: string | undefined;
appointmentTime?: string | undefined;
serviceUnit?: string | undefined;
status?: string | undefined;
submissionDate?: string | undefined;
serviceCode?: string | undefined;
applicationDetails?: ApplicationDetails;
}

View File

@ -154,6 +154,7 @@ const styles = StyleSheet.create({
fontSize: 14, fontSize: 14,
...FontFamily.notoSansBold, ...FontFamily.notoSansBold,
color: Colors.primary30.color, color: Colors.primary30.color,
includeFontPadding: false,
}, },
applicantDetailContentChildButton: { applicantDetailContentChildButton: {
marginTop: 8, marginTop: 8,

View File

@ -1,4 +1,4 @@
import React, {useCallback} from 'react'; import React, {useCallback, useEffect, useState} from 'react';
import {FlatList, Pressable, StatusBar, Text, View} from 'react-native'; import {FlatList, Pressable, StatusBar, Text, View} from 'react-native';
import styles from './styles'; import styles from './styles';
import Colors from '../../../assets/styles/Colors'; import Colors from '../../../assets/styles/Colors';
@ -10,8 +10,10 @@ import {
useNavigationState, useNavigationState,
} from '@react-navigation/native'; } from '@react-navigation/native';
import Icon from 'react-native-vector-icons/MaterialCommunityIcons'; import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
import {RootStackParamList} from '../../navigation/type'; import {PassportAppointment, RootStackParamList} from '../../navigation/type';
import {NativeStackNavigationProp} from '@react-navigation/native-stack'; import {NativeStackNavigationProp} from '@react-navigation/native-stack';
import {getData} from '../../helper/asyncStorageHelper';
import {ActivityIndicator} from 'react-native-paper';
type HistoryScreenNavigationProp = NativeStackNavigationProp< type HistoryScreenNavigationProp = NativeStackNavigationProp<
RootStackParamList, RootStackParamList,
@ -29,6 +31,28 @@ function HistoryScreen() {
const showNavBackAppBar = previousRoute === 'NavigationRoute'; const showNavBackAppBar = previousRoute === 'NavigationRoute';
const [appointments, setAppointments] = useState<PassportAppointment[]>([]);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
const fetchData = async () => {
try {
setIsLoading(true); // mulai loading
const data = await getData('passportAppointments');
if (Array.isArray(data) && data.length > 0) {
setAppointments(data);
} else {
setAppointments([]); // kosongin kalau tidak ada
}
} catch (error) {
console.error('Error fetching data: ', error);
} finally {
setIsLoading(false); // selesai loading
}
};
fetchData();
}, []);
useFocusEffect( useFocusEffect(
useCallback(() => { useCallback(() => {
StatusBar.setBackgroundColor(Colors.secondary30.color); StatusBar.setBackgroundColor(Colors.secondary30.color);
@ -60,31 +84,37 @@ function HistoryScreen() {
</View> </View>
)} )}
<View style={styles.topBackground} /> <View style={styles.topBackground} />
<View style={styles.cardWrapper}> {isLoading ? (
<FlatList <View style={{flex: 1, justifyContent: 'center', alignItems: 'center'}}>
data={passportAppointmentData} <ActivityIndicator size="large" color={Colors.secondary30.color} />
renderItem={({item}) => ( </View>
<Pressable ) : (
onPress={() => <View style={styles.cardWrapper}>
navigation.navigate('ApplicationDetail', {data: item}) <FlatList
} data={appointments}
style={({pressed}) => ({ renderItem={({item}: any) => (
transform: [{scale: pressed ? 0.975 : 1}], <Pressable
})}> onPress={() =>
<PassportAppointmentCard navigation.navigate('ApplicationDetail', {data: item})
applicantName={item.applicantName} }
applicantCode={item.applicantCode} style={({pressed}) => ({
appointmentDate={item.appointmentDate} transform: [{scale: pressed ? 0.975 : 1}],
appointmentTime={item.appointmentTime} })}>
serviceUnit={item.serviceUnit} <PassportAppointmentCard
status={item.status} applicantName={item.applicantName}
/> applicantCode={item.applicantCode}
</Pressable> appointmentDate={item.appointmentDate}
)} appointmentTime={item.appointmentTime}
keyExtractor={item => item.id} serviceUnit={item.serviceUnit}
ItemSeparatorComponent={ItemSeparator} status={item.status}
/> />
</View> </Pressable>
)}
keyExtractor={item => item.id ?? ''}
ItemSeparatorComponent={ItemSeparator}
/>
</View>
)}
</View> </View>
); );
} }

View File

@ -13,15 +13,15 @@ import {
import Colors from '../../../assets/styles/Colors'; import Colors from '../../../assets/styles/Colors';
import Icon from 'react-native-vector-icons/MaterialCommunityIcons'; import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
import {NativeStackNavigationProp} from '@react-navigation/native-stack'; import {NativeStackNavigationProp} from '@react-navigation/native-stack';
import {RootStackParamList} from '../../navigation/type'; import {PassportAppointment, RootStackParamList} from '../../navigation/type';
import {useFocusEffect, useNavigation} from '@react-navigation/native'; import {useFocusEffect, useNavigation} from '@react-navigation/native';
import RegularPassportIcon from '../../../assets/icons/regular_passport.svg'; import RegularPassportIcon from '../../../assets/icons/regular_passport.svg';
import ExpressPassportIcon from '../../../assets/icons/express_passport.svg'; import ExpressPassportIcon from '../../../assets/icons/express_passport.svg';
import GuidebookIcon from '../../../assets/icons/guidebook.svg'; import GuidebookIcon from '../../../assets/icons/guidebook.svg';
import EazyPassportIcon from '../../../assets/icons/eazy_passport.svg'; import EazyPassportIcon from '../../../assets/icons/eazy_passport.svg';
import passportAppointmentData from '../../data/History/PassportAppointmentData';
import PassportAppointmentCard from '../../components/PassportAppointmentCard'; import PassportAppointmentCard from '../../components/PassportAppointmentCard';
import {useCallback, useRef} from 'react'; import {useCallback, useEffect, useRef, useState} from 'react';
import {getData} from '../../helper/asyncStorageHelper';
type HomeScreenNavigationProp = NativeStackNavigationProp< type HomeScreenNavigationProp = NativeStackNavigationProp<
RootStackParamList, RootStackParamList,
@ -32,6 +32,7 @@ const ItemSeparator = () => <View style={styles.flatllistGap} />;
type RenderContentProps = { type RenderContentProps = {
showDialog: () => void; showDialog: () => void;
lastTwoAppointments: PassportAppointment[];
}; };
const RenderBanner = () => { const RenderBanner = () => {
@ -147,7 +148,10 @@ const RenderBanner = () => {
); );
}; };
const RenderContent = ({showDialog}: RenderContentProps) => { const RenderContent = ({
showDialog,
lastTwoAppointments,
}: RenderContentProps) => {
const navigation = useNavigation<HomeScreenNavigationProp>(); const navigation = useNavigation<HomeScreenNavigationProp>();
return ( return (
@ -223,8 +227,8 @@ const RenderContent = ({showDialog}: RenderContentProps) => {
</View> </View>
<View style={styles.cardWrapper}> <View style={styles.cardWrapper}>
<FlatList <FlatList
data={passportAppointmentData.slice(-2)} data={lastTwoAppointments}
renderItem={({item}) => ( renderItem={({item}: any) => (
<Pressable <Pressable
onPress={() => onPress={() =>
navigation.navigate('ApplicationDetail', {data: item}) navigation.navigate('ApplicationDetail', {data: item})
@ -242,7 +246,7 @@ const RenderContent = ({showDialog}: RenderContentProps) => {
/> />
</Pressable> </Pressable>
)} )}
keyExtractor={item => item.id} keyExtractor={item => item.id ?? ''}
ItemSeparatorComponent={ItemSeparator} ItemSeparatorComponent={ItemSeparator}
/> />
</View> </View>
@ -256,9 +260,28 @@ type HomeScreenProps = {
readonly visible: boolean; readonly visible: boolean;
}; };
function HomeScreen({showDialog, visible}: HomeScreenProps) { function HomeScreen(props: HomeScreenProps) {
const {showDialog, visible} = props;
const navigation = useNavigation<HomeScreenNavigationProp>(); const navigation = useNavigation<HomeScreenNavigationProp>();
const [lastTwoAppointments, setLastTwoAppointments] = useState<
PassportAppointment[]
>([]);
useEffect(() => {
const fetchData = async () => {
try {
const data = await getData('passportAppointments');
if (Array.isArray(data) && data.length > 0) {
setLastTwoAppointments(data.slice(-2));
}
} catch (error) {
console.error('Error fetching data: ', error);
}
};
fetchData();
}, []);
useFocusEffect( useFocusEffect(
useCallback(() => { useCallback(() => {
StatusBar.setBackgroundColor( StatusBar.setBackgroundColor(
@ -276,7 +299,9 @@ function HomeScreen({showDialog, visible}: HomeScreenProps) {
return ( return (
<View style={styles.container}> <View style={styles.container}>
<View style={styles.appBarContainer}> <View style={styles.appBarContainer}>
<Text style={styles.appBarTitle}>Halo, X!</Text> <Text style={styles.appBarTitle} numberOfLines={1} ellipsizeMode="tail">
Halo, Salwa Aisyah Adhani!
</Text>
<Icon <Icon
name="bell-outline" name="bell-outline"
size={24} size={24}
@ -286,7 +311,12 @@ function HomeScreen({showDialog, visible}: HomeScreenProps) {
</View> </View>
<FlatList <FlatList
data={[{}]} data={[{}]}
renderItem={() => <RenderContent showDialog={showDialog} />} renderItem={() => (
<RenderContent
showDialog={showDialog}
lastTwoAppointments={lastTwoAppointments}
/>
)}
/> />
</View> </View>
); );

View File

@ -16,6 +16,8 @@ const styles = StyleSheet.create({
fontSize: 28, fontSize: 28,
marginVertical: 14, marginVertical: 14,
includeFontPadding: false, includeFontPadding: false,
flex: 1,
marginEnd: 16,
}, },
appBarContainer: { appBarContainer: {
height: 72, height: 72,

View File

@ -13,8 +13,10 @@ import TextInputComponent from '../../components/TextInput';
import Icon from 'react-native-vector-icons/MaterialIcons'; import Icon from 'react-native-vector-icons/MaterialIcons';
import {useNavigation} from '@react-navigation/native'; import {useNavigation} from '@react-navigation/native';
import {NativeStackNavigationProp} from '@react-navigation/native-stack'; import {NativeStackNavigationProp} from '@react-navigation/native-stack';
import {RootStackParamList} from '../../navigation/type'; import {PassportAppointment, RootStackParamList} from '../../navigation/type';
import Colors from '../../../assets/styles/Colors'; import Colors from '../../../assets/styles/Colors';
import {getData, storeData} from '../../helper/asyncStorageHelper';
import passportAppointmentData from '../../data/History/PassportAppointmentData';
type LoginScreenNavigationProp = NativeStackNavigationProp< type LoginScreenNavigationProp = NativeStackNavigationProp<
RootStackParamList, RootStackParamList,
@ -48,12 +50,18 @@ function LoginScreen() {
<Button <Button
style={styles.loginButton} style={styles.loginButton}
mode="contained" mode="contained"
onPress={() => onPress={async () => {
storeData<PassportAppointment[]>(
'passportAppointments',
passportAppointmentData,
);
const storedData = await getData('passportAppointments');
console.log('Data yang tersimpan:', storedData);
navigation.reset({ navigation.reset({
index: 0, index: 0,
routes: [{name: 'NavigationRoute'}], routes: [{name: 'NavigationRoute'}],
}) });
}> }}>
Masuk Masuk
</Button> </Button>
<View style={styles.registerAccountContainer}> <View style={styles.registerAccountContainer}>

View File

@ -1,23 +1,16 @@
import * as React from 'react'; import * as React from 'react';
import { import {BottomNavigation, PaperProvider, Text} from 'react-native-paper';
BottomNavigation,
Button,
Dialog,
PaperProvider,
Portal,
Text,
} from 'react-native-paper';
import Colors from '../../../assets/styles/Colors'; import Colors from '../../../assets/styles/Colors';
import ProfileScreen from '../profile'; import ProfileScreen from '../profile';
import styles from './styles'; import styles from './styles';
import HomeScreen from '../home'; import HomeScreen from '../home';
import HistoryScreen from '../history'; import HistoryScreen from '../history';
import {View} from 'react-native';
import {useState} from 'react'; import {useState} from 'react';
import {NativeStackNavigationProp} from '@react-navigation/native-stack'; import {NativeStackNavigationProp} from '@react-navigation/native-stack';
import {RootStackParamList} from '../../navigation/type'; import {RootStackParamList} from '../../navigation/type';
import {useNavigation} from '@react-navigation/native'; import {useNavigation} from '@react-navigation/native';
import FontFamily from '../../../assets/styles/FontFamily'; import DialogWarningApplication from '../../components/dialog/DialogWarningApplication';
import DialogLogout from '../../components/dialog/DialogLogout';
type NavigationRouteScreenNavigationProp = NativeStackNavigationProp< type NavigationRouteScreenNavigationProp = NativeStackNavigationProp<
RootStackParamList, RootStackParamList,
@ -26,7 +19,11 @@ type NavigationRouteScreenNavigationProp = NativeStackNavigationProp<
function NavigationRouteScreen() { function NavigationRouteScreen() {
const navigation = useNavigation<NavigationRouteScreenNavigationProp>(); const navigation = useNavigation<NavigationRouteScreenNavigationProp>();
const [visible, setVisible] = useState(false);
const [visibleWarningApplicationDialog, setVisibleWarningApplicationDialog] =
useState(false);
const [visibleLogoutDialog, setVisibleLogoutDialog] = useState(false);
const [index, setIndex] = useState(0); const [index, setIndex] = useState(0);
const [routes] = useState([ const [routes] = useState([
{ {
@ -48,17 +45,32 @@ function NavigationRouteScreen() {
}, },
]); ]);
const showDialog = () => setVisible(true); const showWarningApplicationDialog = () =>
const hideDialog = () => setVisible(false); setVisibleWarningApplicationDialog(true);
const hideWarningApplicationDialog = () =>
setVisibleWarningApplicationDialog(false);
const showLogoutDialog = () => setVisibleLogoutDialog(true);
const hideLogoutDialog = () => setVisibleLogoutDialog(false);
const renderScene = ({route}: {route: {key: string}}) => { const renderScene = ({route}: {route: {key: string}}) => {
switch (route.key) { switch (route.key) {
case 'home': case 'home':
return <HomeScreen showDialog={showDialog} visible={visible} />; return (
<HomeScreen
visible={visibleWarningApplicationDialog}
showDialog={showWarningApplicationDialog}
/>
);
case 'history': case 'history':
return <HistoryScreen />; return <HistoryScreen />;
case 'profile': case 'profile':
return <ProfileScreen />; return (
<ProfileScreen
visible={visibleLogoutDialog}
showDialog={showLogoutDialog}
/>
);
default: default:
return null; return null;
} }
@ -82,35 +94,25 @@ function NavigationRouteScreen() {
<Text style={styles.bottomNavLabel}>{route.title}</Text> <Text style={styles.bottomNavLabel}>{route.title}</Text>
)} )}
/> />
<Portal> {visibleWarningApplicationDialog && (
<Dialog visible={visible} style={styles.dialogContainer}> <DialogWarningApplication
<Dialog.Title style={styles.dialogTitle}>Peringatan</Dialog.Title> visible={visibleWarningApplicationDialog}
<View style={styles.dialogContentContainer}> hideDialog={hideWarningApplicationDialog}
<Text style={styles.dialogDesc}> onNavigate={() => navigation.navigate('RegularPassport')}
Silakan melakukan pengisian kuesioner. />
</Text> )}
<Text style={[styles.dialogDesc, {...FontFamily.notoSansBold}]}> {visibleLogoutDialog && (
Pastikan data dan jawaban yang Anda berikan benar. <DialogLogout
</Text> visible={visibleLogoutDialog}
<Text style={styles.dialogDesc}> hideDialog={hideLogoutDialog}
Pemberian keterangan yang tidak benar merupakan pelanggaran onNavigate={() =>
keimigrasian sebagaimana ketentuan Pasal 126 huruf c UU No. 6 navigation.reset({
tahun 2011 tentang Keimigrasian dan akan mengakibatkan permohonan index: 0,
paspor Anda ditolak dan pembayaran tidak dapat dikembalikan. routes: [{name: 'Login'}],
</Text> })
<Button }
style={styles.buttonContinue} />
mode="contained" )}
textColor={Colors.neutral100.color}
onPress={() => {
hideDialog();
navigation.navigate('RegularPassport');
}}>
Lanjut
</Button>
</View>
</Dialog>
</Portal>
</PaperProvider> </PaperProvider>
); );
} }

View File

@ -10,32 +10,6 @@ const styles = StyleSheet.create({
fontSize: 12, fontSize: 12,
position: 'absolute', position: 'absolute',
}, },
dialogContainer: {
backgroundColor: 'white',
elevation: 0,
shadowColor: 'transparent',
borderRadius: 20,
},
dialogTitle: {
fontSize: 22,
color: Colors.secondary30.color,
},
dialogContentContainer: {
marginHorizontal: 24,
marginBottom: 24,
gap: 16,
},
dialogDesc: {
fontSize: 14,
...FontFamily.notoSansRegular,
includeFontPadding: false,
lineHeight: 22,
color: Colors.primary30.color,
},
buttonContinue: {
backgroundColor: Colors.primary30.color,
marginTop: 12,
},
}); });
export default styles; export default styles;

View File

@ -13,20 +13,29 @@ type ProfileScreenNavigationProp = NativeStackNavigationProp<
'Profile' 'Profile'
>; >;
function ProfileScreen() { type ProfileScreenProps = {
readonly showDialog: () => void;
readonly visible: boolean;
};
function ProfileScreen(props: ProfileScreenProps) {
const {showDialog, visible} = props;
const placeholderProfileImage = require('../../../assets/images/placeholderProfileImage.png'); const placeholderProfileImage = require('../../../assets/images/placeholderProfileImage.png');
const navigation = useNavigation<ProfileScreenNavigationProp>(); const navigation = useNavigation<ProfileScreenNavigationProp>();
useFocusEffect( useFocusEffect(
useCallback(() => { useCallback(() => {
StatusBar.setBackgroundColor(Colors.secondary30.color); StatusBar.setBackgroundColor(
visible ? '#295E70' : Colors.secondary30.color,
);
StatusBar.setBarStyle('light-content'); StatusBar.setBarStyle('light-content');
return () => { return () => {
StatusBar.setBackgroundColor(Colors.secondary30.color); StatusBar.setBackgroundColor(Colors.secondary30.color);
StatusBar.setBarStyle('light-content'); StatusBar.setBarStyle('light-content');
}; };
}, []), }, [visible]),
); );
return ( return (
<View style={styles.container}> <View style={styles.container}>
@ -35,7 +44,7 @@ function ProfileScreen() {
</View> </View>
<View style={styles.topContainer}> <View style={styles.topContainer}>
<Image source={placeholderProfileImage} style={styles.profileImage} /> <Image source={placeholderProfileImage} style={styles.profileImage} />
<Text style={styles.accountName}>X</Text> <Text style={styles.accountName}>Salwa Aisyah Adhani</Text>
<Text style={styles.accountNumber}>3271234560009123456</Text> <Text style={styles.accountNumber}>3271234560009123456</Text>
</View> </View>
<View style={styles.sectionProfileField}> <View style={styles.sectionProfileField}>
@ -80,12 +89,7 @@ function ProfileScreen() {
mode="outlined" mode="outlined"
textColor={Colors.indicatorRed.color} textColor={Colors.indicatorRed.color}
style={styles.logoutButton} style={styles.logoutButton}
onPress={() => onPress={showDialog}>
navigation.reset({
index: 0,
routes: [{name: 'Login'}],
})
}>
Keluar Keluar
</Button> </Button>
</View> </View>

View File

@ -43,6 +43,7 @@ import DialogPassportConditionInfo from '../../components/dialog/DialogPassportC
import DialogPassportTypeInfo from '../../components/dialog/DialogPassportTypeInfo'; import DialogPassportTypeInfo from '../../components/dialog/DialogPassportTypeInfo';
import SheetEditData from '../../components/sheet/SheetEditData'; import SheetEditData from '../../components/sheet/SheetEditData';
import SheetSearchLocation from '../../components/sheet/SheetSearchLocation'; import SheetSearchLocation from '../../components/sheet/SheetSearchLocation';
import SheetSelectDate from '../../components/sheet/SheetSelectDate';
type RegularPassportScreenNavigationProp = NativeStackNavigationProp< type RegularPassportScreenNavigationProp = NativeStackNavigationProp<
RootStackParamList, RootStackParamList,
@ -70,6 +71,7 @@ type RenderApplicationStepsContentProps = {
showPassportTypeInfoDialog: () => void; showPassportTypeInfoDialog: () => void;
showEditDataSheet: () => void; showEditDataSheet: () => void;
showSearchLocationSheet: () => void; showSearchLocationSheet: () => void;
showSelectDateSheet: () => void;
}; };
const RenderApplicationStepsContent = ( const RenderApplicationStepsContent = (
@ -96,6 +98,7 @@ const RenderApplicationStepsContent = (
showPassportTypeInfoDialog, showPassportTypeInfoDialog,
showEditDataSheet, showEditDataSheet,
showSearchLocationSheet, showSearchLocationSheet,
showSelectDateSheet,
} = props; } = props;
if (step === 1) { if (step === 1) {
@ -235,6 +238,7 @@ const RenderApplicationStepsContent = (
} }
showPassportTypeInfoDialog={showPassportTypeInfoDialog} showPassportTypeInfoDialog={showPassportTypeInfoDialog}
showSearchLocationSheet={showSearchLocationSheet} showSearchLocationSheet={showSearchLocationSheet}
showSelectDateSheet={showSelectDateSheet}
/> />
); );
case 7: case 7:
@ -336,6 +340,7 @@ function RegularPassportScreen() {
const [visibleEditDataSheet, setVisibleEditDataSheet] = useState(false); const [visibleEditDataSheet, setVisibleEditDataSheet] = useState(false);
const [visibleSearchLocationSheet, setVisibleSearchLocationSheet] = const [visibleSearchLocationSheet, setVisibleSearchLocationSheet] =
useState(false); useState(false);
const [visibleSelectDateSheet, setVisibleSelectDateSheet] = useState(false);
// Dialog visibility function // Dialog visibility function
const showDialog = () => setVisible(true); const showDialog = () => setVisible(true);
@ -377,6 +382,9 @@ function RegularPassportScreen() {
const showSearchLocationSheet = () => setVisibleSearchLocationSheet(true); const showSearchLocationSheet = () => setVisibleSearchLocationSheet(true);
const hideSearchLocationSheet = () => setVisibleSearchLocationSheet(false); const hideSearchLocationSheet = () => setVisibleSearchLocationSheet(false);
const showSelectDateSheet = () => setVisibleSelectDateSheet(true);
const hideSelectDateSheet = () => setVisibleSelectDateSheet(false);
const stepTitles: {[key: number]: string} = { const stepTitles: {[key: number]: string} = {
1: 'Informasi Pribadi', 1: 'Informasi Pribadi',
2: 'Dokumen Pendukung', 2: 'Dokumen Pendukung',
@ -457,6 +465,7 @@ function RegularPassportScreen() {
showPassportTypeInfoDialog={showPassportTypeInfoDialog} showPassportTypeInfoDialog={showPassportTypeInfoDialog}
showEditDataSheet={showEditDataSheet} showEditDataSheet={showEditDataSheet}
showSearchLocationSheet={showSearchLocationSheet} showSearchLocationSheet={showSearchLocationSheet}
showSelectDateSheet={showSelectDateSheet}
/> />
</View> </View>
@ -501,7 +510,11 @@ function RegularPassportScreen() {
<DialogSubmitSuccess <DialogSubmitSuccess
visible={visibleSubmitSuccessDialog} visible={visibleSubmitSuccessDialog}
onSubmitSuccess={() => { onSubmitSuccess={() => {
navigation.goBack(), hideSubmitSuccessDialog(); navigation.reset({
index: 0,
routes: [{name: 'NavigationRoute'}],
});
hideSubmitSuccessDialog();
}} }}
/> />
)} )}
@ -542,6 +555,14 @@ function RegularPassportScreen() {
onClose={hideSearchLocationSheet} onClose={hideSearchLocationSheet}
/> />
)} )}
{visibleSelectDateSheet && (
<SheetSelectDate
visible={visibleSelectDateSheet}
onClose={hideSelectDateSheet}
onContinue={hideSelectDateSheet}
/>
)}
</> </>
) : ( ) : (
<Questionnaire <Questionnaire
@ -566,7 +587,9 @@ function RegularPassportScreen() {
visibleFinalizationConfirmationDialog || visibleFinalizationConfirmationDialog ||
visiblePassportTypeInfoDialog visiblePassportTypeInfoDialog
? '#295E70' ? '#295E70'
: visibleEditDataSheet || visibleSearchLocationSheet : visibleEditDataSheet ||
visibleSearchLocationSheet ||
visibleSelectDateSheet
? '#185769' ? '#185769'
: Colors.secondary30.color : Colors.secondary30.color
} }

View File

@ -7,6 +7,12 @@ import {Button} from 'react-native-paper';
import Colors from '../../../../../assets/styles/Colors'; import Colors from '../../../../../assets/styles/Colors';
import jobData from '../../../../data/DropdownData/JobData'; import jobData from '../../../../data/DropdownData/JobData';
import nationalityData from '../../../../data/DropdownData/NationalityData'; import nationalityData from '../../../../data/DropdownData/NationalityData';
import {PassportAppointment} from '../../../../navigation/type';
import {
addData,
getData,
storeData,
} from '../../../../helper/asyncStorageHelper';
const Step4DataConfirmationSubStep2 = ({ const Step4DataConfirmationSubStep2 = ({
setStep, setStep,
@ -148,7 +154,49 @@ const Step4DataConfirmationSubStep2 = ({
<Button <Button
mode="contained" mode="contained"
onPress={() => setStep(5)} 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);
}}
style={[styles.subStepButtonContained, {marginBottom: 8}]} style={[styles.subStepButtonContained, {marginBottom: 8}]}
textColor={Colors.neutral100.color}> textColor={Colors.neutral100.color}>
Simpan Draft Simpan Draft

View File

@ -1,9 +1,11 @@
import React from 'react'; import React, {useEffect, useState} from 'react';
import {View, Text, Pressable} from 'react-native'; import {View, Text, Pressable} from 'react-native';
import {Button} from 'react-native-paper'; import {Button} from 'react-native-paper';
import Icon from 'react-native-vector-icons/MaterialCommunityIcons'; import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
import Colors from '../../../../../assets/styles/Colors'; import Colors from '../../../../../assets/styles/Colors';
import styles from '../styles'; import styles from '../styles';
import {getData} from '../../../../helper/asyncStorageHelper';
import {PassportAppointment} from '../../../../navigation/type';
type Step5VerificationProps = { type Step5VerificationProps = {
setStep: (step: number) => void; setStep: (step: number) => void;
@ -15,8 +17,23 @@ type Step5VerificationProps = {
const Step5Content = (props: Step5VerificationProps) => { const Step5Content = (props: Step5VerificationProps) => {
const {setStep, setSubStep, passportAppointmentData, showEditDataSheet} = const {setStep, setSubStep, passportAppointmentData, showEditDataSheet} =
props; props;
const lastAppointment =
passportAppointmentData[passportAppointmentData.length - 1]; const [lastAppointment, setLastAppointment] =
useState<PassportAppointment>();
useEffect(() => {
const fetchData = async () => {
try {
const data = await getData('passportAppointments');
if (Array.isArray(data) && data.length > 0) {
setLastAppointment(data[data.length - 1]);
}
} catch (error) {
console.error('Error fetching data: ', error);
}
};
fetchData();
}, []);
return ( return (
<View style={styles.subStepContainer}> <View style={styles.subStepContainer}>
@ -36,7 +53,7 @@ const Step5Content = (props: Step5VerificationProps) => {
styles.applicantDetailTextDesc, styles.applicantDetailTextDesc,
{textTransform: 'uppercase', flex: 0}, {textTransform: 'uppercase', flex: 0},
]}> ]}>
{lastAppointment.applicantName} {lastAppointment?.applicantName}
</Text> </Text>
</View> </View>
<View style={styles.applicantDetailIconContentWrapper}> <View style={styles.applicantDetailIconContentWrapper}>
@ -63,27 +80,27 @@ const Step5Content = (props: Step5VerificationProps) => {
<View style={styles.applicantDetailContentChildContainer}> <View style={styles.applicantDetailContentChildContainer}>
<DetailRow <DetailRow
label="NIK" label="NIK"
value={lastAppointment.applicationDetails.nationalIdNumber} value={lastAppointment?.applicationDetails?.nationalIdNumber}
/> />
<DetailRow <DetailRow
label="Jenis Kelamin" label="Jenis Kelamin"
value={lastAppointment.applicationDetails.gender} value={lastAppointment?.applicationDetails?.gender}
/> />
<DetailRow <DetailRow
label="Jenis Permohonan" label="Jenis Permohonan"
value={lastAppointment.applicationDetails.applicationType} value={lastAppointment?.applicationDetails?.applicationType}
/> />
<DetailRow <DetailRow
label="Alasan Penggantian" label="Alasan Penggantian"
value={lastAppointment.applicationDetails.replacementReason} value={lastAppointment?.applicationDetails?.replacementReason}
/> />
<DetailRow <DetailRow
label="Tujuan Permohonan" label="Tujuan Permohonan"
value={lastAppointment.applicationDetails.applicationPurpose} value={lastAppointment?.applicationDetails?.applicationPurpose}
/> />
<DetailRow <DetailRow
label="Jenis Paspor" label="Jenis Paspor"
value={lastAppointment.applicationDetails.passportType} value={lastAppointment?.applicationDetails?.passportType}
/> />
</View> </View>
</View> </View>
@ -110,7 +127,7 @@ const Step5Content = (props: Step5VerificationProps) => {
); );
}; };
const DetailRow = ({label, value}: {label: string; value: string}) => ( const DetailRow = ({label, value}: {label: string; value: string | undefined}) => (
<View style={styles.applicantDetailTextContentWrapper}> <View style={styles.applicantDetailTextContentWrapper}>
<Text style={styles.applicantDetailTextTitle}>{label}</Text> <Text style={styles.applicantDetailTextTitle}>{label}</Text>
<Text style={styles.applicantDetailTextDesc}>{value}</Text> <Text style={styles.applicantDetailTextDesc}>{value}</Text>

View File

@ -6,16 +6,22 @@ import styles from '../styles';
import Colors from '../../../../../assets/styles/Colors'; import Colors from '../../../../../assets/styles/Colors';
import FontFamily from '../../../../../assets/styles/FontFamily'; import FontFamily from '../../../../../assets/styles/FontFamily';
import arrivalDateGuidelinesData from '../../../../data/Steps/ArrivalDateGuidelinesData'; import arrivalDateGuidelinesData from '../../../../data/Steps/ArrivalDateGuidelinesData';
import passportTypeData from '../../../../data/DropdownData/PassportTypeData';
type Step6ProcessingProps = { type Step6ProcessingProps = {
showFinalizationConfirmationDialog: () => void; showFinalizationConfirmationDialog: () => void;
showPassportTypeInfoDialog: () => void; showPassportTypeInfoDialog: () => void;
showSearchLocationSheet: () => void; showSearchLocationSheet: () => void;
showSelectDateSheet: () => void;
}; };
const Step6Processing = (props: Step6ProcessingProps) => { const Step6Processing = (props: Step6ProcessingProps) => {
const {showFinalizationConfirmationDialog, showPassportTypeInfoDialog, showSearchLocationSheet} = const {
props; showFinalizationConfirmationDialog,
showPassportTypeInfoDialog,
showSearchLocationSheet,
showSelectDateSheet,
} = props;
return ( return (
<ScrollView> <ScrollView>
<View style={styles.subStepContainer}> <View style={styles.subStepContainer}>
@ -30,13 +36,12 @@ const Step6Processing = (props: Step6ProcessingProps) => {
</View> </View>
<View style={[styles.subStepTextInputContainer, {marginVertical: 16}]}> <View style={[styles.subStepTextInputContainer, {marginVertical: 16}]}>
{/* Trigger Search Location Bottom Sheet */}
<TextInputComponent <TextInputComponent
title="Lokasi Kantor Imigrasi" title="Lokasi Kantor Imigrasi"
placeholder="Pilih lokasi kantor imigrasi" placeholder="Pilih lokasi kantor imigrasi"
isRequired isRequired
isDropdownSearchLocation isDropdownPressedSheet
handlePressSearchLocation={showSearchLocationSheet} handleDropdownPressed={showSearchLocationSheet}
/> />
<TextInputComponent <TextInputComponent
title="Jenis Paspor" title="Jenis Paspor"
@ -44,6 +49,7 @@ const Step6Processing = (props: Step6ProcessingProps) => {
placeholder="Pilih satu jenis paspor" placeholder="Pilih satu jenis paspor"
isRequired isRequired
isDropdown isDropdown
dropdownItemData={passportTypeData}
onIconButtonPress={showPassportTypeInfoDialog} onIconButtonPress={showPassportTypeInfoDialog}
/> />
</View> </View>
@ -80,12 +86,12 @@ const Step6Processing = (props: Step6ProcessingProps) => {
</View> </View>
</View> </View>
{/* TODO: Add calendar functionality here. */}
<TextInputComponent <TextInputComponent
title="Tanggal dan Waktu Kedatangan" title="Tanggal dan Waktu Kedatangan"
placeholder="Pilih tanggal dan waktu kedatangan" placeholder="Pilih tanggal dan waktu kedatangan"
isRequired isRequired
isDate isDropdownPressedSheet
handleDropdownPressed={showSelectDateSheet}
/> />
<Button <Button

View File

@ -1,12 +1,13 @@
import React from 'react'; import React, {useEffect, useState} from 'react';
import {ScrollView, View} from 'react-native'; import {ScrollView, View} from 'react-native';
import {Button, Divider, Text} from 'react-native-paper'; import {Button, Divider, Text} from 'react-native-paper';
import Icon from 'react-native-vector-icons/MaterialCommunityIcons'; import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
import Colors from '../../../../../assets/styles/Colors'; import Colors from '../../../../../assets/styles/Colors';
import styles from '../styles'; import styles from '../styles';
import passportAppointmentData from '../../../../data/History/PassportAppointmentData';
import Accordion from '../../../../components/Accordion'; import Accordion from '../../../../components/Accordion';
import termsAndConditionsData from '../../../../data/Steps/TermsAndContionsData'; import termsAndConditionsData from '../../../../data/Steps/TermsAndContionsData';
import {PassportAppointment} from '../../../../navigation/type';
import {getData} from '../../../../helper/asyncStorageHelper';
type Step7CompletionProps = { type Step7CompletionProps = {
showSubmitSuccessDialog: () => void; showSubmitSuccessDialog: () => void;
@ -15,8 +16,21 @@ type Step7CompletionProps = {
const Step7Completion = (props: Step7CompletionProps) => { const Step7Completion = (props: Step7CompletionProps) => {
const {showSubmitSuccessDialog, setLastCompletedSteps} = props; const {showSubmitSuccessDialog, setLastCompletedSteps} = props;
const lastAppointment = const [lastAppointment, setLastAppointment] = useState<PassportAppointment>();
passportAppointmentData[passportAppointmentData.length - 1];
useEffect(() => {
const fetchData = async () => {
try {
const data = await getData('passportAppointments');
if (Array.isArray(data) && data.length > 0) {
setLastAppointment(data[data.length - 1]);
}
} catch (error) {
console.error('Error fetching data: ', error);
}
};
fetchData();
}, []);
return ( return (
<ScrollView> <ScrollView>
@ -31,7 +45,7 @@ const Step7Completion = (props: Step7CompletionProps) => {
color={Colors.secondary30.color} color={Colors.secondary30.color}
/> />
<Text style={styles.midIconContentTextStyle}> <Text style={styles.midIconContentTextStyle}>
{lastAppointment.appointmentDate} {lastAppointment?.appointmentDate}
</Text> </Text>
</View> </View>
<View style={styles.midIconContentWrapper}> <View style={styles.midIconContentWrapper}>
@ -41,7 +55,7 @@ const Step7Completion = (props: Step7CompletionProps) => {
color={Colors.secondary30.color} color={Colors.secondary30.color}
/> />
<Text style={styles.midIconContentTextStyle}> <Text style={styles.midIconContentTextStyle}>
{lastAppointment.appointmentTime} {lastAppointment?.appointmentTime}
</Text> </Text>
</View> </View>
<View style={styles.midIconContentWrapper}> <View style={styles.midIconContentWrapper}>
@ -51,7 +65,7 @@ const Step7Completion = (props: Step7CompletionProps) => {
color={Colors.secondary30.color} color={Colors.secondary30.color}
/> />
<Text style={styles.midIconContentTextStyle}> <Text style={styles.midIconContentTextStyle}>
{lastAppointment.serviceUnit} {lastAppointment?.serviceUnit}
</Text> </Text>
</View> </View>
</View> </View>
@ -60,13 +74,13 @@ const Step7Completion = (props: Step7CompletionProps) => {
<View style={styles.midTextContentWrapper}> <View style={styles.midTextContentWrapper}>
<Text style={styles.midTextContentTitle}>Tanggal Pengajuan</Text> <Text style={styles.midTextContentTitle}>Tanggal Pengajuan</Text>
<Text style={styles.midTextContentData}> <Text style={styles.midTextContentData}>
{lastAppointment.submissionDate} {lastAppointment?.submissionDate}
</Text> </Text>
</View> </View>
<View style={styles.midTextContentWrapper}> <View style={styles.midTextContentWrapper}>
<Text style={styles.midTextContentTitle}>Kode Layanan</Text> <Text style={styles.midTextContentTitle}>Kode Layanan</Text>
<Text style={styles.midTextContentData}> <Text style={styles.midTextContentData}>
{lastAppointment.serviceCode} {lastAppointment?.serviceCode}
</Text> </Text>
</View> </View>
</View> </View>
@ -130,7 +144,7 @@ const Step7Completion = (props: Step7CompletionProps) => {
styles.applicantDetailTextDesc, styles.applicantDetailTextDesc,
styles.applicantDetailTexDescName, styles.applicantDetailTexDescName,
]}> ]}>
{lastAppointment.applicantName} {lastAppointment?.applicantName}
</Text> </Text>
</View> </View>
<View style={styles.applicantDetailTextContentWrapper}> <View style={styles.applicantDetailTextContentWrapper}>
@ -140,20 +154,20 @@ const Step7Completion = (props: Step7CompletionProps) => {
styles.applicantDetailTextDesc, styles.applicantDetailTextDesc,
styles.applicantDetailTexDescCode, styles.applicantDetailTexDescCode,
]}> ]}>
{lastAppointment.applicantCode} {lastAppointment?.applicantCode}
</Text> </Text>
</View> </View>
<View style={styles.applicantDetailContentChildContainer}> <View style={styles.applicantDetailContentChildContainer}>
<View style={styles.applicantDetailTextContentWrapper}> <View style={styles.applicantDetailTextContentWrapper}>
<Text style={styles.applicantDetailTextTitle}>NIK</Text> <Text style={styles.applicantDetailTextTitle}>NIK</Text>
<Text style={styles.applicantDetailTextDesc}> <Text style={styles.applicantDetailTextDesc}>
{lastAppointment.applicationDetails.nationalIdNumber} {lastAppointment?.applicationDetails?.nationalIdNumber}
</Text> </Text>
</View> </View>
<View style={styles.applicantDetailTextContentWrapper}> <View style={styles.applicantDetailTextContentWrapper}>
<Text style={styles.applicantDetailTextTitle}>Jenis Kelamin</Text> <Text style={styles.applicantDetailTextTitle}>Jenis Kelamin</Text>
<Text style={styles.applicantDetailTextDesc}> <Text style={styles.applicantDetailTextDesc}>
{lastAppointment.applicationDetails.gender} {lastAppointment?.applicationDetails?.gender}
</Text> </Text>
</View> </View>
<View style={styles.applicantDetailTextContentWrapper}> <View style={styles.applicantDetailTextContentWrapper}>
@ -161,7 +175,7 @@ const Step7Completion = (props: Step7CompletionProps) => {
Jenis Permohonan Jenis Permohonan
</Text> </Text>
<Text style={styles.applicantDetailTextDesc}> <Text style={styles.applicantDetailTextDesc}>
{lastAppointment.applicationDetails.applicationType} {lastAppointment?.applicationDetails?.applicationType}
</Text> </Text>
</View> </View>
<View style={styles.applicantDetailTextContentWrapper}> <View style={styles.applicantDetailTextContentWrapper}>
@ -169,7 +183,7 @@ const Step7Completion = (props: Step7CompletionProps) => {
Alasan Penggantian Alasan Penggantian
</Text> </Text>
<Text style={styles.applicantDetailTextDesc}> <Text style={styles.applicantDetailTextDesc}>
{lastAppointment.applicationDetails.replacementReason} {lastAppointment?.applicationDetails?.replacementReason}
</Text> </Text>
</View> </View>
<View style={styles.applicantDetailTextContentWrapper}> <View style={styles.applicantDetailTextContentWrapper}>
@ -177,16 +191,23 @@ const Step7Completion = (props: Step7CompletionProps) => {
Tujuan Permohonan Tujuan Permohonan
</Text> </Text>
<Text style={styles.applicantDetailTextDesc}> <Text style={styles.applicantDetailTextDesc}>
{lastAppointment.applicationDetails.applicationPurpose} {lastAppointment?.applicationDetails?.applicationPurpose}
</Text> </Text>
</View> </View>
<View style={styles.applicantDetailTextContentWrapper}> <View style={styles.applicantDetailTextContentWrapper}>
<Text style={styles.applicantDetailTextTitle}>Jenis Paspor</Text> <Text style={styles.applicantDetailTextTitle}>Jenis Paspor</Text>
<Text style={styles.applicantDetailTextDesc}> <Text style={styles.applicantDetailTextDesc}>
{lastAppointment.applicationDetails.passportType} {lastAppointment?.applicationDetails?.passportType}
</Text> </Text>
</View> </View>
</View> </View>
<Divider style={styles.applicantDetailDividerMargin} />
<View style={styles.applicantDetailBottomContentWrapper}>
<Text style={styles.applicantDetailBottomText}>Biaya</Text>
<Text style={styles.applicantDetailBottomText}>
{lastAppointment?.applicationDetails?.fee}
</Text>
</View>
</View> </View>
</View> </View>
<View style={{margin: 16}}> <View style={{margin: 16}}>

View File

@ -200,6 +200,15 @@ const styles = StyleSheet.create({
applicantDetailTexDescCode: { applicantDetailTexDescCode: {
textAlign: 'right', textAlign: 'right',
}, },
applicantDetailBottomText: {
fontSize: 14,
...FontFamily.notoSansBold,
color: Colors.primary30.color,
includeFontPadding: false,
},
applicantDetailDividerMargin: {
marginVertical: 4,
},
midContainer: { midContainer: {
backgroundColor: Colors.neutral100.color, backgroundColor: Colors.neutral100.color,
}, },