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:
@@ -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}
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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) => (
|
||||||
|
|||||||
57
components/diary/TripFormModal/FormSection.tsx
Normal file
57
components/diary/TripFormModal/FormSection.tsx
Normal 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,
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -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) => (
|
||||||
|
|||||||
@@ -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",
|
||||||
|
}),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user