thêm tab "Xem chi tiết chuyến đi", "Xem chi tiết thành viên chuyến đi", tái sử dụng lại components modal tripForm
This commit is contained in:
312
app/trip-detail.tsx
Normal file
312
app/trip-detail.tsx
Normal file
@@ -0,0 +1,312 @@
|
||||
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";
|
||||
|
||||
// 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, tripData: tripDataParam } = 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
|
||||
|
||||
// Parse trip data from params or fetch from API
|
||||
useEffect(() => {
|
||||
if (tripDataParam) {
|
||||
try {
|
||||
const parsedTrip = JSON.parse(tripDataParam) as Model.Trip;
|
||||
setTrip(parsedTrip);
|
||||
} catch (e) {
|
||||
console.error("Error parsing trip data:", e);
|
||||
}
|
||||
}
|
||||
// TODO: Fetch trip detail from API using tripId if not passed via params
|
||||
setLoading(false);
|
||||
}, [tripDataParam, 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 />
|
||||
</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 />
|
||||
</View>
|
||||
) : (
|
||||
<EmptySection icon="build-outline" message={t("diary.tripDetail.noGears")} />
|
||||
)}
|
||||
</SectionCard>
|
||||
|
||||
{/* Crew List */}
|
||||
<SectionCard
|
||||
title={t("diary.tripDetail.crew")}
|
||||
icon="people-outline"
|
||||
count={trip.crews?.length || 0}
|
||||
collapsible
|
||||
defaultExpanded
|
||||
>
|
||||
{trip.crews && trip.crews.length > 0 ? (
|
||||
<CrewList crews={trip.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: -8,
|
||||
},
|
||||
emptySection: {
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
paddingVertical: 24,
|
||||
gap: 8,
|
||||
},
|
||||
emptyText: {
|
||||
fontSize: 14,
|
||||
fontFamily: Platform.select({ ios: "System", android: "Roboto", default: "System" }),
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user