update tab diary

This commit is contained in:
2025-12-03 00:10:11 +07:00
parent 80c02fef9d
commit 47e9bac0f9
11 changed files with 1323 additions and 13 deletions

View File

@@ -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 { 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() {
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 (
<SafeAreaView style={{ flex: 1 }}>
<ScrollView contentContainerStyle={styles.scrollContent}>
<SafeAreaView style={styles.safeArea}>
<View style={styles.container}>
{/* Header */}
<Text style={styles.titleText}>Nhật 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>
{/* 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>
</View>
{/* Filter Modal */}
<FilterModal
visible={showFilterModal}
onClose={() => setShowFilterModal(false)}
onApply={handleApplyFilters}
/>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
scrollContent: {
flexGrow: 1,
safeArea: {
flex: 1,
backgroundColor: "#F9FAFB",
},
container: {
alignItems: "center",
padding: 15,
flex: 1,
padding: 16,
},
titleText: {
fontSize: 32,
fontSize: 28,
fontWeight: "700",
lineHeight: 40,
marginBottom: 30,
lineHeight: 36,
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({
ios: "System",
android: "Roboto",

View 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",
}),
},
});

View 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",
}),
},
});

View 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",
}),
},
});

View 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,
},
});

View 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",
},
});

View 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",
},
});

View 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
View 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
View File

@@ -14,6 +14,7 @@
"@islacel/react-native-custom-switch": "^1.0.10",
"@legendapp/motion": "^2.5.3",
"@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/elements": "^2.6.3",
"@react-navigation/native": "^7.1.8",
@@ -3974,6 +3975,29 @@
"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": {
"version": "0.81.5",
"resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.81.5.tgz",

View File

@@ -17,6 +17,7 @@
"@islacel/react-native-custom-switch": "^1.0.10",
"@legendapp/motion": "^2.5.3",
"@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/elements": "^2.6.3",
"@react-navigation/native": "^7.1.8",