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 prev = useRef<string | null>(null);
|
||||
const currentSegment = segments[1] ?? segments[segments.length - 1] ?? null;
|
||||
const { t, locale } = useI18n();
|
||||
const { t } = useI18n();
|
||||
useEffect(() => {
|
||||
if (prev.current !== currentSegment) {
|
||||
// console.log("Tab changed ->", { from: prev.current, to: currentSegment });
|
||||
@@ -60,7 +60,11 @@ export default function TabLayout() {
|
||||
options={{
|
||||
title: t("navigation.manager"),
|
||||
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";
|
||||
import { SafeAreaView } from "react-native-safe-area-context";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import SearchBar from "@/components/diary/SearchBar";
|
||||
import FilterButton from "@/components/diary/FilterButton";
|
||||
import TripCard from "@/components/diary/TripCard";
|
||||
import FilterModal, { FilterValues } from "@/components/diary/FilterModal";
|
||||
import { MOCK_TRIPS } from "@/components/diary/mockData";
|
||||
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() {
|
||||
const [searchText, setSearchText] = useState("");
|
||||
const { t } = useI18n();
|
||||
const [showFilterModal, setShowFilterModal] = useState(false);
|
||||
const [filters, setFilters] = useState<FilterValues>({
|
||||
status: null,
|
||||
startDate: 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 = {
|
||||
offset: 0,
|
||||
limit: 200,
|
||||
@@ -34,56 +37,44 @@ export default function diary() {
|
||||
not_empty: "ship_name, ship_reg_number",
|
||||
},
|
||||
};
|
||||
|
||||
// Gọi API things
|
||||
const { things, getThings } = useThings();
|
||||
const { getThings } = useThings();
|
||||
useEffect(() => {
|
||||
getThings(payloadThings);
|
||||
}, []);
|
||||
|
||||
console.log(things);
|
||||
|
||||
// Filter trips based on search text and filters
|
||||
const filteredTrips = MOCK_TRIPS.filter((trip) => {
|
||||
// Search filter
|
||||
if (searchText) {
|
||||
const searchLower = searchText.toLowerCase();
|
||||
const matchesSearch =
|
||||
trip.title.toLowerCase().includes(searchLower) ||
|
||||
trip.code.toLowerCase().includes(searchLower) ||
|
||||
trip.vessel.toLowerCase().includes(searchLower) ||
|
||||
trip.vesselCode.toLowerCase().includes(searchLower);
|
||||
|
||||
if (!matchesSearch) return false;
|
||||
}
|
||||
|
||||
// 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;
|
||||
// State cho payload trips
|
||||
const [payloadTrips, setPayloadTrips] = useState<Model.TripListBody>({
|
||||
name: "",
|
||||
order: "",
|
||||
dir: "desc",
|
||||
offset: 0,
|
||||
limit: 10,
|
||||
metadata: {
|
||||
from: "",
|
||||
to: "",
|
||||
ship_name: "",
|
||||
reg_number: "",
|
||||
province_code: "",
|
||||
owner_id: "",
|
||||
ship_id: "",
|
||||
status: "",
|
||||
},
|
||||
});
|
||||
|
||||
const handleSearch = (text: string) => {
|
||||
setSearchText(text);
|
||||
};
|
||||
const { tripsList, getTripsList } = useTripsList();
|
||||
|
||||
// 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 = () => {
|
||||
setShowFilterModal(true);
|
||||
@@ -91,6 +82,28 @@ export default function diary() {
|
||||
|
||||
const handleApplyFilters = (newFilters: FilterValues) => {
|
||||
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) => {
|
||||
@@ -124,39 +137,44 @@ export default function diary() {
|
||||
};
|
||||
|
||||
return (
|
||||
<SafeAreaView style={styles.safeArea}>
|
||||
<SafeAreaView style={styles.safeArea} edges={["top"]}>
|
||||
<View style={styles.container}>
|
||||
{/* Header */}
|
||||
<Text style={styles.titleText}>Nhật ký chuyến đi</Text>
|
||||
<Text style={styles.titleText}>{t("diary.title")}</Text>
|
||||
|
||||
{/* Search Bar */}
|
||||
<SearchBar onSearch={handleSearch} style={{ marginBottom: 10 }} />
|
||||
|
||||
{/* Filter Button */}
|
||||
<FilterButton onPress={handleFilter} />
|
||||
|
||||
{/* Trip Count & Add Button */}
|
||||
<View style={styles.headerRow}>
|
||||
<Text style={styles.countText}>
|
||||
Danh sách chuyến đi ({filteredTrips.length})
|
||||
</Text>
|
||||
{/* Filter & Add Button Row */}
|
||||
<View style={styles.actionRow}>
|
||||
<FilterButton
|
||||
onPress={handleFilter}
|
||||
isFiltered={
|
||||
filters.status !== null ||
|
||||
filters.startDate !== null ||
|
||||
filters.endDate !== null ||
|
||||
filters.selectedShip !== null
|
||||
}
|
||||
/>
|
||||
<TouchableOpacity
|
||||
style={styles.addButton}
|
||||
onPress={() => console.log("Add trip")}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<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>
|
||||
</View>
|
||||
|
||||
{/* Trip Count */}
|
||||
<Text style={styles.countText}>
|
||||
{t("diary.tripListCount", { count: tripsList?.total || 0 })}
|
||||
</Text>
|
||||
|
||||
{/* Trip List */}
|
||||
<ScrollView
|
||||
style={styles.scrollView}
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
{filteredTrips.map((trip) => (
|
||||
{tripsList?.trips?.map((trip) => (
|
||||
<TripCard
|
||||
key={trip.id}
|
||||
trip={trip}
|
||||
@@ -169,10 +187,10 @@ export default function diary() {
|
||||
/>
|
||||
))}
|
||||
|
||||
{filteredTrips.length === 0 && (
|
||||
{(!tripsList || !tripsList.trips || tripsList.trips.length === 0) && (
|
||||
<View style={styles.emptyState}>
|
||||
<Text style={styles.emptyText}>
|
||||
Không tìm thấy chuyến đi phù hợp
|
||||
{t("diary.noTripsFound")}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
@@ -210,6 +228,13 @@ const styles = StyleSheet.create({
|
||||
default: "System",
|
||||
}),
|
||||
},
|
||||
actionRow: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "flex-start",
|
||||
alignItems: "center",
|
||||
gap: 12,
|
||||
marginBottom: 12,
|
||||
},
|
||||
headerRow: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
@@ -226,6 +251,7 @@ const styles = StyleSheet.create({
|
||||
android: "Roboto",
|
||||
default: "System",
|
||||
}),
|
||||
marginBottom: 10,
|
||||
},
|
||||
addButton: {
|
||||
flexDirection: "row",
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
} from "react-native";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import DateTimePicker from "@react-native-community/datetimepicker";
|
||||
import { useI18n } from "@/hooks/use-i18n";
|
||||
|
||||
interface DateRangePickerProps {
|
||||
startDate: Date | null;
|
||||
@@ -23,6 +24,7 @@ export default function DateRangePicker({
|
||||
onStartDateChange,
|
||||
onEndDateChange,
|
||||
}: DateRangePickerProps) {
|
||||
const { t } = useI18n();
|
||||
const [showStartPicker, setShowStartPicker] = useState(false);
|
||||
const [showEndPicker, setShowEndPicker] = useState(false);
|
||||
|
||||
@@ -50,7 +52,7 @@ export default function DateRangePicker({
|
||||
|
||||
return (
|
||||
<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}>
|
||||
{/* Start Date */}
|
||||
<TouchableOpacity
|
||||
@@ -59,7 +61,7 @@ export default function DateRangePicker({
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Text style={[styles.dateText, !startDate && styles.placeholder]}>
|
||||
{startDate ? formatDate(startDate) : "Ngày bắt đầu"}
|
||||
{startDate ? formatDate(startDate) : t("diary.dateRangePicker.startDate")}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
@@ -77,7 +79,7 @@ export default function DateRangePicker({
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Text style={[styles.dateText, !endDate && styles.placeholder]}>
|
||||
{endDate ? formatDate(endDate) : "Ngày kết thúc"}
|
||||
{endDate ? formatDate(endDate) : t("diary.dateRangePicker.endDate")}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
@@ -96,11 +98,11 @@ export default function DateRangePicker({
|
||||
<View style={styles.pickerContainer}>
|
||||
<View style={styles.pickerHeader}>
|
||||
<TouchableOpacity onPress={() => setShowStartPicker(false)}>
|
||||
<Text style={styles.cancelButton}>Hủy</Text>
|
||||
<Text style={styles.cancelButton}>{t("common.cancel")}</Text>
|
||||
</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)}>
|
||||
<Text style={styles.doneButton}>Xong</Text>
|
||||
<Text style={styles.doneButton}>{t("diary.dateRangePicker.done")}</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<DateTimePicker
|
||||
@@ -122,11 +124,11 @@ export default function DateRangePicker({
|
||||
<View style={styles.pickerContainer}>
|
||||
<View style={styles.pickerHeader}>
|
||||
<TouchableOpacity onPress={() => setShowEndPicker(false)}>
|
||||
<Text style={styles.cancelButton}>Hủy</Text>
|
||||
<Text style={styles.cancelButton}>{t("common.cancel")}</Text>
|
||||
</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)}>
|
||||
<Text style={styles.doneButton}>Xong</Text>
|
||||
<Text style={styles.doneButton}>{t("diary.dateRangePicker.done")}</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<DateTimePicker
|
||||
|
||||
@@ -1,20 +1,41 @@
|
||||
import React from "react";
|
||||
import { TouchableOpacity, Text, StyleSheet, Platform } from "react-native";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { useI18n } from "@/hooks/use-i18n";
|
||||
|
||||
interface FilterButtonProps {
|
||||
onPress?: () => void;
|
||||
isFiltered?: boolean;
|
||||
}
|
||||
|
||||
export default function FilterButton({ onPress }: FilterButtonProps) {
|
||||
export default function FilterButton({
|
||||
onPress,
|
||||
isFiltered,
|
||||
}: FilterButtonProps) {
|
||||
const { t } = useI18n();
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={styles.button}
|
||||
onPress={onPress}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Ionicons name="filter" size={20} color="#374151" />
|
||||
<Text style={styles.text}>Bộ lọc</Text>
|
||||
<Ionicons
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,33 @@ import {
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import StatusDropdown from "./StatusDropdown";
|
||||
import DateRangePicker from "./DateRangePicker";
|
||||
import ShipDropdown from "./ShipDropdown";
|
||||
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 {
|
||||
visible: boolean;
|
||||
@@ -19,10 +45,16 @@ interface FilterModalProps {
|
||||
onApply: (filters: FilterValues) => void;
|
||||
}
|
||||
|
||||
export interface ShipOption {
|
||||
id: string;
|
||||
shipName: string;
|
||||
}
|
||||
|
||||
export interface FilterValues {
|
||||
status: TripStatus | null;
|
||||
status: TripStatus | null; // number (0-5) hoặc null
|
||||
startDate: Date | null;
|
||||
endDate: Date | null;
|
||||
selectedShip: ShipOption | null; // Tàu được chọn
|
||||
}
|
||||
|
||||
export default function FilterModal({
|
||||
@@ -30,22 +62,30 @@ export default function FilterModal({
|
||||
onClose,
|
||||
onApply,
|
||||
}: FilterModalProps) {
|
||||
const { t } = useI18n();
|
||||
const mapStatusNumberToString = useMapStatusNumberToString();
|
||||
const [status, setStatus] = useState<TripStatus | null>(null);
|
||||
const [startDate, setStartDate] = useState<Date | null>(null);
|
||||
const [endDate, setEndDate] = useState<Date | null>(null);
|
||||
const [selectedShip, setSelectedShip] = useState<ShipOption | null>(null);
|
||||
|
||||
const handleReset = () => {
|
||||
setStatus(null);
|
||||
setStartDate(null);
|
||||
setEndDate(null);
|
||||
setSelectedShip(null);
|
||||
};
|
||||
|
||||
const handleApply = () => {
|
||||
onApply({ status, startDate, endDate });
|
||||
onApply({ status, startDate, endDate, selectedShip });
|
||||
onClose();
|
||||
};
|
||||
|
||||
const hasFilters = status !== null || startDate !== null || endDate !== null;
|
||||
const hasFilters =
|
||||
status !== null ||
|
||||
startDate !== null ||
|
||||
endDate !== null ||
|
||||
selectedShip !== null;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
@@ -54,12 +94,12 @@ export default function FilterModal({
|
||||
transparent
|
||||
onRequestClose={onClose}
|
||||
>
|
||||
<TouchableOpacity
|
||||
<TouchableOpacity
|
||||
style={styles.overlay}
|
||||
activeOpacity={1}
|
||||
onPress={onClose}
|
||||
>
|
||||
<TouchableOpacity
|
||||
<TouchableOpacity
|
||||
style={styles.modalContainer}
|
||||
activeOpacity={1}
|
||||
onPress={(e) => e.stopPropagation()}
|
||||
@@ -69,7 +109,7 @@ export default function FilterModal({
|
||||
<TouchableOpacity onPress={onClose} style={styles.closeButton}>
|
||||
<Ionicons name="close" size={24} color="#111827" />
|
||||
</TouchableOpacity>
|
||||
<Text style={styles.title}>Bộ lọc</Text>
|
||||
<Text style={styles.title}>{t("diary.filter")}</Text>
|
||||
<View style={styles.placeholder} />
|
||||
</View>
|
||||
|
||||
@@ -85,29 +125,37 @@ export default function FilterModal({
|
||||
onStartDateChange={setStartDate}
|
||||
onEndDateChange={setEndDate}
|
||||
/>
|
||||
<ShipDropdown value={selectedShip} onChange={setSelectedShip} />
|
||||
|
||||
{/* Filter Results Preview */}
|
||||
{hasFilters && (
|
||||
<View style={styles.previewContainer}>
|
||||
<Text style={styles.previewTitle}>Bộ lọc đã chọn:</Text>
|
||||
{status && (
|
||||
<Text style={styles.previewTitle}>{t("diary.selectedFilters")}</Text>
|
||||
{status !== null && (
|
||||
<View style={styles.filterTag}>
|
||||
<Text style={styles.filterTagText}>
|
||||
Trạng thái: {status}
|
||||
{t("diary.statusLabel")} {mapStatusNumberToString(status)}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
{startDate && (
|
||||
<View style={styles.filterTag}>
|
||||
<Text style={styles.filterTagText}>
|
||||
Từ: {startDate.toLocaleDateString("vi-VN")}
|
||||
{t("diary.fromLabel")} {startDate.toLocaleDateString("vi-VN")}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
{endDate && (
|
||||
<View style={styles.filterTag}>
|
||||
<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>
|
||||
</View>
|
||||
)}
|
||||
@@ -122,14 +170,14 @@ export default function FilterModal({
|
||||
onPress={handleReset}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Text style={styles.resetButtonText}>Đặt lại</Text>
|
||||
<Text style={styles.resetButtonText}>{t("diary.reset")}</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={styles.applyButton}
|
||||
onPress={handleApply}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Text style={styles.applyButtonText}>Áp dụng</Text>
|
||||
<Text style={styles.applyButtonText}>{t("diary.apply")}</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</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,
|
||||
} from "react-native";
|
||||
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 {
|
||||
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({
|
||||
value,
|
||||
onChange,
|
||||
}: StatusDropdownProps) {
|
||||
const { t } = useI18n();
|
||||
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 =
|
||||
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) => {
|
||||
onChange(status);
|
||||
@@ -42,7 +44,7 @@ export default function StatusDropdown({
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.label}>Trạng thái</Text>
|
||||
<Text style={styles.label}>{t("diary.statusDropdown.label")}</Text>
|
||||
<TouchableOpacity
|
||||
style={styles.selector}
|
||||
onPress={() => setIsOpen(true)}
|
||||
|
||||
@@ -7,10 +7,13 @@ import {
|
||||
Platform,
|
||||
} from "react-native";
|
||||
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 {
|
||||
trip: Trip;
|
||||
trip: Model.Trip;
|
||||
onPress?: () => void;
|
||||
onView?: () => void;
|
||||
onEdit?: () => void;
|
||||
@@ -19,13 +22,37 @@ interface TripCardProps {
|
||||
onDelete?: () => void;
|
||||
}
|
||||
|
||||
export default function TripCard({ trip, onPress, onView, onEdit, onTeam, onSend, onDelete }: TripCardProps) {
|
||||
const statusConfig = TRIP_STATUS_CONFIG[trip.status];
|
||||
export default function TripCard({
|
||||
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
|
||||
const showEdit = trip.status === 'created' || trip.status === 'pending';
|
||||
const showSend = trip.status === 'created';
|
||||
const showDelete = trip.status === 'pending';
|
||||
const showEdit = trip.trip_status === 0 || trip.trip_status === 1;
|
||||
const showSend = trip.trip_status === 0;
|
||||
const showDelete = trip.trip_status === 1;
|
||||
|
||||
return (
|
||||
<View style={styles.card}>
|
||||
@@ -39,8 +66,7 @@ export default function TripCard({ trip, onPress, onView, onEdit, onTeam, onSend
|
||||
color={statusConfig.textColor}
|
||||
/>
|
||||
<View style={styles.titleContainer}>
|
||||
<Text style={styles.title}>{trip.title}</Text>
|
||||
<Text style={styles.code}>{trip.code}</Text>
|
||||
<Text style={styles.title}>{trip.name}</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View
|
||||
@@ -67,25 +93,29 @@ export default function TripCard({ trip, onPress, onView, onEdit, onTeam, onSend
|
||||
{/* Info Grid */}
|
||||
<View style={styles.infoGrid}>
|
||||
<View style={styles.infoRow}>
|
||||
<Text style={styles.label}>Tàu</Text>
|
||||
<Text style={styles.label}>{t("diary.tripCard.shipCode")}</Text>
|
||||
<Text style={styles.value}>
|
||||
{trip.vessel} ({trip.vesselCode})
|
||||
{thingOfTrip?.metadata?.ship_reg_number /* hoặc trip.ship_id */}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.infoRow}>
|
||||
<Text style={styles.label}>Khởi hành</Text>
|
||||
<Text style={styles.value}>{trip.departureDate}</Text>
|
||||
<Text style={styles.label}>{t("diary.tripCard.departure")}</Text>
|
||||
<Text style={styles.value}>
|
||||
{trip.departure_time
|
||||
? dayjs(trip.departure_time).format("DD/MM/YYYY HH:mm")
|
||||
: "-"}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.infoRow}>
|
||||
<Text style={styles.label}>Trở về</Text>
|
||||
<Text style={styles.value}>{trip.returnDate || "-"}</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.infoRow}>
|
||||
<Text style={styles.label}>Thời gian</Text>
|
||||
<Text style={[styles.value, styles.duration]}>{trip.duration}</Text>
|
||||
<Text style={styles.label}>{t("diary.tripCard.return")}</Text>
|
||||
{/* FIXME: trip.returnDate không có trong dữ liệu API, cần mapping từ trip.arrival_time */}
|
||||
<Text style={styles.value}>
|
||||
{trip.arrival_time
|
||||
? dayjs(trip.arrival_time).format("DD/MM/YYYY HH:mm")
|
||||
: "-"}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
@@ -93,34 +123,54 @@ export default function TripCard({ trip, onPress, onView, onEdit, onTeam, onSend
|
||||
{/* Action Buttons */}
|
||||
<View style={styles.divider} />
|
||||
<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" />
|
||||
<Text style={styles.actionText}>View</Text>
|
||||
<Text style={styles.actionText}>{t("diary.tripCard.view")}</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
{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" />
|
||||
<Text style={styles.actionText}>Edit</Text>
|
||||
<Text style={styles.actionText}>{t("diary.tripCard.edit")}</Text>
|
||||
</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" />
|
||||
<Text style={styles.actionText}>Team</Text>
|
||||
<Text style={styles.actionText}>{t("diary.tripCard.team")}</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
{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" />
|
||||
<Text style={styles.actionText}>Send</Text>
|
||||
<Text style={styles.actionText}>{t("diary.tripCard.send")}</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
|
||||
{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" />
|
||||
<Text style={[styles.actionText, styles.deleteText]}>Delete</Text>
|
||||
<Text style={[styles.actionText, styles.deleteText]}>{t("diary.tripCard.delete")}</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
@@ -171,15 +221,7 @@ const styles = StyleSheet.create({
|
||||
default: "System",
|
||||
}),
|
||||
},
|
||||
code: {
|
||||
fontSize: 14,
|
||||
color: "#6B7280",
|
||||
fontFamily: Platform.select({
|
||||
ios: "System",
|
||||
android: "Roboto",
|
||||
default: "System",
|
||||
}),
|
||||
},
|
||||
|
||||
badge: {
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 4,
|
||||
@@ -222,9 +264,6 @@ const styles = StyleSheet.create({
|
||||
default: "System",
|
||||
}),
|
||||
},
|
||||
duration: {
|
||||
color: "#3B82F6",
|
||||
},
|
||||
divider: {
|
||||
height: 1,
|
||||
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 =
|
||||
| "created" // Đã khởi tạo
|
||||
| "pending" // Chờ duyệt
|
||||
| "approved" // Đã duyệt
|
||||
| "in-progress" // Đang hoạt động
|
||||
| "completed" // Hoàn thành
|
||||
| "cancelled"; // Đã hủy
|
||||
|
||||
export interface Trip {
|
||||
id: string;
|
||||
title: string;
|
||||
code: string;
|
||||
vessel: string;
|
||||
vesselCode: string;
|
||||
departureDate: string;
|
||||
returnDate: string | null;
|
||||
duration: string;
|
||||
status: TripStatus;
|
||||
}
|
||||
| 0 // Đã khởi tạo
|
||||
| 1 // Chờ duyệt
|
||||
| 2 // Đã duyệt
|
||||
| 3 // Đang hoạt động
|
||||
| 4 // Hoàn thành
|
||||
| 5; // Đã hủy
|
||||
|
||||
// Static config - dùng khi không cần i18n hoặc ngoài React component
|
||||
export const TRIP_STATUS_CONFIG = {
|
||||
created: {
|
||||
0: {
|
||||
label: "Đã khởi tạo",
|
||||
bgColor: "#F3F4F6", // Gray background
|
||||
textColor: "#4B5563", // Gray text
|
||||
icon: "document-text",
|
||||
},
|
||||
pending: {
|
||||
1: {
|
||||
label: "Chờ duyệt",
|
||||
bgColor: "#FEF3C7", // Yellow background
|
||||
textColor: "#92400E", // Dark yellow text
|
||||
icon: "hourglass",
|
||||
},
|
||||
approved: {
|
||||
2: {
|
||||
label: "Đã duyệt",
|
||||
bgColor: "#E0E7FF", // Indigo background
|
||||
textColor: "#3730A3", // Dark indigo text
|
||||
icon: "checkmark-done",
|
||||
},
|
||||
"in-progress": {
|
||||
3: {
|
||||
label: "Đang hoạt động",
|
||||
bgColor: "#DBEAFE", // Blue background
|
||||
textColor: "#1E40AF", // Dark blue text
|
||||
icon: "sync",
|
||||
},
|
||||
completed: {
|
||||
4: {
|
||||
label: "Hoàn thành",
|
||||
bgColor: "#D1FAE5", // Green background
|
||||
textColor: "#065F46", // Dark green text
|
||||
icon: "checkmark-circle",
|
||||
},
|
||||
cancelled: {
|
||||
5: {
|
||||
label: "Đã hủy",
|
||||
bgColor: "#FEE2E2", // Red background
|
||||
textColor: "#991B1B", // Dark red text
|
||||
icon: "close-circle",
|
||||
},
|
||||
} 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_LAYER_INFO = "/api/sgw/geojson";
|
||||
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_UPDATE_TRIP_STATUS = "/api/sgw/tripState";
|
||||
export const API_HAUL_HANDLE = "/api/sgw/fishingLog";
|
||||
|
||||
@@ -2,6 +2,7 @@ import { api } from "@/config";
|
||||
import {
|
||||
API_GET_TRIP,
|
||||
API_HAUL_HANDLE,
|
||||
API_POST_TRIPSLIST,
|
||||
API_UPDATE_FISHING_LOGS,
|
||||
API_UPDATE_TRIP_STATUS,
|
||||
} from "@/constants";
|
||||
@@ -20,4 +21,8 @@ export async function queryStartNewHaul(body: Model.NewFishingLogRequest) {
|
||||
|
||||
export async function queryUpdateFishingLogs(body: Model.FishingLog) {
|
||||
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;
|
||||
started_at?: number;
|
||||
}
|
||||
|
||||
// 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 {
|
||||
id: string;
|
||||
ship_id: string;
|
||||
|
||||
@@ -60,6 +60,64 @@
|
||||
"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": {
|
||||
"infoTrip": "Trip Information",
|
||||
"createNewTrip": "Create New Trip",
|
||||
|
||||
@@ -60,6 +60,64 @@
|
||||
"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": {
|
||||
"infoTrip": "Thông Tin Chuyến Đ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