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:
2025-12-23 23:10:19 +07:00
parent afc6acbfe2
commit 000a4ed856
22 changed files with 3221 additions and 379 deletions

View File

@@ -2,15 +2,14 @@ import { Tabs, useSegments } from "expo-router";
import { HapticTab } from "@/components/haptic-tab";
import { IconSymbol } from "@/components/ui/icon-symbol";
import { Colors } from "@/constants/theme";
import { queryProfile } from "@/controller/AuthController";
import { useI18n } from "@/hooks/use-i18n";
import { useColorScheme } from "@/hooks/use-theme-context";
import { useThemeContext } from "@/hooks/use-theme-context";
import { addUserStorage } from "@/utils/storage";
import { useEffect, useRef } from "react";
export default function TabLayout() {
const colorScheme = useColorScheme();
const { colors, colorScheme } = useThemeContext();
const segments = useSegments() as string[];
const prev = useRef<string | null>(null);
const currentSegment = segments[1] ?? segments[segments.length - 1] ?? null;
@@ -51,9 +50,18 @@ export default function TabLayout() {
return (
<Tabs
screenOptions={{
tabBarActiveTintColor: Colors[colorScheme ?? "light"].tint,
tabBarActiveTintColor: colors.tint,
headerShown: false,
tabBarButton: HapticTab,
// Set tab bar styles based on theme - prevents white flash
tabBarStyle: {
backgroundColor: colors.background,
borderTopColor: colors.separator,
},
// Set screen content background
sceneStyle: {
backgroundColor: colors.background,
},
}}
>
<Tabs.Screen

View File

@@ -10,6 +10,7 @@ import {
} from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
import { Ionicons } from "@expo/vector-icons";
import { useRouter } from "expo-router";
import FilterButton from "@/components/diary/FilterButton";
import TripCard from "@/components/diary/TripCard";
import FilterModal, { FilterValues } from "@/components/diary/FilterModal";
@@ -23,10 +24,10 @@ import { useThemeContext } from "@/hooks/use-theme-context";
export default function diary() {
const { t } = useI18n();
const { colors } = useThemeContext();
const router = useRouter();
const [showFilterModal, setShowFilterModal] = useState(false);
const [showAddTripModal, setShowAddTripModal] = useState(false);
const [editingTrip, setEditingTrip] = useState<Model.Trip | null>(null);
const [viewingTrip, setViewingTrip] = useState<Model.Trip | null>(null);
const [filters, setFilters] = useState<FilterValues>({
status: null,
startDate: null,
@@ -40,6 +41,10 @@ export default function diary() {
const [hasMore, setHasMore] = useState(true);
const isInitialLoad = useRef(true);
const flatListRef = useRef<FlatList>(null);
// Refs to prevent duplicate API calls from React Compiler/Strict Mode
const hasInitializedThings = useRef(false);
const hasInitializedTrips = useRef(false);
// Body call API things (đang fix cứng)
const payloadThings: Model.SearchThingBody = {
@@ -55,6 +60,8 @@ export default function diary() {
// Gọi API things
const { getThings } = useThings();
useEffect(() => {
if (hasInitializedThings.current) return;
hasInitializedThings.current = true;
getThings(payloadThings);
}, []);
@@ -79,10 +86,10 @@ export default function diary() {
const { tripsList, getTripsList, loading } = useTripsList();
// console.log("Payload trips:", payloadTrips);
// Gọi API trips lần đầu
useEffect(() => {
if (hasInitializedTrips.current) return;
hasInitializedTrips.current = true;
isInitialLoad.current = true;
setAllTrips([]);
setHasMore(true);
@@ -157,7 +164,11 @@ export default function diary() {
// Hàm load more data khi scroll đến cuối
const handleLoadMore = useCallback(() => {
if (isLoadingMore || !hasMore) {
// Không load more nếu:
// - Đang loading (ban đầu hoặc loadMore)
// - Không còn data để load
// - Chưa có data nào (tránh FlatList tự trigger khi list rỗng)
if (isLoadingMore || loading || !hasMore || allTrips.length === 0) {
return;
}
@@ -169,7 +180,7 @@ export default function diary() {
};
setPayloadTrips(updatedPayload);
getTripsList(updatedPayload);
}, [isLoadingMore, hasMore, payloadTrips]);
}, [isLoadingMore, loading, hasMore, allTrips.length, payloadTrips]);
// const handleTripPress = (tripId: string) => {
// // TODO: Navigate to trip detail
@@ -177,11 +188,16 @@ export default function diary() {
// };
const handleViewTrip = (tripId: string) => {
// Find the trip from allTrips and open modal in view mode
// Navigate to trip detail page instead of opening modal
const tripToView = allTrips.find((trip) => trip.id === tripId);
if (tripToView) {
setViewingTrip(tripToView);
setShowAddTripModal(true);
router.push({
pathname: "/trip-detail",
params: {
tripId: tripToView.id,
tripData: JSON.stringify(tripToView)
},
});
}
};
@@ -195,8 +211,13 @@ export default function diary() {
};
const handleViewTeam = (tripId: string) => {
console.log("View team:", tripId);
// TODO: Navigate to team management
const trip = allTrips.find((t) => t.id === tripId);
if (trip) {
router.push({
pathname: "/trip-crew",
params: { tripId: trip.id, tripName: trip.name || "" },
});
}
};
const handleSendTrip = (tripId: string) => {
@@ -260,7 +281,13 @@ export default function diary() {
onDelete={() => handleDeleteTrip(item.id)}
/>
),
[handleViewTrip, handleEditTrip, handleViewTeam, handleSendTrip, handleDeleteTrip]
[
handleViewTrip,
handleEditTrip,
handleViewTeam,
handleSendTrip,
handleDeleteTrip,
]
);
// Key extractor cho FlatList
@@ -301,10 +328,15 @@ export default function diary() {
};
return (
<SafeAreaView style={[styles.safeArea, themedStyles.safeArea]} edges={["top"]}>
<SafeAreaView
style={[styles.safeArea, themedStyles.safeArea]}
edges={["top"]}
>
<View style={styles.container}>
{/* Header */}
<Text style={[styles.titleText, themedStyles.titleText]}>{t("diary.title")}</Text>
<Text style={[styles.titleText, themedStyles.titleText]}>
{t("diary.title")}
</Text>
{/* Filter & Add Button Row */}
<View style={styles.actionRow}>
@@ -358,17 +390,16 @@ export default function diary() {
onApply={handleApplyFilters}
/>
{/* Add/Edit/View Trip Modal */}
{/* Add/Edit Trip Modal */}
<AddTripModal
visible={showAddTripModal}
onClose={() => {
setShowAddTripModal(false);
setEditingTrip(null);
setViewingTrip(null);
}}
onSuccess={handleTripAddSuccess}
mode={viewingTrip ? 'view' : editingTrip ? 'edit' : 'add'}
tripData={viewingTrip || editingTrip || undefined}
mode={editingTrip ? "edit" : "add"}
tripData={editingTrip || undefined}
/>
</SafeAreaView>
);
@@ -394,17 +425,10 @@ const styles = StyleSheet.create({
}),
},
actionRow: {
flexDirection: "row",
justifyContent: "flex-start",
alignItems: "center",
gap: 12,
marginBottom: 12,
},
headerRow: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
marginTop: 20,
gap: 12,
marginBottom: 12,
},
countText: {
@@ -421,7 +445,7 @@ const styles = StyleSheet.create({
flexDirection: "row",
alignItems: "center",
paddingHorizontal: 16,
paddingVertical: 8,
height: 40,
borderRadius: 8,
gap: 6,
},

View File

@@ -2,14 +2,14 @@ import {
DarkTheme,
DefaultTheme,
ThemeProvider,
Theme,
} from "@react-navigation/native";
import { Stack, useRouter } from "expo-router";
import { StatusBar } from "expo-status-bar";
import { useEffect } from "react";
import { useEffect, useMemo } from "react";
import { View, StyleSheet } from "react-native";
import "react-native-reanimated";
// import Toast from "react-native-toast-message";
// import { toastConfig } from "@/config";
import { toastConfig } from "@/config";
import { setRouterInstance } from "@/config/auth";
import "@/global.css";
@@ -18,50 +18,101 @@ import {
ThemeProvider as AppThemeProvider,
useThemeContext,
} from "@/hooks/use-theme-context";
import { Colors } from "@/constants/theme";
import Toast from "react-native-toast-message";
import "../global.css";
function AppContent() {
const router = useRouter();
const { colorScheme } = useThemeContext();
console.log("Color Scheme: ", colorScheme);
const { colorScheme, colors } = useThemeContext();
useEffect(() => {
setRouterInstance(router);
}, [router]);
// Create custom navigation theme that uses our app colors
// This ensures the navigation container background matches our theme
const navigationTheme: Theme = useMemo(() => {
const baseTheme = colorScheme === "dark" ? DarkTheme : DefaultTheme;
return {
...baseTheme,
colors: {
...baseTheme.colors,
background: colors.background,
card: colors.card,
text: colors.text,
border: colors.border,
primary: colors.primary,
},
};
}, [colorScheme, colors]);
return (
<ThemeProvider value={colorScheme === "dark" ? DarkTheme : DefaultTheme}>
<Stack
screenOptions={{ headerShown: false }}
initialRouteName="auth/login"
>
<Stack.Screen
name="auth/login"
options={{
title: "Login",
// Wrap entire app with View that has themed background
// This prevents any white flash during screen transitions
<View style={[styles.container, { backgroundColor: colors.background }]}>
<ThemeProvider value={navigationTheme}>
<Stack
screenOptions={{
headerShown: false,
// Set content background to match theme
contentStyle: {
backgroundColor: colors.background,
},
// Animation settings for smoother transitions
animation: "slide_from_right",
}}
/>
initialRouteName="auth/login"
>
<Stack.Screen
name="auth/login"
options={{
title: "Login",
headerShown: false,
}}
/>
<Stack.Screen
name="(tabs)"
options={{
title: "Home",
headerShown: false,
}}
/>
<Stack.Screen
name="(tabs)"
options={{
title: "Home",
headerShown: false,
}}
/>
<Stack.Screen
name="modal"
options={{ presentation: "formSheet", title: "Modal" }}
/>
</Stack>
<StatusBar style="auto" />
<Toast config={toastConfig} visibilityTime={2000} topOffset={60} />
</ThemeProvider>
<Stack.Screen
name="trip-detail"
options={{
title: "Trip Detail",
headerShown: false,
}}
/>
<Stack.Screen
name="trip-crew"
options={{
title: "Trip Crew",
headerShown: false,
}}
/>
<Stack.Screen
name="modal"
options={{ presentation: "formSheet", title: "Modal" }}
/>
</Stack>
<StatusBar style={colorScheme === "dark" ? "light" : "dark"} />
<Toast config={toastConfig} visibilityTime={2000} topOffset={60} />
</ThemeProvider>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
});
export default function RootLayout() {
return (
<I18nProvider>

283
app/trip-crew.tsx Normal file
View File

@@ -0,0 +1,283 @@
import React, { useEffect, useState, useCallback } from "react";
import {
View,
Text,
StyleSheet,
Platform,
TouchableOpacity,
ActivityIndicator,
Alert,
} 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 { queryTripCrew } from "@/controller/TripCrewController";
import CrewList from "@/components/diary/TripCrewModal/CrewList";
import AddEditCrewModal from "@/components/diary/TripCrewModal/AddEditCrewModal";
export default function TripCrewPage() {
const { t } = useI18n();
const { colors } = useThemeContext();
const router = useRouter();
const { tripId, tripName } = useLocalSearchParams<{ tripId: string; tripName?: string }>();
// State
const [loading, setLoading] = useState(false);
const [crews, setCrews] = useState<Model.TripCrews[]>([]);
const [error, setError] = useState<string | null>(null);
// Add/Edit modal state
const [showAddEditModal, setShowAddEditModal] = useState(false);
const [editingCrew, setEditingCrew] = useState<Model.TripCrews | null>(null);
// Fetch crew data
const fetchCrewData = useCallback(async () => {
if (!tripId) return;
setLoading(true);
setError(null);
try {
const response = await queryTripCrew(tripId);
const data = response.data as any;
if (data?.trip_crews && Array.isArray(data.trip_crews)) {
setCrews(data.trip_crews);
} else if (Array.isArray(data)) {
setCrews(data);
} else {
setCrews([]);
}
} catch (err) {
console.error("Error fetching crew:", err);
setError(t("diary.crew.fetchError"));
setCrews([]);
} finally {
setLoading(false);
}
}, [tripId, t]);
useEffect(() => {
fetchCrewData();
}, [fetchCrewData]);
// Add crew handler
const handleAddCrew = () => {
setEditingCrew(null);
setShowAddEditModal(true);
};
// Edit crew handler
const handleEditCrew = (crew: Model.TripCrews) => {
setEditingCrew(crew);
setShowAddEditModal(true);
};
// Delete crew handler
const handleDeleteCrew = (crew: Model.TripCrews) => {
Alert.alert(
t("diary.crew.deleteConfirmTitle"),
t("diary.crew.deleteConfirmMessage", { name: crew.Person?.name || "" }),
[
{ text: t("common.cancel"), style: "cancel" },
{
text: t("common.delete"),
style: "destructive",
onPress: async () => {
// TODO: Call delete API when available
setCrews((prev) => prev.filter((c) => c.PersonalID !== crew.PersonalID));
Alert.alert(t("common.success"), t("diary.crew.deleteSuccess"));
},
},
]
);
};
// Save crew handler
const handleSaveCrew = async (formData: any) => {
// TODO: Call API to add/edit crew when available
await fetchCrewData();
};
const themedStyles = {
container: { backgroundColor: colors.background },
header: { backgroundColor: colors.card, borderBottomColor: colors.separator },
title: { color: colors.text },
subtitle: { color: colors.textSecondary },
};
return (
<SafeAreaView style={[styles.container, themedStyles.container]} edges={["top"]}>
{/* Header */}
<View style={[styles.header, themedStyles.header]}>
<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, themedStyles.title]}>
{t("diary.crew.title")}
</Text>
{tripName && (
<Text style={[styles.subtitle, themedStyles.subtitle]} numberOfLines={1}>
{tripName}
</Text>
)}
</View>
<TouchableOpacity onPress={handleAddCrew} style={styles.addButton}>
<Ionicons name="add" size={24} color={colors.primary} />
</TouchableOpacity>
</View>
{/* Content */}
<View style={styles.content}>
{loading ? (
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color={colors.primary} />
<Text style={[styles.loadingText, { color: colors.textSecondary }]}>
{t("diary.crew.loading")}
</Text>
</View>
) : error ? (
<View style={styles.errorContainer}>
<Ionicons name="alert-circle-outline" size={48} color={colors.error || "#FF3B30"} />
<Text style={[styles.errorText, { color: colors.error || "#FF3B30" }]}>
{error}
</Text>
<TouchableOpacity
style={[styles.retryButton, { backgroundColor: colors.primary }]}
onPress={fetchCrewData}
>
<Text style={styles.retryButtonText}>{t("common.retry")}</Text>
</TouchableOpacity>
</View>
) : (
<CrewList crews={crews} onEdit={handleEditCrew} onDelete={handleDeleteCrew} />
)}
</View>
{/* Footer - Crew count */}
<View style={[styles.footer, { backgroundColor: colors.card, borderTopColor: colors.separator }]}>
<View style={styles.countContainer}>
<Ionicons name="people-outline" size={20} color={colors.primary} />
<Text style={[styles.countText, { color: colors.text }]}>
{t("diary.crew.totalMembers", { count: crews.length })}
</Text>
</View>
</View>
{/* Add/Edit Crew Modal */}
<AddEditCrewModal
visible={showAddEditModal}
onClose={() => {
setShowAddEditModal(false);
setEditingCrew(null);
}}
onSave={handleSaveCrew}
mode={editingCrew ? "edit" : "add"}
initialData={
editingCrew
? {
personalId: editingCrew.PersonalID,
name: editingCrew.Person?.name || "",
phone: editingCrew.Person?.phone || "",
email: editingCrew.Person?.email || "",
address: editingCrew.Person?.address || "",
role: editingCrew.role || "crew",
note: editingCrew.note || "",
}
: undefined
}
/>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
header: {
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
paddingHorizontal: 16,
paddingVertical: 12,
borderBottomWidth: 1,
},
backButton: {
padding: 4,
},
addButton: {
padding: 4,
},
headerTitles: {
flex: 1,
alignItems: "center",
},
title: {
fontSize: 18,
fontWeight: "700",
fontFamily: Platform.select({ ios: "System", android: "Roboto", default: "System" }),
},
subtitle: {
fontSize: 13,
marginTop: 2,
fontFamily: Platform.select({ ios: "System", android: "Roboto", default: "System" }),
},
content: {
flex: 1,
padding: 16,
},
loadingContainer: {
flex: 1,
justifyContent: "center",
alignItems: "center",
gap: 12,
},
loadingText: {
fontSize: 14,
fontFamily: Platform.select({ ios: "System", android: "Roboto", default: "System" }),
},
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" }),
},
retryButton: {
marginTop: 8,
paddingHorizontal: 24,
paddingVertical: 10,
borderRadius: 8,
},
retryButtonText: {
color: "#FFFFFF",
fontSize: 14,
fontWeight: "600",
fontFamily: Platform.select({ ios: "System", android: "Roboto", default: "System" }),
},
footer: {
borderTopWidth: 1,
paddingHorizontal: 20,
paddingTop: 16,
paddingBottom: 30,
},
countContainer: {
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
gap: 8,
},
countText: {
fontSize: 14,
fontWeight: "600",
fontFamily: Platform.select({ ios: "System", android: "Roboto", default: "System" }),
},
});

312
app/trip-detail.tsx Normal file
View 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" }),
},
});