import React, { useState, useEffect, useRef } 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 "@/components/diary/TripFormModal/FishingGearList"; import MaterialCostList from "@/components/diary/TripFormModal/MaterialCostList"; import TripNameInput from "@/components/diary/TripFormModal/TripNameInput"; import TripDurationPicker from "@/components/diary/TripFormModal/TripDurationPicker"; import PortSelector from "@/components/diary/TripFormModal/PortSelector"; import BasicInfoInput from "@/components/diary/TripFormModal/BasicInfoInput"; import ShipSelector from "./ShipSelector"; import AutoFillSection from "./AutoFillSection"; import { createTrip, updateTrip } from "@/controller/TripController"; // 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' | 'view'; tripData?: Model.Trip; } export default function TripFormModal({ visible, onClose, onSuccess, mode = 'add', tripData, }: TripFormModalProps) { const { t } = useI18n(); const { colors } = useThemeContext(); const isEditMode = mode === 'edit'; const isViewMode = mode === 'view'; const isReadOnly = isViewMode; // View mode is read-only // Animation values const fadeAnim = useRef(new Animated.Value(0)).current; const slideAnim = useRef(new Animated.Value(Dimensions.get('window').height)).current; // Handle animation when modal visibility changes useEffect(() => { if (visible) { // Open animation: fade overlay + slide content up Animated.parallel([ Animated.timing(fadeAnim, { toValue: 1, duration: 300, useNativeDriver: true, }), Animated.timing(slideAnim, { toValue: 0, duration: 300, useNativeDriver: true, }), ]).start(); } }, [visible, fadeAnim, slideAnim]); // Form state const [selectedShipId, setSelectedShipId] = useState(""); const [tripName, setTripName] = useState(""); const [fishingGears, setFishingGears] = useState([]); const [tripCosts, setTripCosts] = useState([]); const [startDate, setStartDate] = useState(null); const [endDate, setEndDate] = useState(null); const [departurePortId, setDeparturePortId] = useState(1); const [arrivalPortId, setArrivalPortId] = useState(1); const [fishingGroundCodes, setFishingGroundCodes] = useState(""); const [isSubmitting, setIsSubmitting] = useState(false); // Pre-fill form when in edit or view mode useEffect(() => { if ((isEditMode || isViewMode) && tripData && visible) { // Fill ship ID (use vms_id as thingId) setSelectedShipId(tripData.vms_id || ""); // Fill trip name setTripName(tripData.name || ""); // Fill fishing gears if (tripData.fishing_gears && Array.isArray(tripData.fishing_gears)) { const gears: FishingGear[] = tripData.fishing_gears.map((gear, index) => ({ id: `${mode}-${Date.now()}-${index}`, name: gear.name || "", number: gear.number?.toString() || "", })); setFishingGears(gears); } // Fill trip costs if (tripData.trip_cost && Array.isArray(tripData.trip_cost)) { const costs: TripCost[] = tripData.trip_cost.map((cost, index) => ({ id: `${mode}-${Date.now()}-${index}`, type: cost.type || "", amount: cost.amount || 0, unit: cost.unit || "", cost_per_unit: cost.cost_per_unit || 0, total_cost: cost.total_cost || 0, })); setTripCosts(costs); } // Fill dates if (tripData.departure_time) { setStartDate(new Date(tripData.departure_time)); } if (tripData.arrival_time) { setEndDate(new Date(tripData.arrival_time)); } // Fill ports if (tripData.departure_port_id) { setDeparturePortId(tripData.departure_port_id); } if (tripData.arrival_port_id) { setArrivalPortId(tripData.arrival_port_id); } // Fill fishing ground codes if (tripData.fishing_ground_codes && Array.isArray(tripData.fishing_ground_codes)) { setFishingGroundCodes(tripData.fishing_ground_codes.join(", ")); } } }, [isEditMode, isViewMode, tripData, visible, mode]); const handleCancel = () => { // Close animation: fade overlay + slide content down Animated.parallel([ Animated.timing(fadeAnim, { toValue: 0, duration: 250, useNativeDriver: true, }), Animated.timing(slideAnim, { toValue: Dimensions.get('window').height, duration: 250, useNativeDriver: true, }), ]).start(() => { // Close modal after animation completes onClose(); // Only reset form in add/edit mode, not needed for view mode if (!isViewMode) { // Reset form after closing setTimeout(() => { setSelectedShipId(""); setTripName(""); setFishingGears([]); setTripCosts([]); setStartDate(null); setEndDate(null); setDeparturePortId(1); setArrivalPortId(1); setFishingGroundCodes(""); }, 100); } }); }; // Handle close modal after successful submit - always resets form const handleSuccessClose = () => { // Reset animation values for next open fadeAnim.setValue(0); slideAnim.setValue(Dimensions.get('window').height); // Close modal immediately onClose(); // Reset form after closing setTimeout(() => { setSelectedShipId(""); setTripName(""); setFishingGears([]); setTripCosts([]); setStartDate(null); setEndDate(null); setDeparturePortId(1); setArrivalPortId(1); setFishingGroundCodes(""); }, 100); }; // Handle auto-fill from last trip data const handleAutoFill = (tripData: Model.Trip, selectedThingId: string) => { // Fill ship ID (use the thingId from the selected ship for ShipSelector) setSelectedShipId(selectedThingId); // Fill trip name // if (tripData.name) { // setTripName(tripData.name); // } // Fill fishing gears if (tripData.fishing_gears && Array.isArray(tripData.fishing_gears)) { const gears: FishingGear[] = tripData.fishing_gears.map( (gear, index) => ({ id: `auto-${Date.now()}-${index}`, name: gear.name || "", number: gear.number?.toString() || "", }) ); setFishingGears(gears); } // Fill trip costs if (tripData.trip_cost && Array.isArray(tripData.trip_cost)) { const costs: TripCost[] = tripData.trip_cost.map((cost, index) => ({ id: `auto-${Date.now()}-${index}`, type: cost.type || "", amount: cost.amount || 0, unit: cost.unit || "", cost_per_unit: cost.cost_per_unit || 0, total_cost: cost.total_cost || 0, })); setTripCosts(costs); } // Fill departure and arrival ports if (tripData.departure_port_id) { setDeparturePortId(tripData.departure_port_id); } if (tripData.arrival_port_id) { setArrivalPortId(tripData.arrival_port_id); } // Fill fishing ground codes if ( tripData.fishing_ground_codes && Array.isArray(tripData.fishing_ground_codes) ) { setFishingGroundCodes(tripData.fishing_ground_codes.join(", ")); } }; const handleSubmit = async () => { // Validate thingId is required if (!selectedShipId) { Alert.alert(t("common.error"), t("diary.validation.shipRequired")); return; } // Validate dates are required if (!startDate || !endDate) { Alert.alert(t("common.error"), t("diary.validation.datesRequired")); return; } // Validate trip name is required if (!tripName.trim()) { Alert.alert(t("common.error"), t("diary.validation.tripNameRequired")); return; } // Parse fishing ground codes from comma-separated string to array of numbers const fishingGroundCodesArray = fishingGroundCodes .split(",") .map((code) => parseInt(code.trim())) .filter((code) => !isNaN(code)); // Format API body const apiBody: Model.TripAPIBody = { thing_id: selectedShipId, name: tripName, departure_time: startDate ? startDate.toISOString() : "", departure_port_id: departurePortId, arrival_time: endDate ? endDate.toISOString() : "", arrival_port_id: arrivalPortId, fishing_ground_codes: fishingGroundCodesArray, 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, })), }; setIsSubmitting(true); try { let response; if (isEditMode && tripData) { // Edit mode: call updateTrip response = await updateTrip(tripData.id, apiBody); } else { // Add mode: call createTrip response = await createTrip(selectedShipId, apiBody); } // Check if response is successful (response exists) // Call onSuccess callback first (to refresh data) if (onSuccess) { onSuccess(); } // Show success alert Alert.alert( t("common.success"), isEditMode ? t("diary.updateTripSuccess") : t("diary.createTripSuccess") ); // Reset form and close modal console.log("Calling handleSuccessClose"); handleSuccessClose(); } catch (error: any) { console.error(isEditMode ? "Error updating trip:" : "Error creating trip:", error); // Log detailed error information for debugging if (error.response) { console.error("Response status:", error.response.status); console.error("Response data:", JSON.stringify(error.response.data, null, 2)); } console.log("Request body was:", JSON.stringify(apiBody, null, 2)); Alert.alert( t("common.error"), isEditMode ? t("diary.updateTripError") : t("diary.createTripError") ); } finally { setIsSubmitting(false); } }; 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 */} {isViewMode ? t("diary.viewTrip") : isEditMode ? t("diary.editTrip") : t("diary.addTrip")} {/* Content */} {/* Auto Fill Section - only show in add mode */} {!isEditMode && !isViewMode && } {/* Ship Selector - disabled in edit and view mode */} {/* Trip Name */} {/* Fishing Gear List */} {/* Trip Cost List */} {/* Trip Duration */} {/* Port Selector */} {/* Fishing Ground Codes */} {/* Footer - hide in view mode */} {!isViewMode && ( {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", }), }, });