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:
@@ -14,15 +14,32 @@ export default function AlertsSection({ alerts = [] }: AlertsSectionProps) {
|
||||
const { colors } = useThemeContext();
|
||||
|
||||
const getAlertLevelColor = (level?: number) => {
|
||||
const isDark =
|
||||
colors.background === "#1C1C1E" ||
|
||||
colors.background === "#000000" ||
|
||||
colors.background?.toLowerCase().includes("1c1c1e");
|
||||
|
||||
switch (level) {
|
||||
case 0:
|
||||
return { bg: "#FEF3C7", text: "#92400E" }; // Warning - Yellow
|
||||
case 1:
|
||||
return { bg: "#FEE2E2", text: "#991B1B" }; // Error - Red
|
||||
case 2:
|
||||
return { bg: "#DBEAFE", text: "#1E40AF" }; // Info - Blue
|
||||
default:
|
||||
return { bg: "#F3F4F6", text: "#4B5563" }; // Default - Gray
|
||||
case 0: // Bình thường - Blue/Info
|
||||
return isDark
|
||||
? { bg: "#172554", text: "#93C5FD" } // Dark blue
|
||||
: { bg: "#DBEAFE", text: "#1E40AF" };
|
||||
case 1: // Warning - Yellow
|
||||
return isDark
|
||||
? { bg: "#422006", text: "#FCD34D" } // Dark amber
|
||||
: { bg: "#FEF3C7", text: "#92400E" };
|
||||
case 2: // Error - Red
|
||||
return isDark
|
||||
? { bg: "#450A0A", text: "#FCA5A5" } // Dark red
|
||||
: { bg: "#FEE2E2", text: "#991B1B" };
|
||||
case 3: // SOS - Critical Red
|
||||
return isDark
|
||||
? { bg: "#7F1D1D", text: "#FFFFFF" } // Dark critical
|
||||
: { bg: "#DC2626", text: "#FFFFFF" };
|
||||
default: // Default - Gray
|
||||
return isDark
|
||||
? { bg: "#374151", text: "#D1D5DB" } // Dark gray
|
||||
: { bg: "#F3F4F6", text: "#4B5563" };
|
||||
}
|
||||
};
|
||||
|
||||
@@ -38,11 +55,16 @@ export default function AlertsSection({ alerts = [] }: AlertsSectionProps) {
|
||||
icon="warning-outline"
|
||||
count={alerts.length}
|
||||
collapsible
|
||||
defaultExpanded={alerts.length > 0}
|
||||
//defaultExpanded={alerts.length > 0}
|
||||
defaultExpanded
|
||||
>
|
||||
{alerts.length === 0 ? (
|
||||
<View style={styles.emptyContainer}>
|
||||
<Ionicons name="checkmark-circle-outline" size={40} color={colors.success || "#22C55E"} />
|
||||
<Ionicons
|
||||
name="checkmark-circle-outline"
|
||||
size={40}
|
||||
color={colors.success || "#22C55E"}
|
||||
/>
|
||||
<Text style={[styles.emptyText, { color: colors.textSecondary }]}>
|
||||
{t("diary.tripDetail.noAlerts")}
|
||||
</Text>
|
||||
@@ -63,13 +85,22 @@ export default function AlertsSection({ alerts = [] }: AlertsSectionProps) {
|
||||
size={18}
|
||||
color={levelColor.text}
|
||||
/>
|
||||
<Text style={[styles.alertName, { color: levelColor.text }]}>
|
||||
<Text
|
||||
style={[styles.alertName, { color: levelColor.text }]}
|
||||
>
|
||||
{alert.name || t("diary.tripDetail.unknownAlert")}
|
||||
</Text>
|
||||
</View>
|
||||
{alert.confirmed && (
|
||||
<View style={[styles.confirmedBadge, { backgroundColor: colors.success || "#22C55E" }]}>
|
||||
<Text style={styles.confirmedText}>{t("diary.tripDetail.confirmed")}</Text>
|
||||
<View
|
||||
style={[
|
||||
styles.confirmedBadge,
|
||||
{ backgroundColor: colors.success || "#22C55E" },
|
||||
]}
|
||||
>
|
||||
<Text style={styles.confirmedText}>
|
||||
{t("diary.tripDetail.confirmed")}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import React from "react";
|
||||
import React, { useEffect, useMemo } from "react";
|
||||
import { View, Text, StyleSheet, Platform } from "react-native";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { useI18n } from "@/hooks/use-i18n";
|
||||
import { useThemeContext } from "@/hooks/use-theme-context";
|
||||
import { useShip } from "@/state/use-ship";
|
||||
import { usePort } from "@/state/use-ports";
|
||||
import { useGroup } from "@/state/use-group";
|
||||
import { filterPortsByProvinceCode } from "@/utils/tripDataConverters";
|
||||
|
||||
interface BasicInfoSectionProps {
|
||||
trip: Model.Trip;
|
||||
@@ -15,6 +19,55 @@ export default function BasicInfoSection({ trip }: BasicInfoSectionProps) {
|
||||
const { t } = useI18n();
|
||||
const { colors } = useThemeContext();
|
||||
|
||||
// Get data from zustand stores
|
||||
const { ships, getShip } = useShip();
|
||||
const { ports, getPorts } = usePort();
|
||||
const { groups, getUserGroups } = useGroup();
|
||||
|
||||
// Fetch data if not available
|
||||
useEffect(() => {
|
||||
if (!ships) {
|
||||
getShip();
|
||||
}
|
||||
}, [ships, getShip]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!ports) {
|
||||
getPorts();
|
||||
}
|
||||
}, [ports, getPorts]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!groups) {
|
||||
getUserGroups();
|
||||
}
|
||||
}, [groups, getUserGroups]);
|
||||
|
||||
// Filter ports by province codes from groups
|
||||
const filteredPorts = useMemo(() => {
|
||||
return filterPortsByProvinceCode(ports, groups);
|
||||
}, [ports, groups]);
|
||||
|
||||
// Get ship name by ship_id
|
||||
const shipName = useMemo(() => {
|
||||
if (!trip?.ship_id || !ships) return "--";
|
||||
const ship = ships.find((s) => s.id === trip.ship_id);
|
||||
return ship?.name || "--";
|
||||
}, [trip?.ship_id, ships]);
|
||||
|
||||
// Get ship code (reg_number) by ship_id
|
||||
const shipCode = useMemo(() => {
|
||||
if (!trip?.ship_id || !ships) return "--";
|
||||
const ship = ships.find((s) => s.id === trip.ship_id);
|
||||
return ship?.reg_number || "--";
|
||||
}, [trip?.ship_id, ships]);
|
||||
|
||||
// Get port name by ID
|
||||
const getPortName = (portId: number): string => {
|
||||
const port = filteredPorts.find((p) => p.id === portId);
|
||||
return port?.name || "--";
|
||||
};
|
||||
|
||||
const formatDateTime = (dateStr?: string) => {
|
||||
if (!dateStr) return "--";
|
||||
const date = new Date(dateStr);
|
||||
@@ -30,8 +83,13 @@ export default function BasicInfoSection({ trip }: BasicInfoSectionProps) {
|
||||
const infoItems = [
|
||||
{
|
||||
icon: "boat" as const,
|
||||
label: t("diary.tripDetail.shipId"),
|
||||
value: trip.vms_id || "--",
|
||||
label: t("diary.tripDetail.shipName"),
|
||||
value: shipName,
|
||||
},
|
||||
{
|
||||
icon: "barcode" as const,
|
||||
label: t("diary.tripDetail.shipCode"),
|
||||
value: shipCode,
|
||||
},
|
||||
{
|
||||
icon: "play-circle" as const,
|
||||
@@ -46,26 +104,36 @@ export default function BasicInfoSection({ trip }: BasicInfoSectionProps) {
|
||||
{
|
||||
icon: "location" as const,
|
||||
label: t("diary.tripDetail.departurePort"),
|
||||
value: trip.departure_port_id ? `Cảng #${trip.departure_port_id}` : "--",
|
||||
value: getPortName(trip.departure_port_id),
|
||||
},
|
||||
{
|
||||
icon: "flag" as const,
|
||||
label: t("diary.tripDetail.arrivalPort"),
|
||||
value: trip.arrival_port_id ? `Cảng #${trip.arrival_port_id}` : "--",
|
||||
value: getPortName(trip.arrival_port_id),
|
||||
},
|
||||
{
|
||||
icon: "map" as const,
|
||||
label: t("diary.tripDetail.fishingGrounds"),
|
||||
value: trip.fishing_ground_codes?.length > 0
|
||||
? trip.fishing_ground_codes.join(", ")
|
||||
: "--",
|
||||
value:
|
||||
trip.fishing_ground_codes?.length > 0
|
||||
? trip.fishing_ground_codes.join(", ")
|
||||
: "--",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<View style={[styles.container, { backgroundColor: colors.card, borderColor: colors.separator }]}>
|
||||
<View
|
||||
style={[
|
||||
styles.container,
|
||||
{ backgroundColor: colors.card, borderColor: colors.separator },
|
||||
]}
|
||||
>
|
||||
<View style={styles.header}>
|
||||
<Ionicons name="information-circle-outline" size={20} color={colors.primary} />
|
||||
<Ionicons
|
||||
name="information-circle-outline"
|
||||
size={20}
|
||||
color={colors.primary}
|
||||
/>
|
||||
<Text style={[styles.title, { color: colors.text }]}>
|
||||
{t("diary.tripDetail.basicInfo")}
|
||||
</Text>
|
||||
@@ -74,8 +142,14 @@ export default function BasicInfoSection({ trip }: BasicInfoSectionProps) {
|
||||
{infoItems.map((item, index) => (
|
||||
<View key={index} style={styles.infoItem}>
|
||||
<View style={styles.infoLabel}>
|
||||
<Ionicons name={item.icon} size={16} color={colors.textSecondary} />
|
||||
<Text style={[styles.infoLabelText, { color: colors.textSecondary }]}>
|
||||
<Ionicons
|
||||
name={item.icon}
|
||||
size={16}
|
||||
color={colors.textSecondary}
|
||||
/>
|
||||
<Text
|
||||
style={[styles.infoLabelText, { color: colors.textSecondary }]}
|
||||
>
|
||||
{item.label}
|
||||
</Text>
|
||||
</View>
|
||||
@@ -106,7 +180,11 @@ const styles = StyleSheet.create({
|
||||
title: {
|
||||
fontSize: 16,
|
||||
fontWeight: "600",
|
||||
fontFamily: Platform.select({ ios: "System", android: "Roboto", default: "System" }),
|
||||
fontFamily: Platform.select({
|
||||
ios: "System",
|
||||
android: "Roboto",
|
||||
default: "System",
|
||||
}),
|
||||
},
|
||||
content: {
|
||||
paddingHorizontal: 16,
|
||||
@@ -125,11 +203,19 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
infoLabelText: {
|
||||
fontSize: 13,
|
||||
fontFamily: Platform.select({ ios: "System", android: "Roboto", default: "System" }),
|
||||
fontFamily: Platform.select({
|
||||
ios: "System",
|
||||
android: "Roboto",
|
||||
default: "System",
|
||||
}),
|
||||
},
|
||||
infoValue: {
|
||||
fontSize: 13,
|
||||
fontWeight: "500",
|
||||
fontFamily: Platform.select({ ios: "System", android: "Roboto", default: "System" }),
|
||||
fontFamily: Platform.select({
|
||||
ios: "System",
|
||||
android: "Roboto",
|
||||
default: "System",
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -9,7 +9,9 @@ interface FishingLogsSectionProps {
|
||||
fishingLogs?: Model.FishingLog[] | null;
|
||||
}
|
||||
|
||||
export default function FishingLogsSection({ fishingLogs = [] }: FishingLogsSectionProps) {
|
||||
export default function FishingLogsSection({
|
||||
fishingLogs = [],
|
||||
}: FishingLogsSectionProps) {
|
||||
const { t } = useI18n();
|
||||
const { colors } = useThemeContext();
|
||||
|
||||
@@ -35,13 +37,29 @@ export default function FishingLogsSection({ fishingLogs = [] }: FishingLogsSect
|
||||
const getStatusLabel = (status?: number) => {
|
||||
switch (status) {
|
||||
case 0:
|
||||
return { label: t("diary.tripDetail.logStatusPending"), color: "#FEF3C7", textColor: "#92400E" };
|
||||
return {
|
||||
label: t("diary.tripDetail.logStatusProcessing"),
|
||||
color: "#FEF3C7",
|
||||
textColor: "#92400E",
|
||||
};
|
||||
case 1:
|
||||
return { label: t("diary.tripDetail.logStatusActive"), color: "#DBEAFE", textColor: "#1E40AF" };
|
||||
return {
|
||||
label: t("diary.tripDetail.logStatusSuccess"),
|
||||
color: "#D1FAE5",
|
||||
textColor: "#065F46",
|
||||
};
|
||||
case 2:
|
||||
return { label: t("diary.tripDetail.logStatusCompleted"), color: "#D1FAE5", textColor: "#065F46" };
|
||||
return {
|
||||
label: t("diary.tripDetail.logStatusCancelled"),
|
||||
color: "#FEE2E2",
|
||||
textColor: "#B91C1C",
|
||||
};
|
||||
default:
|
||||
return { label: t("diary.tripDetail.logStatusUnknown"), color: "#F3F4F6", textColor: "#4B5563" };
|
||||
return {
|
||||
label: t("diary.tripDetail.logStatusUnknown"),
|
||||
color: "#F3F4F6",
|
||||
textColor: "#4B5563",
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -55,7 +73,11 @@ export default function FishingLogsSection({ fishingLogs = [] }: FishingLogsSect
|
||||
>
|
||||
{logs.length === 0 ? (
|
||||
<View style={styles.emptyContainer}>
|
||||
<Ionicons name="fish-outline" size={40} color={colors.textSecondary} />
|
||||
<Ionicons
|
||||
name="fish-outline"
|
||||
size={40}
|
||||
color={colors.textSecondary}
|
||||
/>
|
||||
<Text style={[styles.emptyText, { color: colors.textSecondary }]}>
|
||||
{t("diary.tripDetail.noFishingLogs")}
|
||||
</Text>
|
||||
@@ -65,7 +87,7 @@ export default function FishingLogsSection({ fishingLogs = [] }: FishingLogsSect
|
||||
{logs.map((log, index) => {
|
||||
const status = getStatusLabel(log.status);
|
||||
const catchCount = log.info?.length || 0;
|
||||
|
||||
|
||||
return (
|
||||
<View
|
||||
key={log.fishing_log_id || index}
|
||||
@@ -74,12 +96,21 @@ export default function FishingLogsSection({ fishingLogs = [] }: FishingLogsSect
|
||||
{/* Header */}
|
||||
<View style={styles.logHeader}>
|
||||
<View style={styles.logIndex}>
|
||||
<Text style={[styles.logIndexText, { color: colors.primary }]}>
|
||||
<Text
|
||||
style={[styles.logIndexText, { color: colors.primary }]}
|
||||
>
|
||||
#{index + 1}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={[styles.statusBadge, { backgroundColor: status.color }]}>
|
||||
<Text style={[styles.statusText, { color: status.textColor }]}>
|
||||
<View
|
||||
style={[
|
||||
styles.statusBadge,
|
||||
{ backgroundColor: status.color },
|
||||
]}
|
||||
>
|
||||
<Text
|
||||
style={[styles.statusText, { color: status.textColor }]}
|
||||
>
|
||||
{status.label}
|
||||
</Text>
|
||||
</View>
|
||||
@@ -88,8 +119,17 @@ export default function FishingLogsSection({ fishingLogs = [] }: FishingLogsSect
|
||||
{/* Time Info */}
|
||||
<View style={styles.timeRow}>
|
||||
<View style={styles.timeItem}>
|
||||
<Ionicons name="play-circle-outline" size={16} color={colors.success || "#22C55E"} />
|
||||
<Text style={[styles.timeLabel, { color: colors.textSecondary }]}>
|
||||
<Ionicons
|
||||
name="play-circle-outline"
|
||||
size={16}
|
||||
color={colors.success || "#22C55E"}
|
||||
/>
|
||||
<Text
|
||||
style={[
|
||||
styles.timeLabel,
|
||||
{ color: colors.textSecondary },
|
||||
]}
|
||||
>
|
||||
{t("diary.tripDetail.startTime")}:
|
||||
</Text>
|
||||
<Text style={[styles.timeValue, { color: colors.text }]}>
|
||||
@@ -97,8 +137,17 @@ export default function FishingLogsSection({ fishingLogs = [] }: FishingLogsSect
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.timeItem}>
|
||||
<Ionicons name="stop-circle-outline" size={16} color={colors.error || "#EF4444"} />
|
||||
<Text style={[styles.timeLabel, { color: colors.textSecondary }]}>
|
||||
<Ionicons
|
||||
name="stop-circle-outline"
|
||||
size={16}
|
||||
color={colors.error || "#EF4444"}
|
||||
/>
|
||||
<Text
|
||||
style={[
|
||||
styles.timeLabel,
|
||||
{ color: colors.textSecondary },
|
||||
]}
|
||||
>
|
||||
{t("diary.tripDetail.endTime")}:
|
||||
</Text>
|
||||
<Text style={[styles.timeValue, { color: colors.text }]}>
|
||||
@@ -108,22 +157,49 @@ export default function FishingLogsSection({ fishingLogs = [] }: FishingLogsSect
|
||||
</View>
|
||||
|
||||
{/* Location Info */}
|
||||
<View style={[styles.locationContainer, { backgroundColor: colors.backgroundSecondary }]}>
|
||||
<View
|
||||
style={[
|
||||
styles.locationContainer,
|
||||
{ backgroundColor: colors.backgroundSecondary },
|
||||
]}
|
||||
>
|
||||
<View style={styles.locationItem}>
|
||||
<Ionicons name="location" size={14} color={colors.success || "#22C55E"} />
|
||||
<Text style={[styles.locationLabel, { color: colors.textSecondary }]}>
|
||||
<Ionicons
|
||||
name="location"
|
||||
size={14}
|
||||
color={colors.success || "#22C55E"}
|
||||
/>
|
||||
<Text
|
||||
style={[
|
||||
styles.locationLabel,
|
||||
{ color: colors.textSecondary },
|
||||
]}
|
||||
>
|
||||
{t("diary.tripDetail.startLocation")}:
|
||||
</Text>
|
||||
<Text style={[styles.locationValue, { color: colors.text }]}>
|
||||
<Text
|
||||
style={[styles.locationValue, { color: colors.text }]}
|
||||
>
|
||||
{formatCoord(log.start_lat, log.start_lon)}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.locationItem}>
|
||||
<Ionicons name="location" size={14} color={colors.error || "#EF4444"} />
|
||||
<Text style={[styles.locationLabel, { color: colors.textSecondary }]}>
|
||||
<Ionicons
|
||||
name="location"
|
||||
size={14}
|
||||
color={colors.error || "#EF4444"}
|
||||
/>
|
||||
<Text
|
||||
style={[
|
||||
styles.locationLabel,
|
||||
{ color: colors.textSecondary },
|
||||
]}
|
||||
>
|
||||
{t("diary.tripDetail.haulLocation")}:
|
||||
</Text>
|
||||
<Text style={[styles.locationValue, { color: colors.text }]}>
|
||||
<Text
|
||||
style={[styles.locationValue, { color: colors.text }]}
|
||||
>
|
||||
{formatCoord(log.haul_lat, log.haul_lon)}
|
||||
</Text>
|
||||
</View>
|
||||
@@ -135,22 +211,33 @@ export default function FishingLogsSection({ fishingLogs = [] }: FishingLogsSect
|
||||
<View style={styles.catchHeader}>
|
||||
<Ionicons name="fish" size={16} color={colors.primary} />
|
||||
<Text style={[styles.catchLabel, { color: colors.text }]}>
|
||||
{t("diary.tripDetail.catchInfo")} ({catchCount} {t("diary.tripDetail.species")})
|
||||
{t("diary.tripDetail.catchInfo")} ({catchCount}{" "}
|
||||
{t("diary.tripDetail.species")})
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.catchList}>
|
||||
{log.info?.slice(0, 3).map((fish, fishIndex) => (
|
||||
<View key={fishIndex} style={styles.catchItem}>
|
||||
<Text style={[styles.fishName, { color: colors.text }]}>
|
||||
{fish.fish_name || t("diary.tripDetail.unknownFish")}
|
||||
<Text
|
||||
style={[styles.fishName, { color: colors.text }]}
|
||||
>
|
||||
{fish.fish_name ||
|
||||
t("diary.tripDetail.unknownFish")}
|
||||
</Text>
|
||||
<Text style={[styles.fishAmount, { color: colors.textSecondary }]}>
|
||||
<Text
|
||||
style={[
|
||||
styles.fishAmount,
|
||||
{ color: colors.textSecondary },
|
||||
]}
|
||||
>
|
||||
{fish.catch_number} {fish.catch_unit}
|
||||
</Text>
|
||||
</View>
|
||||
))}
|
||||
{catchCount > 3 && (
|
||||
<Text style={[styles.moreText, { color: colors.primary }]}>
|
||||
<Text
|
||||
style={[styles.moreText, { color: colors.primary }]}
|
||||
>
|
||||
+{catchCount - 3} {t("diary.tripDetail.more")}
|
||||
</Text>
|
||||
)}
|
||||
@@ -161,8 +248,17 @@ export default function FishingLogsSection({ fishingLogs = [] }: FishingLogsSect
|
||||
{/* Weather */}
|
||||
{log.weather_description && (
|
||||
<View style={styles.weatherRow}>
|
||||
<Ionicons name="cloudy-outline" size={14} color={colors.textSecondary} />
|
||||
<Text style={[styles.weatherText, { color: colors.textSecondary }]}>
|
||||
<Ionicons
|
||||
name="cloudy-outline"
|
||||
size={14}
|
||||
color={colors.textSecondary}
|
||||
/>
|
||||
<Text
|
||||
style={[
|
||||
styles.weatherText,
|
||||
{ color: colors.textSecondary },
|
||||
]}
|
||||
>
|
||||
{log.weather_description}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
Reference in New Issue
Block a user