update tab diary
This commit is contained in:
@@ -1,31 +1,210 @@
|
|||||||
import { Platform, ScrollView, StyleSheet, Text, View } from "react-native";
|
import { useState } from "react";
|
||||||
|
import { Platform, ScrollView, StyleSheet, Text, TouchableOpacity, View } 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 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";
|
||||||
|
|
||||||
export default function diary() {
|
export default function diary() {
|
||||||
|
const [searchText, setSearchText] = useState("");
|
||||||
|
const [showFilterModal, setShowFilterModal] = useState(false);
|
||||||
|
const [filters, setFilters] = useState<FilterValues>({
|
||||||
|
status: null,
|
||||||
|
startDate: null,
|
||||||
|
endDate: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSearch = (text: string) => {
|
||||||
|
setSearchText(text);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFilter = () => {
|
||||||
|
setShowFilterModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleApplyFilters = (newFilters: FilterValues) => {
|
||||||
|
setFilters(newFilters);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTripPress = (tripId: string) => {
|
||||||
|
// TODO: Navigate to trip detail
|
||||||
|
console.log("Trip pressed:", tripId);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={{ flex: 1 }}>
|
<SafeAreaView style={styles.safeArea}>
|
||||||
<ScrollView contentContainerStyle={styles.scrollContent}>
|
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<Text style={styles.titleText}>Nhật ký chuyến đi </Text>
|
{/* Header */}
|
||||||
|
<Text style={styles.titleText}>Nhật ký chuyến đi</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>
|
||||||
|
<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>
|
||||||
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
{/* Trip List */}
|
||||||
|
<ScrollView
|
||||||
|
style={styles.scrollView}
|
||||||
|
contentContainerStyle={styles.scrollContent}
|
||||||
|
showsVerticalScrollIndicator={false}
|
||||||
|
>
|
||||||
|
{filteredTrips.map((trip) => (
|
||||||
|
<TripCard
|
||||||
|
key={trip.id}
|
||||||
|
trip={trip}
|
||||||
|
onPress={() => handleTripPress(trip.id)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{filteredTrips.length === 0 && (
|
||||||
|
<View style={styles.emptyState}>
|
||||||
|
<Text style={styles.emptyText}>
|
||||||
|
Không tìm thấy chuyến đi phù hợp
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Filter Modal */}
|
||||||
|
<FilterModal
|
||||||
|
visible={showFilterModal}
|
||||||
|
onClose={() => setShowFilterModal(false)}
|
||||||
|
onApply={handleApplyFilters}
|
||||||
|
/>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
scrollContent: {
|
safeArea: {
|
||||||
flexGrow: 1,
|
flex: 1,
|
||||||
|
backgroundColor: "#F9FAFB",
|
||||||
},
|
},
|
||||||
container: {
|
container: {
|
||||||
alignItems: "center",
|
flex: 1,
|
||||||
padding: 15,
|
padding: 16,
|
||||||
},
|
},
|
||||||
titleText: {
|
titleText: {
|
||||||
fontSize: 32,
|
fontSize: 28,
|
||||||
fontWeight: "700",
|
fontWeight: "700",
|
||||||
lineHeight: 40,
|
lineHeight: 36,
|
||||||
marginBottom: 30,
|
marginBottom: 20,
|
||||||
|
color: "#111827",
|
||||||
|
fontFamily: Platform.select({
|
||||||
|
ios: "System",
|
||||||
|
android: "Roboto",
|
||||||
|
default: "System",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
headerRow: {
|
||||||
|
flexDirection: "row",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
marginTop: 20,
|
||||||
|
marginBottom: 12,
|
||||||
|
},
|
||||||
|
countText: {
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: "600",
|
||||||
|
color: "#374151",
|
||||||
|
fontFamily: Platform.select({
|
||||||
|
ios: "System",
|
||||||
|
android: "Roboto",
|
||||||
|
default: "System",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
addButton: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
backgroundColor: "#3B82F6",
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
paddingVertical: 8,
|
||||||
|
borderRadius: 8,
|
||||||
|
gap: 6,
|
||||||
|
},
|
||||||
|
addButtonText: {
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: "600",
|
||||||
|
color: "#FFFFFF",
|
||||||
|
fontFamily: Platform.select({
|
||||||
|
ios: "System",
|
||||||
|
android: "Roboto",
|
||||||
|
default: "System",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
scrollView: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
scrollContent: {
|
||||||
|
paddingBottom: 20,
|
||||||
|
},
|
||||||
|
emptyState: {
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
paddingVertical: 60,
|
||||||
|
},
|
||||||
|
emptyText: {
|
||||||
|
fontSize: 16,
|
||||||
|
color: "#9CA3AF",
|
||||||
fontFamily: Platform.select({
|
fontFamily: Platform.select({
|
||||||
ios: "System",
|
ios: "System",
|
||||||
android: "Roboto",
|
android: "Roboto",
|
||||||
|
|||||||
243
components/diary/DateRangePicker.tsx
Normal file
243
components/diary/DateRangePicker.tsx
Normal file
@@ -0,0 +1,243 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import {
|
||||||
|
View,
|
||||||
|
Text,
|
||||||
|
TouchableOpacity,
|
||||||
|
StyleSheet,
|
||||||
|
Platform,
|
||||||
|
Modal,
|
||||||
|
} from "react-native";
|
||||||
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
|
import DateTimePicker from "@react-native-community/datetimepicker";
|
||||||
|
|
||||||
|
interface DateRangePickerProps {
|
||||||
|
startDate: Date | null;
|
||||||
|
endDate: Date | null;
|
||||||
|
onStartDateChange: (date: Date | null) => void;
|
||||||
|
onEndDateChange: (date: Date | null) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function DateRangePicker({
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
onStartDateChange,
|
||||||
|
onEndDateChange,
|
||||||
|
}: DateRangePickerProps) {
|
||||||
|
const [showStartPicker, setShowStartPicker] = useState(false);
|
||||||
|
const [showEndPicker, setShowEndPicker] = useState(false);
|
||||||
|
|
||||||
|
const formatDate = (date: Date | null) => {
|
||||||
|
if (!date) return "";
|
||||||
|
const day = date.getDate().toString().padStart(2, "0");
|
||||||
|
const month = (date.getMonth() + 1).toString().padStart(2, "0");
|
||||||
|
const year = date.getFullYear();
|
||||||
|
return `${day}/${month}/${year}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleStartDateChange = (event: any, selectedDate?: Date) => {
|
||||||
|
setShowStartPicker(Platform.OS === "ios");
|
||||||
|
if (selectedDate) {
|
||||||
|
onStartDateChange(selectedDate);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEndDateChange = (event: any, selectedDate?: Date) => {
|
||||||
|
setShowEndPicker(Platform.OS === "ios");
|
||||||
|
if (selectedDate) {
|
||||||
|
onEndDateChange(selectedDate);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<Text style={styles.label}>Ngày đi</Text>
|
||||||
|
<View style={styles.dateRangeContainer}>
|
||||||
|
{/* Start Date */}
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.dateInput}
|
||||||
|
onPress={() => setShowStartPicker(true)}
|
||||||
|
activeOpacity={0.7}
|
||||||
|
>
|
||||||
|
<Text style={[styles.dateText, !startDate && styles.placeholder]}>
|
||||||
|
{startDate ? formatDate(startDate) : "Ngày bắt đầu"}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
<Ionicons
|
||||||
|
name="arrow-forward"
|
||||||
|
size={20}
|
||||||
|
color="#9CA3AF"
|
||||||
|
style={styles.arrow}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* End Date */}
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.dateInput}
|
||||||
|
onPress={() => setShowEndPicker(true)}
|
||||||
|
activeOpacity={0.7}
|
||||||
|
>
|
||||||
|
<Text style={[styles.dateText, !endDate && styles.placeholder]}>
|
||||||
|
{endDate ? formatDate(endDate) : "Ngày kết thúc"}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.calendarButton}
|
||||||
|
onPress={() => setShowStartPicker(true)}
|
||||||
|
>
|
||||||
|
<Ionicons name="calendar-outline" size={20} color="#6B7280" />
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Start Date Picker */}
|
||||||
|
{showStartPicker && (
|
||||||
|
<Modal transparent animationType="fade" visible={showStartPicker}>
|
||||||
|
<View style={styles.modalOverlay}>
|
||||||
|
<View style={styles.pickerContainer}>
|
||||||
|
<View style={styles.pickerHeader}>
|
||||||
|
<TouchableOpacity onPress={() => setShowStartPicker(false)}>
|
||||||
|
<Text style={styles.cancelButton}>Hủy</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<Text style={styles.pickerTitle}>Chọn ngày bắt đầu</Text>
|
||||||
|
<TouchableOpacity onPress={() => setShowStartPicker(false)}>
|
||||||
|
<Text style={styles.doneButton}>Xong</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
<DateTimePicker
|
||||||
|
value={startDate || new Date()}
|
||||||
|
mode="date"
|
||||||
|
display={Platform.OS === "ios" ? "spinner" : "default"}
|
||||||
|
onChange={handleStartDateChange}
|
||||||
|
maximumDate={endDate || undefined}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* End Date Picker */}
|
||||||
|
{showEndPicker && (
|
||||||
|
<Modal transparent animationType="fade" visible={showEndPicker}>
|
||||||
|
<View style={styles.modalOverlay}>
|
||||||
|
<View style={styles.pickerContainer}>
|
||||||
|
<View style={styles.pickerHeader}>
|
||||||
|
<TouchableOpacity onPress={() => setShowEndPicker(false)}>
|
||||||
|
<Text style={styles.cancelButton}>Hủy</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<Text style={styles.pickerTitle}>Chọn ngày kết thúc</Text>
|
||||||
|
<TouchableOpacity onPress={() => setShowEndPicker(false)}>
|
||||||
|
<Text style={styles.doneButton}>Xong</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
<DateTimePicker
|
||||||
|
value={endDate || new Date()}
|
||||||
|
mode="date"
|
||||||
|
display={Platform.OS === "ios" ? "spinner" : "default"}
|
||||||
|
onChange={handleEndDateChange}
|
||||||
|
minimumDate={startDate || undefined}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</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",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
dateRangeContainer: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 8,
|
||||||
|
},
|
||||||
|
dateInput: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: "#FFFFFF",
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: "#D1D5DB",
|
||||||
|
borderRadius: 8,
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
paddingVertical: 12,
|
||||||
|
},
|
||||||
|
dateText: {
|
||||||
|
fontSize: 16,
|
||||||
|
color: "#111827",
|
||||||
|
fontFamily: Platform.select({
|
||||||
|
ios: "System",
|
||||||
|
android: "Roboto",
|
||||||
|
default: "System",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
placeholder: {
|
||||||
|
color: "#9CA3AF",
|
||||||
|
},
|
||||||
|
arrow: {
|
||||||
|
marginHorizontal: 4,
|
||||||
|
},
|
||||||
|
calendarButton: {
|
||||||
|
padding: 8,
|
||||||
|
},
|
||||||
|
modalOverlay: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: "rgba(0, 0, 0, 0.5)",
|
||||||
|
justifyContent: "flex-end",
|
||||||
|
},
|
||||||
|
pickerContainer: {
|
||||||
|
backgroundColor: "#FFFFFF",
|
||||||
|
borderTopLeftRadius: 20,
|
||||||
|
borderTopRightRadius: 20,
|
||||||
|
paddingBottom: 20,
|
||||||
|
},
|
||||||
|
pickerHeader: {
|
||||||
|
flexDirection: "row",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
paddingHorizontal: 20,
|
||||||
|
paddingVertical: 16,
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
borderBottomColor: "#F3F4F6",
|
||||||
|
},
|
||||||
|
pickerTitle: {
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: "600",
|
||||||
|
color: "#111827",
|
||||||
|
fontFamily: Platform.select({
|
||||||
|
ios: "System",
|
||||||
|
android: "Roboto",
|
||||||
|
default: "System",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
cancelButton: {
|
||||||
|
fontSize: 16,
|
||||||
|
color: "#6B7280",
|
||||||
|
fontFamily: Platform.select({
|
||||||
|
ios: "System",
|
||||||
|
android: "Roboto",
|
||||||
|
default: "System",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
doneButton: {
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: "600",
|
||||||
|
color: "#3B82F6",
|
||||||
|
fontFamily: Platform.select({
|
||||||
|
ios: "System",
|
||||||
|
android: "Roboto",
|
||||||
|
default: "System",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
});
|
||||||
53
components/diary/FilterButton.tsx
Normal file
53
components/diary/FilterButton.tsx
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { TouchableOpacity, Text, StyleSheet, Platform } from "react-native";
|
||||||
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
|
|
||||||
|
interface FilterButtonProps {
|
||||||
|
onPress?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function FilterButton({ onPress }: FilterButtonProps) {
|
||||||
|
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>
|
||||||
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
button: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
backgroundColor: "#FFFFFF",
|
||||||
|
borderRadius: 12,
|
||||||
|
paddingHorizontal: 20,
|
||||||
|
paddingVertical: 12,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: "#E5E7EB",
|
||||||
|
shadowColor: "#000",
|
||||||
|
shadowOffset: {
|
||||||
|
width: 0,
|
||||||
|
height: 1,
|
||||||
|
},
|
||||||
|
shadowOpacity: 0.05,
|
||||||
|
shadowRadius: 2,
|
||||||
|
elevation: 1,
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: "500",
|
||||||
|
color: "#374151",
|
||||||
|
marginLeft: 8,
|
||||||
|
fontFamily: Platform.select({
|
||||||
|
ios: "System",
|
||||||
|
android: "Roboto",
|
||||||
|
default: "System",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
});
|
||||||
264
components/diary/FilterModal.tsx
Normal file
264
components/diary/FilterModal.tsx
Normal file
@@ -0,0 +1,264 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import {
|
||||||
|
View,
|
||||||
|
Text,
|
||||||
|
Modal,
|
||||||
|
TouchableOpacity,
|
||||||
|
StyleSheet,
|
||||||
|
Platform,
|
||||||
|
ScrollView,
|
||||||
|
} from "react-native";
|
||||||
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
|
import StatusDropdown from "./StatusDropdown";
|
||||||
|
import DateRangePicker from "./DateRangePicker";
|
||||||
|
import { TripStatus } from "./types";
|
||||||
|
|
||||||
|
interface FilterModalProps {
|
||||||
|
visible: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onApply: (filters: FilterValues) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FilterValues {
|
||||||
|
status: TripStatus | null;
|
||||||
|
startDate: Date | null;
|
||||||
|
endDate: Date | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function FilterModal({
|
||||||
|
visible,
|
||||||
|
onClose,
|
||||||
|
onApply,
|
||||||
|
}: FilterModalProps) {
|
||||||
|
const [status, setStatus] = useState<TripStatus | null>(null);
|
||||||
|
const [startDate, setStartDate] = useState<Date | null>(null);
|
||||||
|
const [endDate, setEndDate] = useState<Date | null>(null);
|
||||||
|
|
||||||
|
const handleReset = () => {
|
||||||
|
setStatus(null);
|
||||||
|
setStartDate(null);
|
||||||
|
setEndDate(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleApply = () => {
|
||||||
|
onApply({ status, startDate, endDate });
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const hasFilters = status !== null || startDate !== null || endDate !== null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
visible={visible}
|
||||||
|
animationType="fade"
|
||||||
|
transparent
|
||||||
|
onRequestClose={onClose}
|
||||||
|
>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.overlay}
|
||||||
|
activeOpacity={1}
|
||||||
|
onPress={onClose}
|
||||||
|
>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.modalContainer}
|
||||||
|
activeOpacity={1}
|
||||||
|
onPress={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
{/* Header */}
|
||||||
|
<View style={styles.header}>
|
||||||
|
<TouchableOpacity onPress={onClose} style={styles.closeButton}>
|
||||||
|
<Ionicons name="close" size={24} color="#111827" />
|
||||||
|
</TouchableOpacity>
|
||||||
|
<Text style={styles.title}>Bộ lọc</Text>
|
||||||
|
<View style={styles.placeholder} />
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<ScrollView
|
||||||
|
style={styles.content}
|
||||||
|
showsVerticalScrollIndicator={false}
|
||||||
|
>
|
||||||
|
<StatusDropdown value={status} onChange={setStatus} />
|
||||||
|
<DateRangePicker
|
||||||
|
startDate={startDate}
|
||||||
|
endDate={endDate}
|
||||||
|
onStartDateChange={setStartDate}
|
||||||
|
onEndDateChange={setEndDate}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Filter Results Preview */}
|
||||||
|
{hasFilters && (
|
||||||
|
<View style={styles.previewContainer}>
|
||||||
|
<Text style={styles.previewTitle}>Bộ lọc đã chọn:</Text>
|
||||||
|
{status && (
|
||||||
|
<View style={styles.filterTag}>
|
||||||
|
<Text style={styles.filterTagText}>
|
||||||
|
Trạng thái: {status}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
{startDate && (
|
||||||
|
<View style={styles.filterTag}>
|
||||||
|
<Text style={styles.filterTagText}>
|
||||||
|
Từ: {startDate.toLocaleDateString("vi-VN")}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
{endDate && (
|
||||||
|
<View style={styles.filterTag}>
|
||||||
|
<Text style={styles.filterTagText}>
|
||||||
|
Đến: {endDate.toLocaleDateString("vi-VN")}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<View style={styles.footer}>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.resetButton}
|
||||||
|
onPress={handleReset}
|
||||||
|
activeOpacity={0.7}
|
||||||
|
>
|
||||||
|
<Text style={styles.resetButtonText}>Đặt lại</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.applyButton}
|
||||||
|
onPress={handleApply}
|
||||||
|
activeOpacity={0.7}
|
||||||
|
>
|
||||||
|
<Text style={styles.applyButtonText}>Áp dụng</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
overlay: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: "rgba(0, 0, 0, 0.5)",
|
||||||
|
justifyContent: "flex-end",
|
||||||
|
},
|
||||||
|
modalContainer: {
|
||||||
|
backgroundColor: "#FFFFFF",
|
||||||
|
borderTopLeftRadius: 24,
|
||||||
|
borderTopRightRadius: 24,
|
||||||
|
maxHeight: "80%",
|
||||||
|
shadowColor: "#000",
|
||||||
|
shadowOffset: {
|
||||||
|
width: 0,
|
||||||
|
height: -4,
|
||||||
|
},
|
||||||
|
shadowOpacity: 0.1,
|
||||||
|
shadowRadius: 12,
|
||||||
|
elevation: 8,
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
paddingHorizontal: 20,
|
||||||
|
paddingVertical: 16,
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
borderBottomColor: "#F3F4F6",
|
||||||
|
},
|
||||||
|
closeButton: {
|
||||||
|
padding: 4,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: "700",
|
||||||
|
color: "#111827",
|
||||||
|
fontFamily: Platform.select({
|
||||||
|
ios: "System",
|
||||||
|
android: "Roboto",
|
||||||
|
default: "System",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
placeholder: {
|
||||||
|
width: 32,
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
padding: 20,
|
||||||
|
},
|
||||||
|
previewContainer: {
|
||||||
|
marginTop: 20,
|
||||||
|
padding: 16,
|
||||||
|
backgroundColor: "#F9FAFB",
|
||||||
|
borderRadius: 12,
|
||||||
|
},
|
||||||
|
previewTitle: {
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: "600",
|
||||||
|
color: "#6B7280",
|
||||||
|
marginBottom: 12,
|
||||||
|
fontFamily: Platform.select({
|
||||||
|
ios: "System",
|
||||||
|
android: "Roboto",
|
||||||
|
default: "System",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
filterTag: {
|
||||||
|
backgroundColor: "#EFF6FF",
|
||||||
|
paddingHorizontal: 12,
|
||||||
|
paddingVertical: 6,
|
||||||
|
borderRadius: 16,
|
||||||
|
marginBottom: 8,
|
||||||
|
alignSelf: "flex-start",
|
||||||
|
},
|
||||||
|
filterTagText: {
|
||||||
|
fontSize: 14,
|
||||||
|
color: "#3B82F6",
|
||||||
|
fontFamily: Platform.select({
|
||||||
|
ios: "System",
|
||||||
|
android: "Roboto",
|
||||||
|
default: "System",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
footer: {
|
||||||
|
flexDirection: "row",
|
||||||
|
gap: 12,
|
||||||
|
padding: 20,
|
||||||
|
borderTopWidth: 1,
|
||||||
|
borderTopColor: "#F3F4F6",
|
||||||
|
},
|
||||||
|
resetButton: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: "#F3F4F6",
|
||||||
|
paddingVertical: 14,
|
||||||
|
borderRadius: 12,
|
||||||
|
alignItems: "center",
|
||||||
|
},
|
||||||
|
resetButtonText: {
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: "600",
|
||||||
|
color: "#6B7280",
|
||||||
|
fontFamily: Platform.select({
|
||||||
|
ios: "System",
|
||||||
|
android: "Roboto",
|
||||||
|
default: "System",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
applyButton: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: "#3B82F6",
|
||||||
|
paddingVertical: 14,
|
||||||
|
borderRadius: 12,
|
||||||
|
alignItems: "center",
|
||||||
|
},
|
||||||
|
applyButtonText: {
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: "600",
|
||||||
|
color: "#FFFFFF",
|
||||||
|
fontFamily: Platform.select({
|
||||||
|
ios: "System",
|
||||||
|
android: "Roboto",
|
||||||
|
default: "System",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
});
|
||||||
68
components/diary/SearchBar.tsx
Normal file
68
components/diary/SearchBar.tsx
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
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,
|
||||||
|
},
|
||||||
|
});
|
||||||
183
components/diary/StatusDropdown.tsx
Normal file
183
components/diary/StatusDropdown.tsx
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import {
|
||||||
|
View,
|
||||||
|
Text,
|
||||||
|
TouchableOpacity,
|
||||||
|
StyleSheet,
|
||||||
|
Modal,
|
||||||
|
Platform,
|
||||||
|
ScrollView,
|
||||||
|
} from "react-native";
|
||||||
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
|
import { TripStatus, TRIP_STATUS_CONFIG } from "./types";
|
||||||
|
|
||||||
|
interface StatusDropdownProps {
|
||||||
|
value: TripStatus | null;
|
||||||
|
onChange: (status: TripStatus | null) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const STATUS_OPTIONS: Array<{ value: TripStatus | null; label: string }> = [
|
||||||
|
{ value: null, label: "Vui lòng chọn" },
|
||||||
|
{ value: "completed", label: "Hoàn thành" },
|
||||||
|
{ value: "in-progress", label: "Đang hoạt động" },
|
||||||
|
{ value: "quality-check", label: "Đã khởi tạo" },
|
||||||
|
{ value: "cancelled", label: "Đã hủy" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function StatusDropdown({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
}: StatusDropdownProps) {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
|
const selectedLabel =
|
||||||
|
STATUS_OPTIONS.find((opt) => opt.value === value)?.label || "Vui lòng chọn";
|
||||||
|
|
||||||
|
const handleSelect = (status: TripStatus | null) => {
|
||||||
|
onChange(status);
|
||||||
|
setIsOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<Text style={styles.label}>Trạng thái</Text>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.selector}
|
||||||
|
onPress={() => setIsOpen(true)}
|
||||||
|
activeOpacity={0.7}
|
||||||
|
>
|
||||||
|
<Text style={[styles.selectorText, !value && styles.placeholder]}>
|
||||||
|
{selectedLabel}
|
||||||
|
</Text>
|
||||||
|
<Ionicons name="ellipsis-horizontal" 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}>
|
||||||
|
<ScrollView>
|
||||||
|
{STATUS_OPTIONS.map((option, index) => (
|
||||||
|
<TouchableOpacity
|
||||||
|
key={index}
|
||||||
|
style={[
|
||||||
|
styles.option,
|
||||||
|
value === option.value && styles.selectedOption,
|
||||||
|
]}
|
||||||
|
onPress={() => handleSelect(option.value)}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.optionText,
|
||||||
|
value === option.value && styles.selectedOptionText,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{option.label}
|
||||||
|
</Text>
|
||||||
|
{value === option.value && (
|
||||||
|
<Ionicons name="checkmark" size={20} color="#3B82F6" />
|
||||||
|
)}
|
||||||
|
</TouchableOpacity>
|
||||||
|
))}
|
||||||
|
</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",
|
||||||
|
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: "80%",
|
||||||
|
maxHeight: "60%",
|
||||||
|
overflow: "hidden",
|
||||||
|
shadowColor: "#000",
|
||||||
|
shadowOffset: {
|
||||||
|
width: 0,
|
||||||
|
height: 4,
|
||||||
|
},
|
||||||
|
shadowOpacity: 0.3,
|
||||||
|
shadowRadius: 8,
|
||||||
|
elevation: 8,
|
||||||
|
},
|
||||||
|
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",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
selectedOptionText: {
|
||||||
|
color: "#3B82F6",
|
||||||
|
fontWeight: "600",
|
||||||
|
},
|
||||||
|
});
|
||||||
181
components/diary/TripCard.tsx
Normal file
181
components/diary/TripCard.tsx
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
import React from "react";
|
||||||
|
import {
|
||||||
|
View,
|
||||||
|
Text,
|
||||||
|
StyleSheet,
|
||||||
|
TouchableOpacity,
|
||||||
|
Platform,
|
||||||
|
} from "react-native";
|
||||||
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
|
import { Trip, TRIP_STATUS_CONFIG } from "./types";
|
||||||
|
|
||||||
|
interface TripCardProps {
|
||||||
|
trip: Trip;
|
||||||
|
onPress?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function TripCard({ trip, onPress }: TripCardProps) {
|
||||||
|
const statusConfig = TRIP_STATUS_CONFIG[trip.status];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TouchableOpacity style={styles.card} onPress={onPress} activeOpacity={0.7}>
|
||||||
|
{/* Header */}
|
||||||
|
<View style={styles.header}>
|
||||||
|
<View style={styles.headerLeft}>
|
||||||
|
<Ionicons
|
||||||
|
name={statusConfig.icon as any}
|
||||||
|
size={24}
|
||||||
|
color={statusConfig.textColor}
|
||||||
|
/>
|
||||||
|
<View style={styles.titleContainer}>
|
||||||
|
<Text style={styles.title}>{trip.title}</Text>
|
||||||
|
<Text style={styles.code}>{trip.code}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
styles.badge,
|
||||||
|
{
|
||||||
|
backgroundColor: statusConfig.bgColor,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.badgeText,
|
||||||
|
{
|
||||||
|
color: statusConfig.textColor,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{statusConfig.label}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Info Grid */}
|
||||||
|
<View style={styles.infoGrid}>
|
||||||
|
<View style={styles.infoRow}>
|
||||||
|
<Text style={styles.label}>Tàu</Text>
|
||||||
|
<Text style={styles.value}>
|
||||||
|
{trip.vessel} ({trip.vesselCode})
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.infoRow}>
|
||||||
|
<Text style={styles.label}>Khởi hành</Text>
|
||||||
|
<Text style={styles.value}>{trip.departureDate}</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>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
card: {
|
||||||
|
backgroundColor: "#FFFFFF",
|
||||||
|
borderRadius: 12,
|
||||||
|
padding: 16,
|
||||||
|
marginBottom: 12,
|
||||||
|
shadowColor: "#000",
|
||||||
|
shadowOffset: {
|
||||||
|
width: 0,
|
||||||
|
height: 2,
|
||||||
|
},
|
||||||
|
shadowOpacity: 0.05,
|
||||||
|
shadowRadius: 8,
|
||||||
|
elevation: 2,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: "#F3F4F6",
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
flexDirection: "row",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "flex-start",
|
||||||
|
marginBottom: 16,
|
||||||
|
},
|
||||||
|
headerLeft: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
titleContainer: {
|
||||||
|
marginLeft: 12,
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: "600",
|
||||||
|
color: "#111827",
|
||||||
|
marginBottom: 2,
|
||||||
|
fontFamily: Platform.select({
|
||||||
|
ios: "System",
|
||||||
|
android: "Roboto",
|
||||||
|
default: "System",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
code: {
|
||||||
|
fontSize: 14,
|
||||||
|
color: "#6B7280",
|
||||||
|
fontFamily: Platform.select({
|
||||||
|
ios: "System",
|
||||||
|
android: "Roboto",
|
||||||
|
default: "System",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
badge: {
|
||||||
|
paddingHorizontal: 12,
|
||||||
|
paddingVertical: 4,
|
||||||
|
borderRadius: 12,
|
||||||
|
},
|
||||||
|
badgeText: {
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: "500",
|
||||||
|
fontFamily: Platform.select({
|
||||||
|
ios: "System",
|
||||||
|
android: "Roboto",
|
||||||
|
default: "System",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
infoGrid: {
|
||||||
|
gap: 12,
|
||||||
|
},
|
||||||
|
infoRow: {
|
||||||
|
flexDirection: "row",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
fontSize: 14,
|
||||||
|
color: "#6B7280",
|
||||||
|
fontFamily: Platform.select({
|
||||||
|
ios: "System",
|
||||||
|
android: "Roboto",
|
||||||
|
default: "System",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
fontSize: 14,
|
||||||
|
color: "#111827",
|
||||||
|
fontWeight: "500",
|
||||||
|
textAlign: "right",
|
||||||
|
fontFamily: Platform.select({
|
||||||
|
ios: "System",
|
||||||
|
android: "Roboto",
|
||||||
|
default: "System",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
duration: {
|
||||||
|
color: "#3B82F6",
|
||||||
|
},
|
||||||
|
});
|
||||||
70
components/diary/mockData.ts
Normal file
70
components/diary/mockData.ts
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
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: "quality-check",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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",
|
||||||
|
},
|
||||||
|
];
|
||||||
44
components/diary/types.ts
Normal file
44
components/diary/types.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
export type TripStatus =
|
||||||
|
| "completed"
|
||||||
|
| "in-progress"
|
||||||
|
| "cancelled"
|
||||||
|
| "quality-check";
|
||||||
|
|
||||||
|
export interface Trip {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
code: string;
|
||||||
|
vessel: string;
|
||||||
|
vesselCode: string;
|
||||||
|
departureDate: string;
|
||||||
|
returnDate: string | null;
|
||||||
|
duration: string;
|
||||||
|
status: TripStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TRIP_STATUS_CONFIG = {
|
||||||
|
completed: {
|
||||||
|
label: "Hoàn thành",
|
||||||
|
bgColor: "#D1FAE5",
|
||||||
|
textColor: "#065F46",
|
||||||
|
icon: "checkmark-circle",
|
||||||
|
},
|
||||||
|
"in-progress": {
|
||||||
|
label: "Đang diễn ra",
|
||||||
|
bgColor: "#DBEAFE",
|
||||||
|
textColor: "#1E40AF",
|
||||||
|
icon: "time",
|
||||||
|
},
|
||||||
|
cancelled: {
|
||||||
|
label: "Đã hủy",
|
||||||
|
bgColor: "#FEE2E2",
|
||||||
|
textColor: "#991B1B",
|
||||||
|
icon: "close-circle",
|
||||||
|
},
|
||||||
|
"quality-check": {
|
||||||
|
label: "Khảo sát địa chất",
|
||||||
|
bgColor: "#D1FAE5",
|
||||||
|
textColor: "#065F46",
|
||||||
|
icon: "checkmark-circle",
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
24
package-lock.json
generated
24
package-lock.json
generated
@@ -14,6 +14,7 @@
|
|||||||
"@islacel/react-native-custom-switch": "^1.0.10",
|
"@islacel/react-native-custom-switch": "^1.0.10",
|
||||||
"@legendapp/motion": "^2.5.3",
|
"@legendapp/motion": "^2.5.3",
|
||||||
"@react-native-async-storage/async-storage": "2.2.0",
|
"@react-native-async-storage/async-storage": "2.2.0",
|
||||||
|
"@react-native-community/datetimepicker": "8.4.4",
|
||||||
"@react-navigation/bottom-tabs": "^7.4.0",
|
"@react-navigation/bottom-tabs": "^7.4.0",
|
||||||
"@react-navigation/elements": "^2.6.3",
|
"@react-navigation/elements": "^2.6.3",
|
||||||
"@react-navigation/native": "^7.1.8",
|
"@react-navigation/native": "^7.1.8",
|
||||||
@@ -3974,6 +3975,29 @@
|
|||||||
"react-native": "^0.0.0-0 || >=0.65 <1.0"
|
"react-native": "^0.0.0-0 || >=0.65 <1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@react-native-community/datetimepicker": {
|
||||||
|
"version": "8.4.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@react-native-community/datetimepicker/-/datetimepicker-8.4.4.tgz",
|
||||||
|
"integrity": "sha512-bc4ZixEHxZC9/qf5gbdYvIJiLZ5CLmEsC3j+Yhe1D1KC/3QhaIfGDVdUcid0PdlSoGOSEq4VlB93AWyetEyBSQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"invariant": "^2.2.4"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"expo": ">=52.0.0",
|
||||||
|
"react": "*",
|
||||||
|
"react-native": "*",
|
||||||
|
"react-native-windows": "*"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"expo": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react-native-windows": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@react-native/assets-registry": {
|
"node_modules/@react-native/assets-registry": {
|
||||||
"version": "0.81.5",
|
"version": "0.81.5",
|
||||||
"resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.81.5.tgz",
|
"resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.81.5.tgz",
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
"@islacel/react-native-custom-switch": "^1.0.10",
|
"@islacel/react-native-custom-switch": "^1.0.10",
|
||||||
"@legendapp/motion": "^2.5.3",
|
"@legendapp/motion": "^2.5.3",
|
||||||
"@react-native-async-storage/async-storage": "2.2.0",
|
"@react-native-async-storage/async-storage": "2.2.0",
|
||||||
|
"@react-native-community/datetimepicker": "8.4.4",
|
||||||
"@react-navigation/bottom-tabs": "^7.4.0",
|
"@react-navigation/bottom-tabs": "^7.4.0",
|
||||||
"@react-navigation/elements": "^2.6.3",
|
"@react-navigation/elements": "^2.6.3",
|
||||||
"@react-navigation/native": "^7.1.8",
|
"@react-navigation/native": "^7.1.8",
|
||||||
|
|||||||
Reference in New Issue
Block a user