Files
sgw-owner-app/components/diary/TripCrewModal/index.tsx

438 lines
11 KiB
TypeScript

import React, { useEffect, useRef, useState } from "react";
import {
View,
Text,
Modal,
TouchableOpacity,
StyleSheet,
Platform,
ActivityIndicator,
Animated,
Dimensions,
Alert,
} from "react-native";
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 "./CrewList";
import AddEditCrewModal from "./AddEditCrewModal";
interface TripCrewModalProps {
visible: boolean;
onClose: () => void;
tripId: string | null;
tripName?: string;
}
export default function TripCrewModal({
visible,
onClose,
tripId,
tripName,
}: TripCrewModalProps) {
const { t } = useI18n();
const { colors } = useThemeContext();
// Animation values
const fadeAnim = useRef(new Animated.Value(0)).current;
const slideAnim = useRef(
new Animated.Value(Dimensions.get("window").height)
).current;
// 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 when modal opens
useEffect(() => {
if (visible && tripId) {
fetchCrewData();
}
}, [visible, tripId]);
// Handle animation when modal visibility changes
useEffect(() => {
if (visible) {
setError(null);
Animated.parallel([
Animated.timing(fadeAnim, {
toValue: 1,
duration: 300,
useNativeDriver: true,
}),
Animated.timing(slideAnim, {
toValue: 0,
duration: 300,
useNativeDriver: true,
}),
]).start();
}
}, [visible, fadeAnim, slideAnim]);
const fetchCrewData = 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);
}
};
const handleClose = () => {
Animated.parallel([
Animated.timing(fadeAnim, {
toValue: 0,
duration: 250,
useNativeDriver: true,
}),
Animated.timing(slideAnim, {
toValue: Dimensions.get("window").height,
duration: 250,
useNativeDriver: true,
}),
]).start(() => {
onClose();
setTimeout(() => {
setCrews([]);
setError(null);
}, 100);
});
};
// 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
// For now, just remove from local state
setCrews((prev) =>
prev.filter((c) => c.PersonalID !== crew.PersonalID)
);
Alert.alert(t("common.success"), t("diary.crew.deleteSuccess"));
},
},
]
);
};
// Save crew handler - API được gọi trong AddEditCrewModal, sau đó refresh danh sách
const handleSaveCrew = async () => {
await fetchCrewData();
};
const themedStyles = {
modalContainer: { backgroundColor: colors.card },
header: { borderBottomColor: colors.separator },
title: { color: colors.text },
subtitle: { color: colors.textSecondary },
content: { backgroundColor: colors.background },
};
return (
<>
<Modal
visible={visible}
animationType="none"
transparent
onRequestClose={handleClose}
>
<Animated.View style={[styles.overlay, { opacity: fadeAnim }]}>
<Animated.View
style={[
styles.modalContainer,
themedStyles.modalContainer,
{ transform: [{ translateY: slideAnim }] },
]}
>
{/* Header */}
<View style={[styles.header, themedStyles.header]}>
<TouchableOpacity
onPress={handleClose}
style={styles.closeButton}
>
<Ionicons name="close" 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>
{/* Add Button */}
<TouchableOpacity
onPress={handleAddCrew}
style={styles.addButton}
>
<Ionicons name="add" size={24} color={colors.primary} />
</TouchableOpacity>
</View>
{/* Content */}
<View style={[styles.content, themedStyles.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, { 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>
</Animated.View>
</Animated.View>
</Modal>
{/* Add/Edit Crew Modal */}
<AddEditCrewModal
visible={showAddEditModal}
onClose={() => {
setShowAddEditModal(false);
setEditingCrew(null);
}}
onSave={handleSaveCrew}
mode={editingCrew ? "edit" : "add"}
tripId={tripId || undefined}
existingCrewIds={crews.map((c) => c.PersonalID)}
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
}
/>
</>
);
}
const styles = StyleSheet.create({
overlay: {
flex: 1,
backgroundColor: "rgba(0, 0, 0, 0.5)",
justifyContent: "flex-end",
},
modalContainer: {
borderTopLeftRadius: 24,
borderTopRightRadius: 24,
maxHeight: "92%",
minHeight: "80%",
shadowColor: "#000",
shadowOffset: { width: 0, height: -4 },
shadowOpacity: 0.1,
shadowRadius: 12,
elevation: 8,
},
header: {
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
paddingHorizontal: 20,
paddingVertical: 16,
borderBottomWidth: 1,
},
closeButton: {
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,
paddingVertical: 16,
},
countContainer: {
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
gap: 8,
},
countText: {
fontSize: 14,
fontWeight: "600",
fontFamily: Platform.select({
ios: "System",
android: "Roboto",
default: "System",
}),
},
});