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:
2025-12-29 15:56:47 +07:00
parent 190e44b09e
commit 871360af49
24 changed files with 1451 additions and 407 deletions

View File

@@ -18,6 +18,9 @@ import {
convertFishingGears,
convertTripCosts,
} from "@/utils/tripDataConverters";
import { queryAlarms } from "@/controller/AlarmController";
import { queryTripCrew } from "@/controller/TripCrewController";
import { queryTripById } from "@/controller/TripController";
// Reuse existing components
import CrewList from "@/components/diary/TripCrewModal/CrewList";
@@ -36,28 +39,77 @@ export default function TripDetailPage() {
const { t } = useI18n();
const { colors } = useThemeContext();
const router = useRouter();
const { tripId, tripData: tripDataParam } = useLocalSearchParams<{
const { tripId } = useLocalSearchParams<{
tripId: string;
tripData?: string;
}>();
const [loading, setLoading] = useState(true);
const [trip, setTrip] = useState<Model.Trip | null>(null);
const [alerts] = useState<Model.Alarm[]>([]); // TODO: Fetch from API
const [alerts, setAlerts] = useState<Model.Alarm[]>([]);
const [crews, setCrews] = useState<Model.TripCrews[]>([]);
// Parse trip data from params or fetch from API
// Fetch trip data từ API
useEffect(() => {
if (tripDataParam) {
const fetchTripData = async () => {
if (!tripId) return;
setLoading(true);
try {
const parsedTrip = JSON.parse(tripDataParam) as Model.Trip;
setTrip(parsedTrip);
} catch (e) {
console.error("Error parsing trip data:", e);
const response = await queryTripById(tripId);
if (response.data) {
setTrip(response.data);
}
} catch (error) {
console.error("Lỗi khi tải thông tin chuyến đi:", error);
} finally {
setLoading(false);
}
}
// TODO: Fetch trip detail from API using tripId if not passed via params
setLoading(false);
}, [tripDataParam, tripId]);
};
fetchTripData();
}, [tripId]);
// Fetch alarms cho chuyến đi dựa trên thing_id (vms_id)
useEffect(() => {
const fetchAlarms = async () => {
if (!trip?.vms_id) return;
try {
const response = await queryAlarms({
offset: 0,
limit: 100,
order: "time",
dir: "desc",
thing_id: trip.vms_id,
});
if (response.data?.alarms) {
setAlerts(response.data.alarms);
}
} catch (error) {
console.error("Lỗi khi tải alarms:", error);
}
};
fetchAlarms();
}, [trip?.vms_id]);
// Fetch danh sách thuyền viên
useEffect(() => {
const fetchCrews = async () => {
if (!tripId) return;
try {
const response = await queryTripCrew(tripId);
// API trả về { trip_crews: [...] }
if (response.data?.trip_crews) {
setCrews(response.data.trip_crews);
}
} catch (error) {}
};
fetchCrews();
}, [tripId]);
// Convert trip data to component format using memoization
const fishingGears = useMemo(
@@ -72,11 +124,20 @@ export default function TripDetailPage() {
const statusConfig = useMemo(() => {
const status = trip?.trip_status ?? 0;
return TRIP_STATUS_CONFIG[status as keyof typeof TRIP_STATUS_CONFIG] || TRIP_STATUS_CONFIG[0];
return (
TRIP_STATUS_CONFIG[status as keyof typeof TRIP_STATUS_CONFIG] ||
TRIP_STATUS_CONFIG[0]
);
}, [trip?.trip_status]);
// Empty section component
const EmptySection = ({ icon, message }: { icon: string; message: string }) => (
const EmptySection = ({
icon,
message,
}: {
icon: string;
message: string;
}) => (
<View style={styles.emptySection}>
<Ionicons name={icon as any} size={40} color={colors.textSecondary} />
<Text style={[styles.emptyText, { color: colors.textSecondary }]}>
@@ -88,7 +149,10 @@ export default function TripDetailPage() {
// Render loading state
if (loading) {
return (
<SafeAreaView style={[styles.container, { backgroundColor: colors.background }]} edges={["top"]}>
<SafeAreaView
style={[styles.container, { backgroundColor: colors.background }]}
edges={["top"]}
>
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color={colors.primary} />
<Text style={[styles.loadingText, { color: colors.textSecondary }]}>
@@ -102,10 +166,21 @@ export default function TripDetailPage() {
// Render error state
if (!trip) {
return (
<SafeAreaView style={[styles.container, { backgroundColor: colors.background }]} edges={["top"]}>
<Header title={t("diary.tripDetail.title")} onBack={() => router.back()} colors={colors} />
<SafeAreaView
style={[styles.container, { backgroundColor: colors.background }]}
edges={["top"]}
>
<Header
title={t("diary.tripDetail.title")}
onBack={() => router.back()}
colors={colors}
/>
<View style={styles.errorContainer}>
<Ionicons name="alert-circle-outline" size={48} color={colors.error || "#FF3B30"} />
<Ionicons
name="alert-circle-outline"
size={48}
color={colors.error || "#FF3B30"}
/>
<Text style={[styles.errorText, { color: colors.textSecondary }]}>
{t("diary.tripDetail.notFound")}
</Text>
@@ -115,19 +190,44 @@ export default function TripDetailPage() {
}
return (
<SafeAreaView style={[styles.container, { backgroundColor: colors.background }]} edges={["top"]}>
<SafeAreaView
style={[styles.container, { backgroundColor: colors.background }]}
edges={["top"]}
>
{/* Header with status badge */}
<View style={[styles.header, { backgroundColor: colors.card, borderBottomColor: colors.separator }]}>
<TouchableOpacity onPress={() => router.back()} style={styles.backButton}>
<View
style={[
styles.header,
{ backgroundColor: colors.card, borderBottomColor: colors.separator },
]}
>
<TouchableOpacity
onPress={() => router.back()}
style={styles.backButton}
>
<Ionicons name="arrow-back" size={24} color={colors.text} />
</TouchableOpacity>
<View style={styles.headerTitles}>
<Text style={[styles.title, { color: colors.text }]} numberOfLines={1}>
<Text
style={[styles.title, { color: colors.text }]}
numberOfLines={1}
>
{trip.name || t("diary.tripDetail.title")}
</Text>
<View style={[styles.statusBadge, { backgroundColor: statusConfig.bgColor }]}>
<Ionicons name={statusConfig.icon as any} size={12} color={statusConfig.textColor} />
<Text style={[styles.statusText, { color: statusConfig.textColor }]}>
<View
style={[
styles.statusBadge,
{ backgroundColor: statusConfig.bgColor },
]}
>
<Ionicons
name={statusConfig.icon as any}
size={12}
color={statusConfig.textColor}
/>
<Text
style={[styles.statusText, { color: statusConfig.textColor }]}
>
{statusConfig.label}
</Text>
</View>
@@ -157,10 +257,18 @@ export default function TripDetailPage() {
>
{tripCosts.length > 0 ? (
<View style={styles.sectionInnerContent}>
<MaterialCostList items={tripCosts} onChange={() => {}} disabled hideTitle />
<MaterialCostList
items={tripCosts}
onChange={() => {}}
disabled
hideTitle
/>
</View>
) : (
<EmptySection icon="receipt-outline" message={t("diary.tripDetail.noCosts")} />
<EmptySection
icon="receipt-outline"
message={t("diary.tripDetail.noCosts")}
/>
)}
</SectionCard>
@@ -174,10 +282,18 @@ export default function TripDetailPage() {
>
{fishingGears.length > 0 ? (
<View style={styles.sectionInnerContent}>
<FishingGearList items={fishingGears} onChange={() => {}} disabled hideTitle />
<FishingGearList
items={fishingGears}
onChange={() => {}}
disabled
hideTitle
/>
</View>
) : (
<EmptySection icon="build-outline" message={t("diary.tripDetail.noGears")} />
<EmptySection
icon="build-outline"
message={t("diary.tripDetail.noGears")}
/>
)}
</SectionCard>
@@ -185,14 +301,17 @@ export default function TripDetailPage() {
<SectionCard
title={t("diary.tripDetail.crew")}
icon="people-outline"
count={trip.crews?.length || 0}
count={crews.length}
collapsible
defaultExpanded
>
{trip.crews && trip.crews.length > 0 ? (
<CrewList crews={trip.crews} />
{crews.length > 0 ? (
<CrewList crews={crews} />
) : (
<EmptySection icon="person-add-outline" message={t("diary.tripDetail.noCrew")} />
<EmptySection
icon="person-add-outline"
message={t("diary.tripDetail.noCrew")}
/>
)}
</SectionCard>
@@ -214,7 +333,12 @@ function Header({
colors: any;
}) {
return (
<View style={[styles.header, { backgroundColor: colors.card, borderBottomColor: colors.separator }]}>
<View
style={[
styles.header,
{ backgroundColor: colors.card, borderBottomColor: colors.separator },
]}
>
<TouchableOpacity onPress={onBack} style={styles.backButton}>
<Ionicons name="arrow-back" size={24} color={colors.text} />
</TouchableOpacity>
@@ -238,7 +362,11 @@ const styles = StyleSheet.create({
},
loadingText: {
fontSize: 14,
fontFamily: Platform.select({ ios: "System", android: "Roboto", default: "System" }),
fontFamily: Platform.select({
ios: "System",
android: "Roboto",
default: "System",
}),
},
header: {
flexDirection: "row",
@@ -259,7 +387,11 @@ const styles = StyleSheet.create({
title: {
fontSize: 18,
fontWeight: "700",
fontFamily: Platform.select({ ios: "System", android: "Roboto", default: "System" }),
fontFamily: Platform.select({
ios: "System",
android: "Roboto",
default: "System",
}),
},
statusBadge: {
flexDirection: "row",
@@ -272,7 +404,11 @@ const styles = StyleSheet.create({
statusText: {
fontSize: 11,
fontWeight: "600",
fontFamily: Platform.select({ ios: "System", android: "Roboto", default: "System" }),
fontFamily: Platform.select({
ios: "System",
android: "Roboto",
default: "System",
}),
},
placeholder: {
width: 32,
@@ -294,7 +430,11 @@ const styles = StyleSheet.create({
errorText: {
fontSize: 14,
textAlign: "center",
fontFamily: Platform.select({ ios: "System", android: "Roboto", default: "System" }),
fontFamily: Platform.select({
ios: "System",
android: "Roboto",
default: "System",
}),
},
sectionInnerContent: {
marginTop: 5,
@@ -307,6 +447,10 @@ const styles = StyleSheet.create({
},
emptyText: {
fontSize: 14,
fontFamily: Platform.select({ ios: "System", android: "Roboto", default: "System" }),
fontFamily: Platform.select({
ios: "System",
android: "Roboto",
default: "System",
}),
},
});