457 lines
11 KiB
TypeScript
457 lines
11 KiB
TypeScript
import React, { useEffect, useState, useMemo } from "react";
|
|
import {
|
|
View,
|
|
Text,
|
|
StyleSheet,
|
|
Platform,
|
|
TouchableOpacity,
|
|
ScrollView,
|
|
ActivityIndicator,
|
|
} from "react-native";
|
|
import { SafeAreaView } from "react-native-safe-area-context";
|
|
import { useLocalSearchParams, useRouter } from "expo-router";
|
|
import { Ionicons } from "@expo/vector-icons";
|
|
import { useI18n } from "@/hooks/use-i18n";
|
|
import { useThemeContext } from "@/hooks/use-theme-context";
|
|
import { TRIP_STATUS_CONFIG } from "@/components/diary/types";
|
|
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";
|
|
import FishingGearList from "@/components/diary/TripFormModal/FishingGearList";
|
|
import MaterialCostList from "@/components/diary/TripFormModal/MaterialCostList";
|
|
|
|
// Section components
|
|
import {
|
|
SectionCard,
|
|
AlertsSection,
|
|
FishingLogsSection,
|
|
BasicInfoSection,
|
|
} from "@/components/diary/TripDetailSections";
|
|
|
|
export default function TripDetailPage() {
|
|
const { t } = useI18n();
|
|
const { colors } = useThemeContext();
|
|
const router = useRouter();
|
|
const { tripId } = useLocalSearchParams<{
|
|
tripId: string;
|
|
}>();
|
|
|
|
const [loading, setLoading] = useState(true);
|
|
const [trip, setTrip] = useState<Model.Trip | null>(null);
|
|
const [alerts, setAlerts] = useState<Model.Alarm[]>([]);
|
|
const [crews, setCrews] = useState<Model.TripCrews[]>([]);
|
|
|
|
// Fetch trip data từ API
|
|
useEffect(() => {
|
|
const fetchTripData = async () => {
|
|
if (!tripId) return;
|
|
|
|
setLoading(true);
|
|
try {
|
|
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);
|
|
}
|
|
};
|
|
|
|
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(
|
|
() => convertFishingGears(trip?.fishing_gears, "view"),
|
|
[trip?.fishing_gears]
|
|
);
|
|
|
|
const tripCosts = useMemo(
|
|
() => convertTripCosts(trip?.trip_cost, "view"),
|
|
[trip?.trip_cost]
|
|
);
|
|
|
|
const statusConfig = useMemo(() => {
|
|
const status = trip?.trip_status ?? 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;
|
|
}) => (
|
|
<View style={styles.emptySection}>
|
|
<Ionicons name={icon as any} size={40} color={colors.textSecondary} />
|
|
<Text style={[styles.emptyText, { color: colors.textSecondary }]}>
|
|
{message}
|
|
</Text>
|
|
</View>
|
|
);
|
|
|
|
// Render loading state
|
|
if (loading) {
|
|
return (
|
|
<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 }]}>
|
|
{t("common.loading")}
|
|
</Text>
|
|
</View>
|
|
</SafeAreaView>
|
|
);
|
|
}
|
|
|
|
// 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}
|
|
/>
|
|
<View style={styles.errorContainer}>
|
|
<Ionicons
|
|
name="alert-circle-outline"
|
|
size={48}
|
|
color={colors.error || "#FF3B30"}
|
|
/>
|
|
<Text style={[styles.errorText, { color: colors.textSecondary }]}>
|
|
{t("diary.tripDetail.notFound")}
|
|
</Text>
|
|
</View>
|
|
</SafeAreaView>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<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}
|
|
>
|
|
<Ionicons name="arrow-back" size={24} color={colors.text} />
|
|
</TouchableOpacity>
|
|
<View style={styles.headerTitles}>
|
|
<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 }]}
|
|
>
|
|
{statusConfig.label}
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
<View style={styles.placeholder} />
|
|
</View>
|
|
|
|
{/* Content */}
|
|
<ScrollView
|
|
style={styles.content}
|
|
contentContainerStyle={styles.contentContainer}
|
|
showsVerticalScrollIndicator={false}
|
|
>
|
|
{/* Basic Info */}
|
|
<BasicInfoSection trip={trip} />
|
|
|
|
{/* Alerts */}
|
|
<AlertsSection alerts={alerts} />
|
|
|
|
{/* Trip Costs */}
|
|
<SectionCard
|
|
title={t("diary.tripDetail.costs")}
|
|
icon="wallet-outline"
|
|
count={tripCosts.length}
|
|
collapsible
|
|
defaultExpanded
|
|
>
|
|
{tripCosts.length > 0 ? (
|
|
<View style={styles.sectionInnerContent}>
|
|
<MaterialCostList
|
|
items={tripCosts}
|
|
onChange={() => {}}
|
|
disabled
|
|
hideTitle
|
|
/>
|
|
</View>
|
|
) : (
|
|
<EmptySection
|
|
icon="receipt-outline"
|
|
message={t("diary.tripDetail.noCosts")}
|
|
/>
|
|
)}
|
|
</SectionCard>
|
|
|
|
{/* Fishing Gears */}
|
|
<SectionCard
|
|
title={t("diary.tripDetail.gears")}
|
|
icon="construct-outline"
|
|
count={fishingGears.length}
|
|
collapsible
|
|
defaultExpanded
|
|
>
|
|
{fishingGears.length > 0 ? (
|
|
<View style={styles.sectionInnerContent}>
|
|
<FishingGearList
|
|
items={fishingGears}
|
|
onChange={() => {}}
|
|
disabled
|
|
hideTitle
|
|
/>
|
|
</View>
|
|
) : (
|
|
<EmptySection
|
|
icon="build-outline"
|
|
message={t("diary.tripDetail.noGears")}
|
|
/>
|
|
)}
|
|
</SectionCard>
|
|
|
|
{/* Crew List */}
|
|
<SectionCard
|
|
title={t("diary.tripDetail.crew")}
|
|
icon="people-outline"
|
|
count={crews.length}
|
|
collapsible
|
|
defaultExpanded
|
|
>
|
|
{crews.length > 0 ? (
|
|
<CrewList crews={crews} />
|
|
) : (
|
|
<EmptySection
|
|
icon="person-add-outline"
|
|
message={t("diary.tripDetail.noCrew")}
|
|
/>
|
|
)}
|
|
</SectionCard>
|
|
|
|
{/* Fishing Logs */}
|
|
<FishingLogsSection fishingLogs={trip.fishing_logs} />
|
|
</ScrollView>
|
|
</SafeAreaView>
|
|
);
|
|
}
|
|
|
|
// Header component for reuse
|
|
function Header({
|
|
title,
|
|
onBack,
|
|
colors,
|
|
}: {
|
|
title: string;
|
|
onBack: () => void;
|
|
colors: any;
|
|
}) {
|
|
return (
|
|
<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>
|
|
<View style={styles.headerTitles}>
|
|
<Text style={[styles.title, { color: colors.text }]}>{title}</Text>
|
|
</View>
|
|
<View style={styles.placeholder} />
|
|
</View>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
container: {
|
|
flex: 1,
|
|
},
|
|
loadingContainer: {
|
|
flex: 1,
|
|
justifyContent: "center",
|
|
alignItems: "center",
|
|
gap: 12,
|
|
},
|
|
loadingText: {
|
|
fontSize: 14,
|
|
fontFamily: Platform.select({
|
|
ios: "System",
|
|
android: "Roboto",
|
|
default: "System",
|
|
}),
|
|
},
|
|
header: {
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
justifyContent: "space-between",
|
|
paddingHorizontal: 16,
|
|
paddingVertical: 12,
|
|
borderBottomWidth: 1,
|
|
},
|
|
backButton: {
|
|
padding: 4,
|
|
},
|
|
headerTitles: {
|
|
flex: 1,
|
|
alignItems: "center",
|
|
gap: 6,
|
|
},
|
|
title: {
|
|
fontSize: 18,
|
|
fontWeight: "700",
|
|
fontFamily: Platform.select({
|
|
ios: "System",
|
|
android: "Roboto",
|
|
default: "System",
|
|
}),
|
|
},
|
|
statusBadge: {
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
gap: 4,
|
|
paddingHorizontal: 10,
|
|
paddingVertical: 4,
|
|
borderRadius: 12,
|
|
},
|
|
statusText: {
|
|
fontSize: 11,
|
|
fontWeight: "600",
|
|
fontFamily: Platform.select({
|
|
ios: "System",
|
|
android: "Roboto",
|
|
default: "System",
|
|
}),
|
|
},
|
|
placeholder: {
|
|
width: 32,
|
|
},
|
|
content: {
|
|
flex: 1,
|
|
},
|
|
contentContainer: {
|
|
padding: 16,
|
|
paddingBottom: 40,
|
|
},
|
|
errorContainer: {
|
|
flex: 1,
|
|
justifyContent: "center",
|
|
alignItems: "center",
|
|
gap: 12,
|
|
paddingHorizontal: 20,
|
|
},
|
|
errorText: {
|
|
fontSize: 14,
|
|
textAlign: "center",
|
|
fontFamily: Platform.select({
|
|
ios: "System",
|
|
android: "Roboto",
|
|
default: "System",
|
|
}),
|
|
},
|
|
sectionInnerContent: {
|
|
marginTop: 5,
|
|
},
|
|
emptySection: {
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
paddingVertical: 24,
|
|
gap: 8,
|
|
},
|
|
emptyText: {
|
|
fontSize: 14,
|
|
fontFamily: Platform.select({
|
|
ios: "System",
|
|
android: "Roboto",
|
|
default: "System",
|
|
}),
|
|
},
|
|
});
|