update tab nhật ký (Lọc, Ngôn ngữ,...)
This commit is contained in:
@@ -12,7 +12,7 @@ export default function TabLayout() {
|
|||||||
const segments = useSegments() as string[];
|
const segments = useSegments() as string[];
|
||||||
const prev = useRef<string | null>(null);
|
const prev = useRef<string | null>(null);
|
||||||
const currentSegment = segments[1] ?? segments[segments.length - 1] ?? null;
|
const currentSegment = segments[1] ?? segments[segments.length - 1] ?? null;
|
||||||
const { t, locale } = useI18n();
|
const { t } = useI18n();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (prev.current !== currentSegment) {
|
if (prev.current !== currentSegment) {
|
||||||
// console.log("Tab changed ->", { from: prev.current, to: currentSegment });
|
// console.log("Tab changed ->", { from: prev.current, to: currentSegment });
|
||||||
@@ -60,7 +60,11 @@ export default function TabLayout() {
|
|||||||
options={{
|
options={{
|
||||||
title: t("navigation.manager"),
|
title: t("navigation.manager"),
|
||||||
tabBarIcon: ({ color }) => (
|
tabBarIcon: ({ color }) => (
|
||||||
<IconSymbol size={28} name="square.stack.3d.up.fill" color={color} />
|
<IconSymbol
|
||||||
|
size={28}
|
||||||
|
name="square.stack.3d.up.fill"
|
||||||
|
color={color}
|
||||||
|
/>
|
||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -9,22 +9,25 @@ import {
|
|||||||
} from "react-native";
|
} from "react-native";
|
||||||
import { SafeAreaView } from "react-native-safe-area-context";
|
import { SafeAreaView } from "react-native-safe-area-context";
|
||||||
import { Ionicons } from "@expo/vector-icons";
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
import SearchBar from "@/components/diary/SearchBar";
|
|
||||||
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 { MOCK_TRIPS } from "@/components/diary/mockData";
|
|
||||||
import { useThings } from "@/state/use-thing";
|
import { useThings } from "@/state/use-thing";
|
||||||
|
import { useTripsList } from "@/state/use-tripslist";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import { useI18n } from "@/hooks/use-i18n";
|
||||||
|
|
||||||
export default function diary() {
|
export default function diary() {
|
||||||
const [searchText, setSearchText] = useState("");
|
const { t } = useI18n();
|
||||||
const [showFilterModal, setShowFilterModal] = useState(false);
|
const [showFilterModal, setShowFilterModal] = useState(false);
|
||||||
const [filters, setFilters] = useState<FilterValues>({
|
const [filters, setFilters] = useState<FilterValues>({
|
||||||
status: null,
|
status: null,
|
||||||
startDate: null,
|
startDate: null,
|
||||||
endDate: null,
|
endDate: null,
|
||||||
|
selectedShip: null, // Tàu được chọn
|
||||||
});
|
});
|
||||||
// Body things (đang fix cứng)
|
|
||||||
|
// Body call API things (đang fix cứng)
|
||||||
const payloadThings: Model.SearchThingBody = {
|
const payloadThings: Model.SearchThingBody = {
|
||||||
offset: 0,
|
offset: 0,
|
||||||
limit: 200,
|
limit: 200,
|
||||||
@@ -34,56 +37,44 @@ export default function diary() {
|
|||||||
not_empty: "ship_name, ship_reg_number",
|
not_empty: "ship_name, ship_reg_number",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Gọi API things
|
// Gọi API things
|
||||||
const { things, getThings } = useThings();
|
const { getThings } = useThings();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getThings(payloadThings);
|
getThings(payloadThings);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
console.log(things);
|
// State cho payload trips
|
||||||
|
const [payloadTrips, setPayloadTrips] = useState<Model.TripListBody>({
|
||||||
// Filter trips based on search text and filters
|
name: "",
|
||||||
const filteredTrips = MOCK_TRIPS.filter((trip) => {
|
order: "",
|
||||||
// Search filter
|
dir: "desc",
|
||||||
if (searchText) {
|
offset: 0,
|
||||||
const searchLower = searchText.toLowerCase();
|
limit: 10,
|
||||||
const matchesSearch =
|
metadata: {
|
||||||
trip.title.toLowerCase().includes(searchLower) ||
|
from: "",
|
||||||
trip.code.toLowerCase().includes(searchLower) ||
|
to: "",
|
||||||
trip.vessel.toLowerCase().includes(searchLower) ||
|
ship_name: "",
|
||||||
trip.vesselCode.toLowerCase().includes(searchLower);
|
reg_number: "",
|
||||||
|
province_code: "",
|
||||||
if (!matchesSearch) return false;
|
owner_id: "",
|
||||||
}
|
ship_id: "",
|
||||||
|
status: "",
|
||||||
// Status filter
|
},
|
||||||
if (filters.status && trip.status !== filters.status) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Date range filter
|
|
||||||
if (filters.startDate || filters.endDate) {
|
|
||||||
const tripDate = new Date(trip.departureDate);
|
|
||||||
|
|
||||||
if (filters.startDate && tripDate < filters.startDate) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filters.endDate) {
|
|
||||||
const endOfDay = new Date(filters.endDate);
|
|
||||||
endOfDay.setHours(23, 59, 59, 999);
|
|
||||||
if (tripDate > endOfDay) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleSearch = (text: string) => {
|
const { tripsList, getTripsList } = useTripsList();
|
||||||
setSearchText(text);
|
|
||||||
};
|
// Gọi API trips lần đầu
|
||||||
|
useEffect(() => {
|
||||||
|
getTripsList(payloadTrips);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Gọi lại API khi payload thay đổi (do filter)
|
||||||
|
useEffect(() => {
|
||||||
|
getTripsList(payloadTrips);
|
||||||
|
console.log("Payload trips:", payloadTrips);
|
||||||
|
}, [payloadTrips]);
|
||||||
|
|
||||||
const handleFilter = () => {
|
const handleFilter = () => {
|
||||||
setShowFilterModal(true);
|
setShowFilterModal(true);
|
||||||
@@ -91,6 +82,28 @@ export default function diary() {
|
|||||||
|
|
||||||
const handleApplyFilters = (newFilters: FilterValues) => {
|
const handleApplyFilters = (newFilters: FilterValues) => {
|
||||||
setFilters(newFilters);
|
setFilters(newFilters);
|
||||||
|
|
||||||
|
// Cập nhật payload với filter mới
|
||||||
|
// Lưu ý: status gửi lên server là string
|
||||||
|
const updatedPayload: Model.TripListBody = {
|
||||||
|
...payloadTrips,
|
||||||
|
metadata: {
|
||||||
|
...payloadTrips.metadata,
|
||||||
|
from: newFilters.startDate
|
||||||
|
? dayjs(newFilters.startDate).startOf("day").toISOString()
|
||||||
|
: "",
|
||||||
|
to: newFilters.endDate
|
||||||
|
? dayjs(newFilters.endDate).endOf("day").toISOString()
|
||||||
|
: "",
|
||||||
|
// Convert number status sang string để gửi lên server
|
||||||
|
status: newFilters.status !== null ? String(newFilters.status) : "",
|
||||||
|
// Thêm ship_id từ tàu đã chọn
|
||||||
|
ship_name: newFilters.selectedShip?.shipName || "",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
setPayloadTrips(updatedPayload);
|
||||||
|
setShowFilterModal(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTripPress = (tripId: string) => {
|
const handleTripPress = (tripId: string) => {
|
||||||
@@ -124,39 +137,44 @@ export default function diary() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={styles.safeArea}>
|
<SafeAreaView style={styles.safeArea} edges={["top"]}>
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<Text style={styles.titleText}>Nhật ký chuyến đi</Text>
|
<Text style={styles.titleText}>{t("diary.title")}</Text>
|
||||||
|
|
||||||
{/* Search Bar */}
|
{/* Filter & Add Button Row */}
|
||||||
<SearchBar onSearch={handleSearch} style={{ marginBottom: 10 }} />
|
<View style={styles.actionRow}>
|
||||||
|
<FilterButton
|
||||||
{/* Filter Button */}
|
onPress={handleFilter}
|
||||||
<FilterButton onPress={handleFilter} />
|
isFiltered={
|
||||||
|
filters.status !== null ||
|
||||||
{/* Trip Count & Add Button */}
|
filters.startDate !== null ||
|
||||||
<View style={styles.headerRow}>
|
filters.endDate !== null ||
|
||||||
<Text style={styles.countText}>
|
filters.selectedShip !== null
|
||||||
Danh sách chuyến đi ({filteredTrips.length})
|
}
|
||||||
</Text>
|
/>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.addButton}
|
style={styles.addButton}
|
||||||
onPress={() => console.log("Add trip")}
|
onPress={() => console.log("Add trip")}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
>
|
>
|
||||||
<Ionicons name="add" size={20} color="#FFFFFF" />
|
<Ionicons name="add" size={20} color="#FFFFFF" />
|
||||||
<Text style={styles.addButtonText}>Thêm chuyến đi</Text>
|
<Text style={styles.addButtonText}>{t("diary.addTrip")}</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
{/* Trip Count */}
|
||||||
|
<Text style={styles.countText}>
|
||||||
|
{t("diary.tripListCount", { count: tripsList?.total || 0 })}
|
||||||
|
</Text>
|
||||||
|
|
||||||
{/* Trip List */}
|
{/* Trip List */}
|
||||||
<ScrollView
|
<ScrollView
|
||||||
style={styles.scrollView}
|
style={styles.scrollView}
|
||||||
contentContainerStyle={styles.scrollContent}
|
contentContainerStyle={styles.scrollContent}
|
||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
>
|
>
|
||||||
{filteredTrips.map((trip) => (
|
{tripsList?.trips?.map((trip) => (
|
||||||
<TripCard
|
<TripCard
|
||||||
key={trip.id}
|
key={trip.id}
|
||||||
trip={trip}
|
trip={trip}
|
||||||
@@ -169,10 +187,10 @@ export default function diary() {
|
|||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{filteredTrips.length === 0 && (
|
{(!tripsList || !tripsList.trips || tripsList.trips.length === 0) && (
|
||||||
<View style={styles.emptyState}>
|
<View style={styles.emptyState}>
|
||||||
<Text style={styles.emptyText}>
|
<Text style={styles.emptyText}>
|
||||||
Không tìm thấy chuyến đi phù hợp
|
{t("diary.noTripsFound")}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
@@ -210,6 +228,13 @@ const styles = StyleSheet.create({
|
|||||||
default: "System",
|
default: "System",
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
actionRow: {
|
||||||
|
flexDirection: "row",
|
||||||
|
justifyContent: "flex-start",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 12,
|
||||||
|
marginBottom: 12,
|
||||||
|
},
|
||||||
headerRow: {
|
headerRow: {
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
justifyContent: "space-between",
|
justifyContent: "space-between",
|
||||||
@@ -226,6 +251,7 @@ const styles = StyleSheet.create({
|
|||||||
android: "Roboto",
|
android: "Roboto",
|
||||||
default: "System",
|
default: "System",
|
||||||
}),
|
}),
|
||||||
|
marginBottom: 10,
|
||||||
},
|
},
|
||||||
addButton: {
|
addButton: {
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
} from "react-native";
|
} from "react-native";
|
||||||
import { Ionicons } from "@expo/vector-icons";
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
import DateTimePicker from "@react-native-community/datetimepicker";
|
import DateTimePicker from "@react-native-community/datetimepicker";
|
||||||
|
import { useI18n } from "@/hooks/use-i18n";
|
||||||
|
|
||||||
interface DateRangePickerProps {
|
interface DateRangePickerProps {
|
||||||
startDate: Date | null;
|
startDate: Date | null;
|
||||||
@@ -23,6 +24,7 @@ export default function DateRangePicker({
|
|||||||
onStartDateChange,
|
onStartDateChange,
|
||||||
onEndDateChange,
|
onEndDateChange,
|
||||||
}: DateRangePickerProps) {
|
}: DateRangePickerProps) {
|
||||||
|
const { t } = useI18n();
|
||||||
const [showStartPicker, setShowStartPicker] = useState(false);
|
const [showStartPicker, setShowStartPicker] = useState(false);
|
||||||
const [showEndPicker, setShowEndPicker] = useState(false);
|
const [showEndPicker, setShowEndPicker] = useState(false);
|
||||||
|
|
||||||
@@ -50,7 +52,7 @@ export default function DateRangePicker({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<Text style={styles.label}>Ngày đi</Text>
|
<Text style={styles.label}>{t("diary.dateRangePicker.label")}</Text>
|
||||||
<View style={styles.dateRangeContainer}>
|
<View style={styles.dateRangeContainer}>
|
||||||
{/* Start Date */}
|
{/* Start Date */}
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
@@ -59,7 +61,7 @@ export default function DateRangePicker({
|
|||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
>
|
>
|
||||||
<Text style={[styles.dateText, !startDate && styles.placeholder]}>
|
<Text style={[styles.dateText, !startDate && styles.placeholder]}>
|
||||||
{startDate ? formatDate(startDate) : "Ngày bắt đầu"}
|
{startDate ? formatDate(startDate) : t("diary.dateRangePicker.startDate")}
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
@@ -77,7 +79,7 @@ export default function DateRangePicker({
|
|||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
>
|
>
|
||||||
<Text style={[styles.dateText, !endDate && styles.placeholder]}>
|
<Text style={[styles.dateText, !endDate && styles.placeholder]}>
|
||||||
{endDate ? formatDate(endDate) : "Ngày kết thúc"}
|
{endDate ? formatDate(endDate) : t("diary.dateRangePicker.endDate")}
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
@@ -96,11 +98,11 @@ export default function DateRangePicker({
|
|||||||
<View style={styles.pickerContainer}>
|
<View style={styles.pickerContainer}>
|
||||||
<View style={styles.pickerHeader}>
|
<View style={styles.pickerHeader}>
|
||||||
<TouchableOpacity onPress={() => setShowStartPicker(false)}>
|
<TouchableOpacity onPress={() => setShowStartPicker(false)}>
|
||||||
<Text style={styles.cancelButton}>Hủy</Text>
|
<Text style={styles.cancelButton}>{t("common.cancel")}</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<Text style={styles.pickerTitle}>Chọn ngày bắt đầu</Text>
|
<Text style={styles.pickerTitle}>{t("diary.dateRangePicker.selectStartDate")}</Text>
|
||||||
<TouchableOpacity onPress={() => setShowStartPicker(false)}>
|
<TouchableOpacity onPress={() => setShowStartPicker(false)}>
|
||||||
<Text style={styles.doneButton}>Xong</Text>
|
<Text style={styles.doneButton}>{t("diary.dateRangePicker.done")}</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
<DateTimePicker
|
<DateTimePicker
|
||||||
@@ -122,11 +124,11 @@ export default function DateRangePicker({
|
|||||||
<View style={styles.pickerContainer}>
|
<View style={styles.pickerContainer}>
|
||||||
<View style={styles.pickerHeader}>
|
<View style={styles.pickerHeader}>
|
||||||
<TouchableOpacity onPress={() => setShowEndPicker(false)}>
|
<TouchableOpacity onPress={() => setShowEndPicker(false)}>
|
||||||
<Text style={styles.cancelButton}>Hủy</Text>
|
<Text style={styles.cancelButton}>{t("common.cancel")}</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<Text style={styles.pickerTitle}>Chọn ngày kết thúc</Text>
|
<Text style={styles.pickerTitle}>{t("diary.dateRangePicker.selectEndDate")}</Text>
|
||||||
<TouchableOpacity onPress={() => setShowEndPicker(false)}>
|
<TouchableOpacity onPress={() => setShowEndPicker(false)}>
|
||||||
<Text style={styles.doneButton}>Xong</Text>
|
<Text style={styles.doneButton}>{t("diary.dateRangePicker.done")}</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
<DateTimePicker
|
<DateTimePicker
|
||||||
|
|||||||
@@ -1,20 +1,41 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { TouchableOpacity, Text, StyleSheet, Platform } from "react-native";
|
import { TouchableOpacity, Text, StyleSheet, Platform } from "react-native";
|
||||||
import { Ionicons } from "@expo/vector-icons";
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
|
import { useI18n } from "@/hooks/use-i18n";
|
||||||
|
|
||||||
interface FilterButtonProps {
|
interface FilterButtonProps {
|
||||||
onPress?: () => void;
|
onPress?: () => void;
|
||||||
|
isFiltered?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function FilterButton({ onPress }: FilterButtonProps) {
|
export default function FilterButton({
|
||||||
|
onPress,
|
||||||
|
isFiltered,
|
||||||
|
}: FilterButtonProps) {
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.button}
|
style={styles.button}
|
||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
>
|
>
|
||||||
<Ionicons name="filter" size={20} color="#374151" />
|
<Ionicons
|
||||||
<Text style={styles.text}>Bộ lọc</Text>
|
name="filter"
|
||||||
|
size={20}
|
||||||
|
color={isFiltered ? "#3B82F6" : "#374151"}
|
||||||
|
/>
|
||||||
|
<Text style={[styles.text, isFiltered && { color: "#3B82F6" }]}>
|
||||||
|
{t("diary.filter")}
|
||||||
|
</Text>
|
||||||
|
{isFiltered && (
|
||||||
|
<Ionicons
|
||||||
|
name="ellipse"
|
||||||
|
size={10}
|
||||||
|
color="#3B82F6"
|
||||||
|
style={{ marginLeft: 4 }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,33 @@ import {
|
|||||||
import { Ionicons } from "@expo/vector-icons";
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
import StatusDropdown from "./StatusDropdown";
|
import StatusDropdown from "./StatusDropdown";
|
||||||
import DateRangePicker from "./DateRangePicker";
|
import DateRangePicker from "./DateRangePicker";
|
||||||
|
import ShipDropdown from "./ShipDropdown";
|
||||||
import { TripStatus } from "./types";
|
import { TripStatus } from "./types";
|
||||||
|
import { useI18n } from "@/hooks/use-i18n";
|
||||||
|
|
||||||
|
// Map status number to string - now uses i18n
|
||||||
|
export function useMapStatusNumberToString() {
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
return (status: TripStatus | null): string => {
|
||||||
|
switch (status) {
|
||||||
|
case 0:
|
||||||
|
return t("diary.tripStatus.created");
|
||||||
|
case 1:
|
||||||
|
return t("diary.tripStatus.pending");
|
||||||
|
case 2:
|
||||||
|
return t("diary.tripStatus.approved");
|
||||||
|
case 3:
|
||||||
|
return t("diary.tripStatus.departed");
|
||||||
|
case 4:
|
||||||
|
return t("diary.tripStatus.completed");
|
||||||
|
case 5:
|
||||||
|
return t("diary.tripStatus.cancelled");
|
||||||
|
default:
|
||||||
|
return "-";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
interface FilterModalProps {
|
interface FilterModalProps {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
@@ -19,10 +45,16 @@ interface FilterModalProps {
|
|||||||
onApply: (filters: FilterValues) => void;
|
onApply: (filters: FilterValues) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ShipOption {
|
||||||
|
id: string;
|
||||||
|
shipName: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface FilterValues {
|
export interface FilterValues {
|
||||||
status: TripStatus | null;
|
status: TripStatus | null; // number (0-5) hoặc null
|
||||||
startDate: Date | null;
|
startDate: Date | null;
|
||||||
endDate: Date | null;
|
endDate: Date | null;
|
||||||
|
selectedShip: ShipOption | null; // Tàu được chọn
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function FilterModal({
|
export default function FilterModal({
|
||||||
@@ -30,22 +62,30 @@ export default function FilterModal({
|
|||||||
onClose,
|
onClose,
|
||||||
onApply,
|
onApply,
|
||||||
}: FilterModalProps) {
|
}: FilterModalProps) {
|
||||||
|
const { t } = useI18n();
|
||||||
|
const mapStatusNumberToString = useMapStatusNumberToString();
|
||||||
const [status, setStatus] = useState<TripStatus | null>(null);
|
const [status, setStatus] = useState<TripStatus | null>(null);
|
||||||
const [startDate, setStartDate] = useState<Date | null>(null);
|
const [startDate, setStartDate] = useState<Date | null>(null);
|
||||||
const [endDate, setEndDate] = useState<Date | null>(null);
|
const [endDate, setEndDate] = useState<Date | null>(null);
|
||||||
|
const [selectedShip, setSelectedShip] = useState<ShipOption | null>(null);
|
||||||
|
|
||||||
const handleReset = () => {
|
const handleReset = () => {
|
||||||
setStatus(null);
|
setStatus(null);
|
||||||
setStartDate(null);
|
setStartDate(null);
|
||||||
setEndDate(null);
|
setEndDate(null);
|
||||||
|
setSelectedShip(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleApply = () => {
|
const handleApply = () => {
|
||||||
onApply({ status, startDate, endDate });
|
onApply({ status, startDate, endDate, selectedShip });
|
||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
const hasFilters = status !== null || startDate !== null || endDate !== null;
|
const hasFilters =
|
||||||
|
status !== null ||
|
||||||
|
startDate !== null ||
|
||||||
|
endDate !== null ||
|
||||||
|
selectedShip !== null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
@@ -54,12 +94,12 @@ export default function FilterModal({
|
|||||||
transparent
|
transparent
|
||||||
onRequestClose={onClose}
|
onRequestClose={onClose}
|
||||||
>
|
>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.overlay}
|
style={styles.overlay}
|
||||||
activeOpacity={1}
|
activeOpacity={1}
|
||||||
onPress={onClose}
|
onPress={onClose}
|
||||||
>
|
>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.modalContainer}
|
style={styles.modalContainer}
|
||||||
activeOpacity={1}
|
activeOpacity={1}
|
||||||
onPress={(e) => e.stopPropagation()}
|
onPress={(e) => e.stopPropagation()}
|
||||||
@@ -69,7 +109,7 @@ export default function FilterModal({
|
|||||||
<TouchableOpacity onPress={onClose} style={styles.closeButton}>
|
<TouchableOpacity onPress={onClose} style={styles.closeButton}>
|
||||||
<Ionicons name="close" size={24} color="#111827" />
|
<Ionicons name="close" size={24} color="#111827" />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<Text style={styles.title}>Bộ lọc</Text>
|
<Text style={styles.title}>{t("diary.filter")}</Text>
|
||||||
<View style={styles.placeholder} />
|
<View style={styles.placeholder} />
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
@@ -85,29 +125,37 @@ export default function FilterModal({
|
|||||||
onStartDateChange={setStartDate}
|
onStartDateChange={setStartDate}
|
||||||
onEndDateChange={setEndDate}
|
onEndDateChange={setEndDate}
|
||||||
/>
|
/>
|
||||||
|
<ShipDropdown value={selectedShip} onChange={setSelectedShip} />
|
||||||
|
|
||||||
{/* Filter Results Preview */}
|
{/* Filter Results Preview */}
|
||||||
{hasFilters && (
|
{hasFilters && (
|
||||||
<View style={styles.previewContainer}>
|
<View style={styles.previewContainer}>
|
||||||
<Text style={styles.previewTitle}>Bộ lọc đã chọn:</Text>
|
<Text style={styles.previewTitle}>{t("diary.selectedFilters")}</Text>
|
||||||
{status && (
|
{status !== null && (
|
||||||
<View style={styles.filterTag}>
|
<View style={styles.filterTag}>
|
||||||
<Text style={styles.filterTagText}>
|
<Text style={styles.filterTagText}>
|
||||||
Trạng thái: {status}
|
{t("diary.statusLabel")} {mapStatusNumberToString(status)}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
{startDate && (
|
{startDate && (
|
||||||
<View style={styles.filterTag}>
|
<View style={styles.filterTag}>
|
||||||
<Text style={styles.filterTagText}>
|
<Text style={styles.filterTagText}>
|
||||||
Từ: {startDate.toLocaleDateString("vi-VN")}
|
{t("diary.fromLabel")} {startDate.toLocaleDateString("vi-VN")}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
{endDate && (
|
{endDate && (
|
||||||
<View style={styles.filterTag}>
|
<View style={styles.filterTag}>
|
||||||
<Text style={styles.filterTagText}>
|
<Text style={styles.filterTagText}>
|
||||||
Đến: {endDate.toLocaleDateString("vi-VN")}
|
{t("diary.toLabel")} {endDate.toLocaleDateString("vi-VN")}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
{selectedShip && (
|
||||||
|
<View style={styles.filterTag}>
|
||||||
|
<Text style={styles.filterTagText}>
|
||||||
|
{t("diary.shipLabel")} {selectedShip.shipName}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
@@ -122,14 +170,14 @@ export default function FilterModal({
|
|||||||
onPress={handleReset}
|
onPress={handleReset}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
>
|
>
|
||||||
<Text style={styles.resetButtonText}>Đặt lại</Text>
|
<Text style={styles.resetButtonText}>{t("diary.reset")}</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.applyButton}
|
style={styles.applyButton}
|
||||||
onPress={handleApply}
|
onPress={handleApply}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
>
|
>
|
||||||
<Text style={styles.applyButtonText}>Áp dụng</Text>
|
<Text style={styles.applyButtonText}>{t("diary.apply")}</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|||||||
@@ -1,68 +0,0 @@
|
|||||||
import React, { useState } from "react";
|
|
||||||
import { View, TextInput, StyleSheet, Platform, StyleProp, ViewStyle } from "react-native";
|
|
||||||
import { Ionicons } from "@expo/vector-icons";
|
|
||||||
|
|
||||||
interface SearchBarProps {
|
|
||||||
onSearch?: (text: string) => void;
|
|
||||||
style?: StyleProp<ViewStyle>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function SearchBar({ onSearch, style }: SearchBarProps) {
|
|
||||||
const [searchText, setSearchText] = useState("");
|
|
||||||
|
|
||||||
const handleChangeText = (text: string) => {
|
|
||||||
setSearchText(text);
|
|
||||||
onSearch?.(text);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View style={[styles.container, style]}>
|
|
||||||
<Ionicons name="search" size={20} color="#9CA3AF" style={styles.icon} />
|
|
||||||
<TextInput
|
|
||||||
style={styles.input}
|
|
||||||
placeholder="Tìm kiếm chuyến đi, tàu..."
|
|
||||||
placeholderTextColor="#9CA3AF"
|
|
||||||
value={searchText}
|
|
||||||
onChangeText={handleChangeText}
|
|
||||||
/>
|
|
||||||
{searchText.length > 0 && (
|
|
||||||
<Ionicons
|
|
||||||
name="close-circle"
|
|
||||||
size={20}
|
|
||||||
color="#9CA3AF"
|
|
||||||
style={styles.clearIcon}
|
|
||||||
onPress={() => handleChangeText("")}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
container: {
|
|
||||||
flexDirection: "row",
|
|
||||||
alignItems: "center",
|
|
||||||
backgroundColor: "#F9FAFB",
|
|
||||||
borderRadius: 12,
|
|
||||||
paddingHorizontal: 16,
|
|
||||||
paddingVertical: 12,
|
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: "#E5E7EB",
|
|
||||||
},
|
|
||||||
icon: {
|
|
||||||
marginRight: 8,
|
|
||||||
},
|
|
||||||
input: {
|
|
||||||
flex: 1,
|
|
||||||
fontSize: 16,
|
|
||||||
color: "#111827",
|
|
||||||
fontFamily: Platform.select({
|
|
||||||
ios: "System",
|
|
||||||
android: "Roboto",
|
|
||||||
default: "System",
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
clearIcon: {
|
|
||||||
marginLeft: 8,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
319
components/diary/ShipDropdown.tsx
Normal file
319
components/diary/ShipDropdown.tsx
Normal file
@@ -0,0 +1,319 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import {
|
||||||
|
View,
|
||||||
|
Text,
|
||||||
|
TouchableOpacity,
|
||||||
|
StyleSheet,
|
||||||
|
Modal,
|
||||||
|
Platform,
|
||||||
|
ScrollView,
|
||||||
|
TextInput,
|
||||||
|
} from "react-native";
|
||||||
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
|
import { useThings } from "@/state/use-thing";
|
||||||
|
import { useI18n } from "@/hooks/use-i18n";
|
||||||
|
|
||||||
|
interface ShipOption {
|
||||||
|
id: string;
|
||||||
|
shipName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ShipDropdownProps {
|
||||||
|
value: ShipOption | null;
|
||||||
|
onChange: (value: ShipOption | null) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ShipDropdown({ value, onChange }: ShipDropdownProps) {
|
||||||
|
const { t } = useI18n();
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [searchText, setSearchText] = useState("");
|
||||||
|
|
||||||
|
const { things } = useThings();
|
||||||
|
|
||||||
|
// Convert things to ship options, filter out items without id
|
||||||
|
const shipOptions: ShipOption[] =
|
||||||
|
things
|
||||||
|
?.filter((thing) => thing.id != null)
|
||||||
|
.map((thing) => ({
|
||||||
|
id: thing.id as string,
|
||||||
|
shipName: thing.metadata?.ship_name || "",
|
||||||
|
})) || [];
|
||||||
|
|
||||||
|
// Filter ships based on search text
|
||||||
|
const filteredShips = shipOptions.filter((ship) => {
|
||||||
|
const searchLower = searchText.toLowerCase();
|
||||||
|
return ship.shipName.toLowerCase().includes(searchLower);
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSelect = (ship: ShipOption | null) => {
|
||||||
|
onChange(ship);
|
||||||
|
setIsOpen(false);
|
||||||
|
setSearchText("");
|
||||||
|
};
|
||||||
|
|
||||||
|
const displayValue = value ? value.shipName : t("diary.shipDropdown.placeholder");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<Text style={styles.label}>{t("diary.shipDropdown.label")}</Text>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.selector}
|
||||||
|
onPress={() => setIsOpen(true)}
|
||||||
|
activeOpacity={0.7}
|
||||||
|
>
|
||||||
|
<Text style={[styles.selectorText, !value && styles.placeholder]}>
|
||||||
|
{displayValue}
|
||||||
|
</Text>
|
||||||
|
<Ionicons name="chevron-down" size={20} color="#6B7280" />
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
visible={isOpen}
|
||||||
|
transparent
|
||||||
|
animationType="fade"
|
||||||
|
onRequestClose={() => setIsOpen(false)}
|
||||||
|
>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.modalOverlay}
|
||||||
|
activeOpacity={1}
|
||||||
|
onPress={() => setIsOpen(false)}
|
||||||
|
>
|
||||||
|
<View
|
||||||
|
style={styles.modalContent}
|
||||||
|
onStartShouldSetResponder={() => true}
|
||||||
|
>
|
||||||
|
{/* Search Input */}
|
||||||
|
<View style={styles.searchContainer}>
|
||||||
|
<Ionicons
|
||||||
|
name="search"
|
||||||
|
size={20}
|
||||||
|
color="#9CA3AF"
|
||||||
|
style={styles.searchIcon}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
style={styles.searchInput}
|
||||||
|
placeholder={t("diary.shipDropdown.searchPlaceholder")}
|
||||||
|
placeholderTextColor="#9CA3AF"
|
||||||
|
value={searchText}
|
||||||
|
onChangeText={setSearchText}
|
||||||
|
autoCapitalize="none"
|
||||||
|
/>
|
||||||
|
{searchText.length > 0 && (
|
||||||
|
<TouchableOpacity onPress={() => setSearchText("")}>
|
||||||
|
<Ionicons name="close-circle" size={20} color="#9CA3AF" />
|
||||||
|
</TouchableOpacity>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<ScrollView style={styles.optionsList}>
|
||||||
|
{/* Option to clear selection */}
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.option, !value && styles.selectedOption]}
|
||||||
|
onPress={() => handleSelect(null)}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.optionText,
|
||||||
|
styles.placeholderOption,
|
||||||
|
!value && styles.selectedOptionText,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{t("diary.shipDropdown.allShips")}
|
||||||
|
</Text>
|
||||||
|
{!value && (
|
||||||
|
<Ionicons name="checkmark" size={20} color="#3B82F6" />
|
||||||
|
)}
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
{filteredShips.map((ship) => (
|
||||||
|
<TouchableOpacity
|
||||||
|
key={ship.id}
|
||||||
|
style={[
|
||||||
|
styles.option,
|
||||||
|
value?.id === ship.id && styles.selectedOption,
|
||||||
|
]}
|
||||||
|
onPress={() => handleSelect(ship)}
|
||||||
|
>
|
||||||
|
<View style={styles.shipInfo}>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.shipName,
|
||||||
|
value?.id === ship.id && styles.selectedOptionText,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{ship.shipName}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
{value?.id === ship.id && (
|
||||||
|
<Ionicons name="checkmark" size={20} color="#3B82F6" />
|
||||||
|
)}
|
||||||
|
</TouchableOpacity>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{filteredShips.length === 0 && searchText.length > 0 && (
|
||||||
|
<View style={styles.emptyState}>
|
||||||
|
<Text style={styles.emptyText}>
|
||||||
|
{t("diary.shipDropdown.noShipsFound")}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</ScrollView>
|
||||||
|
</View>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</Modal>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
marginBottom: 20,
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: "600",
|
||||||
|
color: "#111827",
|
||||||
|
marginBottom: 8,
|
||||||
|
fontFamily: Platform.select({
|
||||||
|
ios: "System",
|
||||||
|
android: "Roboto",
|
||||||
|
default: "System",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
selector: {
|
||||||
|
flexDirection: "row",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
backgroundColor: "#FFFFFF",
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: "#D1D5DB",
|
||||||
|
borderRadius: 8,
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
paddingVertical: 12,
|
||||||
|
},
|
||||||
|
selectorText: {
|
||||||
|
fontSize: 16,
|
||||||
|
color: "#111827",
|
||||||
|
flex: 1,
|
||||||
|
fontFamily: Platform.select({
|
||||||
|
ios: "System",
|
||||||
|
android: "Roboto",
|
||||||
|
default: "System",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
placeholder: {
|
||||||
|
color: "#9CA3AF",
|
||||||
|
},
|
||||||
|
modalOverlay: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: "rgba(0, 0, 0, 0.5)",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
},
|
||||||
|
modalContent: {
|
||||||
|
backgroundColor: "#FFFFFF",
|
||||||
|
borderRadius: 12,
|
||||||
|
width: "85%",
|
||||||
|
maxHeight: "70%",
|
||||||
|
overflow: "hidden",
|
||||||
|
shadowColor: "#000",
|
||||||
|
shadowOffset: {
|
||||||
|
width: 0,
|
||||||
|
height: 4,
|
||||||
|
},
|
||||||
|
shadowOpacity: 0.3,
|
||||||
|
shadowRadius: 8,
|
||||||
|
elevation: 8,
|
||||||
|
},
|
||||||
|
searchContainer: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
paddingVertical: 12,
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
borderBottomColor: "#F3F4F6",
|
||||||
|
backgroundColor: "#F9FAFB",
|
||||||
|
},
|
||||||
|
searchIcon: {
|
||||||
|
marginRight: 8,
|
||||||
|
},
|
||||||
|
searchInput: {
|
||||||
|
flex: 1,
|
||||||
|
fontSize: 16,
|
||||||
|
color: "#111827",
|
||||||
|
padding: 0,
|
||||||
|
fontFamily: Platform.select({
|
||||||
|
ios: "System",
|
||||||
|
android: "Roboto",
|
||||||
|
default: "System",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
optionsList: {
|
||||||
|
maxHeight: 350,
|
||||||
|
},
|
||||||
|
option: {
|
||||||
|
flexDirection: "row",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
paddingHorizontal: 20,
|
||||||
|
paddingVertical: 16,
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
borderBottomColor: "#F3F4F6",
|
||||||
|
},
|
||||||
|
selectedOption: {
|
||||||
|
backgroundColor: "#EFF6FF",
|
||||||
|
},
|
||||||
|
optionText: {
|
||||||
|
fontSize: 16,
|
||||||
|
color: "#111827",
|
||||||
|
fontFamily: Platform.select({
|
||||||
|
ios: "System",
|
||||||
|
android: "Roboto",
|
||||||
|
default: "System",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
placeholderOption: {
|
||||||
|
fontStyle: "italic",
|
||||||
|
color: "#6B7280",
|
||||||
|
},
|
||||||
|
selectedOptionText: {
|
||||||
|
color: "#3B82F6",
|
||||||
|
fontWeight: "600",
|
||||||
|
},
|
||||||
|
shipInfo: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
shipName: {
|
||||||
|
fontSize: 16,
|
||||||
|
color: "#111827",
|
||||||
|
fontWeight: "500",
|
||||||
|
marginBottom: 2,
|
||||||
|
fontFamily: Platform.select({
|
||||||
|
ios: "System",
|
||||||
|
android: "Roboto",
|
||||||
|
default: "System",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
regNumber: {
|
||||||
|
fontSize: 14,
|
||||||
|
color: "#6B7280",
|
||||||
|
fontFamily: Platform.select({
|
||||||
|
ios: "System",
|
||||||
|
android: "Roboto",
|
||||||
|
default: "System",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
emptyState: {
|
||||||
|
paddingVertical: 24,
|
||||||
|
alignItems: "center",
|
||||||
|
},
|
||||||
|
emptyText: {
|
||||||
|
fontSize: 14,
|
||||||
|
color: "#9CA3AF",
|
||||||
|
fontFamily: Platform.select({
|
||||||
|
ios: "System",
|
||||||
|
android: "Roboto",
|
||||||
|
default: "System",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -9,31 +9,33 @@ import {
|
|||||||
ScrollView,
|
ScrollView,
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
import { Ionicons } from "@expo/vector-icons";
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
import { TripStatus, TRIP_STATUS_CONFIG } from "./types";
|
import { TripStatus } from "./types";
|
||||||
|
import { useI18n } from "@/hooks/use-i18n";
|
||||||
|
|
||||||
interface StatusDropdownProps {
|
interface StatusDropdownProps {
|
||||||
value: TripStatus | null;
|
value: TripStatus | null;
|
||||||
onChange: (status: TripStatus | null) => void;
|
onChange: (value: TripStatus | null) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const STATUS_OPTIONS: Array<{ value: TripStatus | null; label: string }> = [
|
|
||||||
{ value: null, label: "Vui lòng chọn" },
|
|
||||||
{ value: "created", label: "Đã khởi tạo" },
|
|
||||||
{ value: "pending", label: "Chờ duyệt" },
|
|
||||||
{ value: "approved", label: "Đã duyệt" },
|
|
||||||
{ value: "in-progress", label: "Đang hoạt động" },
|
|
||||||
{ value: "completed", label: "Hoàn thành" },
|
|
||||||
{ value: "cancelled", label: "Đã hủy" },
|
|
||||||
];
|
|
||||||
|
|
||||||
export default function StatusDropdown({
|
export default function StatusDropdown({
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
}: StatusDropdownProps) {
|
}: StatusDropdownProps) {
|
||||||
|
const { t } = useI18n();
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
|
const STATUS_OPTIONS: Array<{ value: TripStatus | null; label: string }> = [
|
||||||
|
{ value: null, label: t("diary.statusDropdown.placeholder") },
|
||||||
|
{ value: 0, label: t("diary.statusDropdown.created") },
|
||||||
|
{ value: 1, label: t("diary.statusDropdown.pending") },
|
||||||
|
{ value: 2, label: t("diary.statusDropdown.approved") },
|
||||||
|
{ value: 3, label: t("diary.statusDropdown.active") },
|
||||||
|
{ value: 4, label: t("diary.statusDropdown.completed") },
|
||||||
|
{ value: 5, label: t("diary.statusDropdown.cancelled") },
|
||||||
|
];
|
||||||
|
|
||||||
const selectedLabel =
|
const selectedLabel =
|
||||||
STATUS_OPTIONS.find((opt) => opt.value === value)?.label || "Vui lòng chọn";
|
STATUS_OPTIONS.find((opt) => opt.value === value)?.label || t("diary.statusDropdown.placeholder");
|
||||||
|
|
||||||
const handleSelect = (status: TripStatus | null) => {
|
const handleSelect = (status: TripStatus | null) => {
|
||||||
onChange(status);
|
onChange(status);
|
||||||
@@ -42,7 +44,7 @@ export default function StatusDropdown({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<Text style={styles.label}>Trạng thái</Text>
|
<Text style={styles.label}>{t("diary.statusDropdown.label")}</Text>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.selector}
|
style={styles.selector}
|
||||||
onPress={() => setIsOpen(true)}
|
onPress={() => setIsOpen(true)}
|
||||||
|
|||||||
@@ -7,10 +7,13 @@ import {
|
|||||||
Platform,
|
Platform,
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
import { Ionicons } from "@expo/vector-icons";
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
import { Trip, TRIP_STATUS_CONFIG } from "./types";
|
import { useTripStatusConfig } from "./types";
|
||||||
|
import { useThings } from "@/state/use-thing";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import { useI18n } from "@/hooks/use-i18n";
|
||||||
|
|
||||||
interface TripCardProps {
|
interface TripCardProps {
|
||||||
trip: Trip;
|
trip: Model.Trip;
|
||||||
onPress?: () => void;
|
onPress?: () => void;
|
||||||
onView?: () => void;
|
onView?: () => void;
|
||||||
onEdit?: () => void;
|
onEdit?: () => void;
|
||||||
@@ -19,13 +22,37 @@ interface TripCardProps {
|
|||||||
onDelete?: () => void;
|
onDelete?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function TripCard({ trip, onPress, onView, onEdit, onTeam, onSend, onDelete }: TripCardProps) {
|
export default function TripCard({
|
||||||
const statusConfig = TRIP_STATUS_CONFIG[trip.status];
|
trip,
|
||||||
|
onPress,
|
||||||
|
onView,
|
||||||
|
onEdit,
|
||||||
|
onTeam,
|
||||||
|
onSend,
|
||||||
|
onDelete,
|
||||||
|
}: TripCardProps) {
|
||||||
|
const { t } = useI18n();
|
||||||
|
const { things } = useThings();
|
||||||
|
const TRIP_STATUS_CONFIG = useTripStatusConfig();
|
||||||
|
|
||||||
|
// Tìm thing có id trùng với vms_id của trip
|
||||||
|
const thingOfTrip: Model.Thing | undefined = things?.find(
|
||||||
|
(thing) => thing.id === trip.vms_id
|
||||||
|
);
|
||||||
|
|
||||||
|
// Lấy config status từ trip_status (number)
|
||||||
|
const statusKey = trip.trip_status as keyof typeof TRIP_STATUS_CONFIG;
|
||||||
|
const statusConfig = TRIP_STATUS_CONFIG[statusKey] || {
|
||||||
|
label: "-",
|
||||||
|
bgColor: "#eee",
|
||||||
|
textColor: "#333",
|
||||||
|
icon: "help",
|
||||||
|
};
|
||||||
|
|
||||||
// Determine which actions to show based on status
|
// Determine which actions to show based on status
|
||||||
const showEdit = trip.status === 'created' || trip.status === 'pending';
|
const showEdit = trip.trip_status === 0 || trip.trip_status === 1;
|
||||||
const showSend = trip.status === 'created';
|
const showSend = trip.trip_status === 0;
|
||||||
const showDelete = trip.status === 'pending';
|
const showDelete = trip.trip_status === 1;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.card}>
|
<View style={styles.card}>
|
||||||
@@ -39,8 +66,7 @@ export default function TripCard({ trip, onPress, onView, onEdit, onTeam, onSend
|
|||||||
color={statusConfig.textColor}
|
color={statusConfig.textColor}
|
||||||
/>
|
/>
|
||||||
<View style={styles.titleContainer}>
|
<View style={styles.titleContainer}>
|
||||||
<Text style={styles.title}>{trip.title}</Text>
|
<Text style={styles.title}>{trip.name}</Text>
|
||||||
<Text style={styles.code}>{trip.code}</Text>
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<View
|
<View
|
||||||
@@ -67,25 +93,29 @@ export default function TripCard({ trip, onPress, onView, onEdit, onTeam, onSend
|
|||||||
{/* Info Grid */}
|
{/* Info Grid */}
|
||||||
<View style={styles.infoGrid}>
|
<View style={styles.infoGrid}>
|
||||||
<View style={styles.infoRow}>
|
<View style={styles.infoRow}>
|
||||||
<Text style={styles.label}>Tàu</Text>
|
<Text style={styles.label}>{t("diary.tripCard.shipCode")}</Text>
|
||||||
<Text style={styles.value}>
|
<Text style={styles.value}>
|
||||||
{trip.vessel} ({trip.vesselCode})
|
{thingOfTrip?.metadata?.ship_reg_number /* hoặc trip.ship_id */}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.infoRow}>
|
<View style={styles.infoRow}>
|
||||||
<Text style={styles.label}>Khởi hành</Text>
|
<Text style={styles.label}>{t("diary.tripCard.departure")}</Text>
|
||||||
<Text style={styles.value}>{trip.departureDate}</Text>
|
<Text style={styles.value}>
|
||||||
|
{trip.departure_time
|
||||||
|
? dayjs(trip.departure_time).format("DD/MM/YYYY HH:mm")
|
||||||
|
: "-"}
|
||||||
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.infoRow}>
|
<View style={styles.infoRow}>
|
||||||
<Text style={styles.label}>Trở về</Text>
|
<Text style={styles.label}>{t("diary.tripCard.return")}</Text>
|
||||||
<Text style={styles.value}>{trip.returnDate || "-"}</Text>
|
{/* FIXME: trip.returnDate không có trong dữ liệu API, cần mapping từ trip.arrival_time */}
|
||||||
</View>
|
<Text style={styles.value}>
|
||||||
|
{trip.arrival_time
|
||||||
<View style={styles.infoRow}>
|
? dayjs(trip.arrival_time).format("DD/MM/YYYY HH:mm")
|
||||||
<Text style={styles.label}>Thời gian</Text>
|
: "-"}
|
||||||
<Text style={[styles.value, styles.duration]}>{trip.duration}</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
@@ -93,34 +123,54 @@ export default function TripCard({ trip, onPress, onView, onEdit, onTeam, onSend
|
|||||||
{/* Action Buttons */}
|
{/* Action Buttons */}
|
||||||
<View style={styles.divider} />
|
<View style={styles.divider} />
|
||||||
<View style={styles.actionsContainer}>
|
<View style={styles.actionsContainer}>
|
||||||
<TouchableOpacity style={styles.actionButton} onPress={onView} activeOpacity={0.7}>
|
<TouchableOpacity
|
||||||
|
style={styles.actionButton}
|
||||||
|
onPress={onView}
|
||||||
|
activeOpacity={0.7}
|
||||||
|
>
|
||||||
<Ionicons name="eye-outline" size={20} color="#6B7280" />
|
<Ionicons name="eye-outline" size={20} color="#6B7280" />
|
||||||
<Text style={styles.actionText}>View</Text>
|
<Text style={styles.actionText}>{t("diary.tripCard.view")}</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
{showEdit && (
|
{showEdit && (
|
||||||
<TouchableOpacity style={styles.actionButton} onPress={onEdit} activeOpacity={0.7}>
|
<TouchableOpacity
|
||||||
|
style={styles.actionButton}
|
||||||
|
onPress={onEdit}
|
||||||
|
activeOpacity={0.7}
|
||||||
|
>
|
||||||
<Ionicons name="create-outline" size={20} color="#6B7280" />
|
<Ionicons name="create-outline" size={20} color="#6B7280" />
|
||||||
<Text style={styles.actionText}>Edit</Text>
|
<Text style={styles.actionText}>{t("diary.tripCard.edit")}</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<TouchableOpacity style={styles.actionButton} onPress={onTeam} activeOpacity={0.7}>
|
<TouchableOpacity
|
||||||
|
style={styles.actionButton}
|
||||||
|
onPress={onTeam}
|
||||||
|
activeOpacity={0.7}
|
||||||
|
>
|
||||||
<Ionicons name="people-outline" size={20} color="#6B7280" />
|
<Ionicons name="people-outline" size={20} color="#6B7280" />
|
||||||
<Text style={styles.actionText}>Team</Text>
|
<Text style={styles.actionText}>{t("diary.tripCard.team")}</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
{showSend && (
|
{showSend && (
|
||||||
<TouchableOpacity style={styles.actionButton} onPress={onSend} activeOpacity={0.7}>
|
<TouchableOpacity
|
||||||
|
style={styles.actionButton}
|
||||||
|
onPress={onSend}
|
||||||
|
activeOpacity={0.7}
|
||||||
|
>
|
||||||
<Ionicons name="send-outline" size={20} color="#6B7280" />
|
<Ionicons name="send-outline" size={20} color="#6B7280" />
|
||||||
<Text style={styles.actionText}>Send</Text>
|
<Text style={styles.actionText}>{t("diary.tripCard.send")}</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{showDelete && (
|
{showDelete && (
|
||||||
<TouchableOpacity style={styles.actionButton} onPress={onDelete} activeOpacity={0.7}>
|
<TouchableOpacity
|
||||||
|
style={styles.actionButton}
|
||||||
|
onPress={onDelete}
|
||||||
|
activeOpacity={0.7}
|
||||||
|
>
|
||||||
<Ionicons name="trash-outline" size={20} color="#EF4444" />
|
<Ionicons name="trash-outline" size={20} color="#EF4444" />
|
||||||
<Text style={[styles.actionText, styles.deleteText]}>Delete</Text>
|
<Text style={[styles.actionText, styles.deleteText]}>{t("diary.tripCard.delete")}</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
@@ -171,15 +221,7 @@ const styles = StyleSheet.create({
|
|||||||
default: "System",
|
default: "System",
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
code: {
|
|
||||||
fontSize: 14,
|
|
||||||
color: "#6B7280",
|
|
||||||
fontFamily: Platform.select({
|
|
||||||
ios: "System",
|
|
||||||
android: "Roboto",
|
|
||||||
default: "System",
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
badge: {
|
badge: {
|
||||||
paddingHorizontal: 12,
|
paddingHorizontal: 12,
|
||||||
paddingVertical: 4,
|
paddingVertical: 4,
|
||||||
@@ -222,9 +264,6 @@ const styles = StyleSheet.create({
|
|||||||
default: "System",
|
default: "System",
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
duration: {
|
|
||||||
color: "#3B82F6",
|
|
||||||
},
|
|
||||||
divider: {
|
divider: {
|
||||||
height: 1,
|
height: 1,
|
||||||
backgroundColor: "#F3F4F6",
|
backgroundColor: "#F3F4F6",
|
||||||
|
|||||||
@@ -1,92 +0,0 @@
|
|||||||
import { Trip } from "./types";
|
|
||||||
|
|
||||||
export const MOCK_TRIPS: Trip[] = [
|
|
||||||
{
|
|
||||||
id: "T001",
|
|
||||||
title: "Chuyến đi Hoàng Sa",
|
|
||||||
code: "T001",
|
|
||||||
vessel: "Hải Âu 1",
|
|
||||||
vesselCode: "V001",
|
|
||||||
departureDate: "2025-11-20 06:00",
|
|
||||||
returnDate: "2025-11-27 18:30",
|
|
||||||
duration: "7 ngày 12 giờ",
|
|
||||||
status: "completed",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "T002",
|
|
||||||
title: "Tuần tra vùng biển",
|
|
||||||
code: "T002",
|
|
||||||
vessel: "Bình Minh",
|
|
||||||
vesselCode: "V004",
|
|
||||||
departureDate: "2025-11-26 08:00",
|
|
||||||
returnDate: null,
|
|
||||||
duration: "2 ngày 6 giờ",
|
|
||||||
status: "in-progress",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "T003",
|
|
||||||
title: "Đánh cá Trường Sa",
|
|
||||||
code: "T003",
|
|
||||||
vessel: "Ngọc Lan",
|
|
||||||
vesselCode: "V002",
|
|
||||||
departureDate: "2025-11-15 05:30",
|
|
||||||
returnDate: "2025-11-25 16:00",
|
|
||||||
duration: "10 ngày 10 giờ",
|
|
||||||
status: "completed",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "T004",
|
|
||||||
title: "Vận chuyển hàng hóa",
|
|
||||||
code: "T004",
|
|
||||||
vessel: "Việt Thắng",
|
|
||||||
vesselCode: "V003",
|
|
||||||
departureDate: "2025-11-22 10:00",
|
|
||||||
returnDate: null,
|
|
||||||
duration: "-",
|
|
||||||
status: "cancelled",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "T005",
|
|
||||||
title: "Khảo sát địa chất",
|
|
||||||
code: "T005",
|
|
||||||
vessel: "Thanh Bình",
|
|
||||||
vesselCode: "V005",
|
|
||||||
departureDate: "2025-11-18 07:00",
|
|
||||||
returnDate: "2025-11-23 14:00",
|
|
||||||
duration: "5 ngày 7 giờ",
|
|
||||||
status: "created",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "T006",
|
|
||||||
title: "Đánh cá ven bờ",
|
|
||||||
code: "T006",
|
|
||||||
vessel: "Hải Âu 1",
|
|
||||||
vesselCode: "V001",
|
|
||||||
departureDate: "2025-11-28 04:00",
|
|
||||||
returnDate: null,
|
|
||||||
duration: "6 giờ",
|
|
||||||
status: "in-progress",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "T007",
|
|
||||||
title: "Khảo sát vùng biển",
|
|
||||||
code: "T007",
|
|
||||||
vessel: "Ngọc Lan",
|
|
||||||
vesselCode: "V002",
|
|
||||||
departureDate: "2025-12-01 07:00",
|
|
||||||
returnDate: null,
|
|
||||||
duration: "-",
|
|
||||||
status: "pending",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "T008",
|
|
||||||
title: "Đánh cá xa bờ",
|
|
||||||
code: "T008",
|
|
||||||
vessel: "Việt Thắng",
|
|
||||||
vesselCode: "V003",
|
|
||||||
departureDate: "2025-12-05 05:00",
|
|
||||||
returnDate: null,
|
|
||||||
duration: "-",
|
|
||||||
status: "approved",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
@@ -1,58 +1,93 @@
|
|||||||
|
import { useI18n } from "@/hooks/use-i18n";
|
||||||
|
|
||||||
export type TripStatus =
|
export type TripStatus =
|
||||||
| "created" // Đã khởi tạo
|
| 0 // Đã khởi tạo
|
||||||
| "pending" // Chờ duyệt
|
| 1 // Chờ duyệt
|
||||||
| "approved" // Đã duyệt
|
| 2 // Đã duyệt
|
||||||
| "in-progress" // Đang hoạt động
|
| 3 // Đang hoạt động
|
||||||
| "completed" // Hoàn thành
|
| 4 // Hoàn thành
|
||||||
| "cancelled"; // Đã hủy
|
| 5; // Đã hủy
|
||||||
|
|
||||||
export interface Trip {
|
|
||||||
id: string;
|
|
||||||
title: string;
|
|
||||||
code: string;
|
|
||||||
vessel: string;
|
|
||||||
vesselCode: string;
|
|
||||||
departureDate: string;
|
|
||||||
returnDate: string | null;
|
|
||||||
duration: string;
|
|
||||||
status: TripStatus;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Static config - dùng khi không cần i18n hoặc ngoài React component
|
||||||
export const TRIP_STATUS_CONFIG = {
|
export const TRIP_STATUS_CONFIG = {
|
||||||
created: {
|
0: {
|
||||||
label: "Đã khởi tạo",
|
label: "Đã khởi tạo",
|
||||||
bgColor: "#F3F4F6", // Gray background
|
bgColor: "#F3F4F6", // Gray background
|
||||||
textColor: "#4B5563", // Gray text
|
textColor: "#4B5563", // Gray text
|
||||||
icon: "document-text",
|
icon: "document-text",
|
||||||
},
|
},
|
||||||
pending: {
|
1: {
|
||||||
label: "Chờ duyệt",
|
label: "Chờ duyệt",
|
||||||
bgColor: "#FEF3C7", // Yellow background
|
bgColor: "#FEF3C7", // Yellow background
|
||||||
textColor: "#92400E", // Dark yellow text
|
textColor: "#92400E", // Dark yellow text
|
||||||
icon: "hourglass",
|
icon: "hourglass",
|
||||||
},
|
},
|
||||||
approved: {
|
2: {
|
||||||
label: "Đã duyệt",
|
label: "Đã duyệt",
|
||||||
bgColor: "#E0E7FF", // Indigo background
|
bgColor: "#E0E7FF", // Indigo background
|
||||||
textColor: "#3730A3", // Dark indigo text
|
textColor: "#3730A3", // Dark indigo text
|
||||||
icon: "checkmark-done",
|
icon: "checkmark-done",
|
||||||
},
|
},
|
||||||
"in-progress": {
|
3: {
|
||||||
label: "Đang hoạt động",
|
label: "Đang hoạt động",
|
||||||
bgColor: "#DBEAFE", // Blue background
|
bgColor: "#DBEAFE", // Blue background
|
||||||
textColor: "#1E40AF", // Dark blue text
|
textColor: "#1E40AF", // Dark blue text
|
||||||
icon: "sync",
|
icon: "sync",
|
||||||
},
|
},
|
||||||
completed: {
|
4: {
|
||||||
label: "Hoàn thành",
|
label: "Hoàn thành",
|
||||||
bgColor: "#D1FAE5", // Green background
|
bgColor: "#D1FAE5", // Green background
|
||||||
textColor: "#065F46", // Dark green text
|
textColor: "#065F46", // Dark green text
|
||||||
icon: "checkmark-circle",
|
icon: "checkmark-circle",
|
||||||
},
|
},
|
||||||
cancelled: {
|
5: {
|
||||||
label: "Đã hủy",
|
label: "Đã hủy",
|
||||||
bgColor: "#FEE2E2", // Red background
|
bgColor: "#FEE2E2", // Red background
|
||||||
textColor: "#991B1B", // Dark red text
|
textColor: "#991B1B", // Dark red text
|
||||||
icon: "close-circle",
|
icon: "close-circle",
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
// Hook để lấy config với i18n - dùng trong React component
|
||||||
|
export function useTripStatusConfig() {
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
return {
|
||||||
|
0: {
|
||||||
|
label: t("diary.statusDropdown.created"),
|
||||||
|
bgColor: "#F3F4F6",
|
||||||
|
textColor: "#4B5563",
|
||||||
|
icon: "document-text",
|
||||||
|
},
|
||||||
|
1: {
|
||||||
|
label: t("diary.statusDropdown.pending"),
|
||||||
|
bgColor: "#FEF3C7",
|
||||||
|
textColor: "#92400E",
|
||||||
|
icon: "hourglass",
|
||||||
|
},
|
||||||
|
2: {
|
||||||
|
label: t("diary.statusDropdown.approved"),
|
||||||
|
bgColor: "#E0E7FF",
|
||||||
|
textColor: "#3730A3",
|
||||||
|
icon: "checkmark-done",
|
||||||
|
},
|
||||||
|
3: {
|
||||||
|
label: t("diary.statusDropdown.active"),
|
||||||
|
bgColor: "#DBEAFE",
|
||||||
|
textColor: "#1E40AF",
|
||||||
|
icon: "sync",
|
||||||
|
},
|
||||||
|
4: {
|
||||||
|
label: t("diary.statusDropdown.completed"),
|
||||||
|
bgColor: "#D1FAE5",
|
||||||
|
textColor: "#065F46",
|
||||||
|
icon: "checkmark-circle",
|
||||||
|
},
|
||||||
|
5: {
|
||||||
|
label: t("diary.statusDropdown.cancelled"),
|
||||||
|
bgColor: "#FEE2E2",
|
||||||
|
textColor: "#991B1B",
|
||||||
|
icon: "close-circle",
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
}
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ export const API_PATH_SHIP_INFO = "/api/sgw/shipinfo";
|
|||||||
export const API_GET_ALL_LAYER = "/api/sgw/geojsonlist";
|
export const API_GET_ALL_LAYER = "/api/sgw/geojsonlist";
|
||||||
export const API_GET_LAYER_INFO = "/api/sgw/geojson";
|
export const API_GET_LAYER_INFO = "/api/sgw/geojson";
|
||||||
export const API_GET_TRIP = "/api/sgw/trip";
|
export const API_GET_TRIP = "/api/sgw/trip";
|
||||||
|
export const API_POST_TRIPSLIST = "api/sgw/tripslist";
|
||||||
export const API_GET_ALARMS = "/api/io/alarms";
|
export const API_GET_ALARMS = "/api/io/alarms";
|
||||||
export const API_UPDATE_TRIP_STATUS = "/api/sgw/tripState";
|
export const API_UPDATE_TRIP_STATUS = "/api/sgw/tripState";
|
||||||
export const API_HAUL_HANDLE = "/api/sgw/fishingLog";
|
export const API_HAUL_HANDLE = "/api/sgw/fishingLog";
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { api } from "@/config";
|
|||||||
import {
|
import {
|
||||||
API_GET_TRIP,
|
API_GET_TRIP,
|
||||||
API_HAUL_HANDLE,
|
API_HAUL_HANDLE,
|
||||||
|
API_POST_TRIPSLIST,
|
||||||
API_UPDATE_FISHING_LOGS,
|
API_UPDATE_FISHING_LOGS,
|
||||||
API_UPDATE_TRIP_STATUS,
|
API_UPDATE_TRIP_STATUS,
|
||||||
} from "@/constants";
|
} from "@/constants";
|
||||||
@@ -20,4 +21,8 @@ export async function queryStartNewHaul(body: Model.NewFishingLogRequest) {
|
|||||||
|
|
||||||
export async function queryUpdateFishingLogs(body: Model.FishingLog) {
|
export async function queryUpdateFishingLogs(body: Model.FishingLog) {
|
||||||
return api.put(API_UPDATE_FISHING_LOGS, body);
|
return api.put(API_UPDATE_FISHING_LOGS, body);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function queryTripsList(body: Model.TripListBody) {
|
||||||
|
return api.post(API_POST_TRIPSLIST, body);
|
||||||
|
}
|
||||||
|
|||||||
30
controller/typings.d.ts
vendored
30
controller/typings.d.ts
vendored
@@ -93,7 +93,37 @@ declare namespace Model {
|
|||||||
message?: string;
|
message?: string;
|
||||||
started_at?: number;
|
started_at?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Trip
|
// Trip
|
||||||
|
// Body API trip
|
||||||
|
interface TripListBody {
|
||||||
|
name?: string;
|
||||||
|
order?: string;
|
||||||
|
dir?: "asc" | "desc";
|
||||||
|
limit: number;
|
||||||
|
offset: number;
|
||||||
|
metadata?: TripRequestMetadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TripRequestMetadata {
|
||||||
|
status?: string;
|
||||||
|
from?: string;
|
||||||
|
to?: string;
|
||||||
|
ship_name?: string;
|
||||||
|
reg_number?: string;
|
||||||
|
province_code?: string;
|
||||||
|
owner_id?: string;
|
||||||
|
ship_id?: string;
|
||||||
|
thing_id?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TripsListResponse {
|
||||||
|
total?: number;
|
||||||
|
offset?: number;
|
||||||
|
limit?: number;
|
||||||
|
trips?: Trip[];
|
||||||
|
}
|
||||||
|
|
||||||
interface Trip {
|
interface Trip {
|
||||||
id: string;
|
id: string;
|
||||||
ship_id: string;
|
ship_id: string;
|
||||||
|
|||||||
@@ -60,6 +60,64 @@
|
|||||||
"sendError": "Unable to send SOS signal"
|
"sendError": "Unable to send SOS signal"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"diary": {
|
||||||
|
"title": "Trip Diary",
|
||||||
|
"filter": "Filter",
|
||||||
|
"addTrip": "Add Trip",
|
||||||
|
"tripList": "Trip List",
|
||||||
|
"tripListCount": "Trip List ({{count}})",
|
||||||
|
"noTripsFound": "No matching trips found",
|
||||||
|
"reset": "Reset",
|
||||||
|
"apply": "Apply",
|
||||||
|
"selectedFilters": "Selected filters:",
|
||||||
|
"statusLabel": "Status:",
|
||||||
|
"fromLabel": "From:",
|
||||||
|
"toLabel": "To:",
|
||||||
|
"shipLabel": "Ship:",
|
||||||
|
"statusDropdown": {
|
||||||
|
"label": "Status",
|
||||||
|
"placeholder": "Please select",
|
||||||
|
"created": "Created",
|
||||||
|
"pending": "Pending Approval",
|
||||||
|
"approved": "Approved",
|
||||||
|
"active": "Active",
|
||||||
|
"completed": "Completed",
|
||||||
|
"cancelled": "Cancelled"
|
||||||
|
},
|
||||||
|
"shipDropdown": {
|
||||||
|
"label": "Ship",
|
||||||
|
"placeholder": "Select ship",
|
||||||
|
"allShips": "All ships",
|
||||||
|
"searchPlaceholder": "Search ship...",
|
||||||
|
"noShipsFound": "No matching ships found"
|
||||||
|
},
|
||||||
|
"dateRangePicker": {
|
||||||
|
"label": "Trip Date",
|
||||||
|
"startDate": "Start Date",
|
||||||
|
"endDate": "End Date",
|
||||||
|
"selectStartDate": "Select start date",
|
||||||
|
"selectEndDate": "Select end date",
|
||||||
|
"done": "Done"
|
||||||
|
},
|
||||||
|
"tripCard": {
|
||||||
|
"shipCode": "Ship Code",
|
||||||
|
"departure": "Departure",
|
||||||
|
"return": "Return",
|
||||||
|
"view": "View",
|
||||||
|
"edit": "Edit",
|
||||||
|
"team": "Team",
|
||||||
|
"send": "Send",
|
||||||
|
"delete": "Delete"
|
||||||
|
},
|
||||||
|
"tripStatus": {
|
||||||
|
"created": "Not approved, creating",
|
||||||
|
"pending": "Pending approval",
|
||||||
|
"approved": "Approved",
|
||||||
|
"departed": "Departed",
|
||||||
|
"completed": "Completed",
|
||||||
|
"cancelled": "Cancelled"
|
||||||
|
}
|
||||||
|
},
|
||||||
"trip": {
|
"trip": {
|
||||||
"infoTrip": "Trip Information",
|
"infoTrip": "Trip Information",
|
||||||
"createNewTrip": "Create New Trip",
|
"createNewTrip": "Create New Trip",
|
||||||
|
|||||||
@@ -60,6 +60,64 @@
|
|||||||
"sendError": "Không thể gửi tín hiệu SOS"
|
"sendError": "Không thể gửi tín hiệu SOS"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"diary": {
|
||||||
|
"title": "Nhật ký chuyến đi",
|
||||||
|
"filter": "Bộ lọc",
|
||||||
|
"addTrip": "Thêm chuyến đi",
|
||||||
|
"tripList": "Danh sách chuyến đi",
|
||||||
|
"tripListCount": "Danh sách chuyến đi ({{count}})",
|
||||||
|
"noTripsFound": "Không tìm thấy chuyến đi phù hợp",
|
||||||
|
"reset": "Đặt lại",
|
||||||
|
"apply": "Áp dụng",
|
||||||
|
"selectedFilters": "Bộ lọc đã chọn:",
|
||||||
|
"statusLabel": "Trạng thái:",
|
||||||
|
"fromLabel": "Từ:",
|
||||||
|
"toLabel": "Đến:",
|
||||||
|
"shipLabel": "Tàu:",
|
||||||
|
"statusDropdown": {
|
||||||
|
"label": "Trạng thái",
|
||||||
|
"placeholder": "Vui lòng chọn",
|
||||||
|
"created": "Đã khởi tạo",
|
||||||
|
"pending": "Chờ duyệt",
|
||||||
|
"approved": "Đã duyệt",
|
||||||
|
"active": "Đang hoạt động",
|
||||||
|
"completed": "Hoàn thành",
|
||||||
|
"cancelled": "Đã hủy"
|
||||||
|
},
|
||||||
|
"shipDropdown": {
|
||||||
|
"label": "Tàu",
|
||||||
|
"placeholder": "Chọn tàu",
|
||||||
|
"allShips": "Tất cả tàu",
|
||||||
|
"searchPlaceholder": "Tìm kiếm tàu...",
|
||||||
|
"noShipsFound": "Không tìm thấy tàu phù hợp"
|
||||||
|
},
|
||||||
|
"dateRangePicker": {
|
||||||
|
"label": "Ngày đi",
|
||||||
|
"startDate": "Ngày bắt đầu",
|
||||||
|
"endDate": "Ngày kết thúc",
|
||||||
|
"selectStartDate": "Chọn ngày bắt đầu",
|
||||||
|
"selectEndDate": "Chọn ngày kết thúc",
|
||||||
|
"done": "Xong"
|
||||||
|
},
|
||||||
|
"tripCard": {
|
||||||
|
"shipCode": "Mã Tàu",
|
||||||
|
"departure": "Khởi hành",
|
||||||
|
"return": "Trở về",
|
||||||
|
"view": "Xem",
|
||||||
|
"edit": "Sửa",
|
||||||
|
"team": "Đội",
|
||||||
|
"send": "Gửi",
|
||||||
|
"delete": "Xóa"
|
||||||
|
},
|
||||||
|
"tripStatus": {
|
||||||
|
"created": "Chưa phê duyệt, đang tạo",
|
||||||
|
"pending": "Đang gửi yêu cầu phê duyệt, chờ được phê duyệt",
|
||||||
|
"approved": "Đã phê duyệt",
|
||||||
|
"departed": "Đã xuất bến",
|
||||||
|
"completed": "Đã hoàn thành",
|
||||||
|
"cancelled": "Đã huỷ"
|
||||||
|
}
|
||||||
|
},
|
||||||
"trip": {
|
"trip": {
|
||||||
"infoTrip": "Thông Tin Chuyến Đi",
|
"infoTrip": "Thông Tin Chuyến Đi",
|
||||||
"createNewTrip": "Tạo chuyến mới",
|
"createNewTrip": "Tạo chuyến mới",
|
||||||
|
|||||||
31
state/use-tripslist.ts
Normal file
31
state/use-tripslist.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { queryTripsList } from "@/controller/TripController";
|
||||||
|
import { create } from "zustand";
|
||||||
|
|
||||||
|
type TripsListState = {
|
||||||
|
tripsList: Model.TripsListResponse | null;
|
||||||
|
getTripsList: (body: Model.TripListBody) => Promise<void>;
|
||||||
|
error: string | null;
|
||||||
|
loading?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useTripsList = create<TripsListState>((set) => ({
|
||||||
|
tripsList: null,
|
||||||
|
getTripsList: async (body: Model.TripListBody) => {
|
||||||
|
set({ loading: true, error: null });
|
||||||
|
try {
|
||||||
|
const response = await queryTripsList(body);
|
||||||
|
console.log("Trip fetching API: ", response.data.trips?.length);
|
||||||
|
|
||||||
|
set({ tripsList: response.data ?? [], loading: false });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error when fetch things: ", error);
|
||||||
|
set({
|
||||||
|
error: "Failed to fetch things data",
|
||||||
|
loading: false,
|
||||||
|
tripsList: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: null,
|
||||||
|
loading: false,
|
||||||
|
}));
|
||||||
Reference in New Issue
Block a user