cập nhật thông tin cảng trong modal add/edit trip, tối ưu lại UI modal add/edit trip

This commit is contained in:
2025-12-24 11:38:23 +07:00
parent 000a4ed856
commit 24847504b1
9 changed files with 491 additions and 127 deletions

View File

@@ -14,7 +14,7 @@ import { useRouter } from "expo-router";
import FilterButton from "@/components/diary/FilterButton"; import FilterButton from "@/components/diary/FilterButton";
import TripCard from "@/components/diary/TripCard"; import TripCard from "@/components/diary/TripCard";
import FilterModal, { FilterValues } from "@/components/diary/FilterModal"; import FilterModal, { FilterValues } from "@/components/diary/FilterModal";
import AddTripModal from "@/components/diary/TripFormModal"; import TripFormModal from "@/components/diary/TripFormModal";
import { useThings } from "@/state/use-thing"; import { useThings } from "@/state/use-thing";
import { useTripsList } from "@/state/use-tripslist"; import { useTripsList } from "@/state/use-tripslist";
import dayjs from "dayjs"; import dayjs from "dayjs";
@@ -26,7 +26,7 @@ export default function diary() {
const { colors } = useThemeContext(); const { colors } = useThemeContext();
const router = useRouter(); const router = useRouter();
const [showFilterModal, setShowFilterModal] = useState(false); const [showFilterModal, setShowFilterModal] = useState(false);
const [showAddTripModal, setShowAddTripModal] = useState(false); const [showTripFormModal, setShowTripFormModal] = useState(false);
const [editingTrip, setEditingTrip] = useState<Model.Trip | null>(null); const [editingTrip, setEditingTrip] = useState<Model.Trip | null>(null);
const [filters, setFilters] = useState<FilterValues>({ const [filters, setFilters] = useState<FilterValues>({
status: null, status: null,
@@ -195,7 +195,7 @@ export default function diary() {
pathname: "/trip-detail", pathname: "/trip-detail",
params: { params: {
tripId: tripToView.id, tripId: tripToView.id,
tripData: JSON.stringify(tripToView) tripData: JSON.stringify(tripToView),
}, },
}); });
} }
@@ -206,7 +206,7 @@ export default function diary() {
const tripToEdit = allTrips.find((trip) => trip.id === tripId); const tripToEdit = allTrips.find((trip) => trip.id === tripId);
if (tripToEdit) { if (tripToEdit) {
setEditingTrip(tripToEdit); setEditingTrip(tripToEdit);
setShowAddTripModal(true); setShowTripFormModal(true);
} }
}; };
@@ -351,7 +351,7 @@ export default function diary() {
/> />
<TouchableOpacity <TouchableOpacity
style={[styles.addButton, themedStyles.addButton]} style={[styles.addButton, themedStyles.addButton]}
onPress={() => setShowAddTripModal(true)} onPress={() => setShowTripFormModal(true)}
activeOpacity={0.7} activeOpacity={0.7}
> >
<Ionicons name="add" size={20} color="#FFFFFF" /> <Ionicons name="add" size={20} color="#FFFFFF" />
@@ -391,10 +391,10 @@ export default function diary() {
/> />
{/* Add/Edit Trip Modal */} {/* Add/Edit Trip Modal */}
<AddTripModal <TripFormModal
visible={showAddTripModal} visible={showTripFormModal}
onClose={() => { onClose={() => {
setShowAddTripModal(false); setShowTripFormModal(false);
setEditingTrip(null); setEditingTrip(null);
}} }}
onSuccess={handleTripAddSuccess} onSuccess={handleTripAddSuccess}

View File

@@ -157,7 +157,7 @@ export default function TripDetailPage() {
> >
{tripCosts.length > 0 ? ( {tripCosts.length > 0 ? (
<View style={styles.sectionInnerContent}> <View style={styles.sectionInnerContent}>
<MaterialCostList items={tripCosts} onChange={() => {}} disabled /> <MaterialCostList items={tripCosts} onChange={() => {}} disabled hideTitle />
</View> </View>
) : ( ) : (
<EmptySection icon="receipt-outline" message={t("diary.tripDetail.noCosts")} /> <EmptySection icon="receipt-outline" message={t("diary.tripDetail.noCosts")} />
@@ -174,7 +174,7 @@ export default function TripDetailPage() {
> >
{fishingGears.length > 0 ? ( {fishingGears.length > 0 ? (
<View style={styles.sectionInnerContent}> <View style={styles.sectionInnerContent}>
<FishingGearList items={fishingGears} onChange={() => {}} disabled /> <FishingGearList items={fishingGears} onChange={() => {}} disabled hideTitle />
</View> </View>
) : ( ) : (
<EmptySection icon="build-outline" message={t("diary.tripDetail.noGears")} /> <EmptySection icon="build-outline" message={t("diary.tripDetail.noGears")} />
@@ -297,7 +297,7 @@ const styles = StyleSheet.create({
fontFamily: Platform.select({ ios: "System", android: "Roboto", default: "System" }), fontFamily: Platform.select({ ios: "System", android: "Roboto", default: "System" }),
}, },
sectionInnerContent: { sectionInnerContent: {
marginTop: -8, marginTop: 5,
}, },
emptySection: { emptySection: {
alignItems: "center", alignItems: "center",

View File

@@ -21,12 +21,14 @@ interface FishingGearListProps {
items: FishingGear[]; items: FishingGear[];
onChange: (items: FishingGear[]) => void; onChange: (items: FishingGear[]) => void;
disabled?: boolean; disabled?: boolean;
hideTitle?: boolean;
} }
export default function FishingGearList({ export default function FishingGearList({
items, items,
onChange, onChange,
disabled = false, disabled = false,
hideTitle = false,
}: FishingGearListProps) { }: FishingGearListProps) {
const { t } = useI18n(); const { t } = useI18n();
const { colors } = useThemeContext(); const { colors } = useThemeContext();
@@ -77,9 +79,11 @@ export default function FishingGearList({
return ( return (
<View style={styles.container}> <View style={styles.container}>
<Text style={[styles.sectionTitle, themedStyles.sectionTitle]}> {!hideTitle && (
{t("diary.fishingGearList")} <Text style={[styles.sectionTitle, themedStyles.sectionTitle]}>
</Text> {t("diary.fishingGearList")}
</Text>
)}
{/* Gear Items List */} {/* Gear Items List */}
{items.map((gear, index) => ( {items.map((gear, index) => (

View File

@@ -0,0 +1,57 @@
import React from "react";
import { View, Text, StyleSheet, Platform } from "react-native";
import { Ionicons } from "@expo/vector-icons";
import { useThemeContext } from "@/hooks/use-theme-context";
interface FormSectionProps {
title: string;
icon?: keyof typeof Ionicons.glyphMap;
children: React.ReactNode;
}
/**
* A styled section wrapper for grouping related form fields
*/
export default function FormSection({ title, icon, children }: FormSectionProps) {
const { colors } = useThemeContext();
return (
<View style={[styles.container, { borderColor: colors.separator }]}>
<View style={styles.header}>
{icon && (
<Ionicons name={icon} size={18} color={colors.primary} style={styles.icon} />
)}
<Text style={[styles.title, { color: colors.text }]}>{title}</Text>
</View>
<View style={styles.content}>{children}</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
marginBottom: 20,
borderBottomWidth: 1,
paddingBottom: 16,
},
header: {
flexDirection: "row",
alignItems: "center",
marginBottom: 16,
},
icon: {
marginRight: 8,
},
title: {
fontSize: 17,
fontWeight: "700",
fontFamily: Platform.select({
ios: "System",
android: "Roboto",
default: "System",
}),
},
content: {
gap: 0,
},
});

View File

@@ -26,6 +26,7 @@ interface MaterialCostListProps {
items: TripCost[]; items: TripCost[];
onChange: (items: TripCost[]) => void; onChange: (items: TripCost[]) => void;
disabled?: boolean; disabled?: boolean;
hideTitle?: boolean;
} }
// Predefined cost types // Predefined cost types
@@ -41,6 +42,7 @@ export default function MaterialCostList({
items, items,
onChange, onChange,
disabled = false, disabled = false,
hideTitle = false,
}: MaterialCostListProps) { }: MaterialCostListProps) {
const { t } = useI18n(); const { t } = useI18n();
const { colors } = useThemeContext(); const { colors } = useThemeContext();
@@ -133,9 +135,11 @@ export default function MaterialCostList({
return ( return (
<View style={styles.container}> <View style={styles.container}>
<Text style={[styles.sectionTitle, themedStyles.sectionTitle]}> {!hideTitle && (
{t("trip.costTable.title")} <Text style={[styles.sectionTitle, themedStyles.sectionTitle]}>
</Text> {t("trip.costTable.title")}
</Text>
)}
{/* Cost Items List */} {/* Cost Items List */}
{items.map((cost) => ( {items.map((cost) => (

View File

@@ -1,14 +1,21 @@
import React from "react"; import React, { useState, useEffect, useMemo } from "react";
import { import {
View, View,
Text, Text,
TouchableOpacity, TouchableOpacity,
StyleSheet, StyleSheet,
Platform, Platform,
Modal,
ScrollView,
TextInput,
ActivityIndicator,
} from "react-native"; } from "react-native";
import { Ionicons } from "@expo/vector-icons"; import { Ionicons } from "@expo/vector-icons";
import { useI18n } from "@/hooks/use-i18n"; import { useI18n } from "@/hooks/use-i18n";
import { useThemeContext } from "@/hooks/use-theme-context"; import { useThemeContext } from "@/hooks/use-theme-context";
import { useGroup } from "@/state/use-group";
import { usePort } from "@/state/use-ports";
import { filterPortsByProvinceCode } from "@/utils/tripDataConverters";
interface PortSelectorProps { interface PortSelectorProps {
departurePortId: number; departurePortId: number;
@@ -18,6 +25,8 @@ interface PortSelectorProps {
disabled?: boolean; disabled?: boolean;
} }
type PortType = "departure" | "arrival";
export default function PortSelector({ export default function PortSelector({
departurePortId, departurePortId,
arrivalPortId, arrivalPortId,
@@ -28,26 +37,70 @@ export default function PortSelector({
const { t } = useI18n(); const { t } = useI18n();
const { colors } = useThemeContext(); const { colors } = useThemeContext();
const handleSelectDeparturePort = () => { // State from zustand stores
console.log("Select departure port pressed"); const { groups, getUserGroups, loading: groupsLoading } = useGroup();
// TODO: Implement port selection modal/dropdown const { ports, getPorts, loading: portsLoading } = usePort();
// For now, just set a dummy ID
onDeparturePortChange(1); // Local state - which dropdown is open
const [activeDropdown, setActiveDropdown] = useState<PortType | null>(null);
const [searchText, setSearchText] = useState("");
// Fetch groups and ports if not available
useEffect(() => {
if (!groups) {
getUserGroups();
}
}, [groups, getUserGroups]);
useEffect(() => {
if (!ports) {
getPorts();
}
}, [ports, getPorts]);
// Filter ports by province codes from groups
const filteredPorts = useMemo(() => {
return filterPortsByProvinceCode(ports, groups);
}, [ports, groups]);
// Filter by search text
const searchFilteredPorts = useMemo(() => {
if (!searchText) return filteredPorts;
return filteredPorts.filter((port) =>
port.name?.toLowerCase().includes(searchText.toLowerCase())
);
}, [filteredPorts, searchText]);
const isLoading = groupsLoading || portsLoading;
const handleSelectPort = (portId: number) => {
if (activeDropdown === "departure") {
onDeparturePortChange(portId);
} else {
onArrivalPortChange(portId);
}
setActiveDropdown(null);
setSearchText("");
}; };
const handleSelectArrivalPort = () => { const openDropdown = (type: PortType) => {
console.log("Select arrival port pressed"); setActiveDropdown(type);
// TODO: Implement port selection modal/dropdown setSearchText("");
// For now, just set a dummy ID
onArrivalPortChange(1);
}; };
// Helper to display port name (in production, fetch from port list by ID) const closeDropdown = () => {
setActiveDropdown(null);
setSearchText("");
};
// Get port name by ID
const getPortDisplayName = (portId: number): string => { const getPortDisplayName = (portId: number): string => {
// TODO: Fetch actual port name by ID from port list const port = filteredPorts.find((p) => p.id === portId);
return portId ? `Cảng (ID: ${portId})` : t("diary.selectPort"); return port?.name || t("diary.selectPort");
}; };
const currentSelectedId = activeDropdown === "departure" ? departurePortId : arrivalPortId;
const themedStyles = { const themedStyles = {
label: { color: colors.text }, label: { color: colors.text },
portSelector: { portSelector: {
@@ -56,6 +109,15 @@ export default function PortSelector({
}, },
portText: { color: colors.text }, portText: { color: colors.text },
placeholder: { color: colors.textSecondary }, placeholder: { color: colors.textSecondary },
modalOverlay: { backgroundColor: "rgba(0, 0, 0, 0.5)" },
modalContent: { backgroundColor: colors.card },
searchInput: {
backgroundColor: colors.backgroundSecondary,
color: colors.text,
borderColor: colors.border,
},
option: { borderBottomColor: colors.separator },
optionText: { color: colors.text },
}; };
return ( return (
@@ -70,26 +132,32 @@ export default function PortSelector({
{t("diary.departurePort")} {t("diary.departurePort")}
</Text> </Text>
<TouchableOpacity <TouchableOpacity
style={[styles.portSelector, themedStyles.portSelector]} style={[styles.dropdown, themedStyles.portSelector]}
onPress={disabled ? undefined : handleSelectDeparturePort} onPress={disabled ? undefined : () => openDropdown("departure")}
activeOpacity={disabled ? 1 : 0.7} activeOpacity={disabled ? 1 : 0.7}
disabled={disabled} disabled={disabled}
> >
<Text {isLoading ? (
style={[ <ActivityIndicator size="small" color={colors.primary} />
styles.portText, ) : (
themedStyles.portText, <>
!departurePortId && themedStyles.placeholder, <Text
]} style={[
> styles.dropdownText,
{getPortDisplayName(departurePortId)} themedStyles.portText,
</Text> !departurePortId && themedStyles.placeholder,
{!disabled && ( ]}
<Ionicons >
name="ellipsis-horizontal" {getPortDisplayName(departurePortId)}
size={20} </Text>
color={colors.textSecondary} {!disabled && (
/> <Ionicons
name="chevron-down"
size={16}
color={colors.textSecondary}
/>
)}
</>
)} )}
</TouchableOpacity> </TouchableOpacity>
</View> </View>
@@ -100,30 +168,99 @@ export default function PortSelector({
{t("diary.arrivalPort")} {t("diary.arrivalPort")}
</Text> </Text>
<TouchableOpacity <TouchableOpacity
style={[styles.portSelector, themedStyles.portSelector]} style={[styles.dropdown, themedStyles.portSelector]}
onPress={disabled ? undefined : handleSelectArrivalPort} onPress={disabled ? undefined : () => openDropdown("arrival")}
activeOpacity={disabled ? 1 : 0.7} activeOpacity={disabled ? 1 : 0.7}
disabled={disabled} disabled={disabled}
> >
<Text {isLoading ? (
style={[ <ActivityIndicator size="small" color={colors.primary} />
styles.portText, ) : (
themedStyles.portText, <>
!arrivalPortId && themedStyles.placeholder, <Text
]} style={[
> styles.dropdownText,
{getPortDisplayName(arrivalPortId)} themedStyles.portText,
</Text> !arrivalPortId && themedStyles.placeholder,
{!disabled && ( ]}
<Ionicons >
name="ellipsis-horizontal" {getPortDisplayName(arrivalPortId)}
size={20} </Text>
color={colors.textSecondary} {!disabled && (
/> <Ionicons
name="chevron-down"
size={16}
color={colors.textSecondary}
/>
)}
</>
)} )}
</TouchableOpacity> </TouchableOpacity>
</View> </View>
</View> </View>
{/* Port Dropdown Modal - Fade style like MaterialCostList */}
<Modal
visible={activeDropdown !== null}
transparent
animationType="fade"
onRequestClose={closeDropdown}
>
<TouchableOpacity
style={[styles.modalOverlay, themedStyles.modalOverlay]}
activeOpacity={1}
onPress={closeDropdown}
>
<View style={[styles.modalContent, themedStyles.modalContent]}>
{/* Search Input */}
<View style={styles.searchContainer}>
<Ionicons
name="search"
size={18}
color={colors.textSecondary}
style={styles.searchIcon}
/>
<TextInput
style={[styles.searchInput, themedStyles.searchInput]}
placeholder={t("diary.searchPort")}
placeholderTextColor={colors.textSecondary}
value={searchText}
onChangeText={setSearchText}
/>
</View>
{/* Port List */}
<ScrollView style={styles.optionsList}>
{isLoading ? (
<View style={styles.loadingContainer}>
<ActivityIndicator size="small" color={colors.primary} />
</View>
) : searchFilteredPorts.length === 0 ? (
<View style={styles.emptyContainer}>
<Text style={[styles.emptyText, { color: colors.textSecondary }]}>
{t("diary.noPortsFound")}
</Text>
</View>
) : (
searchFilteredPorts.map((port) => (
<TouchableOpacity
key={port.id}
style={[styles.option, themedStyles.option]}
onPress={() => handleSelectPort(port.id || 0)}
>
<Text style={[styles.optionText, themedStyles.optionText]}>
{port.name}
</Text>
{currentSelectedId === port.id && (
<Ionicons name="checkmark" size={20} color={colors.primary} />
)}
</TouchableOpacity>
))
)}
</ScrollView>
</View>
</TouchableOpacity>
</Modal>
</View> </View>
); );
} }
@@ -152,22 +289,23 @@ const styles = StyleSheet.create({
}), }),
}, },
portContainer: { portContainer: {
flexDirection: "row", flexDirection: "column",
gap: 12, gap: 12,
}, },
portSection: { portSection: {
flex: 1, flex: 1,
}, },
portSelector: { dropdown: {
flexDirection: "row", flexDirection: "row",
justifyContent: "space-between", justifyContent: "space-between",
alignItems: "center", alignItems: "center",
borderWidth: 1, borderWidth: 1,
borderRadius: 8, borderRadius: 8,
paddingHorizontal: 12, paddingHorizontal: 12,
paddingVertical: 12, paddingVertical: 10,
minHeight: 44,
}, },
portText: { dropdownText: {
fontSize: 15, fontSize: 15,
flex: 1, flex: 1,
fontFamily: Platform.select({ fontFamily: Platform.select({
@@ -176,4 +314,77 @@ const styles = StyleSheet.create({
default: "System", default: "System",
}), }),
}, },
// Modal styles - same as MaterialCostList
modalOverlay: {
flex: 1,
justifyContent: "center",
alignItems: "center",
},
modalContent: {
borderRadius: 12,
width: "85%",
maxHeight: "60%",
overflow: "hidden",
shadowColor: "#000",
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.3,
shadowRadius: 8,
elevation: 8,
},
searchContainer: {
flexDirection: "row",
alignItems: "center",
paddingHorizontal: 12,
paddingVertical: 10,
borderBottomWidth: 1,
borderBottomColor: "#E5E5E7",
},
searchIcon: {
marginRight: 8,
},
searchInput: {
flex: 1,
fontSize: 15,
paddingVertical: 6,
fontFamily: Platform.select({
ios: "System",
android: "Roboto",
default: "System",
}),
},
optionsList: {
maxHeight: 300,
},
option: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
paddingHorizontal: 20,
paddingVertical: 16,
borderBottomWidth: 1,
},
optionText: {
fontSize: 16,
fontFamily: Platform.select({
ios: "System",
android: "Roboto",
default: "System",
}),
},
loadingContainer: {
padding: 20,
alignItems: "center",
},
emptyContainer: {
padding: 20,
alignItems: "center",
},
emptyText: {
fontSize: 14,
fontFamily: Platform.select({
ios: "System",
android: "Roboto",
default: "System",
}),
},
}); });

View File

@@ -23,6 +23,7 @@ import PortSelector from "./PortSelector";
import BasicInfoInput from "./BasicInfoInput"; import BasicInfoInput from "./BasicInfoInput";
import ShipSelector from "./ShipSelector"; import ShipSelector from "./ShipSelector";
import AutoFillSection from "./AutoFillSection"; import AutoFillSection from "./AutoFillSection";
import FormSection from "./FormSection";
import { createTrip, updateTrip } from "@/controller/TripController"; import { createTrip, updateTrip } from "@/controller/TripController";
import { import {
convertFishingGears, convertFishingGears,
@@ -80,15 +81,31 @@ export default function TripFormModal({
).current; ).current;
// Form state // Form state
const [selectedShipId, setSelectedShipId] = useState(DEFAULT_FORM_STATE.selectedShipId); const [selectedShipId, setSelectedShipId] = useState(
DEFAULT_FORM_STATE.selectedShipId
);
const [tripName, setTripName] = useState(DEFAULT_FORM_STATE.tripName); const [tripName, setTripName] = useState(DEFAULT_FORM_STATE.tripName);
const [fishingGears, setFishingGears] = useState<FishingGear[]>(DEFAULT_FORM_STATE.fishingGears); const [fishingGears, setFishingGears] = useState<FishingGear[]>(
const [tripCosts, setTripCosts] = useState<TripCost[]>(DEFAULT_FORM_STATE.tripCosts); DEFAULT_FORM_STATE.fishingGears
const [startDate, setStartDate] = useState<Date | null>(DEFAULT_FORM_STATE.startDate); );
const [endDate, setEndDate] = useState<Date | null>(DEFAULT_FORM_STATE.endDate); const [tripCosts, setTripCosts] = useState<TripCost[]>(
const [departurePortId, setDeparturePortId] = useState(DEFAULT_FORM_STATE.departurePortId); DEFAULT_FORM_STATE.tripCosts
const [arrivalPortId, setArrivalPortId] = useState(DEFAULT_FORM_STATE.arrivalPortId); );
const [fishingGroundCodes, setFishingGroundCodes] = useState(DEFAULT_FORM_STATE.fishingGroundCodes); 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); const [isSubmitting, setIsSubmitting] = useState(false);
// Reset form to default state // Reset form to default state
@@ -105,17 +122,22 @@ export default function TripFormModal({
}, []); }, []);
// Fill form with trip data // Fill form with trip data
const fillFormWithTripData = useCallback((data: Model.Trip, prefix: string) => { const fillFormWithTripData = useCallback(
setSelectedShipId(data.vms_id || ""); (data: Model.Trip, prefix: string) => {
setTripName(data.name || ""); setSelectedShipId(data.vms_id || "");
setFishingGears(convertFishingGears(data.fishing_gears, prefix)); setTripName(data.name || "");
setTripCosts(convertTripCosts(data.trip_cost, prefix)); setFishingGears(convertFishingGears(data.fishing_gears, prefix));
if (data.departure_time) setStartDate(new Date(data.departure_time)); setTripCosts(convertTripCosts(data.trip_cost, prefix));
if (data.arrival_time) setEndDate(new Date(data.arrival_time)); if (data.departure_time) setStartDate(new Date(data.departure_time));
if (data.departure_port_id) setDeparturePortId(data.departure_port_id); if (data.arrival_time) setEndDate(new Date(data.arrival_time));
if (data.arrival_port_id) setArrivalPortId(data.arrival_port_id); if (data.departure_port_id) setDeparturePortId(data.departure_port_id);
setFishingGroundCodes(convertFishingGroundCodes(data.fishing_ground_codes)); if (data.arrival_port_id) setArrivalPortId(data.arrival_port_id);
}, []); setFishingGroundCodes(
convertFishingGroundCodes(data.fishing_ground_codes)
);
},
[]
);
// Handle animation when modal visibility changes // Handle animation when modal visibility changes
useEffect(() => { useEffect(() => {
@@ -185,9 +207,12 @@ export default function TripFormModal({
setSelectedShipId(selectedThingId); setSelectedShipId(selectedThingId);
setFishingGears(convertFishingGears(tripData.fishing_gears, "auto")); setFishingGears(convertFishingGears(tripData.fishing_gears, "auto"));
setTripCosts(convertTripCosts(tripData.trip_cost, "auto")); setTripCosts(convertTripCosts(tripData.trip_cost, "auto"));
if (tripData.departure_port_id) setDeparturePortId(tripData.departure_port_id); if (tripData.departure_port_id)
setDeparturePortId(tripData.departure_port_id);
if (tripData.arrival_port_id) setArrivalPortId(tripData.arrival_port_id); if (tripData.arrival_port_id) setArrivalPortId(tripData.arrival_port_id);
setFishingGroundCodes(convertFishingGroundCodes(tripData.fishing_ground_codes)); setFishingGroundCodes(
convertFishingGroundCodes(tripData.fishing_ground_codes)
);
}, },
[] []
); );
@@ -323,47 +348,60 @@ export default function TripFormModal({
</View> </View>
{/* Content */} {/* Content */}
<ScrollView style={styles.content} showsVerticalScrollIndicator={false}> <ScrollView
style={styles.content}
showsVerticalScrollIndicator={false}
>
{/* Auto Fill Section - only show in add mode */} {/* Auto Fill Section - only show in add mode */}
{!isEditMode && <AutoFillSection onAutoFill={handleAutoFill} />} {!isEditMode && <AutoFillSection onAutoFill={handleAutoFill} />}
{/* Ship Selector - disabled in edit mode */} {/* Section 1: Basic Information */}
<ShipSelector <FormSection title={t("diary.formSection.basicInfo")} icon="boat-outline">
selectedShipId={selectedShipId} {/* Ship Selector - disabled in edit mode */}
onChange={setSelectedShipId} <ShipSelector
disabled={isEditMode} selectedShipId={selectedShipId}
/> onChange={setSelectedShipId}
disabled={isEditMode}
/>
{/* Trip Name */} {/* Trip Name */}
<TripNameInput value={tripName} onChange={setTripName} /> <TripNameInput value={tripName} onChange={setTripName} />
</FormSection>
{/* Fishing Gear List */} {/* Section 2: Schedule & Location */}
<FishingGearList items={fishingGears} onChange={setFishingGears} /> <FormSection title={t("diary.formSection.schedule")} icon="calendar-outline">
{/* Trip Duration */}
<TripDurationPicker
startDate={startDate}
endDate={endDate}
onStartDateChange={setStartDate}
onEndDateChange={setEndDate}
/>
{/* Trip Cost List */} {/* Port Selector */}
<MaterialCostList items={tripCosts} onChange={setTripCosts} /> <PortSelector
departurePortId={departurePortId}
arrivalPortId={arrivalPortId}
onDeparturePortChange={setDeparturePortId}
onArrivalPortChange={setArrivalPortId}
/>
{/* Trip Duration */} {/* Fishing Ground Codes */}
<TripDurationPicker <BasicInfoInput
startDate={startDate} fishingGroundCodes={fishingGroundCodes}
endDate={endDate} onChange={setFishingGroundCodes}
onStartDateChange={setStartDate} />
onEndDateChange={setEndDate} </FormSection>
/>
{/* Port Selector */} {/* Section 3: Equipment */}
<PortSelector <FormSection title={t("diary.formSection.equipment")} icon="construct-outline">
departurePortId={departurePortId} <FishingGearList items={fishingGears} onChange={setFishingGears} hideTitle />
arrivalPortId={arrivalPortId} </FormSection>
onDeparturePortChange={setDeparturePortId}
onArrivalPortChange={setArrivalPortId}
/>
{/* Fishing Ground Codes */} {/* Section 4: Costs */}
<BasicInfoInput <FormSection title={t("diary.formSection.costs")} icon="wallet-outline">
fishingGroundCodes={fishingGroundCodes} <MaterialCostList items={tripCosts} onChange={setTripCosts} hideTitle />
onChange={setFishingGroundCodes} </FormSection>
/>
</ScrollView> </ScrollView>
{/* Footer */} {/* Footer */}
@@ -373,7 +411,9 @@ export default function TripFormModal({
onPress={handleCancel} onPress={handleCancel}
activeOpacity={0.7} activeOpacity={0.7}
> >
<Text style={[styles.cancelButtonText, themedStyles.cancelButtonText]}> <Text
style={[styles.cancelButtonText, themedStyles.cancelButtonText]}
>
{t("common.cancel")} {t("common.cancel")}
</Text> </Text>
</TouchableOpacity> </TouchableOpacity>

View File

@@ -150,13 +150,21 @@
"selectDate": "Chọn ngày", "selectDate": "Chọn ngày",
"selectStartDate": "Chọn ngày bắt đầu", "selectStartDate": "Chọn ngày bắt đầu",
"selectEndDate": "Chọn ngày kết thúc", "selectEndDate": "Chọn ngày kết thúc",
"portLabel": " Cả", "portLabel": " Cảng",
"departurePort": "Cảng khởi hành", "departurePort": "Cảng khởi hành",
"arrivalPort": "Cảng cập bến", "arrivalPort": "Cảng cập bến",
"selectPort": "Chọn cảng", "selectPort": "Chọn cảng",
"searchPort": "Tìm kiếm cảng...",
"noPortsFound": "Không tìm thấy cảng phù hợp",
"fishingGroundCodes": "Ô ngư trường khai thác", "fishingGroundCodes": "Ô ngư trường khai thác",
"fishingGroundCodesHint": "Nhập mã ô ngư trường (cách nhau bằng dấu phẩy)", "fishingGroundCodesHint": "Nhập mã ô ngư trường (cách nhau bằng dấu phẩy)",
"fishingGroundCodesPlaceholder": "Ví dụ: 1,2,3", "fishingGroundCodesPlaceholder": "Ví dụ: 1,2,3",
"formSection": {
"basicInfo": "Thông tin cơ bản",
"schedule": "Lịch trình & Vị trí",
"equipment": "Ngư cụ",
"costs": "Chi phí chuyến đi"
},
"autoFill": { "autoFill": {
"title": "Tự động điền dữ liệu", "title": "Tự động điền dữ liệu",
"description": "Điền từ chuyến đi cuối cùng của tàu", "description": "Điền từ chuyến đi cuối cùng của tàu",

View File

@@ -52,3 +52,43 @@ export function parseFishingGroundCodes(codesString: string): number[] {
.map((code) => parseInt(code.trim())) .map((code) => parseInt(code.trim()))
.filter((code) => !isNaN(code)); .filter((code) => !isNaN(code));
} }
/**
* Extract province codes from groups
* @param groups - Model.GroupResponse containing groups with metadata.code
* @returns Array of province codes
*/
export function getProvinceCodesFromGroups(
groups: Model.GroupResponse | null | undefined
): string[] {
if (!groups?.groups) return [];
return groups.groups
.map((group) => group.metadata?.code)
.filter((code): code is string => !!code);
}
/**
* Filter ports by province codes extracted from groups
* @param ports - Model.PortResponse containing all ports
* @param groups - Model.GroupResponse to extract province_code from metadata.code
* @returns Filtered ports that match the province codes from groups
*/
export function filterPortsByProvinceCode(
ports: Model.PortResponse | null | undefined,
groups: Model.GroupResponse | null | undefined
): Model.Port[] {
if (!ports?.ports) return [];
if (!groups?.groups) return ports.ports; // Return all ports if no groups
// Extract province codes from groups
const provinceCodes = getProvinceCodesFromGroups(groups);
// If no province codes found, return all ports
if (provinceCodes.length === 0) return ports.ports;
// Filter ports by province codes
return ports.ports.filter(
(port) => port.province_code && provinceCodes.includes(port.province_code)
);
}