From 67e9fc22a38232bc45c56ff4d48efa4471b8cf47 Mon Sep 17 00:00:00 2001 From: MinhNN Date: Mon, 22 Dec 2025 15:22:06 +0700 Subject: [PATCH] =?UTF-8?q?C=E1=BA=ADp=20nh=E1=BA=ADt=20API=20th=C3=AAm=20?= =?UTF-8?q?trip=20(validate)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/(tabs)/diary.tsx | 22 +++ .../diary/addTripModal/AutoFillSection.tsx | 16 +- .../diary/addTripModal/TripDurationPicker.tsx | 68 ++++++-- components/diary/addTripModal/index.tsx | 162 +++++++++++------- constants/index.ts | 1 + controller/TripController.ts | 5 + controller/typings.d.ts | 22 +++ locales/en.json | 9 +- locales/vi.json | 9 +- 9 files changed, 228 insertions(+), 86 deletions(-) diff --git a/app/(tabs)/diary.tsx b/app/(tabs)/diary.tsx index 9e7de36..46f15ed 100644 --- a/app/(tabs)/diary.tsx +++ b/app/(tabs)/diary.tsx @@ -37,6 +37,7 @@ export default function diary() { const [isLoadingMore, setIsLoadingMore] = useState(false); const [hasMore, setHasMore] = useState(true); const isInitialLoad = useRef(true); + const flatListRef = useRef(null); // Body call API things (đang fix cứng) const payloadThings: Model.SearchThingBody = { @@ -198,6 +199,25 @@ export default function diary() { // TODO: Show confirmation dialog and delete trip }; + // Handle sau khi thêm chuyến đi thành công + const handleTripAddSuccess = useCallback(() => { + // Reset về trang đầu và gọi lại API + isInitialLoad.current = true; + setAllTrips([]); + setHasMore(true); + const resetPayload: Model.TripListBody = { + ...payloadTrips, + offset: 0, + }; + setPayloadTrips(resetPayload); + getTripsList(resetPayload); + + // Scroll FlatList lên đầu + setTimeout(() => { + flatListRef.current?.scrollToOffset({ offset: 0, animated: true }); + }, 100); + }, [payloadTrips, getTripsList]); + // Dynamic styles based on theme const themedStyles = { safeArea: { @@ -304,6 +324,7 @@ export default function diary() { {/* Trip List with FlatList */} setShowAddTripModal(false)} + onSuccess={handleTripAddSuccess} /> ); diff --git a/components/diary/addTripModal/AutoFillSection.tsx b/components/diary/addTripModal/AutoFillSection.tsx index 58c1b4d..41e9015 100644 --- a/components/diary/addTripModal/AutoFillSection.tsx +++ b/components/diary/addTripModal/AutoFillSection.tsx @@ -19,7 +19,7 @@ import { queryLastTrip } from "@/controller/TripController"; import { showErrorToast } from "@/services/toast_service"; interface AutoFillSectionProps { - onAutoFill: (tripData: Model.Trip, selectedShipId: string) => void; + onAutoFill: (tripData: Model.Trip, selectedThingId: string) => void; } export default function AutoFillSection({ onAutoFill }: AutoFillSectionProps) { @@ -36,7 +36,7 @@ export default function AutoFillSection({ onAutoFill }: AutoFillSectionProps) { things ?.filter((thing) => thing.id != null) .map((thing) => ({ - id: thing.id as string, + thingId: thing.id as string, shipName: thing.metadata?.ship_name || "", })) || []; @@ -46,17 +46,17 @@ export default function AutoFillSection({ onAutoFill }: AutoFillSectionProps) { return ship.shipName.toLowerCase().includes(searchLower); }); - const handleSelectShip = async (shipId: string) => { + const handleSelectShip = async (thingId: string) => { setIsLoading(true); try { - const response = await queryLastTrip(shipId); + const response = await queryLastTrip(thingId); if (response.data) { // Close the modal first before showing alert setIsOpen(false); setSearchText(""); - // Pass shipId (thingId) along with trip data for filling ShipSelector - onAutoFill(response.data, shipId); + // Pass thingId along with trip data for filling ShipSelector + onAutoFill(response.data, thingId); // Use Alert instead of Toast so it appears above all modals Alert.alert( @@ -182,9 +182,9 @@ export default function AutoFillSection({ onAutoFill }: AutoFillSectionProps) { {filteredShips.length > 0 ? ( filteredShips.map((ship) => ( handleSelectShip(ship.id)} + onPress={() => handleSelectShip(ship.thingId)} > (new Date()); + const [tempEndDate, setTempEndDate] = useState(new Date()); const formatDate = (date: Date | null) => { if (!date) return ""; @@ -38,20 +42,64 @@ export default function TripDurationPicker({ return `${day}/${month}/${year}`; }; + const handleOpenStartPicker = () => { + const today = new Date(); + const dateToUse = startDate || today; + // If no date selected, immediately set to today + if (!startDate) { + onStartDateChange(today); + } + // Always set tempStartDate to the date we're using (today if no date was selected) + setTempStartDate(dateToUse); + setShowStartPicker(true); + }; + + const handleOpenEndPicker = () => { + const today = new Date(); + const dateToUse = endDate || today; + // If no date selected, immediately set to today + if (!endDate) { + onEndDateChange(today); + } + // Always set tempEndDate to the date we're using (today if no date was selected) + setTempEndDate(dateToUse); + setShowEndPicker(true); + }; + const handleStartDateChange = (event: any, selectedDate?: Date) => { - setShowStartPicker(Platform.OS === "ios"); - if (selectedDate) { + if (Platform.OS === "android") { + setShowStartPicker(false); + if (event.type === "set" && selectedDate) { + onStartDateChange(selectedDate); + } + } else if (selectedDate) { + // For iOS, update both temp and actual date immediately + setTempStartDate(selectedDate); onStartDateChange(selectedDate); } }; const handleEndDateChange = (event: any, selectedDate?: Date) => { - setShowEndPicker(Platform.OS === "ios"); - if (selectedDate) { + if (Platform.OS === "android") { + setShowEndPicker(false); + if (event.type === "set" && selectedDate) { + onEndDateChange(selectedDate); + } + } else if (selectedDate) { + // For iOS, update both temp and actual date immediately + setTempEndDate(selectedDate); onEndDateChange(selectedDate); } }; + const handleConfirmStartDate = () => { + setShowStartPicker(false); + }; + + const handleConfirmEndDate = () => { + setShowEndPicker(false); + }; + const themedStyles = { label: { color: colors.text }, dateInput: { @@ -79,7 +127,7 @@ export default function TripDurationPicker({ setShowStartPicker(true)} + onPress={handleOpenStartPicker} activeOpacity={0.7} > setShowEndPicker(true)} + onPress={handleOpenEndPicker} activeOpacity={0.7} > {t("diary.selectStartDate")} - setShowStartPicker(false)}> + {t("common.done")} {t("diary.selectEndDate")} - setShowEndPicker(false)}> + {t("common.done")} ; - trip_cost: Array<{ - type: string; - amount: number; - unit: string; - cost_per_unit: number; - total_cost: number; - }>; } interface AddTripModalProps { visible: boolean; onClose: () => void; + onSuccess?: () => void; // Callback khi thêm chuyến đi thành công } -export default function AddTripModal({ visible, onClose }: AddTripModalProps) { +export default function AddTripModal({ visible, onClose, onSuccess }: AddTripModalProps) { const { t } = useI18n(); const { colors } = useThemeContext(); @@ -78,6 +52,7 @@ export default function AddTripModal({ visible, onClose }: AddTripModalProps) { const [departurePortId, setDeparturePortId] = useState(1); const [arrivalPortId, setArrivalPortId] = useState(1); const [fishingGroundCodes, setFishingGroundCodes] = useState(""); // Input as string, convert to array + const [isSubmitting, setIsSubmitting] = useState(false); const handleCancel = () => { // Reset form @@ -99,17 +74,19 @@ export default function AddTripModal({ visible, onClose }: AddTripModalProps) { setSelectedShipId(selectedThingId); // Fill trip name - if (tripData.name) { - setTripName(tripData.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() || "", - })); + const gears: FishingGear[] = tripData.fishing_gears.map( + (gear, index) => ({ + id: `auto-${Date.now()}-${index}`, + name: gear.name || "", + number: gear.number?.toString() || "", + }) + ); setFishingGears(gears); } @@ -135,12 +112,33 @@ export default function AddTripModal({ visible, onClose }: AddTripModalProps) { } // Fill fishing ground codes - if (tripData.fishing_ground_codes && Array.isArray(tripData.fishing_ground_codes)) { + if ( + tripData.fishing_ground_codes && + Array.isArray(tripData.fishing_ground_codes) + ) { setFishingGroundCodes(tripData.fishing_ground_codes.join(", ")); } }; - const handleSubmit = () => { + 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(",") @@ -148,8 +146,8 @@ export default function AddTripModal({ visible, onClose }: AddTripModalProps) { .filter((code) => !isNaN(code)); // Format API body - const apiBody: TripAPIBody = { - thing_id: selectedShipId || undefined, + const apiBody: Model.TripAPIBody = { + thing_id: selectedShipId, name: tripName, departure_time: startDate ? startDate.toISOString() : "", departure_port_id: departurePortId, @@ -169,13 +167,37 @@ export default function AddTripModal({ visible, onClose }: AddTripModalProps) { })), }; - // Simulate API call - log the formatted data - console.log("=== Submitting Trip Data (API Format) ==="); - console.log(JSON.stringify(apiBody, null, 2)); - console.log("=== End Trip Data ==="); - - // Reset form and close modal - handleCancel(); + setIsSubmitting(true); + try { + const response = await createTrip(selectedShipId, apiBody); + + 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(); + } + } catch (error: any) { + console.error("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")); + } finally { + setIsSubmitting(false); + } }; const themedStyles = { @@ -240,16 +262,10 @@ export default function AddTripModal({ visible, onClose }: AddTripModalProps) { {/* Fishing Gear List */} - + {/* Trip Cost List */} - + {/* Trip Duration */} - + {t("common.cancel")} - - {t("diary.createTrip")} - + {isSubmitting ? ( + + ) : ( + + {t("diary.createTrip")} + + )} @@ -373,6 +400,9 @@ const styles = StyleSheet.create({ borderRadius: 12, alignItems: "center", }, + submitButtonDisabled: { + opacity: 0.7, + }, submitButtonText: { fontSize: 16, fontWeight: "600", diff --git a/constants/index.ts b/constants/index.ts index 3cb373a..e392ddd 100644 --- a/constants/index.ts +++ b/constants/index.ts @@ -58,6 +58,7 @@ export const API_GET_ALL_BANZONES = "/api/sgw/banzones"; 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_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 ba89daf..e1dd970 100644 --- a/controller/TripController.ts +++ b/controller/TripController.ts @@ -6,6 +6,7 @@ import { API_UPDATE_FISHING_LOGS, API_UPDATE_TRIP_STATUS, API_GET_LAST_TRIP, + API_POST_TRIP, } from "@/constants"; export async function queryTrip() { @@ -31,3 +32,7 @@ export async function queryUpdateFishingLogs(body: Model.FishingLog) { export async function queryTripsList(body: Model.TripListBody) { return api.post(API_POST_TRIPSLIST, body); } + +export async function createTrip(thingId: string, body: Model.TripAPIBody) { + return api.post(`${API_POST_TRIP}/${thingId}`, body); +} diff --git a/controller/typings.d.ts b/controller/typings.d.ts index 02e5171..31166a8 100644 --- a/controller/typings.d.ts +++ b/controller/typings.d.ts @@ -200,6 +200,28 @@ declare namespace Model { status: number; note?: string; } + + // API body interface for creating a new trip + interface TripAPIBody { + thing_id?: string; + name: string; + departure_time: string; // ISO string + departure_port_id: number; + arrival_time: string; // ISO string + arrival_port_id: number; + fishing_ground_codes: number[]; + fishing_gears: Array<{ + name: string; + number: string; + }>; + trip_cost: Array<{ + type: string; + amount: number; + unit: string; + cost_per_unit: number; + total_cost: number; + }>; + } //Fish interface FishSpeciesResponse { id: number; diff --git a/locales/en.json b/locales/en.json index 3b0f9c3..ceae34d 100644 --- a/locales/en.json +++ b/locales/en.json @@ -165,7 +165,14 @@ "success": "Data filled from last trip", "error": "Unable to fetch trip data", "noData": "No previous trip data available" - } + }, + "validation": { + "shipRequired": "Please select a ship before creating the trip", + "datesRequired": "Please select departure and arrival dates", + "tripNameRequired": "Please enter a trip name" + }, + "createTripSuccess": "Trip created successfully!", + "createTripError": "Unable to create trip. Please try again." }, "trip": { "infoTrip": "Trip Information", diff --git a/locales/vi.json b/locales/vi.json index cc0c329..04cf367 100644 --- a/locales/vi.json +++ b/locales/vi.json @@ -165,7 +165,14 @@ "success": "Đã điền dữ liệu từ chuyến đi cuối cùng", "error": "Không thể lấy dữ liệu chuyến đi", "noData": "Không có dữ liệu chuyến đi trước đó" - } + }, + "validation": { + "shipRequired": "Vui lòng chọn tàu trước khi tạo chuyến đi", + "datesRequired": "Vui lòng chọn ngày khởi hành và ngày kết thúc", + "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." }, "trip": { "infoTrip": "Thông Tin Chuyến Đi",