update tab nhật ký (Lọc, Ngôn ngữ,...)
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user