From afc6acbfe24b34c509b50fd8d6fcfcfbce1e5fe1 Mon Sep 17 00:00:00 2001 From: MinhNN Date: Mon, 22 Dec 2025 22:47:08 +0700 Subject: [PATCH] =?UTF-8?q?c=E1=BA=ADp=20nh=E1=BA=ADt=20animation=20hi?= =?UTF-8?q?=E1=BB=83n=20th=E1=BB=8B=20modal,=20call=20API=20edit?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/(tabs)/diary.tsx | 42 ++- components/diary/FilterModal.tsx | 97 ++++- components/diary/TripCard.tsx | 4 +- .../AutoFillSection.tsx | 0 .../BasicInfoInput.tsx | 5 +- .../FishingGearList.tsx | 60 ++-- .../MaterialCostList.tsx | 86 +++-- .../PortSelector.tsx | 36 +- .../ShipSelector.tsx | 24 +- .../TripDurationPicker.tsx | 36 +- .../TripNameInput.tsx | 6 +- .../{addTripModal => TripFormModal}/index.tsx | 330 +++++++++++++----- constants/index.ts | 1 + controller/TripController.ts | 5 + locales/en.json | 7 +- locales/vi.json | 7 +- 16 files changed, 530 insertions(+), 216 deletions(-) rename components/diary/{addTripModal => TripFormModal}/AutoFillSection.tsx (100%) rename components/diary/{addTripModal => TripFormModal}/BasicInfoInput.tsx (93%) rename components/diary/{addTripModal => TripFormModal}/FishingGearList.tsx (77%) rename components/diary/{addTripModal => TripFormModal}/MaterialCostList.tsx (86%) rename components/diary/{addTripModal => TripFormModal}/PortSelector.tsx (84%) rename components/diary/{addTripModal => TripFormModal}/ShipSelector.tsx (94%) rename components/diary/{addTripModal => TripFormModal}/TripDurationPicker.tsx (93%) rename components/diary/{addTripModal => TripFormModal}/TripNameInput.tsx (86%) rename components/diary/{addTripModal => TripFormModal}/index.tsx (51%) diff --git a/app/(tabs)/diary.tsx b/app/(tabs)/diary.tsx index 46f15ed..eac582f 100644 --- a/app/(tabs)/diary.tsx +++ b/app/(tabs)/diary.tsx @@ -13,7 +13,7 @@ import { Ionicons } from "@expo/vector-icons"; import FilterButton from "@/components/diary/FilterButton"; import TripCard from "@/components/diary/TripCard"; import FilterModal, { FilterValues } from "@/components/diary/FilterModal"; -import AddTripModal from "@/components/diary/addTripModal"; +import AddTripModal from "@/components/diary/TripFormModal"; import { useThings } from "@/state/use-thing"; import { useTripsList } from "@/state/use-tripslist"; import dayjs from "dayjs"; @@ -25,6 +25,8 @@ export default function diary() { const { colors } = useThemeContext(); const [showFilterModal, setShowFilterModal] = useState(false); const [showAddTripModal, setShowAddTripModal] = useState(false); + const [editingTrip, setEditingTrip] = useState(null); + const [viewingTrip, setViewingTrip] = useState(null); const [filters, setFilters] = useState({ status: null, startDate: null, @@ -169,19 +171,27 @@ export default function diary() { getTripsList(updatedPayload); }, [isLoadingMore, hasMore, payloadTrips]); - const handleTripPress = (tripId: string) => { - // TODO: Navigate to trip detail - console.log("Trip pressed:", tripId); - }; + // const handleTripPress = (tripId: string) => { + // // TODO: Navigate to trip detail + // console.log("Trip pressed:", tripId); + // }; const handleViewTrip = (tripId: string) => { - console.log("View trip:", tripId); - // TODO: Navigate to trip detail view + // Find the trip from allTrips and open modal in view mode + const tripToView = allTrips.find((trip) => trip.id === tripId); + if (tripToView) { + setViewingTrip(tripToView); + setShowAddTripModal(true); + } }; const handleEditTrip = (tripId: string) => { - console.log("Edit trip:", tripId); - // TODO: Navigate to trip edit screen + // Find the trip from allTrips + const tripToEdit = allTrips.find((trip) => trip.id === tripId); + if (tripToEdit) { + setEditingTrip(tripToEdit); + setShowAddTripModal(true); + } }; const handleViewTeam = (tripId: string) => { @@ -242,7 +252,7 @@ export default function diary() { ({ item }: { item: any }) => ( handleTripPress(item.id)} + // onPress={() => handleTripPress(item.id)} onView={() => handleViewTrip(item.id)} onEdit={() => handleEditTrip(item.id)} onTeam={() => handleViewTeam(item.id)} @@ -250,7 +260,7 @@ export default function diary() { onDelete={() => handleDeleteTrip(item.id)} /> ), - [] + [handleViewTrip, handleEditTrip, handleViewTeam, handleSendTrip, handleDeleteTrip] ); // Key extractor cho FlatList @@ -348,11 +358,17 @@ export default function diary() { onApply={handleApplyFilters} /> - {/* Add Trip Modal */} + {/* Add/Edit/View Trip Modal */} setShowAddTripModal(false)} + onClose={() => { + setShowAddTripModal(false); + setEditingTrip(null); + setViewingTrip(null); + }} onSuccess={handleTripAddSuccess} + mode={viewingTrip ? 'view' : editingTrip ? 'edit' : 'add'} + tripData={viewingTrip || editingTrip || undefined} /> ); diff --git a/components/diary/FilterModal.tsx b/components/diary/FilterModal.tsx index 7bc4abd..824a859 100644 --- a/components/diary/FilterModal.tsx +++ b/components/diary/FilterModal.tsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useState, useRef, useEffect } from "react"; import { View, Text, @@ -7,6 +7,8 @@ import { StyleSheet, Platform, ScrollView, + Animated, + Dimensions, } from "react-native"; import { Ionicons } from "@expo/vector-icons"; import StatusDropdown from "./StatusDropdown"; @@ -71,6 +73,47 @@ export default function FilterModal({ const [endDate, setEndDate] = useState(null); const [selectedShip, setSelectedShip] = useState(null); + // 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]); + + const handleClose = () => { + // 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(() => { + onClose(); + }); + }; + const handleReset = () => { setStatus(null); setStartDate(null); @@ -79,8 +122,22 @@ export default function FilterModal({ }; const handleApply = () => { - onApply({ status, startDate, endDate, selectedShip }); - onClose(); + // Close animation then apply + Animated.parallel([ + Animated.timing(fadeAnim, { + toValue: 0, + duration: 250, + useNativeDriver: true, + }), + Animated.timing(slideAnim, { + toValue: Dimensions.get('window').height, + duration: 250, + useNativeDriver: true, + }), + ]).start(() => { + onApply({ status, startDate, endDate, selectedShip }); + onClose(); + }); }; const hasFilters = @@ -128,23 +185,26 @@ export default function FilterModal({ return ( - + e.stopPropagation()} + onPress={handleClose} + /> + {/* Header */} - + {t("diary.filter")} @@ -218,8 +278,8 @@ export default function FilterModal({ {t("diary.apply")} - - + + ); } @@ -231,6 +291,13 @@ const styles = StyleSheet.create({ backgroundColor: "rgba(0, 0, 0, 0.5)", justifyContent: "flex-end", }, + overlayTouchable: { + position: 'absolute', + top: 0, + left: 0, + right: 0, + bottom: 0, + }, modalContainer: { borderTopLeftRadius: 24, borderTopRightRadius: 24, diff --git a/components/diary/TripCard.tsx b/components/diary/TripCard.tsx index cd3b217..7166028 100644 --- a/components/diary/TripCard.tsx +++ b/components/diary/TripCard.tsx @@ -80,7 +80,7 @@ export default function TripCard({ return ( - + {/* Header */} @@ -142,7 +142,7 @@ export default function TripCard({ - + {/* Action Buttons */} diff --git a/components/diary/addTripModal/AutoFillSection.tsx b/components/diary/TripFormModal/AutoFillSection.tsx similarity index 100% rename from components/diary/addTripModal/AutoFillSection.tsx rename to components/diary/TripFormModal/AutoFillSection.tsx diff --git a/components/diary/addTripModal/BasicInfoInput.tsx b/components/diary/TripFormModal/BasicInfoInput.tsx similarity index 93% rename from components/diary/addTripModal/BasicInfoInput.tsx rename to components/diary/TripFormModal/BasicInfoInput.tsx index 76f71ae..febd876 100644 --- a/components/diary/addTripModal/BasicInfoInput.tsx +++ b/components/diary/TripFormModal/BasicInfoInput.tsx @@ -12,11 +12,13 @@ import { useThemeContext } from "@/hooks/use-theme-context"; interface BasicInfoInputProps { fishingGroundCodes: string; onChange: (value: string) => void; + disabled?: boolean; } export default function BasicInfoInput({ fishingGroundCodes, onChange, + disabled = false, }: BasicInfoInputProps) { const { t } = useI18n(); const { colors } = useThemeContext(); @@ -25,7 +27,7 @@ export default function BasicInfoInput({ label: { color: colors.text }, subLabel: { color: colors.textSecondary }, input: { - backgroundColor: colors.card, + backgroundColor: disabled ? colors.backgroundSecondary : colors.card, borderColor: colors.border, color: colors.text, }, @@ -46,6 +48,7 @@ export default function BasicInfoInput({ placeholder={t("diary.fishingGroundCodesPlaceholder")} placeholderTextColor={colors.textSecondary} keyboardType="numeric" + editable={!disabled} /> ); diff --git a/components/diary/addTripModal/FishingGearList.tsx b/components/diary/TripFormModal/FishingGearList.tsx similarity index 77% rename from components/diary/addTripModal/FishingGearList.tsx rename to components/diary/TripFormModal/FishingGearList.tsx index ed69389..3fe59ed 100644 --- a/components/diary/addTripModal/FishingGearList.tsx +++ b/components/diary/TripFormModal/FishingGearList.tsx @@ -20,11 +20,13 @@ interface FishingGear { interface FishingGearListProps { items: FishingGear[]; onChange: (items: FishingGear[]) => void; + disabled?: boolean; } export default function FishingGearList({ items, onChange, + disabled = false, }: FishingGearListProps) { const { t } = useI18n(); const { colors } = useThemeContext(); @@ -93,6 +95,7 @@ export default function FishingGearList({ onChangeText={(value) => handleUpdateGear(gear.id, "name", value)} placeholder={t("diary.gearNamePlaceholder")} placeholderTextColor={colors.textSecondary} + editable={!disabled} /> @@ -108,38 +111,43 @@ export default function FishingGearList({ placeholder={t("diary.gearNumberPlaceholder")} placeholderTextColor={colors.textSecondary} keyboardType="numeric" + editable={!disabled} /> - {/* Action Buttons */} - - handleDuplicateGear(gear)} - hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }} - > - - - handleRemoveGear(gear.id)} - hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }} - > - - - + {/* Action Buttons - hide when disabled */} + {!disabled && ( + + handleDuplicateGear(gear)} + hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }} + > + + + handleRemoveGear(gear.id)} + hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }} + > + + + + )} ))} - {/* Add Button */} - - - - {t("diary.addFishingGear")} - - + {/* Add Button - hide when disabled */} + {!disabled && ( + + + + {t("diary.addFishingGear")} + + + )} ); } diff --git a/components/diary/addTripModal/MaterialCostList.tsx b/components/diary/TripFormModal/MaterialCostList.tsx similarity index 86% rename from components/diary/addTripModal/MaterialCostList.tsx rename to components/diary/TripFormModal/MaterialCostList.tsx index d424c6b..e9dff4a 100644 --- a/components/diary/addTripModal/MaterialCostList.tsx +++ b/components/diary/TripFormModal/MaterialCostList.tsx @@ -25,6 +25,7 @@ interface TripCost { interface MaterialCostListProps { items: TripCost[]; onChange: (items: TripCost[]) => void; + disabled?: boolean; } // Predefined cost types @@ -39,6 +40,7 @@ const COST_TYPES = [ export default function MaterialCostList({ items, onChange, + disabled = false, }: MaterialCostListProps) { const { t } = useI18n(); const { colors } = useThemeContext(); @@ -146,8 +148,9 @@ export default function MaterialCostList({ setTypeDropdownVisible(cost.id)} - activeOpacity={0.7} + onPress={disabled ? undefined : () => setTypeDropdownVisible(cost.id)} + activeOpacity={disabled ? 1 : 0.7} + disabled={disabled} > {getTypeLabel(cost.type)} - + {!disabled && ( + + )} {/* Type Dropdown Modal */} @@ -222,6 +227,7 @@ export default function MaterialCostList({ placeholder="0" placeholderTextColor={colors.textSecondary} keyboardType="numeric" + editable={!disabled} /> @@ -238,6 +244,7 @@ export default function MaterialCostList({ } placeholder={t("diary.unitPlaceholder")} placeholderTextColor={colors.textSecondary} + editable={!disabled} /> @@ -261,6 +268,7 @@ export default function MaterialCostList({ placeholder="0" placeholderTextColor={colors.textSecondary} keyboardType="numeric" + editable={!disabled} /> @@ -276,40 +284,44 @@ export default function MaterialCostList({ - {/* Action Buttons */} - - handleDuplicateMaterial(cost)} - hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }} - > - - - handleRemoveMaterial(cost.id)} - hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }} - > - - - + {/* Action Buttons - hide when disabled */} + {!disabled && ( + + handleDuplicateMaterial(cost)} + hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }} + > + + + handleRemoveMaterial(cost.id)} + hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }} + > + + + + )} ))} - {/* Add Button */} - - - - {t("diary.addMaterialCost")} - - + {/* Add Button - hide when disabled */} + {!disabled && ( + + + + {t("diary.addMaterialCost")} + + + )} ); } diff --git a/components/diary/addTripModal/PortSelector.tsx b/components/diary/TripFormModal/PortSelector.tsx similarity index 84% rename from components/diary/addTripModal/PortSelector.tsx rename to components/diary/TripFormModal/PortSelector.tsx index e301fce..5076a4c 100644 --- a/components/diary/addTripModal/PortSelector.tsx +++ b/components/diary/TripFormModal/PortSelector.tsx @@ -15,6 +15,7 @@ interface PortSelectorProps { arrivalPortId: number; onDeparturePortChange: (portId: number) => void; onArrivalPortChange: (portId: number) => void; + disabled?: boolean; } export default function PortSelector({ @@ -22,6 +23,7 @@ export default function PortSelector({ arrivalPortId, onDeparturePortChange, onArrivalPortChange, + disabled = false, }: PortSelectorProps) { const { t } = useI18n(); const { colors } = useThemeContext(); @@ -69,8 +71,9 @@ export default function PortSelector({ {getPortDisplayName(departurePortId)} - + {!disabled && ( + + )} @@ -96,8 +101,9 @@ export default function PortSelector({ {getPortDisplayName(arrivalPortId)} - + {!disabled && ( + + )} diff --git a/components/diary/addTripModal/ShipSelector.tsx b/components/diary/TripFormModal/ShipSelector.tsx similarity index 94% rename from components/diary/addTripModal/ShipSelector.tsx rename to components/diary/TripFormModal/ShipSelector.tsx index d03f8a3..5d0b01b 100644 --- a/components/diary/addTripModal/ShipSelector.tsx +++ b/components/diary/TripFormModal/ShipSelector.tsx @@ -17,11 +17,13 @@ import { useThemeContext } from "@/hooks/use-theme-context"; interface ShipSelectorProps { selectedShipId: string; onChange: (shipId: string) => void; + disabled?: boolean; } export default function ShipSelector({ selectedShipId, onChange, + disabled = false, }: ShipSelectorProps) { const { t } = useI18n(); const { colors } = useThemeContext(); @@ -58,7 +60,10 @@ export default function ShipSelector({ const themedStyles = { label: { color: colors.text }, - selector: { backgroundColor: colors.card, borderColor: colors.border }, + selector: { + backgroundColor: disabled ? colors.backgroundSecondary : colors.card, + borderColor: colors.border, + }, selectorText: { color: colors.text }, placeholder: { color: colors.textSecondary }, modalContent: { backgroundColor: colors.card }, @@ -81,8 +86,9 @@ export default function ShipSelector({ setIsOpen(true)} - activeOpacity={0.7} + onPress={() => !disabled && setIsOpen(true)} + activeOpacity={disabled ? 1 : 0.7} + disabled={disabled} > {displayValue} - + {!disabled && ( + + )} void; onEndDateChange: (date: Date | null) => void; + disabled?: boolean; } export default function TripDurationPicker({ @@ -24,6 +25,7 @@ export default function TripDurationPicker({ endDate, onStartDateChange, onEndDateChange, + disabled = false, }: TripDurationPickerProps) { const { t } = useI18n(); const { colors, colorScheme } = useThemeContext(); @@ -127,8 +129,9 @@ export default function TripDurationPicker({ {startDate ? formatDate(startDate) : t("diary.selectDate")} - + {!disabled && ( + + )} @@ -154,8 +159,9 @@ export default function TripDurationPicker({ {endDate ? formatDate(endDate) : t("diary.selectDate")} - + {!disabled && ( + + )} diff --git a/components/diary/addTripModal/TripNameInput.tsx b/components/diary/TripFormModal/TripNameInput.tsx similarity index 86% rename from components/diary/addTripModal/TripNameInput.tsx rename to components/diary/TripFormModal/TripNameInput.tsx index 0b8d63e..8c071e3 100644 --- a/components/diary/addTripModal/TripNameInput.tsx +++ b/components/diary/TripFormModal/TripNameInput.tsx @@ -12,16 +12,17 @@ import { useThemeContext } from "@/hooks/use-theme-context"; interface TripNameInputProps { value: string; onChange: (value: string) => void; + disabled?: boolean; } -export default function TripNameInput({ value, onChange }: TripNameInputProps) { +export default function TripNameInput({ value, onChange, disabled = false }: TripNameInputProps) { const { t } = useI18n(); const { colors } = useThemeContext(); const themedStyles = { label: { color: colors.text }, input: { - backgroundColor: colors.card, + backgroundColor: disabled ? colors.backgroundSecondary : colors.card, borderColor: colors.border, color: colors.text, }, @@ -38,6 +39,7 @@ export default function TripNameInput({ value, onChange }: TripNameInputProps) { onChangeText={onChange} placeholder={t("diary.tripNamePlaceholder")} placeholderTextColor={colors.textSecondary} + editable={!disabled} /> ); diff --git a/components/diary/addTripModal/index.tsx b/components/diary/TripFormModal/index.tsx similarity index 51% rename from components/diary/addTripModal/index.tsx rename to components/diary/TripFormModal/index.tsx index ef27397..5ca8706 100644 --- a/components/diary/addTripModal/index.tsx +++ b/components/diary/TripFormModal/index.tsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useState, useEffect, useRef } from "react"; import { View, Text, @@ -9,19 +9,21 @@ import { 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/addTripModal/FishingGearList"; -import MaterialCostList from "@/components/diary/addTripModal/MaterialCostList"; -import TripNameInput from "@/components/diary/addTripModal/TripNameInput"; -import TripDurationPicker from "@/components/diary/addTripModal/TripDurationPicker"; -import PortSelector from "@/components/diary/addTripModal/PortSelector"; -import BasicInfoInput from "@/components/diary/addTripModal/BasicInfoInput"; +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 } from "@/controller/TripController"; +import { createTrip, updateTrip } from "@/controller/TripController"; // Internal component interfaces - extend from Model with local id for state management export interface FishingGear extends Model.FishingGear { @@ -32,16 +34,51 @@ export interface TripCost extends Model.TripCost { id: string; } -interface AddTripModalProps { +interface TripFormModalProps { visible: boolean; onClose: () => void; - onSuccess?: () => void; // Callback khi thêm chuyến đi thành công + onSuccess?: () => void; + mode?: 'add' | 'edit' | 'view'; + tripData?: Model.Trip; } -export default function AddTripModal({ visible, onClose, onSuccess }: AddTripModalProps) { +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(""); @@ -51,21 +88,120 @@ export default function AddTripModal({ visible, onClose, onSuccess }: AddTripMod const [endDate, setEndDate] = useState(null); const [departurePortId, setDeparturePortId] = useState(1); const [arrivalPortId, setArrivalPortId] = useState(1); - const [fishingGroundCodes, setFishingGroundCodes] = useState(""); // Input as string, convert to array + 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 = () => { - // Reset form - setSelectedShipId(""); - setTripName(""); - setFishingGears([]); - setTripCosts([]); - setStartDate(null); - setEndDate(null); - setDeparturePortId(1); - setArrivalPortId(1); - setFishingGroundCodes(""); + // 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 @@ -169,32 +305,45 @@ export default function AddTripModal({ visible, onClose, onSuccess }: AddTripMod setIsSubmitting(true); try { - const response = await createTrip(selectedShipId, apiBody); + let response; - if (response.data) { - // Show success alert - Alert.alert( - t("common.success"), - t("diary.createTripSuccess") - ); - - // Call onSuccess callback - if (onSuccess) { - onSuccess(); - } - - // Reset form and close modal - handleCancel(); + 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("Error creating trip:", error); + 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"), t("diary.createTripError")); + Alert.alert( + t("common.error"), + isEditMode ? t("diary.updateTripError") : t("diary.createTripError") + ); } finally { setIsSubmitting(false); } @@ -227,19 +376,29 @@ export default function AddTripModal({ visible, onClose, onSuccess }: AddTripMod return ( - - + + {/* Header */} - {t("diary.addTrip")} + {isViewMode + ? t("diary.viewTrip") + : isEditMode + ? t("diary.editTrip") + : t("diary.addTrip")} @@ -248,24 +407,26 @@ export default function AddTripModal({ visible, onClose, onSuccess }: AddTripMod - {/* Auto Fill Section */} - + {/* Auto Fill Section - only show in add mode */} + {!isEditMode && !isViewMode && } - {/* Ship Selector */} + {/* Ship Selector - disabled in edit and view mode */} {/* Trip Name */} - + {/* Fishing Gear List */} - + {/* Trip Cost List */} - + {/* Trip Duration */} {/* Port Selector */} @@ -281,49 +443,53 @@ export default function AddTripModal({ visible, onClose, onSuccess }: AddTripMod arrivalPortId={arrivalPortId} onDeparturePortChange={setDeparturePortId} onArrivalPortChange={setArrivalPortId} + disabled={isReadOnly} /> {/* Fishing Ground Codes */} - {/* Footer */} - - - + - {t("common.cancel")} - - - - {isSubmitting ? ( - - ) : ( - - {t("diary.createTrip")} + + {t("common.cancel")} - )} - - - - + + + {isSubmitting ? ( + + ) : ( + + {isEditMode ? t("diary.saveChanges") : t("diary.createTrip")} + + )} + + + )} + + ); } diff --git a/constants/index.ts b/constants/index.ts index e392ddd..487015c 100644 --- a/constants/index.ts +++ b/constants/index.ts @@ -59,6 +59,7 @@ export const API_GET_SHIP_TYPES = "/api/sgw/ships/types"; export const API_GET_SHIP_GROUPS = "/api/sgw/shipsgroup"; export const API_GET_LAST_TRIP = "/api/sgw/trips/last"; export const API_POST_TRIP = "/api/sgw/trips"; +export const API_PUT_TRIP = "/api/sgw/trips"; export const API_GET_ALARM = "/api/alarms"; export const API_MANAGER_ALARM = "/api/alarms/confirm"; export const API_GET_ALL_SHIP = "/api/sgw/ships"; diff --git a/controller/TripController.ts b/controller/TripController.ts index e1dd970..963ebb9 100644 --- a/controller/TripController.ts +++ b/controller/TripController.ts @@ -7,6 +7,7 @@ import { API_UPDATE_TRIP_STATUS, API_GET_LAST_TRIP, API_POST_TRIP, + API_PUT_TRIP, } from "@/constants"; export async function queryTrip() { @@ -36,3 +37,7 @@ export async function queryTripsList(body: Model.TripListBody) { export async function createTrip(thingId: string, body: Model.TripAPIBody) { return api.post(`${API_POST_TRIP}/${thingId}`, body); } + +export async function updateTrip(tripId: string, body: Model.TripAPIBody) { + return api.put(`${API_PUT_TRIP}/${tripId}`, body); +} diff --git a/locales/en.json b/locales/en.json index ceae34d..08d4f66 100644 --- a/locales/en.json +++ b/locales/en.json @@ -172,7 +172,12 @@ "tripNameRequired": "Please enter a trip name" }, "createTripSuccess": "Trip created successfully!", - "createTripError": "Unable to create trip. Please try again." + "createTripError": "Unable to create trip. Please try again.", + "editTrip": "Edit Trip", + "viewTrip": "Trip Details", + "saveChanges": "Save Changes", + "updateTripSuccess": "Trip updated successfully!", + "updateTripError": "Unable to update trip. Please try again." }, "trip": { "infoTrip": "Trip Information", diff --git a/locales/vi.json b/locales/vi.json index 04cf367..0c98628 100644 --- a/locales/vi.json +++ b/locales/vi.json @@ -172,7 +172,12 @@ "tripNameRequired": "Vui lòng nhập tên chuyến đi" }, "createTripSuccess": "Tạo chuyến đi thành công!", - "createTripError": "Không thể tạo chuyến đi. Vui lòng thử lại." + "createTripError": "Không thể tạo chuyến đi. Vui lòng thử lại.", + "editTrip": "Chỉnh sửa chuyến đi", + "viewTrip": "Chi tiết chuyến đi", + "saveChanges": "Lưu thay đổi", + "updateTripSuccess": "Cập nhật chuyến đi thành công!", + "updateTripError": "Không thể cập nhật chuyến đi. Vui lòng thử lại." }, "trip": { "infoTrip": "Thông Tin Chuyến Đi",