thêm tab "Xem chi tiết chuyến đi", "Xem chi tiết thành viên chuyến đi", tái sử dụng lại components modal tripForm
This commit is contained in:
@@ -134,7 +134,7 @@ export default function MaterialCostList({
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text style={[styles.sectionTitle, themedStyles.sectionTitle]}>
|
||||
{t("diary.materialCostList")}
|
||||
{t("trip.costTable.title")}
|
||||
</Text>
|
||||
|
||||
{/* Cost Items List */}
|
||||
|
||||
120
components/diary/TripFormModal/TripFormBody.tsx
Normal file
120
components/diary/TripFormModal/TripFormBody.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
import React from "react";
|
||||
import { View, StyleSheet } from "react-native";
|
||||
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 { FishingGear, TripCost } from "./index";
|
||||
|
||||
interface TripFormBodyProps {
|
||||
mode: "add" | "edit" | "view";
|
||||
tripData?: Model.Trip;
|
||||
// Form state
|
||||
selectedShipId: string;
|
||||
setSelectedShipId: (id: string) => void;
|
||||
tripName: string;
|
||||
setTripName: (name: string) => void;
|
||||
fishingGears: FishingGear[];
|
||||
setFishingGears: (gears: FishingGear[]) => void;
|
||||
tripCosts: TripCost[];
|
||||
setTripCosts: (costs: TripCost[]) => void;
|
||||
startDate: Date | null;
|
||||
setStartDate: (date: Date | null) => void;
|
||||
endDate: Date | null;
|
||||
setEndDate: (date: Date | null) => void;
|
||||
departurePortId: number;
|
||||
setDeparturePortId: (id: number) => void;
|
||||
arrivalPortId: number;
|
||||
setArrivalPortId: (id: number) => void;
|
||||
fishingGroundCodes: string;
|
||||
setFishingGroundCodes: (codes: string) => void;
|
||||
// Callbacks
|
||||
onAutoFill?: (tripData: Model.Trip, selectedThingId: string) => void;
|
||||
}
|
||||
|
||||
export default function TripFormBody({
|
||||
mode,
|
||||
selectedShipId,
|
||||
setSelectedShipId,
|
||||
tripName,
|
||||
setTripName,
|
||||
fishingGears,
|
||||
setFishingGears,
|
||||
tripCosts,
|
||||
setTripCosts,
|
||||
startDate,
|
||||
setStartDate,
|
||||
endDate,
|
||||
setEndDate,
|
||||
departurePortId,
|
||||
setDeparturePortId,
|
||||
arrivalPortId,
|
||||
setArrivalPortId,
|
||||
fishingGroundCodes,
|
||||
setFishingGroundCodes,
|
||||
onAutoFill,
|
||||
}: TripFormBodyProps) {
|
||||
const isEditMode = mode === "edit";
|
||||
const isViewMode = mode === "view";
|
||||
const isReadOnly = isViewMode;
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
{/* Auto Fill Section - only show in add mode */}
|
||||
{!isEditMode && !isViewMode && onAutoFill && (
|
||||
<AutoFillSection onAutoFill={onAutoFill} />
|
||||
)}
|
||||
|
||||
{/* Ship Selector - disabled in edit and view mode */}
|
||||
<ShipSelector
|
||||
selectedShipId={selectedShipId}
|
||||
onChange={setSelectedShipId}
|
||||
disabled={isEditMode || isViewMode}
|
||||
/>
|
||||
|
||||
{/* Trip Name */}
|
||||
<TripNameInput value={tripName} onChange={setTripName} disabled={isReadOnly} />
|
||||
|
||||
{/* Fishing Gear List */}
|
||||
<FishingGearList items={fishingGears} onChange={setFishingGears} disabled={isReadOnly} />
|
||||
|
||||
{/* Trip Cost List */}
|
||||
<MaterialCostList items={tripCosts} onChange={setTripCosts} disabled={isReadOnly} />
|
||||
|
||||
{/* Trip Duration */}
|
||||
<TripDurationPicker
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
onStartDateChange={setStartDate}
|
||||
onEndDateChange={setEndDate}
|
||||
disabled={isReadOnly}
|
||||
/>
|
||||
|
||||
{/* Port Selector */}
|
||||
<PortSelector
|
||||
departurePortId={departurePortId}
|
||||
arrivalPortId={arrivalPortId}
|
||||
onDeparturePortChange={setDeparturePortId}
|
||||
onArrivalPortChange={setArrivalPortId}
|
||||
disabled={isReadOnly}
|
||||
/>
|
||||
|
||||
{/* Fishing Ground Codes */}
|
||||
<BasicInfoInput
|
||||
fishingGroundCodes={fishingGroundCodes}
|
||||
onChange={setFishingGroundCodes}
|
||||
disabled={isReadOnly}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import React, { useState, useEffect, useRef, useCallback } from "react";
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
@@ -15,15 +15,21 @@ import {
|
||||
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 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 {
|
||||
@@ -38,32 +44,82 @@ interface TripFormModalProps {
|
||||
visible: boolean;
|
||||
onClose: () => void;
|
||||
onSuccess?: () => void;
|
||||
mode?: 'add' | 'edit' | 'view';
|
||||
mode?: "add" | "edit";
|
||||
tripData?: Model.Trip;
|
||||
}
|
||||
|
||||
export default function TripFormModal({
|
||||
visible,
|
||||
onClose,
|
||||
// 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',
|
||||
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
|
||||
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;
|
||||
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<FishingGear[]>(DEFAULT_FORM_STATE.fishingGears);
|
||||
const [tripCosts, setTripCosts] = useState<TripCost[]>(DEFAULT_FORM_STATE.tripCosts);
|
||||
const [startDate, setStartDate] = useState<Date | null>(DEFAULT_FORM_STATE.startDate);
|
||||
const [endDate, setEndDate] = useState<Date | null>(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) {
|
||||
// Open animation: fade overlay + slide content up
|
||||
Animated.parallel([
|
||||
Animated.timing(fadeAnim, {
|
||||
toValue: 1,
|
||||
@@ -79,217 +135,90 @@ export default function TripFormModal({
|
||||
}
|
||||
}, [visible, fadeAnim, slideAnim]);
|
||||
|
||||
// Form state
|
||||
const [selectedShipId, setSelectedShipId] = useState<string>("");
|
||||
const [tripName, setTripName] = useState("");
|
||||
const [fishingGears, setFishingGears] = useState<FishingGear[]>([]);
|
||||
const [tripCosts, setTripCosts] = useState<TripCost[]>([]);
|
||||
const [startDate, setStartDate] = useState<Date | null>(null);
|
||||
const [endDate, setEndDate] = useState<Date | null>(null);
|
||||
const [departurePortId, setDeparturePortId] = useState<number>(1);
|
||||
const [arrivalPortId, setArrivalPortId] = useState<number>(1);
|
||||
const [fishingGroundCodes, setFishingGroundCodes] = useState<string>("");
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
// Pre-fill form when in edit or view mode
|
||||
// Pre-fill form when in edit 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(", "));
|
||||
}
|
||||
if (isEditMode && tripData && visible) {
|
||||
fillFormWithTripData(tripData, "edit");
|
||||
}
|
||||
}, [isEditMode, isViewMode, tripData, visible, mode]);
|
||||
}, [isEditMode, tripData, visible, fillFormWithTripData]);
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
};
|
||||
// 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 close modal after successful submit - always resets form
|
||||
const handleSuccessClose = () => {
|
||||
// Reset animation values for next open
|
||||
// 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);
|
||||
|
||||
// Close modal immediately
|
||||
slideAnim.setValue(Dimensions.get("window").height);
|
||||
onClose();
|
||||
|
||||
// Reset form after closing
|
||||
setTimeout(() => {
|
||||
setSelectedShipId("");
|
||||
setTripName("");
|
||||
setFishingGears([]);
|
||||
setTripCosts([]);
|
||||
setStartDate(null);
|
||||
setEndDate(null);
|
||||
setDeparturePortId(1);
|
||||
setArrivalPortId(1);
|
||||
setFishingGroundCodes("");
|
||||
}, 100);
|
||||
};
|
||||
setTimeout(resetForm, 100);
|
||||
}, [fadeAnim, slideAnim, onClose, resetForm]);
|
||||
|
||||
// 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);
|
||||
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));
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
// 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
|
||||
// Validate form
|
||||
const validateForm = useCallback((): boolean => {
|
||||
if (!selectedShipId) {
|
||||
Alert.alert(t("common.error"), t("diary.validation.shipRequired"));
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate dates are required
|
||||
if (!startDate || !endDate) {
|
||||
Alert.alert(t("common.error"), t("diary.validation.datesRequired"));
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate trip name is required
|
||||
if (!tripName.trim()) {
|
||||
Alert.alert(t("common.error"), t("diary.validation.tripNameRequired"));
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}, [selectedShipId, startDate, endDate, tripName, t]);
|
||||
|
||||
// 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 = {
|
||||
// Build API body
|
||||
const buildApiBody = useCallback((): Model.TripAPIBody => {
|
||||
return {
|
||||
thing_id: selectedShipId,
|
||||
name: tripName,
|
||||
departure_time: startDate ? startDate.toISOString() : "",
|
||||
departure_time: startDate?.toISOString() || "",
|
||||
departure_port_id: departurePortId,
|
||||
arrival_time: endDate ? endDate.toISOString() : "",
|
||||
arrival_time: endDate?.toISOString() || "",
|
||||
arrival_port_id: arrivalPortId,
|
||||
fishing_ground_codes: fishingGroundCodesArray,
|
||||
fishing_ground_codes: parseFishingGroundCodes(fishingGroundCodes),
|
||||
fishing_gears: fishingGears.map((gear) => ({
|
||||
name: gear.name,
|
||||
number: gear.number,
|
||||
@@ -302,75 +231,69 @@ export default function TripFormModal({
|
||||
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 {
|
||||
let response;
|
||||
|
||||
if (isEditMode && tripData) {
|
||||
// Edit mode: call updateTrip
|
||||
response = await updateTrip(tripData.id, apiBody);
|
||||
await updateTrip(tripData.id, apiBody);
|
||||
} else {
|
||||
// Add mode: call createTrip
|
||||
response = await createTrip(selectedShipId, apiBody);
|
||||
await createTrip(selectedShipId, apiBody);
|
||||
}
|
||||
|
||||
// Check if response is successful (response exists)
|
||||
|
||||
// Call onSuccess callback first (to refresh data)
|
||||
if (onSuccess) {
|
||||
onSuccess();
|
||||
}
|
||||
|
||||
// Show success alert
|
||||
|
||||
onSuccess?.();
|
||||
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));
|
||||
console.error(
|
||||
isEditMode ? "Error updating trip:" : "Error creating trip:",
|
||||
error
|
||||
);
|
||||
Alert.alert(
|
||||
t("common.error"),
|
||||
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,
|
||||
},
|
||||
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 (
|
||||
@@ -381,11 +304,11 @@ export default function TripFormModal({
|
||||
onRequestClose={handleCancel}
|
||||
>
|
||||
<Animated.View style={[styles.overlay, { opacity: fadeAnim }]}>
|
||||
<Animated.View
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.modalContainer,
|
||||
styles.modalContainer,
|
||||
themedStyles.modalContainer,
|
||||
{ transform: [{ translateY: slideAnim }] }
|
||||
{ transform: [{ translateY: slideAnim }] },
|
||||
]}
|
||||
>
|
||||
{/* Header */}
|
||||
@@ -394,39 +317,31 @@ export default function TripFormModal({
|
||||
<Ionicons name="close" size={24} color={colors.text} />
|
||||
</TouchableOpacity>
|
||||
<Text style={[styles.title, themedStyles.title]}>
|
||||
{isViewMode
|
||||
? t("diary.viewTrip")
|
||||
: isEditMode
|
||||
? t("diary.editTrip")
|
||||
: t("diary.addTrip")}
|
||||
{isEditMode ? t("diary.editTrip") : t("diary.addTrip")}
|
||||
</Text>
|
||||
<View style={styles.placeholder} />
|
||||
</View>
|
||||
|
||||
{/* Content */}
|
||||
<ScrollView
|
||||
style={styles.content}
|
||||
showsVerticalScrollIndicator={false}
|
||||
contentContainerStyle={isViewMode ? { paddingBottom: 40 } : undefined}
|
||||
>
|
||||
<ScrollView style={styles.content} showsVerticalScrollIndicator={false}>
|
||||
{/* Auto Fill Section - only show in add mode */}
|
||||
{!isEditMode && !isViewMode && <AutoFillSection onAutoFill={handleAutoFill} />}
|
||||
{!isEditMode && <AutoFillSection onAutoFill={handleAutoFill} />}
|
||||
|
||||
{/* Ship Selector - disabled in edit and view mode */}
|
||||
{/* Ship Selector - disabled in edit mode */}
|
||||
<ShipSelector
|
||||
selectedShipId={selectedShipId}
|
||||
onChange={setSelectedShipId}
|
||||
disabled={isEditMode || isViewMode}
|
||||
disabled={isEditMode}
|
||||
/>
|
||||
|
||||
{/* Trip Name */}
|
||||
<TripNameInput value={tripName} onChange={setTripName} disabled={isReadOnly} />
|
||||
<TripNameInput value={tripName} onChange={setTripName} />
|
||||
|
||||
{/* Fishing Gear List */}
|
||||
<FishingGearList items={fishingGears} onChange={setFishingGears} disabled={isReadOnly} />
|
||||
<FishingGearList items={fishingGears} onChange={setFishingGears} />
|
||||
|
||||
{/* Trip Cost List */}
|
||||
<MaterialCostList items={tripCosts} onChange={setTripCosts} disabled={isReadOnly} />
|
||||
<MaterialCostList items={tripCosts} onChange={setTripCosts} />
|
||||
|
||||
{/* Trip Duration */}
|
||||
<TripDurationPicker
|
||||
@@ -434,7 +349,6 @@ export default function TripFormModal({
|
||||
endDate={endDate}
|
||||
onStartDateChange={setStartDate}
|
||||
onEndDateChange={setEndDate}
|
||||
disabled={isReadOnly}
|
||||
/>
|
||||
|
||||
{/* Port Selector */}
|
||||
@@ -443,51 +357,45 @@ export default function TripFormModal({
|
||||
arrivalPortId={arrivalPortId}
|
||||
onDeparturePortChange={setDeparturePortId}
|
||||
onArrivalPortChange={setArrivalPortId}
|
||||
disabled={isReadOnly}
|
||||
/>
|
||||
|
||||
{/* Fishing Ground Codes */}
|
||||
<BasicInfoInput
|
||||
fishingGroundCodes={fishingGroundCodes}
|
||||
onChange={setFishingGroundCodes}
|
||||
disabled={isReadOnly}
|
||||
/>
|
||||
</ScrollView>
|
||||
|
||||
{/* Footer - hide in view mode */}
|
||||
{!isViewMode && (
|
||||
<View style={[styles.footer, themedStyles.footer]}>
|
||||
<TouchableOpacity
|
||||
style={[styles.cancelButton, themedStyles.cancelButton]}
|
||||
onPress={handleCancel}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Text
|
||||
style={[styles.cancelButtonText, themedStyles.cancelButtonText]}
|
||||
>
|
||||
{t("common.cancel")}
|
||||
{/* Footer */}
|
||||
<View style={[styles.footer, themedStyles.footer]}>
|
||||
<TouchableOpacity
|
||||
style={[styles.cancelButton, themedStyles.cancelButton]}
|
||||
onPress={handleCancel}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Text style={[styles.cancelButtonText, themedStyles.cancelButtonText]}>
|
||||
{t("common.cancel")}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.submitButton,
|
||||
themedStyles.submitButton,
|
||||
isSubmitting && styles.submitButtonDisabled,
|
||||
]}
|
||||
onPress={handleSubmit}
|
||||
activeOpacity={0.7}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<ActivityIndicator size="small" color="#FFFFFF" />
|
||||
) : (
|
||||
<Text style={styles.submitButtonText}>
|
||||
{isEditMode ? t("diary.saveChanges") : t("diary.createTrip")}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.submitButton,
|
||||
themedStyles.submitButton,
|
||||
isSubmitting && styles.submitButtonDisabled
|
||||
]}
|
||||
onPress={handleSubmit}
|
||||
activeOpacity={0.7}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<ActivityIndicator size="small" color="#FFFFFF" />
|
||||
) : (
|
||||
<Text style={styles.submitButtonText}>
|
||||
{isEditMode ? t("diary.saveChanges") : t("diary.createTrip")}
|
||||
</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
)}
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</Animated.View>
|
||||
</Animated.View>
|
||||
</Modal>
|
||||
@@ -505,10 +413,7 @@ const styles = StyleSheet.create({
|
||||
borderTopRightRadius: 24,
|
||||
maxHeight: "90%",
|
||||
shadowColor: "#000",
|
||||
shadowOffset: {
|
||||
width: 0,
|
||||
height: -4,
|
||||
},
|
||||
shadowOffset: { width: 0, height: -4 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 12,
|
||||
elevation: 8,
|
||||
|
||||
Reference in New Issue
Block a user