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:
283
app/trip-crew.tsx
Normal file
283
app/trip-crew.tsx
Normal 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" }),
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user