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",
"version": "0.0.1",
"dependencies": {
"@react-native-async-storage/async-storage": "^2.1.2",
"@react-native-community/datetimepicker": "^8.3.0",
"@react-navigation/elements": "^2.3.8",
"@react-navigation/native": "^7.1.6",
"@react-navigation/native-stack": "^7.3.10",
"dayjs": "^1.11.13",
"moment": "^2.30.1",
"react": "19.0.0",
"react-native": "0.78.0",
"react-native-calendars": "^1.1311.1",
"react-native-element-dropdown": "^2.12.4",
"react-native-paper": "^5.13.2",
"react-native-safe-area-context": "^5.4.0",
@ -2609,6 +2612,18 @@
"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": {
"version": "15.0.1",
"resolved": "https://registry.npmjs.org/@react-native-community/cli/-/cli-15.0.1.tgz",
@ -7576,6 +7591,15 @@
"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": {
"version": "2.0.4",
"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",
"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": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
@ -9678,6 +9714,15 @@
"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": {
"version": "2.1.3",
"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": {
"version": "2.12.4",
"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"
}
},
"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": {
"version": "10.2.0",
"resolved": "https://registry.npmjs.org/react-native-vector-icons/-/react-native-vector-icons-10.2.0.tgz",
@ -10832,6 +10915,21 @@
"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": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
@ -12058,6 +12156,12 @@
"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": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
@ -12569,6 +12673,12 @@
"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": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",

View File

@ -10,13 +10,16 @@
"test": "jest"
},
"dependencies": {
"@react-native-async-storage/async-storage": "^2.1.2",
"@react-native-community/datetimepicker": "^8.3.0",
"@react-navigation/elements": "^2.3.8",
"@react-navigation/native": "^7.1.6",
"@react-navigation/native-stack": "^7.3.10",
"dayjs": "^1.11.13",
"moment": "^2.30.1",
"react": "19.0.0",
"react-native": "0.78.0",
"react-native-calendars": "^1.1311.1",
"react-native-element-dropdown": "^2.12.4",
"react-native-paper": "^5.13.2",
"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';
type PassportAppointmentCardProps = {
applicantName: string;
applicantCode: string;
appointmentDate: string;
appointmentTime: string;
serviceUnit: string;
status: string;
applicantName: string | undefined;
applicantCode: string | undefined;
appointmentDate: string | undefined;
appointmentTime: string | undefined;
serviceUnit: string | undefined;
status: string | undefined;
};
const renderStatusContent = (status: string) => {
const renderStatusContent = (status: string | undefined) => {
let backgroundColor;
let IconComponent;

View File

@ -38,8 +38,8 @@ interface TextInputComponentProps {
supportText?: string;
containerHeight?: any;
isMultiline?: boolean;
isDropdownSearchLocation?: boolean;
handlePressSearchLocation?: () => void;
isDropdownPressedSheet?: boolean;
handleDropdownPressed?: () => void;
}
const TextInputComponent = (props: TextInputComponentProps) => {
@ -59,8 +59,8 @@ const TextInputComponent = (props: TextInputComponentProps) => {
supportText,
containerHeight,
isMultiline = false,
isDropdownSearchLocation = false,
handlePressSearchLocation,
isDropdownPressedSheet = false,
handleDropdownPressed,
} = props;
const [secureText, setSecureText] = useState(isPassword);
@ -281,7 +281,7 @@ const TextInputComponent = (props: TextInputComponentProps) => {
);
}
if (isDropdownSearchLocation) {
if (isDropdownPressedSheet) {
return (
<View>
{title && (
@ -291,7 +291,7 @@ const TextInputComponent = (props: TextInputComponentProps) => {
</View>
)}
<Pressable
onPress={handlePressSearchLocation}
onPress={handleDropdownPressed}
style={({pressed}) => ({
transform: [{scale: pressed ? 0.99 : 1}],
})}>
@ -303,7 +303,14 @@ const TextInputComponent = (props: TextInputComponentProps) => {
placeholderTextColor={Colors.primary60.color}
editable={false}
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}
textColor="#48454E"
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 {Dialog, Portal, Button} from 'react-native-paper';
import {
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 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 = {
visible: boolean;
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 {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 (
<Portal>
<Dialog visible={visible} style={styles.dialogContainer}>
<Dialog.Title style={styles.dialogTitle}>
Jenis dan Manfaat Paspor
</Dialog.Title>
<View style={styles.dialogContentContainer}>
<Text style={[styles.dialogDesc, {...FontFamily.notoSansBold}]}>
1. Paspor Biasa
</Text>
<Text style={styles.dialogDesc}>
Biaya pemrosesan lebih murah, tersedia di seluruh lokasi Kanim.
{'\n'}Biaya pembuatan: Rp350.000
</Text>
<Text style={[styles.dialogDesc, {...FontFamily.notoSansBold}]}>
2. Paspor Elektronik
</Text>
<Text style={styles.dialogDesc}>
Terdapat chip pada cover paspor yang menyimpan data pemegang paspor
sehingga meningkatkan fitur keamanan, tersedia di 35 Kanim.{'\n'}
Biaya pembuatan: Rp650.000
</Text>
<Text style={[styles.dialogDesc, {...FontFamily.notoSansBold}]}>
3. Paspor Elektronik Polikarbonat
</Text>
<Text style={styles.dialogDesc}>
Berbahan polikarbonat (PC) pada halaman data pemegang paspor
(halaman 2) yang memiliki fitur keamanan tinggi dan lebih kuat,
tersedia di Kanim Jakarta Selatan, Kanim Jakarta Barat, dan Kanim
Soekarno Hatta.{'\n'}Biaya pembuatan: Rp650.000
</Text>
<View>
<Button
style={styles.buttonContinue}
mode="contained"
textColor={Colors.neutral100.color}
onPress={() => {
onClose();
}}>
Lanjut
</Button>
</View>
<Modal visible={visible} transparent animationType="slide">
<Pressable style={styles.modalBackdrop} onPress={onClose} />
<View style={styles.modalContentContainer}>
<ScrollView
keyboardShouldPersistTaps="handled"
showsVerticalScrollIndicator={false}
contentContainerStyle={styles.modalScrollViewContent}>
<View style={styles.modalCloseButtonContainer}>
<Pressable
style={({pressed}) => [
styles.closeButton,
pressed && styles.closeButtonPressed,
]}
onPress={onClose}>
<Icon name="close" size={24} color={Colors.primary30.color} />
</Pressable>
</View>
<View style={styles.calendarContainer}>
<Calendar
key={currentMonth}
theme={{
textDayFontSize: 14,
textDayStyle: {
includeFontPadding: false,
marginTop: 6,
...FontFamily.notoSansRegular,
},
textDayHeaderFontFamily: 'NotoSans-Medium',
textDayHeaderFontSize: 14,
textSectionTitleColor: Colors.primary30.color,
arrowColor: Colors.secondary30.color,
}}
onDayPress={day => setSelectedDate(day.dateString)}
markedDates={markedDates}
hideArrows={true}
current={currentMonth}
onMonthChange={month => {
const newMonth = `${month.year}-${String(
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>
</Dialog>
</Modal>
</Portal>
);
};
const styles = StyleSheet.create({
dialogContainer: {
backgroundColor: 'white',
elevation: 0,
shadowColor: 'transparent',
borderRadius: 20,
modalBackdrop: {
flex: 1,
backgroundColor: 'rgba(0,0,0,0.3)',
},
dialogContentContainer: {
marginHorizontal: 24,
marginBottom: 24,
modalContentContainer: {
backgroundColor: 'white',
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,
},
dialogTitle: {
fontSize: 22,
color: Colors.secondary30.color,
legendItemContainer: {
flexDirection: 'row',
alignItems: 'center',
gap: 4,
},
dialogDesc: {
fontSize: 14,
legendColorBox: {
width: 16,
height: 16,
marginRight: 6,
borderRadius: 8,
},
legendColorBoxBorder: {
borderWidth: 1,
borderColor: '#ccc',
},
legendLabel: {
fontSize: 11,
...FontFamily.notoSansRegular,
includeFontPadding: false,
color: Colors.primary30.color,
lineHeight: 22,
},
dialogDescRed: {
...FontFamily.notoSansBold,
color: Colors.indicatorRed.color,
includeFontPadding: false,
continueButtonWrapper: {
marginTop: 16,
},
buttonContinue: {
continueButton: {
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',
applicantName: 'Salwa Aisyah Adhani',
applicantName: 'Mochammad Adhi Buchori',
applicantCode: '1038000002223344',
appointmentDate: 'Senin, 14 April 2025',
appointmentTime: '08:00 - 09:00 WIB',
serviceUnit: 'Kantor Imigrasi Depok',
serviceUnit: 'Kantor Imigrasi Jakarta',
status: 'Menunggu Pembayaran',
submissionDate: 'Sabtu, 12 April 2025 18:30',
serviceCode: 'EH-GT4JWR',
applicationDetails: {
nationalIdNumber: '3271234560009120003',
gender: 'Wanita',
gender: 'Pria',
applicationType: 'Baru',
replacementReason: 'Sekolah di Luar Negeri',
applicationPurpose: 'Wisata/Liburan',
@ -101,26 +101,6 @@ const passportAppointmentData = [
},
{
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',
applicantCode: '1038000007773344',
appointmentDate: 'Rabu, 19 April 2025',
@ -139,6 +119,26 @@ const passportAppointmentData = [
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;

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

View File

@ -23,3 +23,26 @@ export type RootStackParamList = {
OtherMethod: 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,
...FontFamily.notoSansBold,
color: Colors.primary30.color,
includeFontPadding: false,
},
applicantDetailContentChildButton: {
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 styles from './styles';
import Colors from '../../../assets/styles/Colors';
@ -10,8 +10,10 @@ import {
useNavigationState,
} from '@react-navigation/native';
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 {getData} from '../../helper/asyncStorageHelper';
import {ActivityIndicator} from 'react-native-paper';
type HistoryScreenNavigationProp = NativeStackNavigationProp<
RootStackParamList,
@ -29,6 +31,28 @@ function HistoryScreen() {
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(
useCallback(() => {
StatusBar.setBackgroundColor(Colors.secondary30.color);
@ -60,31 +84,37 @@ function HistoryScreen() {
</View>
)}
<View style={styles.topBackground} />
<View style={styles.cardWrapper}>
<FlatList
data={passportAppointmentData}
renderItem={({item}) => (
<Pressable
onPress={() =>
navigation.navigate('ApplicationDetail', {data: item})
}
style={({pressed}) => ({
transform: [{scale: pressed ? 0.975 : 1}],
})}>
<PassportAppointmentCard
applicantName={item.applicantName}
applicantCode={item.applicantCode}
appointmentDate={item.appointmentDate}
appointmentTime={item.appointmentTime}
serviceUnit={item.serviceUnit}
status={item.status}
/>
</Pressable>
)}
keyExtractor={item => item.id}
ItemSeparatorComponent={ItemSeparator}
/>
</View>
{isLoading ? (
<View style={{flex: 1, justifyContent: 'center', alignItems: 'center'}}>
<ActivityIndicator size="large" color={Colors.secondary30.color} />
</View>
) : (
<View style={styles.cardWrapper}>
<FlatList
data={appointments}
renderItem={({item}: any) => (
<Pressable
onPress={() =>
navigation.navigate('ApplicationDetail', {data: item})
}
style={({pressed}) => ({
transform: [{scale: pressed ? 0.975 : 1}],
})}>
<PassportAppointmentCard
applicantName={item.applicantName}
applicantCode={item.applicantCode}
appointmentDate={item.appointmentDate}
appointmentTime={item.appointmentTime}
serviceUnit={item.serviceUnit}
status={item.status}
/>
</Pressable>
)}
keyExtractor={item => item.id ?? ''}
ItemSeparatorComponent={ItemSeparator}
/>
</View>
)}
</View>
);
}

View File

@ -13,15 +13,15 @@ import {
import Colors from '../../../assets/styles/Colors';
import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
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 RegularPassportIcon from '../../../assets/icons/regular_passport.svg';
import ExpressPassportIcon from '../../../assets/icons/express_passport.svg';
import GuidebookIcon from '../../../assets/icons/guidebook.svg';
import EazyPassportIcon from '../../../assets/icons/eazy_passport.svg';
import passportAppointmentData from '../../data/History/PassportAppointmentData';
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<
RootStackParamList,
@ -32,6 +32,7 @@ const ItemSeparator = () => <View style={styles.flatllistGap} />;
type RenderContentProps = {
showDialog: () => void;
lastTwoAppointments: PassportAppointment[];
};
const RenderBanner = () => {
@ -147,7 +148,10 @@ const RenderBanner = () => {
);
};
const RenderContent = ({showDialog}: RenderContentProps) => {
const RenderContent = ({
showDialog,
lastTwoAppointments,
}: RenderContentProps) => {
const navigation = useNavigation<HomeScreenNavigationProp>();
return (
@ -223,8 +227,8 @@ const RenderContent = ({showDialog}: RenderContentProps) => {
</View>
<View style={styles.cardWrapper}>
<FlatList
data={passportAppointmentData.slice(-2)}
renderItem={({item}) => (
data={lastTwoAppointments}
renderItem={({item}: any) => (
<Pressable
onPress={() =>
navigation.navigate('ApplicationDetail', {data: item})
@ -242,7 +246,7 @@ const RenderContent = ({showDialog}: RenderContentProps) => {
/>
</Pressable>
)}
keyExtractor={item => item.id}
keyExtractor={item => item.id ?? ''}
ItemSeparatorComponent={ItemSeparator}
/>
</View>
@ -256,9 +260,28 @@ type HomeScreenProps = {
readonly visible: boolean;
};
function HomeScreen({showDialog, visible}: HomeScreenProps) {
function HomeScreen(props: HomeScreenProps) {
const {showDialog, visible} = props;
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(
useCallback(() => {
StatusBar.setBackgroundColor(
@ -276,7 +299,9 @@ function HomeScreen({showDialog, visible}: HomeScreenProps) {
return (
<View style={styles.container}>
<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
name="bell-outline"
size={24}
@ -286,7 +311,12 @@ function HomeScreen({showDialog, visible}: HomeScreenProps) {
</View>
<FlatList
data={[{}]}
renderItem={() => <RenderContent showDialog={showDialog} />}
renderItem={() => (
<RenderContent
showDialog={showDialog}
lastTwoAppointments={lastTwoAppointments}
/>
)}
/>
</View>
);

View File

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

View File

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

View File

@ -1,23 +1,16 @@
import * as React from 'react';
import {
BottomNavigation,
Button,
Dialog,
PaperProvider,
Portal,
Text,
} from 'react-native-paper';
import {BottomNavigation, PaperProvider, Text} from 'react-native-paper';
import Colors from '../../../assets/styles/Colors';
import ProfileScreen from '../profile';
import styles from './styles';
import HomeScreen from '../home';
import HistoryScreen from '../history';
import {View} from 'react-native';
import {useState} from 'react';
import {NativeStackNavigationProp} from '@react-navigation/native-stack';
import {RootStackParamList} from '../../navigation/type';
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<
RootStackParamList,
@ -26,7 +19,11 @@ type NavigationRouteScreenNavigationProp = NativeStackNavigationProp<
function NavigationRouteScreen() {
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 [routes] = useState([
{
@ -48,17 +45,32 @@ function NavigationRouteScreen() {
},
]);
const showDialog = () => setVisible(true);
const hideDialog = () => setVisible(false);
const showWarningApplicationDialog = () =>
setVisibleWarningApplicationDialog(true);
const hideWarningApplicationDialog = () =>
setVisibleWarningApplicationDialog(false);
const showLogoutDialog = () => setVisibleLogoutDialog(true);
const hideLogoutDialog = () => setVisibleLogoutDialog(false);
const renderScene = ({route}: {route: {key: string}}) => {
switch (route.key) {
case 'home':
return <HomeScreen showDialog={showDialog} visible={visible} />;
return (
<HomeScreen
visible={visibleWarningApplicationDialog}
showDialog={showWarningApplicationDialog}
/>
);
case 'history':
return <HistoryScreen />;
case 'profile':
return <ProfileScreen />;
return (
<ProfileScreen
visible={visibleLogoutDialog}
showDialog={showLogoutDialog}
/>
);
default:
return null;
}
@ -82,35 +94,25 @@ function NavigationRouteScreen() {
<Text style={styles.bottomNavLabel}>{route.title}</Text>
)}
/>
<Portal>
<Dialog visible={visible} style={styles.dialogContainer}>
<Dialog.Title style={styles.dialogTitle}>Peringatan</Dialog.Title>
<View style={styles.dialogContentContainer}>
<Text style={styles.dialogDesc}>
Silakan melakukan pengisian kuesioner.
</Text>
<Text style={[styles.dialogDesc, {...FontFamily.notoSansBold}]}>
Pastikan data dan jawaban yang Anda berikan benar.
</Text>
<Text style={styles.dialogDesc}>
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.buttonContinue}
mode="contained"
textColor={Colors.neutral100.color}
onPress={() => {
hideDialog();
navigation.navigate('RegularPassport');
}}>
Lanjut
</Button>
</View>
</Dialog>
</Portal>
{visibleWarningApplicationDialog && (
<DialogWarningApplication
visible={visibleWarningApplicationDialog}
hideDialog={hideWarningApplicationDialog}
onNavigate={() => navigation.navigate('RegularPassport')}
/>
)}
{visibleLogoutDialog && (
<DialogLogout
visible={visibleLogoutDialog}
hideDialog={hideLogoutDialog}
onNavigate={() =>
navigation.reset({
index: 0,
routes: [{name: 'Login'}],
})
}
/>
)}
</PaperProvider>
);
}

View File

@ -10,32 +10,6 @@ const styles = StyleSheet.create({
fontSize: 12,
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;

View File

@ -13,20 +13,29 @@ type ProfileScreenNavigationProp = NativeStackNavigationProp<
'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 navigation = useNavigation<ProfileScreenNavigationProp>();
useFocusEffect(
useCallback(() => {
StatusBar.setBackgroundColor(Colors.secondary30.color);
StatusBar.setBackgroundColor(
visible ? '#295E70' : Colors.secondary30.color,
);
StatusBar.setBarStyle('light-content');
return () => {
StatusBar.setBackgroundColor(Colors.secondary30.color);
StatusBar.setBarStyle('light-content');
};
}, []),
}, [visible]),
);
return (
<View style={styles.container}>
@ -35,7 +44,7 @@ function ProfileScreen() {
</View>
<View style={styles.topContainer}>
<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>
</View>
<View style={styles.sectionProfileField}>
@ -80,12 +89,7 @@ function ProfileScreen() {
mode="outlined"
textColor={Colors.indicatorRed.color}
style={styles.logoutButton}
onPress={() =>
navigation.reset({
index: 0,
routes: [{name: 'Login'}],
})
}>
onPress={showDialog}>
Keluar
</Button>
</View>

View File

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

View File

@ -7,6 +7,12 @@ import {Button} from 'react-native-paper';
import Colors from '../../../../../assets/styles/Colors';
import jobData from '../../../../data/DropdownData/JobData';
import nationalityData from '../../../../data/DropdownData/NationalityData';
import {PassportAppointment} from '../../../../navigation/type';
import {
addData,
getData,
storeData,
} from '../../../../helper/asyncStorageHelper';
const Step4DataConfirmationSubStep2 = ({
setStep,
@ -148,7 +154,49 @@ const Step4DataConfirmationSubStep2 = ({
<Button
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}]}
textColor={Colors.neutral100.color}>
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 {Button} from 'react-native-paper';
import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
import Colors from '../../../../../assets/styles/Colors';
import styles from '../styles';
import {getData} from '../../../../helper/asyncStorageHelper';
import {PassportAppointment} from '../../../../navigation/type';
type Step5VerificationProps = {
setStep: (step: number) => void;
@ -15,8 +17,23 @@ type Step5VerificationProps = {
const Step5Content = (props: Step5VerificationProps) => {
const {setStep, setSubStep, passportAppointmentData, showEditDataSheet} =
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 (
<View style={styles.subStepContainer}>
@ -36,7 +53,7 @@ const Step5Content = (props: Step5VerificationProps) => {
styles.applicantDetailTextDesc,
{textTransform: 'uppercase', flex: 0},
]}>
{lastAppointment.applicantName}
{lastAppointment?.applicantName}
</Text>
</View>
<View style={styles.applicantDetailIconContentWrapper}>
@ -63,27 +80,27 @@ const Step5Content = (props: Step5VerificationProps) => {
<View style={styles.applicantDetailContentChildContainer}>
<DetailRow
label="NIK"
value={lastAppointment.applicationDetails.nationalIdNumber}
value={lastAppointment?.applicationDetails?.nationalIdNumber}
/>
<DetailRow
label="Jenis Kelamin"
value={lastAppointment.applicationDetails.gender}
value={lastAppointment?.applicationDetails?.gender}
/>
<DetailRow
label="Jenis Permohonan"
value={lastAppointment.applicationDetails.applicationType}
value={lastAppointment?.applicationDetails?.applicationType}
/>
<DetailRow
label="Alasan Penggantian"
value={lastAppointment.applicationDetails.replacementReason}
value={lastAppointment?.applicationDetails?.replacementReason}
/>
<DetailRow
label="Tujuan Permohonan"
value={lastAppointment.applicationDetails.applicationPurpose}
value={lastAppointment?.applicationDetails?.applicationPurpose}
/>
<DetailRow
label="Jenis Paspor"
value={lastAppointment.applicationDetails.passportType}
value={lastAppointment?.applicationDetails?.passportType}
/>
</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}>
<Text style={styles.applicantDetailTextTitle}>{label}</Text>
<Text style={styles.applicantDetailTextDesc}>{value}</Text>

View File

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

View File

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

View File

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