378 lines
10 KiB
TypeScript
378 lines
10 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 (add or edit)
|
|
const handleSaveCrew = async (formData: any) => {
|
|
// TODO: Call API to add/edit crew when available
|
|
// For now, refresh the list
|
|
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"}
|
|
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" }),
|
|
},
|
|
});
|