Cập nhật tab Nhật ký ( CRUD chuyến đi, CRUD thuyền viên trong chuyến đi )
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import React, { useState } from "react";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
@@ -20,6 +20,8 @@ interface TripDurationPickerProps {
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
type PickerType = "startDate" | "startTime" | "endDate" | "endTime" | null;
|
||||
|
||||
export default function TripDurationPicker({
|
||||
startDate,
|
||||
endDate,
|
||||
@@ -27,15 +29,27 @@ export default function TripDurationPicker({
|
||||
onEndDateChange,
|
||||
disabled = false,
|
||||
}: TripDurationPickerProps) {
|
||||
const { t } = useI18n();
|
||||
const { t, locale } = useI18n();
|
||||
const { colors, colorScheme } = useThemeContext();
|
||||
const [showStartPicker, setShowStartPicker] = useState(false);
|
||||
const [showEndPicker, setShowEndPicker] = useState(false);
|
||||
|
||||
|
||||
// Single state for which picker is showing
|
||||
const [activePicker, setActivePicker] = useState<PickerType>(null);
|
||||
|
||||
// Temp states to hold the picker value before confirming
|
||||
const [tempStartDate, setTempStartDate] = useState<Date>(new Date());
|
||||
const [tempEndDate, setTempEndDate] = useState<Date>(new Date());
|
||||
|
||||
// State hiển thị thời gian hiện tại
|
||||
const [currentTime, setCurrentTime] = useState<Date>(new Date());
|
||||
|
||||
// Update current time every second
|
||||
useEffect(() => {
|
||||
const timer = setInterval(() => {
|
||||
setCurrentTime(new Date());
|
||||
}, 1000);
|
||||
return () => clearInterval(timer);
|
||||
}, []);
|
||||
|
||||
const formatDate = (date: Date | null) => {
|
||||
if (!date) return "";
|
||||
const day = date.getDate().toString().padStart(2, "0");
|
||||
@@ -44,62 +58,102 @@ export default function TripDurationPicker({
|
||||
return `${day}/${month}/${year}`;
|
||||
};
|
||||
|
||||
const handleOpenStartPicker = () => {
|
||||
const formatTime = (date: Date | null) => {
|
||||
if (!date) return "";
|
||||
const hours = date.getHours().toString().padStart(2, "0");
|
||||
const minutes = date.getMinutes().toString().padStart(2, "0");
|
||||
return `${hours}:${minutes}`;
|
||||
};
|
||||
|
||||
// Open start date picker
|
||||
const handleOpenStartDatePicker = () => {
|
||||
const today = new Date();
|
||||
const dateToUse = startDate || today;
|
||||
// If no date selected, immediately set to today
|
||||
if (!startDate) {
|
||||
onStartDateChange(today);
|
||||
}
|
||||
// Always set tempStartDate to the date we're using (today if no date was selected)
|
||||
setTempStartDate(dateToUse);
|
||||
setShowStartPicker(true);
|
||||
setActivePicker("startDate");
|
||||
};
|
||||
|
||||
const handleOpenEndPicker = () => {
|
||||
// Open start time picker
|
||||
const handleOpenStartTimePicker = () => {
|
||||
const today = new Date();
|
||||
const dateToUse = startDate || today;
|
||||
if (!startDate) {
|
||||
onStartDateChange(today);
|
||||
}
|
||||
setTempStartDate(dateToUse);
|
||||
setActivePicker("startTime");
|
||||
};
|
||||
|
||||
// Open end date picker
|
||||
const handleOpenEndDatePicker = () => {
|
||||
const today = new Date();
|
||||
const dateToUse = endDate || today;
|
||||
// If no date selected, immediately set to today
|
||||
if (!endDate) {
|
||||
onEndDateChange(today);
|
||||
}
|
||||
// Always set tempEndDate to the date we're using (today if no date was selected)
|
||||
setTempEndDate(dateToUse);
|
||||
setShowEndPicker(true);
|
||||
setActivePicker("endDate");
|
||||
};
|
||||
|
||||
const handleStartDateChange = (event: any, selectedDate?: Date) => {
|
||||
// Open end time picker
|
||||
const handleOpenEndTimePicker = () => {
|
||||
const today = new Date();
|
||||
const dateToUse = endDate || today;
|
||||
if (!endDate) {
|
||||
onEndDateChange(today);
|
||||
}
|
||||
setTempEndDate(dateToUse);
|
||||
setActivePicker("endTime");
|
||||
};
|
||||
|
||||
const handleStartPickerChange = (event: any, selectedDate?: Date) => {
|
||||
if (Platform.OS === "android") {
|
||||
setShowStartPicker(false);
|
||||
setActivePicker(null);
|
||||
if (event.type === "set" && selectedDate) {
|
||||
onStartDateChange(selectedDate);
|
||||
}
|
||||
} else if (selectedDate) {
|
||||
// For iOS, update both temp and actual date immediately
|
||||
setTempStartDate(selectedDate);
|
||||
onStartDateChange(selectedDate);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEndDateChange = (event: any, selectedDate?: Date) => {
|
||||
const handleEndPickerChange = (event: any, selectedDate?: Date) => {
|
||||
if (Platform.OS === "android") {
|
||||
setShowEndPicker(false);
|
||||
setActivePicker(null);
|
||||
if (event.type === "set" && selectedDate) {
|
||||
onEndDateChange(selectedDate);
|
||||
}
|
||||
} else if (selectedDate) {
|
||||
// For iOS, update both temp and actual date immediately
|
||||
setTempEndDate(selectedDate);
|
||||
onEndDateChange(selectedDate);
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfirmStartDate = () => {
|
||||
setShowStartPicker(false);
|
||||
const handleConfirm = () => {
|
||||
setActivePicker(null);
|
||||
};
|
||||
|
||||
const handleConfirmEndDate = () => {
|
||||
setShowEndPicker(false);
|
||||
const handleCancel = () => {
|
||||
setActivePicker(null);
|
||||
};
|
||||
|
||||
const getPickerTitle = () => {
|
||||
switch (activePicker) {
|
||||
case "startDate":
|
||||
return t("diary.selectStartDate");
|
||||
case "startTime":
|
||||
return t("diary.selectStartTime") || "Chọn giờ khởi hành";
|
||||
case "endDate":
|
||||
return t("diary.selectEndDate");
|
||||
case "endTime":
|
||||
return t("diary.selectEndTime") || "Chọn giờ kết thúc";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
const themedStyles = {
|
||||
@@ -114,133 +168,230 @@ export default function TripDurationPicker({
|
||||
pickerHeader: { borderBottomColor: colors.border },
|
||||
pickerTitle: { color: colors.text },
|
||||
cancelButton: { color: colors.textSecondary },
|
||||
sectionCard: {
|
||||
backgroundColor: colors.backgroundSecondary || colors.card,
|
||||
borderColor: colors.border,
|
||||
},
|
||||
sectionTitle: { color: colors.text },
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text style={[styles.label, themedStyles.label]}>
|
||||
{t("diary.tripDuration")}
|
||||
</Text>
|
||||
<View style={styles.dateRangeContainer}>
|
||||
{/* Start Date */}
|
||||
<View style={styles.dateSection}>
|
||||
<Text style={[styles.subLabel, themedStyles.placeholder]}>
|
||||
{t("diary.startDate")}
|
||||
const renderDateTimeSection = (
|
||||
type: "start" | "end",
|
||||
date: Date | null,
|
||||
onOpenDate: () => void,
|
||||
onOpenTime: () => void
|
||||
) => {
|
||||
const isStart = type === "start";
|
||||
const icon = isStart ? "boat-outline" : "flag-outline";
|
||||
const title = isStart ? t("diary.startDate") : t("diary.endDate");
|
||||
const dateLabel = t("diary.date") || "Ngày";
|
||||
const timeLabel = t("diary.time") || "Giờ";
|
||||
|
||||
return (
|
||||
<View style={[styles.sectionCard, themedStyles.sectionCard]}>
|
||||
{/* Section Header */}
|
||||
<View style={styles.sectionHeader}>
|
||||
<View
|
||||
style={[
|
||||
styles.iconContainer,
|
||||
{ backgroundColor: isStart ? "#3B82F620" : "#10B98120" },
|
||||
]}
|
||||
>
|
||||
<Ionicons
|
||||
name={icon as any}
|
||||
size={18}
|
||||
color={isStart ? "#3B82F6" : "#10B981"}
|
||||
/>
|
||||
</View>
|
||||
<Text style={[styles.sectionTitle, themedStyles.sectionTitle]}>
|
||||
{title}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* Date and Time Row */}
|
||||
<View style={styles.dateTimeRow}>
|
||||
{/* Date Picker */}
|
||||
<TouchableOpacity
|
||||
style={[styles.dateInput, themedStyles.dateInput]}
|
||||
onPress={disabled ? undefined : handleOpenStartPicker}
|
||||
style={[styles.dateTimeInput, themedStyles.dateInput]}
|
||||
onPress={disabled ? undefined : onOpenDate}
|
||||
activeOpacity={disabled ? 1 : 0.7}
|
||||
disabled={disabled}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.dateText,
|
||||
themedStyles.dateText,
|
||||
!startDate && themedStyles.placeholder,
|
||||
]}
|
||||
>
|
||||
{startDate ? formatDate(startDate) : t("diary.selectDate")}
|
||||
</Text>
|
||||
{!disabled && (
|
||||
<View style={styles.inputContent}>
|
||||
<Ionicons
|
||||
name="calendar-outline"
|
||||
size={20}
|
||||
size={18}
|
||||
color={date ? colors.primary : colors.textSecondary}
|
||||
style={styles.inputIcon}
|
||||
/>
|
||||
<View style={styles.inputTextContainer}>
|
||||
<Text style={[styles.inputLabel, themedStyles.placeholder]}>
|
||||
{dateLabel}
|
||||
</Text>
|
||||
<Text
|
||||
style={[
|
||||
styles.inputValue,
|
||||
themedStyles.dateText,
|
||||
!date && themedStyles.placeholder,
|
||||
]}
|
||||
>
|
||||
{date ? formatDate(date) : t("diary.selectDate")}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
{!disabled && (
|
||||
<Ionicons
|
||||
name="chevron-forward"
|
||||
size={16}
|
||||
color={colors.textSecondary}
|
||||
/>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* End Date */}
|
||||
<View style={styles.dateSection}>
|
||||
<Text style={[styles.subLabel, themedStyles.placeholder]}>
|
||||
{t("diary.endDate")}
|
||||
</Text>
|
||||
{/* Time Picker */}
|
||||
<TouchableOpacity
|
||||
style={[styles.dateInput, themedStyles.dateInput]}
|
||||
onPress={disabled ? undefined : handleOpenEndPicker}
|
||||
style={[styles.dateTimeInput, themedStyles.dateInput]}
|
||||
onPress={disabled ? undefined : onOpenTime}
|
||||
activeOpacity={disabled ? 1 : 0.7}
|
||||
disabled={disabled}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.dateText,
|
||||
themedStyles.dateText,
|
||||
!endDate && themedStyles.placeholder,
|
||||
]}
|
||||
>
|
||||
{endDate ? formatDate(endDate) : t("diary.selectDate")}
|
||||
</Text>
|
||||
<View style={styles.inputContent}>
|
||||
<Ionicons
|
||||
name="time-outline"
|
||||
size={18}
|
||||
color={date ? colors.primary : colors.textSecondary}
|
||||
style={styles.inputIcon}
|
||||
/>
|
||||
<View style={styles.inputTextContainer}>
|
||||
<Text style={[styles.inputLabel, themedStyles.placeholder]}>
|
||||
{timeLabel}
|
||||
</Text>
|
||||
<Text
|
||||
style={[
|
||||
styles.inputValue,
|
||||
themedStyles.dateText,
|
||||
!date && themedStyles.placeholder,
|
||||
]}
|
||||
>
|
||||
{date ? formatTime(date) : "--:--"}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
{!disabled && (
|
||||
<Ionicons
|
||||
name="calendar-outline"
|
||||
size={20}
|
||||
name="chevron-forward"
|
||||
size={16}
|
||||
color={colors.textSecondary}
|
||||
/>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
{/* Start Date Picker */}
|
||||
{showStartPicker && (
|
||||
<Modal transparent animationType="fade" visible={showStartPicker}>
|
||||
<View style={styles.modalOverlay}>
|
||||
<View style={[styles.pickerContainer, themedStyles.pickerContainer]}>
|
||||
<View style={[styles.pickerHeader, themedStyles.pickerHeader]}>
|
||||
<TouchableOpacity onPress={() => setShowStartPicker(false)}>
|
||||
<Text style={[styles.cancelButton, themedStyles.cancelButton]}>
|
||||
{t("common.cancel")}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<Text style={[styles.pickerTitle, themedStyles.pickerTitle]}>
|
||||
{t("diary.selectStartDate")}
|
||||
</Text>
|
||||
<TouchableOpacity onPress={handleConfirmStartDate}>
|
||||
<Text style={styles.doneButton}>{t("common.done")}</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<DateTimePicker
|
||||
value={tempStartDate}
|
||||
mode="date"
|
||||
display={Platform.OS === "ios" ? "spinner" : "default"}
|
||||
onChange={handleStartDateChange}
|
||||
maximumDate={endDate || undefined}
|
||||
themeVariant={colorScheme}
|
||||
textColor={colors.text}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</Modal>
|
||||
const isStartPicker =
|
||||
activePicker === "startDate" || activePicker === "startTime";
|
||||
const isTimePicker =
|
||||
activePicker === "startTime" || activePicker === "endTime";
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text style={[styles.label, themedStyles.label]}>
|
||||
{t("diary.tripDuration")}
|
||||
</Text>
|
||||
|
||||
{/* Hiển thị thời gian hiện tại */}
|
||||
<View
|
||||
style={[
|
||||
styles.currentTimeContainer,
|
||||
{ backgroundColor: colors.backgroundSecondary || colors.card },
|
||||
]}
|
||||
>
|
||||
<Ionicons name="time-outline" size={18} color={colors.primary} />
|
||||
<Text
|
||||
style={[styles.currentTimeLabel, { color: colors.textSecondary }]}
|
||||
>
|
||||
{t("diary.currentTime") || "Thời gian hiện tại"}:
|
||||
</Text>
|
||||
<Text style={[styles.currentTimeValue, { color: colors.primary }]}>
|
||||
{formatDate(currentTime)} {formatTime(currentTime)}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* Start Section */}
|
||||
{renderDateTimeSection(
|
||||
"start",
|
||||
startDate,
|
||||
handleOpenStartDatePicker,
|
||||
handleOpenStartTimePicker
|
||||
)}
|
||||
|
||||
{/* End Date Picker */}
|
||||
{showEndPicker && (
|
||||
<Modal transparent animationType="fade" visible={showEndPicker}>
|
||||
{/* Connection Line */}
|
||||
<View style={styles.connectionContainer}>
|
||||
<View
|
||||
style={[styles.connectionLine, { backgroundColor: colors.border }]}
|
||||
/>
|
||||
<View
|
||||
style={[styles.connectionDot, { backgroundColor: colors.primary }]}
|
||||
/>
|
||||
<View
|
||||
style={[styles.connectionLine, { backgroundColor: colors.border }]}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* End Section */}
|
||||
{renderDateTimeSection(
|
||||
"end",
|
||||
endDate,
|
||||
handleOpenEndDatePicker,
|
||||
handleOpenEndTimePicker
|
||||
)}
|
||||
|
||||
{/* Unified Picker Modal */}
|
||||
{activePicker && (
|
||||
<Modal transparent animationType="fade" visible={!!activePicker}>
|
||||
<View style={styles.modalOverlay}>
|
||||
<View style={[styles.pickerContainer, themedStyles.pickerContainer]}>
|
||||
<View
|
||||
style={[styles.pickerContainer, themedStyles.pickerContainer]}
|
||||
>
|
||||
<View style={[styles.pickerHeader, themedStyles.pickerHeader]}>
|
||||
<TouchableOpacity onPress={() => setShowEndPicker(false)}>
|
||||
<Text style={[styles.cancelButton, themedStyles.cancelButton]}>
|
||||
<TouchableOpacity onPress={handleCancel}>
|
||||
<Text
|
||||
style={[styles.cancelButtonText, themedStyles.cancelButton]}
|
||||
>
|
||||
{t("common.cancel")}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<Text style={[styles.pickerTitle, themedStyles.pickerTitle]}>
|
||||
{t("diary.selectEndDate")}
|
||||
{getPickerTitle()}
|
||||
</Text>
|
||||
<TouchableOpacity onPress={handleConfirmEndDate}>
|
||||
<TouchableOpacity onPress={handleConfirm}>
|
||||
<Text style={styles.doneButton}>{t("common.done")}</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<DateTimePicker
|
||||
value={tempEndDate}
|
||||
mode="date"
|
||||
value={isStartPicker ? tempStartDate : tempEndDate}
|
||||
mode={isTimePicker ? "time" : "date"}
|
||||
display={Platform.OS === "ios" ? "spinner" : "default"}
|
||||
onChange={handleEndDateChange}
|
||||
minimumDate={startDate || undefined}
|
||||
onChange={
|
||||
isStartPicker
|
||||
? handleStartPickerChange
|
||||
: handleEndPickerChange
|
||||
}
|
||||
maximumDate={
|
||||
isStartPicker && !isTimePicker
|
||||
? endDate || undefined
|
||||
: undefined
|
||||
}
|
||||
minimumDate={
|
||||
!isStartPicker && !isTimePicker
|
||||
? startDate || undefined
|
||||
: undefined
|
||||
}
|
||||
themeVariant={colorScheme}
|
||||
textColor={colors.text}
|
||||
locale={locale}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
@@ -257,46 +408,100 @@ const styles = StyleSheet.create({
|
||||
label: {
|
||||
fontSize: 16,
|
||||
fontWeight: "600",
|
||||
marginBottom: 12,
|
||||
marginBottom: 16,
|
||||
fontFamily: Platform.select({
|
||||
ios: "System",
|
||||
android: "Roboto",
|
||||
default: "System",
|
||||
}),
|
||||
},
|
||||
subLabel: {
|
||||
fontSize: 14,
|
||||
marginBottom: 6,
|
||||
fontFamily: Platform.select({
|
||||
ios: "System",
|
||||
android: "Roboto",
|
||||
default: "System",
|
||||
}),
|
||||
sectionCard: {
|
||||
borderRadius: 12,
|
||||
borderWidth: 1,
|
||||
padding: 14,
|
||||
},
|
||||
dateRangeContainer: {
|
||||
sectionHeader: {
|
||||
flexDirection: "row",
|
||||
gap: 12,
|
||||
alignItems: "center",
|
||||
marginBottom: 12,
|
||||
},
|
||||
dateSection: {
|
||||
iconContainer: {
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: 8,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
marginRight: 10,
|
||||
},
|
||||
sectionTitle: {
|
||||
fontSize: 15,
|
||||
fontWeight: "600",
|
||||
fontFamily: Platform.select({
|
||||
ios: "System",
|
||||
android: "Roboto",
|
||||
default: "System",
|
||||
}),
|
||||
},
|
||||
dateTimeRow: {
|
||||
flexDirection: "row",
|
||||
gap: 10,
|
||||
},
|
||||
dateTimeInput: {
|
||||
flex: 1,
|
||||
},
|
||||
dateInput: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
borderWidth: 1,
|
||||
borderRadius: 8,
|
||||
borderRadius: 10,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 12,
|
||||
paddingVertical: 10,
|
||||
},
|
||||
dateText: {
|
||||
fontSize: 15,
|
||||
inputContent: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
flex: 1,
|
||||
},
|
||||
inputIcon: {
|
||||
marginRight: 10,
|
||||
},
|
||||
inputTextContainer: {
|
||||
flex: 1,
|
||||
},
|
||||
inputLabel: {
|
||||
fontSize: 11,
|
||||
marginBottom: 2,
|
||||
fontFamily: Platform.select({
|
||||
ios: "System",
|
||||
android: "Roboto",
|
||||
default: "System",
|
||||
}),
|
||||
},
|
||||
inputValue: {
|
||||
fontSize: 14,
|
||||
fontWeight: "500",
|
||||
fontFamily: Platform.select({
|
||||
ios: "System",
|
||||
android: "Roboto",
|
||||
default: "System",
|
||||
}),
|
||||
},
|
||||
connectionContainer: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
paddingVertical: 8,
|
||||
paddingHorizontal: 20,
|
||||
},
|
||||
connectionLine: {
|
||||
flex: 1,
|
||||
height: 1,
|
||||
},
|
||||
connectionDot: {
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: 4,
|
||||
marginHorizontal: 8,
|
||||
},
|
||||
modalOverlay: {
|
||||
flex: 1,
|
||||
backgroundColor: "rgba(0, 0, 0, 0.5)",
|
||||
@@ -324,7 +529,7 @@ const styles = StyleSheet.create({
|
||||
default: "System",
|
||||
}),
|
||||
},
|
||||
cancelButton: {
|
||||
cancelButtonText: {
|
||||
fontSize: 16,
|
||||
fontFamily: Platform.select({
|
||||
ios: "System",
|
||||
@@ -342,4 +547,29 @@ const styles = StyleSheet.create({
|
||||
default: "System",
|
||||
}),
|
||||
},
|
||||
currentTimeContainer: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
padding: 12,
|
||||
borderRadius: 8,
|
||||
marginBottom: 12,
|
||||
gap: 8,
|
||||
},
|
||||
currentTimeLabel: {
|
||||
fontSize: 13,
|
||||
fontFamily: Platform.select({
|
||||
ios: "System",
|
||||
android: "Roboto",
|
||||
default: "System",
|
||||
}),
|
||||
},
|
||||
currentTimeValue: {
|
||||
fontSize: 14,
|
||||
fontWeight: "600",
|
||||
fontFamily: Platform.select({
|
||||
ios: "System",
|
||||
android: "Roboto",
|
||||
default: "System",
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -227,6 +227,30 @@ export default function TripFormModal({
|
||||
Alert.alert(t("common.error"), t("diary.validation.datesRequired"));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate: thời điểm khởi hành phải từ hiện tại trở đi
|
||||
const now = new Date();
|
||||
const startMinutes = Math.floor(startDate.getTime() / 60000);
|
||||
const nowMinutes = Math.floor(now.getTime() / 60000);
|
||||
if (startMinutes < nowMinutes) {
|
||||
Alert.alert(
|
||||
t("common.error"),
|
||||
t("diary.validation.startDateNotInPast") ||
|
||||
"Thời điểm khởi hành không được ở quá khứ"
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate: thời điểm kết thúc phải sau thời điểm khởi hành
|
||||
if (endDate <= startDate) {
|
||||
Alert.alert(
|
||||
t("common.error"),
|
||||
t("diary.validation.endDateAfterStart") ||
|
||||
"Thời điểm kết thúc phải sau thời điểm khởi hành"
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!tripName.trim()) {
|
||||
Alert.alert(t("common.error"), t("diary.validation.tripNameRequired"));
|
||||
return false;
|
||||
@@ -237,7 +261,6 @@ export default function TripFormModal({
|
||||
// Build API body
|
||||
const buildApiBody = useCallback((): Model.TripAPIBody => {
|
||||
return {
|
||||
thing_id: selectedShipId,
|
||||
name: tripName,
|
||||
departure_time: startDate?.toISOString() || "",
|
||||
departure_port_id: departurePortId,
|
||||
@@ -257,7 +280,6 @@ export default function TripFormModal({
|
||||
})),
|
||||
};
|
||||
}, [
|
||||
selectedShipId,
|
||||
tripName,
|
||||
startDate,
|
||||
endDate,
|
||||
@@ -293,10 +315,28 @@ export default function TripFormModal({
|
||||
isEditMode ? "Error updating trip:" : "Error creating trip:",
|
||||
error
|
||||
);
|
||||
Alert.alert(
|
||||
t("common.error"),
|
||||
isEditMode ? t("diary.updateTripError") : t("diary.createTripError")
|
||||
);
|
||||
|
||||
// Lấy message từ server response
|
||||
const serverMessage = error.response?.data || "";
|
||||
|
||||
// Kiểm tra lỗi cụ thể: trip already exists
|
||||
if (
|
||||
serverMessage.includes &&
|
||||
serverMessage.includes("already exists and not completed")
|
||||
) {
|
||||
// Đánh dấu lỗi đã được xử lý (axios sẽ không hiển thị toast cho status 400)
|
||||
Alert.alert(
|
||||
t("common.warning") || "Cảnh báo",
|
||||
t("diary.tripAlreadyExistsError") ||
|
||||
"Chuyến đi đang diễn ra chưa hoàn thành. Vui lòng hoàn thành chuyến đi hiện tại trước khi tạo chuyến mới."
|
||||
);
|
||||
} else {
|
||||
// Các lỗi khác
|
||||
Alert.alert(
|
||||
t("common.error"),
|
||||
isEditMode ? t("diary.updateTripError") : t("diary.createTripError")
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
@@ -356,7 +396,10 @@ export default function TripFormModal({
|
||||
{!isEditMode && <AutoFillSection onAutoFill={handleAutoFill} />}
|
||||
|
||||
{/* Section 1: Basic Information */}
|
||||
<FormSection title={t("diary.formSection.basicInfo")} icon="boat-outline">
|
||||
<FormSection
|
||||
title={t("diary.formSection.basicInfo")}
|
||||
icon="boat-outline"
|
||||
>
|
||||
{/* Ship Selector - disabled in edit mode */}
|
||||
<ShipSelector
|
||||
selectedShipId={selectedShipId}
|
||||
@@ -369,7 +412,10 @@ export default function TripFormModal({
|
||||
</FormSection>
|
||||
|
||||
{/* Section 2: Schedule & Location */}
|
||||
<FormSection title={t("diary.formSection.schedule")} icon="calendar-outline">
|
||||
<FormSection
|
||||
title={t("diary.formSection.schedule")}
|
||||
icon="calendar-outline"
|
||||
>
|
||||
{/* Trip Duration */}
|
||||
<TripDurationPicker
|
||||
startDate={startDate}
|
||||
@@ -394,13 +440,27 @@ export default function TripFormModal({
|
||||
</FormSection>
|
||||
|
||||
{/* Section 3: Equipment */}
|
||||
<FormSection title={t("diary.formSection.equipment")} icon="construct-outline">
|
||||
<FishingGearList items={fishingGears} onChange={setFishingGears} hideTitle />
|
||||
<FormSection
|
||||
title={t("diary.formSection.equipment")}
|
||||
icon="construct-outline"
|
||||
>
|
||||
<FishingGearList
|
||||
items={fishingGears}
|
||||
onChange={setFishingGears}
|
||||
hideTitle
|
||||
/>
|
||||
</FormSection>
|
||||
|
||||
{/* Section 4: Costs */}
|
||||
<FormSection title={t("diary.formSection.costs")} icon="wallet-outline">
|
||||
<MaterialCostList items={tripCosts} onChange={setTripCosts} hideTitle />
|
||||
<FormSection
|
||||
title={t("diary.formSection.costs")}
|
||||
icon="wallet-outline"
|
||||
>
|
||||
<MaterialCostList
|
||||
items={tripCosts}
|
||||
onChange={setTripCosts}
|
||||
hideTitle
|
||||
/>
|
||||
</FormSection>
|
||||
</ScrollView>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user