import Select, { SelectOption } from "@/components/Select"; import { ThemedText } from "@/components/themed-text"; import { Colors } from "@/config"; import { ColorScheme, useTheme } from "@/hooks/use-theme-context"; import { usePort } from "@/state/use-ports"; import { useShipGroups } from "@/state/use-ship-groups"; import { useShipTypes } from "@/state/use-ship-types"; import { useThings } from "@/state/use-thing"; import DateTimePicker from "@react-native-community/datetimepicker"; import { useEffect, useMemo, useState } from "react"; import { Controller, useForm } from "react-hook-form"; import { KeyboardAvoidingView, Modal, Platform, Pressable, ScrollView, StyleSheet, TextInput, TouchableOpacity, View, } from "react-native"; import { SafeAreaView } from "react-native-safe-area-context"; interface CreateOrUpdateShipProps { initialValue?: Model.ShipBodyRequest; isOpen?: boolean; type?: "create" | "update"; onSubmit?: (data: Model.ShipBodyRequest) => void; onClose?: () => void; } const CreateOrUpdateShip = (props: CreateOrUpdateShipProps) => { const { colors, colorScheme } = useTheme(); const styles = useMemo( () => createStyles(colors, colorScheme), [colors, colorScheme] ); const { shipTypes, getShipTypes } = useShipTypes(); const { ports, getPorts } = usePort(); const { shipGroups, getShipGroups } = useShipGroups(); const { things, getThings } = useThings(); // State for date picker const [showDatePicker, setShowDatePicker] = useState(false); // Initialize form with react-hook-form const { control, handleSubmit, formState: { errors }, setValue, watch, reset, } = useForm({ defaultValues: props.initialValue || { fishing_license_expiry_date: new Date(), }, }); // Watch the date field for picker display const dateValue = watch("fishing_license_expiry_date"); // Fetch data when modal opens useEffect(() => { if (props.isOpen) { // Fetch ship types if not loaded if (shipTypes === null || shipTypes.length === 0) { getShipTypes(); } // Fetch ports if not loaded if (ports === null) { getPorts(); } // Fetch ship groups if not loaded if (shipGroups === null) { getShipGroups(); } // Fetch things when modal opens const payloadThings: Model.SearchThingBody = { offset: 0, limit: 200, order: "name", dir: "asc", }; getThings(payloadThings); // Reset form with initial values if provided if (props.initialValue) { reset(props.initialValue); } } }, [props.isOpen, props.initialValue, reset]); useEffect(() => { if (props.type === "create") { reset(props.initialValue); } }, [props.isOpen]); // Prepare options for selects const shipTypeOptions = useMemo(() => { return (shipTypes || []).map((type) => ({ label: type.name || "", value: type.id || 0, })); }, [shipTypes]); const portOptions = useMemo(() => { return (ports?.ports || []).map((port) => ({ label: port.name || "", value: port.id || 0, })); }, [ports]); const shipGroupOptions = useMemo(() => { return (shipGroups || []).map((group) => ({ label: group.name || "", value: group.id || "", })); }, [shipGroups]); const thingOptions = useMemo(() => { // Filter things that are not assigned to any ship const unassignedThings = (things || []).filter( (thing) => !thing.metadata?.ship_id ); return unassignedThings.map((thing) => ({ label: thing.name || "", value: thing.id || "", })); }, [things]); // Handle date picker change const handleDateChange = (_: any, selectedDate?: Date) => { if (selectedDate) { setValue("fishing_license_expiry_date", selectedDate); } // On Android, close picker after selection // On iOS, keep it open until user confirms with the button if (Platform.OS === "android") { setShowDatePicker(false); } }; // Format date for display const formatDateForDisplay = (date: Date | string | undefined) => { if (!date) return ""; const d = typeof date === "string" ? new Date(date) : date; return d.toLocaleDateString("vi-VN", { day: "2-digit", month: "2-digit", year: "numeric", }); }; // Handle form submission const onSubmit = (data: Model.ShipBodyRequest) => { // Ensure numeric fields are numbers const payload: Model.ShipBodyRequest = { ...data, ship_type: Number(data.ship_type), home_port: Number(data.home_port), ship_length: Number(data.ship_length), ship_power: Number(data.ship_power), fishing_license_expiry_date: data.fishing_license_expiry_date, }; props.onSubmit?.(payload); }; return ( {/* Header */} {props.type === "create" ? "Thêm tàu mới" : "Cập nhật tàu"} {/* Form Content */} {/* Registration Number - Only show in create mode */} {props.type === "create" && ( Số đăng ký * ( onChange(text.trim())} value={value} placeholderTextColor={colors.textSecondary} /> )} /> {errors.reg_number && ( {errors.reg_number.message} )} )} {/* Ship Name */} Tên tàu * ( )} /> {errors.name && ( {errors.name.message} )} {/* Ship Type */} Loại tàu * ( )} /> {errors.home_port && ( {errors.home_port.message} )} {/* Fishing License Number */} Số giấy phép * ( )} /> {errors.fishing_license_number && ( {errors.fishing_license_number.message} )} {/* Fishing License Expiry Date */} Ngày hết hạn * { if (!date) return "Vui lòng chọn ngày hết hạn"; const selectedDate = new Date(date); const today = new Date(); today.setHours(0, 0, 0, 0); if (selectedDate < today) { return "Ngày hết hạn không thể là ngày trong quá khứ"; } return true; }, }} render={({ field: { onChange, value } }) => ( setShowDatePicker(true)} style={[ styles.input, styles.dateInput, { borderColor: errors.fishing_license_expiry_date ? "red" : colors.border, }, ]} > {formatDateForDisplay(value) || "Chọn ngày hết hạn"} )} /> {errors.fishing_license_expiry_date && ( {errors.fishing_license_expiry_date.message} )} {/* Ship Length */} Chiều dài (m) * { const num = Number(value); if (isNaN(num) || num <= 0) { return "Chiều dài phải lớn hơn 0"; } return true; }, }} render={({ field: { onChange, onBlur, value } }) => ( )} /> {errors.ship_length && ( {errors.ship_length.message} )} {/* Ship Power */} Công suất (mã lực) * { const num = Number(value); if (isNaN(num) || num <= 0) { return "Công suất phải lớn hơn 0"; } return true; }, }} render={({ field: { onChange, onBlur, value } }) => ( )} /> {errors.ship_power && ( {errors.ship_power.message} )} {/* Ship Group - Only show in update mode */} {props.type === "update" && ( Đội tàu ( )} /> {errors.thing_id && ( {errors.thing_id.message} )} )} {/* Action Buttons */} { reset(props.initialValue || {}); }} > Nhập lại {props.type === "create" ? "Thêm tàu" : "Cập nhật"} {/* Date Picker Modal - Only show on Android as modal, iOS shows inline */} {Platform.OS === "android" && showDatePicker && ( )} {Platform.OS === "ios" && showDatePicker && ( setShowDatePicker(false)} > Chọn ngày hết hạn setShowDatePicker(false)}> setShowDatePicker(false)} > Xác nhận )} ); }; const createStyles = (colors: typeof Colors.light, scheme: ColorScheme) => StyleSheet.create({ container: { flex: 1, position: "relative", }, keyboardAvoidingView: { flex: 1, justifyContent: "flex-end", }, backdrop: { position: "absolute", top: 0, left: 0, right: 0, bottom: 0, backgroundColor: "rgba(0, 0, 0, 0.3)", }, modalContent: { height: "90%", backgroundColor: colors.background, borderTopLeftRadius: 24, borderTopRightRadius: 24, shadowColor: "#000", shadowOffset: { width: 0, height: -4, }, shadowOpacity: 0.25, shadowRadius: 8, elevation: 10, }, header: { flexDirection: "row", alignItems: "center", justifyContent: "center", paddingVertical: 16, paddingHorizontal: 20, borderBottomWidth: 1, borderBottomColor: colors.border, position: "relative", }, dragIndicator: { position: "absolute", top: 8, width: 40, height: 4, backgroundColor: colors.border, borderRadius: 2, }, headerTitle: { fontSize: 18, fontWeight: "700", textAlign: "center", color: colors.text, }, closeButton: { position: "absolute", right: 16, top: 16, width: 32, height: 32, alignItems: "center", justifyContent: "center", borderRadius: 16, }, closeButtonText: { fontSize: 20, fontWeight: "300", color: colors.text, }, scrollView: { flex: 1, padding: 20, }, scrollContent: { paddingBottom: Platform.OS === "ios" ? 120 : 80, }, fieldGroup: { marginBottom: 24, }, label: { fontSize: 15, fontWeight: "600", marginBottom: 8, color: colors.text, }, input: { borderWidth: 1, borderRadius: 12, paddingHorizontal: 16, paddingVertical: 14, fontSize: 15, backgroundColor: colors.surface, color: colors.text, }, selectInput: { borderWidth: 1, borderRadius: 12, backgroundColor: colors.surface, }, dateInput: { justifyContent: "center", }, errorText: { fontSize: 13, color: "red", marginTop: 4, }, actionButtons: { flexDirection: "row", paddingHorizontal: 20, paddingVertical: 16, gap: 12, borderTopWidth: 1, borderTopColor: colors.border, }, resetButton: { flex: 1, paddingVertical: 14, borderRadius: 12, borderWidth: 1, alignItems: "center", justifyContent: "center", }, resetButtonText: { fontSize: 16, fontWeight: "600", }, submitButton: { flex: 1, paddingVertical: 14, borderRadius: 12, alignItems: "center", justifyContent: "center", }, submitButtonText: { color: "#ffffff", fontSize: 16, fontWeight: "600", }, // Date Picker Modal Styles datePickerModal: { flex: 1, justifyContent: "flex-end", backgroundColor: "rgba(0, 0, 0, 0.5)", }, datePickerContent: { backgroundColor: colors.background, borderTopLeftRadius: 24, borderTopRightRadius: 24, paddingBottom: 20, }, datePickerHeader: { flexDirection: "row", justifyContent: "space-between", alignItems: "center", paddingHorizontal: 20, paddingVertical: 16, borderBottomWidth: 1, borderBottomColor: colors.border, }, datePickerTitle: { fontSize: 16, fontWeight: "600", color: colors.text, }, datePickerClose: { fontSize: 20, color: colors.text, }, datePickerIOS: { height: 200, marginTop: 20, }, datePickerButton: { marginHorizontal: 20, paddingVertical: 14, borderRadius: 12, alignItems: "center", marginTop: 20, }, datePickerButtonText: { color: "#ffffff", fontSize: 16, fontWeight: "600", }, }); export default CreateOrUpdateShip;