From e535aaa1e8bf39526c2e6926cf7c9b057db3117b Mon Sep 17 00:00:00 2001 From: MinhNN Date: Tue, 4 Nov 2025 14:18:30 +0700 Subject: [PATCH] update NetDetailModal --- components/map/PolygonWithLabel.tsx | 4 +- .../modal/NetDetailModal/NetDetailModal.tsx | 164 ++++++++++++++++-- .../components/FishCardForm.tsx | 45 +++-- .../components/FishCardList.tsx | 84 +++++++-- .../tripInfo/modal/TripCostDetailModal.tsx | 2 +- .../modal/style/NetDetailModal.styles.ts | 63 ++++--- components/ui/icon-symbol.tsx | 1 + 7 files changed, 302 insertions(+), 61 deletions(-) diff --git a/components/map/PolygonWithLabel.tsx b/components/map/PolygonWithLabel.tsx index 3944166..314f13a 100644 --- a/components/map/PolygonWithLabel.tsx +++ b/components/map/PolygonWithLabel.tsx @@ -47,11 +47,11 @@ export const PolygonWithLabel: React.FC = ({ const labelFontSize = calculateFontSize(12); const contentFontSize = calculateFontSize(10); - console.log("zoom level: ", zoomLevel); + // console.log("zoom level: ", zoomLevel); const paddingScale = Math.max(Math.pow(2, (zoomLevel - 10) * 0.2), 0.5); const minWidthScale = Math.max(Math.pow(2, (zoomLevel - 10) * 0.25), 0.9); - console.log("Min Width Scale: ", minWidthScale); + // console.log("Min Width Scale: ", minWidthScale); return ( <> diff --git a/components/tripInfo/modal/NetDetailModal/NetDetailModal.tsx b/components/tripInfo/modal/NetDetailModal/NetDetailModal.tsx index 504e804..b5349d3 100644 --- a/components/tripInfo/modal/NetDetailModal/NetDetailModal.tsx +++ b/components/tripInfo/modal/NetDetailModal/NetDetailModal.tsx @@ -1,6 +1,13 @@ import { IconSymbol } from "@/components/ui/icon-symbol"; import React, { useState } from "react"; -import { Modal, ScrollView, Text, TouchableOpacity, View } from "react-native"; +import { + Alert, + Modal, + ScrollView, + Text, + TouchableOpacity, + View, +} from "react-native"; import styles from "../style/NetDetailModal.styles"; import { CatchSectionHeader } from "./components/CatchSectionHeader"; import { FishCardList } from "./components/FishCardList"; @@ -57,9 +64,12 @@ const NetDetailModal: React.FC = ({ const [selectedUnitIndex, setSelectedUnitIndex] = useState( null ); - const [selectedConditionIndex, setSelectedConditionIndex] = useState< - number | null - >(null); + // const [selectedConditionIndex, setSelectedConditionIndex] = useState< + // number | null + // >(null); + // const [selectedGearIndex, setSelectedGearIndex] = useState( + // null + // ); const [expandedFishIndices, setExpandedFishIndices] = useState([]); // Khởi tạo dữ liệu khi netData thay đổi @@ -75,7 +85,8 @@ const NetDetailModal: React.FC = ({ setExpandedFishIndices([]); setSelectedFishIndex(null); setSelectedUnitIndex(null); - setSelectedConditionIndex(null); + // setSelectedConditionIndex(null); + // setSelectedGearIndex(null); setIsEditing(false); } }, [visible]); @@ -102,13 +113,97 @@ const NetDetailModal: React.FC = ({ const unitOptions = ["kg", "con", "tấn"]; // Danh sách tình trạng - const conditionOptions = ["Còn sống", "Chết", "Bị thương"]; + // const conditionOptions = ["Còn sống", "Chết", "Bị thương"]; + + // Danh sách ngư cụ + // const gearOptions = [ + // "Lưới kéo", + // "Lưới vây", + // "Lưới rê", + // "Lưới cào", + // "Lưới lồng", + // "Câu cần", + // "Câu dây", + // "Chài cá", + // "Lồng bẫy", + // "Đăng", + // ]; const handleEdit = () => { setIsEditing(!isEditing); }; const handleSave = () => { + // Validate từng cá trong danh sách và thu thập tất cả lỗi + const allErrors: { index: number; errors: string[] }[] = []; + + for (let i = 0; i < editableCatchList.length; i++) { + const fish = editableCatchList[i]; + const errors: string[] = []; + + if (!fish.fish_name || fish.fish_name.trim() === "") { + errors.push("- Tên loài cá"); + } + if (!fish.catch_number || fish.catch_number <= 0) { + errors.push("- Số lượng bắt được"); + } + if (!fish.catch_unit || fish.catch_unit.trim() === "") { + errors.push("- Đơn vị"); + } + if (!fish.fish_size || fish.fish_size <= 0) { + errors.push("- Kích thước cá"); + } + // if (!fish.fish_condition || fish.fish_condition.trim() === "") { + // errors.push("- Tình trạng cá"); + // } + // if (!fish.gear_usage || fish.gear_usage.trim() === "") { + // errors.push("- Dụng cụ sử dụng"); + // } + + if (errors.length > 0) { + allErrors.push({ index: i, errors }); + } + } + + // Nếu có lỗi, hiển thị tất cả + if (allErrors.length > 0) { + const errorMessage = allErrors + .map((item) => { + return `Cá số ${item.index + 1}:\n${item.errors.join("\n")}`; + }) + .join("\n\n"); + + Alert.alert( + "Thông tin không đầy đủ", + errorMessage, + [ + { + text: "Tiếp tục chỉnh sửa", + onPress: () => { + // Mở rộng tất cả các card bị lỗi + setExpandedFishIndices((prev) => { + const errorIndices = allErrors.map((item) => item.index); + const newIndices = [...prev]; + errorIndices.forEach((idx) => { + if (!newIndices.includes(idx)) { + newIndices.push(idx); + } + }); + return newIndices; + }); + }, + }, + { + text: "Hủy", + onPress: () => {}, + }, + ], + { cancelable: false } + ); + return; + } + + // Nếu validation pass, lưu dữ liệu setIsEditing(false); console.log("Saved catch list:", editableCatchList); }; @@ -149,8 +244,52 @@ const NetDetailModal: React.FC = ({ ); }; + const handleAddNewFish = () => { + const newFish: FishCatch = { + fish_species_id: 0, + fish_name: "", + catch_number: 0, + catch_unit: "kg", + fish_size: 0, + fish_rarity: 0, + fish_condition: "", + gear_usage: "", + }; + setEditableCatchList((prev) => [...prev, newFish]); + // Tự động expand card mới + setExpandedFishIndices((prev) => [...prev, editableCatchList.length]); + }; + + const handleDeleteFish = (index: number) => { + Alert.alert( + "Xác nhận xóa", + `Bạn có chắc muốn xóa loài cá này?`, + [ + { + text: "Hủy", + style: "cancel", + }, + { + text: "Xóa", + style: "destructive", + onPress: () => { + setEditableCatchList((prev) => prev.filter((_, i) => i !== index)); + // Cập nhật lại expandedFishIndices sau khi xóa + setExpandedFishIndices((prev) => + prev + .filter((i) => i !== index) + .map((i) => (i > index ? i - 1 : i)) + ); + }, + }, + ], + { cancelable: true } + ); + }; + + // Chỉ tính tổng số lượng cá có đơn vị là 'kg' const totalCatch = editableCatchList.reduce( - (sum, item) => sum + item.catch_number, + (sum, item) => (item.catch_unit === "kg" ? sum + item.catch_number : sum), 0 ); @@ -216,15 +355,20 @@ const NetDetailModal: React.FC = ({ expandedFishIndex={expandedFishIndices} selectedFishIndex={selectedFishIndex} selectedUnitIndex={selectedUnitIndex} - selectedConditionIndex={selectedConditionIndex} + // selectedConditionIndex={selectedConditionIndex} + // selectedGearIndex={selectedGearIndex} fishNameOptions={fishNameOptions} unitOptions={unitOptions} - conditionOptions={conditionOptions} + // conditionOptions={conditionOptions} + // gearOptions={gearOptions} onToggleExpanded={handleToggleExpanded} onUpdateCatchItem={updateCatchItem} setSelectedFishIndex={setSelectedFishIndex} setSelectedUnitIndex={setSelectedUnitIndex} - setSelectedConditionIndex={setSelectedConditionIndex} + // setSelectedConditionIndex={setSelectedConditionIndex} + // setSelectedGearIndex={setSelectedGearIndex} + onAddNewFish={handleAddNewFish} + onDeleteFish={handleDeleteFish} /> {/* Ghi chú */} diff --git a/components/tripInfo/modal/NetDetailModal/components/FishCardForm.tsx b/components/tripInfo/modal/NetDetailModal/components/FishCardForm.tsx index 11fb66d..3ccd479 100644 --- a/components/tripInfo/modal/NetDetailModal/components/FishCardForm.tsx +++ b/components/tripInfo/modal/NetDetailModal/components/FishCardForm.tsx @@ -20,13 +20,16 @@ interface FishCardFormProps { isEditing: boolean; fishNameOptions: string[]; unitOptions: string[]; - conditionOptions: string[]; + // conditionOptions: string[]; + // gearOptions: string[]; selectedFishIndex: number | null; selectedUnitIndex: number | null; - selectedConditionIndex: number | null; + // selectedConditionIndex: number | null; + // selectedGearIndex: number | null; setSelectedFishIndex: (index: number | null) => void; setSelectedUnitIndex: (index: number | null) => void; - setSelectedConditionIndex: (index: number | null) => void; + // setSelectedConditionIndex: (index: number | null) => void; + // setSelectedGearIndex: (index: number | null) => void; onUpdateCatchItem: ( index: number, field: keyof FishCatch, @@ -40,13 +43,16 @@ export const FishCardForm: React.FC = ({ isEditing, fishNameOptions, unitOptions, - conditionOptions, + // conditionOptions, + // gearOptions, selectedFishIndex, selectedUnitIndex, - selectedConditionIndex, + // selectedConditionIndex, + // selectedGearIndex, setSelectedFishIndex, setSelectedUnitIndex, - setSelectedConditionIndex, + // setSelectedConditionIndex, + // setSelectedGearIndex, onUpdateCatchItem, }) => { return ( @@ -69,6 +75,7 @@ export const FishCardForm: React.FC = ({ setSelectedFishIndex(null); }} zIndex={1000 - index} + styleOverride={styles.fishNameDropdown} /> ) : ( {fish.fish_name} @@ -157,7 +164,7 @@ export const FishCardForm: React.FC = ({ {/* Tình trạng */} - + {/* Tình trạng {isEditing ? ( = ({ ) : ( {fish.fish_condition} )} - + */} {/* Ngư cụ sử dụng */} - + {/* Ngư cụ sử dụng {isEditing ? ( - - onUpdateCatchItem(index, "gear_usage", value) + + setSelectedGearIndex(selectedGearIndex === index ? null : index) } - placeholder="Nhập ngư cụ..." + onSelect={(value: string) => { + onUpdateCatchItem(index, "gear_usage", value); + setSelectedGearIndex(null); + }} + zIndex={700 - index} + styleOverride={styles.optionsStatusFishList} /> ) : ( {fish.gear_usage || "Không có"} )} - + */} ); }; diff --git a/components/tripInfo/modal/NetDetailModal/components/FishCardList.tsx b/components/tripInfo/modal/NetDetailModal/components/FishCardList.tsx index 50712fb..780c8ab 100644 --- a/components/tripInfo/modal/NetDetailModal/components/FishCardList.tsx +++ b/components/tripInfo/modal/NetDetailModal/components/FishCardList.tsx @@ -1,6 +1,6 @@ import { IconSymbol } from "@/components/ui/icon-symbol"; import React from "react"; -import { TouchableOpacity, View } from "react-native"; +import { Text, TouchableOpacity, View } from "react-native"; import styles from "../../style/NetDetailModal.styles"; import { FishCardForm } from "./FishCardForm"; import { FishCardHeader } from "./FishCardHeader"; @@ -22,10 +22,12 @@ interface FishCardListProps { expandedFishIndex: number[]; selectedFishIndex: number | null; selectedUnitIndex: number | null; - selectedConditionIndex: number | null; + // selectedConditionIndex: number | null; + // selectedGearIndex: number | null; fishNameOptions: string[]; unitOptions: string[]; - conditionOptions: string[]; + // conditionOptions: string[]; + // gearOptions: string[]; onToggleExpanded: (index: number) => void; onUpdateCatchItem: ( index: number, @@ -34,7 +36,10 @@ interface FishCardListProps { ) => void; setSelectedFishIndex: (index: number | null) => void; setSelectedUnitIndex: (index: number | null) => void; - setSelectedConditionIndex: (index: number | null) => void; + // setSelectedConditionIndex: (index: number | null) => void; + // setSelectedGearIndex: (index: number | null) => void; + onAddNewFish?: () => void; + onDeleteFish?: (index: number) => void; } export const FishCardList: React.FC = ({ @@ -43,15 +48,20 @@ export const FishCardList: React.FC = ({ expandedFishIndex, selectedFishIndex, selectedUnitIndex, - selectedConditionIndex, + // selectedConditionIndex, + // selectedGearIndex, fishNameOptions, unitOptions, - conditionOptions, + // conditionOptions, + // gearOptions, onToggleExpanded, onUpdateCatchItem, setSelectedFishIndex, setSelectedUnitIndex, - setSelectedConditionIndex, + // setSelectedConditionIndex, + // setSelectedGearIndex, + onAddNewFish, + onDeleteFish, }) => { // Chuyển về logic đơn giản, không animation const handleToggleCard = (index: number) => { @@ -62,20 +72,57 @@ export const FishCardList: React.FC = ({ <> {catchList.map((fish, index) => ( - {/* Chevron button - always on top */} + {/* Delete + Chevron buttons - always on top, right side, horizontal row */} + {isEditing && ( + onDeleteFish?.(index)} + style={{ + backgroundColor: "#FF3B30", + borderRadius: 8, + width: 40, + height: 40, + justifyContent: "center", + alignItems: "center", + shadowColor: "#000", + shadowOpacity: 0.08, + shadowRadius: 2, + shadowOffset: { width: 0, height: 1 }, + elevation: 2, + }} + hitSlop={{ top: 12, bottom: 12, left: 12, right: 12 }} + activeOpacity={0.7} + > + + + )} handleToggleCard(index)} - style={[styles.chevronIconRight, { zIndex: 9999 }]} + style={{ + backgroundColor: "#007AFF", + borderRadius: 8, + width: 40, + height: 40, + justifyContent: "center", + alignItems: "center", + shadowColor: "#000", + shadowOpacity: 0.08, + shadowRadius: 2, + shadowOffset: { width: 0, height: 1 }, + elevation: 2, + }} hitSlop={{ top: 12, bottom: 12, left: 12, right: 12 }} activeOpacity={0.7} > @@ -102,18 +149,31 @@ export const FishCardList: React.FC = ({ isEditing={isEditing} fishNameOptions={fishNameOptions} unitOptions={unitOptions} - conditionOptions={conditionOptions} + // conditionOptions={conditionOptions} + // gearOptions={gearOptions} selectedFishIndex={selectedFishIndex} selectedUnitIndex={selectedUnitIndex} - selectedConditionIndex={selectedConditionIndex} + // selectedConditionIndex={selectedConditionIndex} + // selectedGearIndex={selectedGearIndex} setSelectedFishIndex={setSelectedFishIndex} setSelectedUnitIndex={setSelectedUnitIndex} - setSelectedConditionIndex={setSelectedConditionIndex} + // setSelectedConditionIndex={setSelectedConditionIndex} + // setSelectedGearIndex={setSelectedGearIndex} onUpdateCatchItem={onUpdateCatchItem} /> )} ))} + + {/* Nút thêm loài cá mới - hiển thị khi đang chỉnh sửa */} + {isEditing && ( + + + + Thêm loài cá + + + )} ); }; diff --git a/components/tripInfo/modal/TripCostDetailModal.tsx b/components/tripInfo/modal/TripCostDetailModal.tsx index d1cfe7f..e6f9b1d 100644 --- a/components/tripInfo/modal/TripCostDetailModal.tsx +++ b/components/tripInfo/modal/TripCostDetailModal.tsx @@ -50,7 +50,7 @@ const TripCostDetailModal: React.FC = ({ const handleSave = () => { setIsEditing(false); // TODO: Save data to backend - console.log("Saved data:", editableData); + // console.log("Saved data:", editableData); }; const handleCancel = () => { diff --git a/components/tripInfo/modal/style/NetDetailModal.styles.ts b/components/tripInfo/modal/style/NetDetailModal.styles.ts index 98a73e5..48df0f9 100644 --- a/components/tripInfo/modal/style/NetDetailModal.styles.ts +++ b/components/tripInfo/modal/style/NetDetailModal.styles.ts @@ -200,7 +200,7 @@ const styles = StyleSheet.create({ borderRadius: 8, marginTop: 4, backgroundColor: "#fff", - maxHeight: 200, + maxHeight: 100, zIndex: 1000, elevation: 5, shadowColor: "#000", @@ -224,7 +224,25 @@ const styles = StyleSheet.create({ borderRadius: 8, marginTop: 4, backgroundColor: "#fff", - maxHeight: 200, + maxHeight: 120, + zIndex: 1000, + elevation: 5, + shadowColor: "#000", + shadowOpacity: 0.15, + shadowRadius: 8, + shadowOffset: { width: 0, height: 4 }, + }, + fishNameDropdown: { + position: "absolute", + top: 46, + left: 0, + right: 0, + borderWidth: 1, + borderColor: "#007AFF", + borderRadius: 8, + marginTop: 4, + backgroundColor: "#fff", + maxHeight: 180, zIndex: 1000, elevation: 5, shadowColor: "#000", @@ -236,24 +254,6 @@ const styles = StyleSheet.create({ flexDirection: "row", gap: 5, }, - chevronIconRight: { - position: "absolute", - top: 6, - right: 12, - zIndex: 1000, - backgroundColor: "#007AFF", - borderRadius: 8, - width: 40, - height: 40, - padding: 0, - justifyContent: "center", - alignItems: "center", - shadowColor: "#000", - shadowOpacity: 0.08, - shadowRadius: 2, - shadowOffset: { width: 0, height: 1 }, - elevation: 2, - }, fishCardTitle: { fontSize: 16, fontWeight: "600", @@ -264,6 +264,29 @@ const styles = StyleSheet.create({ color: "#ff6600", marginTop: 0, }, + addFishButton: { + backgroundColor: "#007AFF", + borderRadius: 12, + padding: 16, + marginBottom: 12, + justifyContent: "center", + alignItems: "center", + shadowColor: "#000", + shadowOpacity: 0.05, + shadowRadius: 4, + shadowOffset: { width: 0, height: 2 }, + elevation: 2, + }, + addFishButtonContent: { + flexDirection: "row", + alignItems: "center", + gap: 8, + }, + addFishButtonText: { + fontSize: 16, + fontWeight: "600", + color: "#fff", + }, }); export default styles; diff --git a/components/ui/icon-symbol.tsx b/components/ui/icon-symbol.tsx index fa59933..213217f 100644 --- a/components/ui/icon-symbol.tsx +++ b/components/ui/icon-symbol.tsx @@ -30,6 +30,7 @@ const MAPPING = { "dot.radiowaves.left.and.right": "sensors", xmark: "close", pencil: "edit", + trash: "delete", } as IconMapping; /**