import React, { useState, useEffect, useRef, useCallback } from "react"; import { View, Text, Modal, TouchableOpacity, StyleSheet, Platform, ScrollView, Alert, ActivityIndicator, Animated, Dimensions, } from "react-native"; import { Ionicons } from "@expo/vector-icons"; import { useI18n } from "@/hooks/use-i18n"; import { useThemeContext } from "@/hooks/use-theme-context"; import FishingGearList from "./FishingGearList"; import MaterialCostList from "./MaterialCostList"; import TripNameInput from "./TripNameInput"; import TripDurationPicker from "./TripDurationPicker"; import PortSelector from "./PortSelector"; import BasicInfoInput from "./BasicInfoInput"; import ShipSelector from "./ShipSelector"; import AutoFillSection from "./AutoFillSection"; import { createTrip, updateTrip } from "@/controller/TripController"; import { convertFishingGears, convertTripCosts, convertFishingGroundCodes, parseFishingGroundCodes, } from "@/utils/tripDataConverters"; // Internal component interfaces - extend from Model with local id for state management export interface FishingGear extends Model.FishingGear { id: string; } export interface TripCost extends Model.TripCost { id: string; } interface TripFormModalProps { visible: boolean; onClose: () => void; onSuccess?: () => void; mode?: "add" | "edit"; tripData?: Model.Trip; } // Default form state const DEFAULT_FORM_STATE = { selectedShipId: "", tripName: "", fishingGears: [] as FishingGear[], tripCosts: [] as TripCost[], startDate: null as Date | null, endDate: null as Date | null, departurePortId: 1, arrivalPortId: 1, fishingGroundCodes: "", }; export default function TripFormModal({ visible, onClose, onSuccess, mode = "add", tripData, }: TripFormModalProps) { const { t } = useI18n(); const { colors } = useThemeContext(); const isEditMode = mode === "edit"; // Animation values const fadeAnim = useRef(new Animated.Value(0)).current; const slideAnim = useRef( new Animated.Value(Dimensions.get("window").height) ).current; // Form state const [selectedShipId, setSelectedShipId] = useState(DEFAULT_FORM_STATE.selectedShipId); const [tripName, setTripName] = useState(DEFAULT_FORM_STATE.tripName); const [fishingGears, setFishingGears] = useState(DEFAULT_FORM_STATE.fishingGears); const [tripCosts, setTripCosts] = useState(DEFAULT_FORM_STATE.tripCosts); const [startDate, setStartDate] = useState(DEFAULT_FORM_STATE.startDate); const [endDate, setEndDate] = useState(DEFAULT_FORM_STATE.endDate); const [departurePortId, setDeparturePortId] = useState(DEFAULT_FORM_STATE.departurePortId); const [arrivalPortId, setArrivalPortId] = useState(DEFAULT_FORM_STATE.arrivalPortId); const [fishingGroundCodes, setFishingGroundCodes] = useState(DEFAULT_FORM_STATE.fishingGroundCodes); const [isSubmitting, setIsSubmitting] = useState(false); // Reset form to default state const resetForm = useCallback(() => { setSelectedShipId(DEFAULT_FORM_STATE.selectedShipId); setTripName(DEFAULT_FORM_STATE.tripName); setFishingGears(DEFAULT_FORM_STATE.fishingGears); setTripCosts(DEFAULT_FORM_STATE.tripCosts); setStartDate(DEFAULT_FORM_STATE.startDate); setEndDate(DEFAULT_FORM_STATE.endDate); setDeparturePortId(DEFAULT_FORM_STATE.departurePortId); setArrivalPortId(DEFAULT_FORM_STATE.arrivalPortId); setFishingGroundCodes(DEFAULT_FORM_STATE.fishingGroundCodes); }, []); // Fill form with trip data const fillFormWithTripData = useCallback((data: Model.Trip, prefix: string) => { setSelectedShipId(data.vms_id || ""); setTripName(data.name || ""); setFishingGears(convertFishingGears(data.fishing_gears, prefix)); setTripCosts(convertTripCosts(data.trip_cost, prefix)); if (data.departure_time) setStartDate(new Date(data.departure_time)); if (data.arrival_time) setEndDate(new Date(data.arrival_time)); if (data.departure_port_id) setDeparturePortId(data.departure_port_id); if (data.arrival_port_id) setArrivalPortId(data.arrival_port_id); setFishingGroundCodes(convertFishingGroundCodes(data.fishing_ground_codes)); }, []); // Handle animation when modal visibility changes useEffect(() => { if (visible) { Animated.parallel([ Animated.timing(fadeAnim, { toValue: 1, duration: 300, useNativeDriver: true, }), Animated.timing(slideAnim, { toValue: 0, duration: 300, useNativeDriver: true, }), ]).start(); } }, [visible, fadeAnim, slideAnim]); // Pre-fill form when in edit mode useEffect(() => { if (isEditMode && tripData && visible) { fillFormWithTripData(tripData, "edit"); } }, [isEditMode, tripData, visible, fillFormWithTripData]); // Close modal with animation const closeWithAnimation = useCallback( (shouldResetForm: boolean = true) => { Animated.parallel([ Animated.timing(fadeAnim, { toValue: 0, duration: 250, useNativeDriver: true, }), Animated.timing(slideAnim, { toValue: Dimensions.get("window").height, duration: 250, useNativeDriver: true, }), ]).start(() => { onClose(); if (shouldResetForm) { setTimeout(resetForm, 100); } }); }, [fadeAnim, slideAnim, onClose, resetForm] ); // Handle cancel const handleCancel = useCallback(() => { closeWithAnimation(true); }, [closeWithAnimation]); // Handle close after successful submit const handleSuccessClose = useCallback(() => { fadeAnim.setValue(0); slideAnim.setValue(Dimensions.get("window").height); onClose(); setTimeout(resetForm, 100); }, [fadeAnim, slideAnim, onClose, resetForm]); // Handle auto-fill from last trip data const handleAutoFill = useCallback( (tripData: Model.Trip, selectedThingId: string) => { setSelectedShipId(selectedThingId); setFishingGears(convertFishingGears(tripData.fishing_gears, "auto")); setTripCosts(convertTripCosts(tripData.trip_cost, "auto")); if (tripData.departure_port_id) setDeparturePortId(tripData.departure_port_id); if (tripData.arrival_port_id) setArrivalPortId(tripData.arrival_port_id); setFishingGroundCodes(convertFishingGroundCodes(tripData.fishing_ground_codes)); }, [] ); // Validate form const validateForm = useCallback((): boolean => { if (!selectedShipId) { Alert.alert(t("common.error"), t("diary.validation.shipRequired")); return false; } if (!startDate || !endDate) { Alert.alert(t("common.error"), t("diary.validation.datesRequired")); return false; } if (!tripName.trim()) { Alert.alert(t("common.error"), t("diary.validation.tripNameRequired")); return false; } return true; }, [selectedShipId, startDate, endDate, tripName, t]); // Build API body const buildApiBody = useCallback((): Model.TripAPIBody => { return { thing_id: selectedShipId, name: tripName, departure_time: startDate?.toISOString() || "", departure_port_id: departurePortId, arrival_time: endDate?.toISOString() || "", arrival_port_id: arrivalPortId, fishing_ground_codes: parseFishingGroundCodes(fishingGroundCodes), fishing_gears: fishingGears.map((gear) => ({ name: gear.name, number: gear.number, })), trip_cost: tripCosts.map((cost) => ({ type: cost.type, amount: cost.amount, unit: cost.unit, cost_per_unit: cost.cost_per_unit, total_cost: cost.total_cost, })), }; }, [ selectedShipId, tripName, startDate, endDate, departurePortId, arrivalPortId, fishingGroundCodes, fishingGears, tripCosts, ]); // Handle form submit const handleSubmit = useCallback(async () => { if (!validateForm()) return; const apiBody = buildApiBody(); setIsSubmitting(true); try { if (isEditMode && tripData) { await updateTrip(tripData.id, apiBody); } else { await createTrip(selectedShipId, apiBody); } onSuccess?.(); Alert.alert( t("common.success"), isEditMode ? t("diary.updateTripSuccess") : t("diary.createTripSuccess") ); handleSuccessClose(); } catch (error: any) { console.error( isEditMode ? "Error updating trip:" : "Error creating trip:", error ); Alert.alert( t("common.error"), isEditMode ? t("diary.updateTripError") : t("diary.createTripError") ); } finally { setIsSubmitting(false); } }, [ validateForm, buildApiBody, isEditMode, tripData, selectedShipId, onSuccess, t, handleSuccessClose, ]); const themedStyles = { modalContainer: { backgroundColor: colors.card }, header: { borderBottomColor: colors.separator }, title: { color: colors.text }, footer: { borderTopColor: colors.separator }, cancelButton: { backgroundColor: colors.backgroundSecondary }, cancelButtonText: { color: colors.textSecondary }, submitButton: { backgroundColor: colors.primary }, }; return ( {/* Header */} {isEditMode ? t("diary.editTrip") : t("diary.addTrip")} {/* Content */} {/* Auto Fill Section - only show in add mode */} {!isEditMode && } {/* Ship Selector - disabled in edit mode */} {/* Trip Name */} {/* Fishing Gear List */} {/* Trip Cost List */} {/* Trip Duration */} {/* Port Selector */} {/* Fishing Ground Codes */} {/* Footer */} {t("common.cancel")} {isSubmitting ? ( ) : ( {isEditMode ? t("diary.saveChanges") : t("diary.createTrip")} )} ); } const styles = StyleSheet.create({ overlay: { flex: 1, backgroundColor: "rgba(0, 0, 0, 0.5)", justifyContent: "flex-end", }, modalContainer: { borderTopLeftRadius: 24, borderTopRightRadius: 24, maxHeight: "90%", shadowColor: "#000", shadowOffset: { width: 0, height: -4 }, shadowOpacity: 0.1, shadowRadius: 12, elevation: 8, }, header: { flexDirection: "row", alignItems: "center", justifyContent: "space-between", paddingHorizontal: 20, paddingVertical: 16, borderBottomWidth: 1, }, closeButton: { padding: 4, }, title: { fontSize: 18, fontWeight: "700", fontFamily: Platform.select({ ios: "System", android: "Roboto", default: "System", }), }, placeholder: { width: 32, }, content: { padding: 20, }, footer: { flexDirection: "row", gap: 12, padding: 20, borderTopWidth: 1, }, cancelButton: { flex: 1, paddingVertical: 14, borderRadius: 12, alignItems: "center", }, cancelButtonText: { fontSize: 16, fontWeight: "600", fontFamily: Platform.select({ ios: "System", android: "Roboto", default: "System", }), }, submitButton: { flex: 1, paddingVertical: 14, borderRadius: 12, alignItems: "center", }, submitButtonDisabled: { opacity: 0.7, }, submitButtonText: { fontSize: 16, fontWeight: "600", color: "#FFFFFF", fontFamily: Platform.select({ ios: "System", android: "Roboto", default: "System", }), }, });