import React, { useState, useEffect, useRef, useCallback } from "react"; import { View, Text, Modal, TouchableOpacity, StyleSheet, Platform, TextInput, ScrollView, ActivityIndicator, Animated, Dimensions, Alert, Image, } from "react-native"; import { Ionicons } from "@expo/vector-icons"; import { useI18n } from "@/hooks/use-i18n"; import { useThemeContext } from "@/hooks/use-theme-context"; import { newTripCrew, updateTripCrew } from "@/controller/TripCrewController"; import { newCrew, searchCrew, updateCrewInfo, queryCrewImage, uploadCrewImage, } from "@/controller/CrewController"; import * as ImagePicker from "expo-image-picker"; import { Buffer } from "buffer"; interface CrewFormData { personalId: string; name: string; phone: string; email: string; address: string; role: string; note: string; } interface AddEditCrewModalProps { visible: boolean; onClose: () => void; onSave: (data: CrewFormData) => Promise; mode: "add" | "edit"; initialData?: Partial; tripId?: string; // Required for add mode to add crew to trip existingCrewIds?: string[]; // List of existing crew IDs in trip tripStatus?: number; // Trạng thái chuyến đi để validate (type number) } const ROLES = ["captain", "crew"]; const DEBOUNCE_DELAY = 1000; // 1 seconds debounce export default function AddEditCrewModal({ visible, onClose, onSave, mode, initialData, tripId, existingCrewIds = [], tripStatus, }: AddEditCrewModalProps) { const { t } = useI18n(); const { colors } = useThemeContext(); // Animation values const fadeAnim = useState(new Animated.Value(0))[0]; const slideAnim = useState( new Animated.Value(Dimensions.get("window").height) )[0]; // Form state const [formData, setFormData] = useState({ personalId: "", name: "", phone: "", email: "", address: "", role: "crew", note: "", }); const [isSubmitting, setIsSubmitting] = useState(false); const [isSearching, setIsSearching] = useState(false); const [isWaitingDebounce, setIsWaitingDebounce] = useState(false); const [searchStatus, setSearchStatus] = useState< "idle" | "found" | "not_found" | "error" >("idle"); const [foundPersonData, setFoundPersonData] = useState(null); // State quản lý ảnh thuyền viên const [imageUri, setImageUri] = useState(null); const [newImageUri, setNewImageUri] = useState(null); const [isLoadingImage, setIsLoadingImage] = useState(false); // Debounce timer ref const debounceTimerRef = useRef | null>(null); // Pre-fill form when editing useEffect(() => { if (visible && initialData) { setFormData({ personalId: initialData.personalId || "", name: initialData.name || "", phone: initialData.phone || "", email: initialData.email || "", address: initialData.address || "", role: initialData.role || "crew", note: initialData.note || "", }); setSearchStatus("idle"); setFoundPersonData(null); // Reset ảnh khi mở modal edit setNewImageUri(null); setImageUri(null); } else if (visible && mode === "add") { // Reset form for add mode setFormData({ personalId: "", name: "", phone: "", email: "", address: "", role: "crew", note: "", }); setSearchStatus("idle"); setFoundPersonData(null); // Reset ảnh setNewImageUri(null); setImageUri(null); } }, [visible, initialData, mode]); // Load ảnh thuyền viên khi edit useEffect(() => { const loadCrewImage = async () => { if (visible && mode === "edit" && initialData?.personalId) { setIsLoadingImage(true); try { const response = await queryCrewImage(initialData.personalId); if (response.data) { const base64 = Buffer.from(response.data as ArrayBuffer).toString( "base64" ); setImageUri(`data:image/jpeg;base64,${base64}`); } } catch (error) { // Không có ảnh hoặc lỗi - hiển thị placeholder setImageUri(null); } finally { setIsLoadingImage(false); } } }; loadCrewImage(); }, [visible, mode, initialData?.personalId]); // Cleanup debounce timer on unmount useEffect(() => { return () => { if (debounceTimerRef.current) { clearTimeout(debounceTimerRef.current); } }; }, []); // Search crew by personal ID with debounce const searchCrewByPersonalId = useCallback(async (personalId: string) => { if (!personalId.trim() || personalId.length < 5) { setSearchStatus("idle"); return; } setIsSearching(true); setSearchStatus("idle"); try { const response = await searchCrew(personalId.trim()); if (response.data) { const person = response.data; setFoundPersonData(person); setSearchStatus("found"); // Auto-fill form with found data setFormData((prev) => ({ ...prev, name: person.name || prev.name, phone: person.phone || prev.phone, email: person.email || prev.email, address: person.address || prev.address, })); // Load avatar của thuyền viên đã tìm thấy try { setIsLoadingImage(true); const imageResponse = await queryCrewImage(personalId.trim()); if (imageResponse.data) { const base64 = Buffer.from( imageResponse.data as ArrayBuffer ).toString("base64"); setImageUri(`data:image/jpeg;base64,${base64}`); } } catch (imageError) { // Không có ảnh - hiển thị placeholder setImageUri(null); } finally { setIsLoadingImage(false); } } else { setSearchStatus("not_found"); setFoundPersonData(null); } } catch (error: any) { // 404 or 401 means not found or unauthorized - treat as not found silently const status = error?.response?.status; if (status === 404 || status === 401) { setSearchStatus("not_found"); } else { // For other errors, just set to not_found without logging to console setSearchStatus("not_found"); } setFoundPersonData(null); } finally { setIsSearching(false); } }, []); // Handle personal ID change with debounce const handlePersonalIdChange = (value: string) => { // If crew was previously found and user is changing personal ID, reset form if (foundPersonData && value !== formData.personalId) { setFormData({ personalId: value, name: "", phone: "", email: "", address: "", role: "crew", note: "", }); setFoundPersonData(null); } else { setFormData((prev) => ({ ...prev, personalId: value })); } setSearchStatus("idle"); setIsWaitingDebounce(false); // Clear previous timer if (debounceTimerRef.current) { clearTimeout(debounceTimerRef.current); } // Only search in add mode and when value is long enough if (mode === "add" && value.trim().length >= 5) { // Show waiting indicator setIsWaitingDebounce(true); // Set new timer debounceTimerRef.current = setTimeout(() => { setIsWaitingDebounce(false); searchCrewByPersonalId(value); }, DEBOUNCE_DELAY); } }; // Chọn ảnh từ thư viện const pickImage = async () => { // Xin quyền truy cập thư viện ảnh const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync(); if (status !== "granted") { Alert.alert(t("common.error"), "Cần cấp quyền truy cập thư viện ảnh"); return; } // Mở image picker const result = await ImagePicker.launchImageLibraryAsync({ mediaTypes: ["images"], allowsEditing: true, aspect: [1, 1], quality: 0.8, }); if (!result.canceled && result.assets[0]) { setNewImageUri(result.assets[0].uri); } }; // Handle animation useEffect(() => { if (visible) { 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 handleClose = () => { // Clear debounce timer if (debounceTimerRef.current) { clearTimeout(debounceTimerRef.current); } 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(); }); }; const handleSave = async () => { // === VALIDATE DỮ LIỆU === if (!formData.personalId.trim()) { Alert.alert(t("common.error"), t("diary.crew.form.personalIdRequired")); return; } if (!formData.name.trim()) { Alert.alert(t("common.error"), t("diary.crew.form.nameRequired")); return; } // Kiểm tra thuyền viên đã có trong chuyến đi chưa (chỉ với mode add) if ( mode === "add" && existingCrewIds.includes(formData.personalId.trim()) ) { Alert.alert(t("common.error"), t("diary.crew.form.crewAlreadyExists")); return; } setIsSubmitting(true); try { if (mode === "add" && tripId) { // === CHẾ ĐỘ THÊM MỚI === const isNewCrew = searchStatus === "not_found" || !foundPersonData; // Bước 1: Nếu không tìm thấy thuyền viên trong hệ thống -> tạo mới // Note: KHÔNG gửi note vào newCrew vì note là riêng cho từng chuyến đi if (isNewCrew) { await newCrew({ personal_id: formData.personalId.trim(), name: formData.name.trim(), phone: formData.phone || "", email: formData.email || "", birth_date: new Date(), note: "", // Không gửi note - note là riêng cho từng chuyến đi address: formData.address || "", }); } // Bước 2: Thêm thuyền viên vào chuyến đi với role await newTripCrew({ trip_id: tripId, personal_id: formData.personalId.trim(), role: formData.role as "captain" | "crew", }); } else if (mode === "edit" && tripId) { // === CHẾ ĐỘ CHỈNH SỬA === // Bước 1: Cập nhật thông tin cá nhân của thuyền viên (không bao gồm note) try { await updateCrewInfo(formData.personalId.trim(), { name: formData.name.trim(), phone: formData.phone || "", email: formData.email || "", birth_date: new Date(), address: formData.address || "", }); console.log("✅ updateCrewInfo thành công"); } catch (crewError: any) { console.error("❌ Lỗi updateCrewInfo:", crewError.response?.data); } // Bước 2: Cập nhật role và note (chỉ khi chuyến đi chưa hoàn thành) // TripStatus: 0=created, 1=pending, 2=approved, 3=active, 4=completed, 5=cancelled if (tripStatus !== 4) { try { await updateTripCrew({ trip_id: tripId, personal_id: formData.personalId.trim(), role: formData.role as "captain" | "crew", note: formData.note || "", }); console.log("✅ updateTripCrew thành công"); } catch (tripCrewError: any) { console.error( "❌ Lỗi updateTripCrew:", tripCrewError.response?.data ); } } else { // Chuyến đi đã hoàn thành - không cho update role/note console.log("⚠️ Chuyến đi đã hoàn thành - bỏ qua updateTripCrew"); } } // Upload ảnh nếu có ảnh mới được chọn if (newImageUri && formData.personalId.trim()) { try { console.log("📤 Uploading image:", newImageUri); await uploadCrewImage(formData.personalId.trim(), newImageUri); console.log("✅ Upload ảnh thành công"); } catch (uploadError: any) { console.error("❌ Lỗi upload ảnh:", uploadError); console.error("Response data:", uploadError.response?.data); console.error("Response status:", uploadError.response?.status); // Không throw error vì thông tin crew đã được lưu thành công } } // Gọi callback để reload danh sách await onSave(formData); handleClose(); } catch (error: any) { console.error("Lỗi khi lưu thuyền viên:", error); console.error("Response data:", error.response?.data); console.error("Response status:", error.response?.status); Alert.alert(t("common.error"), t("diary.crew.form.saveError")); } finally { setIsSubmitting(false); } }; const updateField = (field: keyof CrewFormData, value: string) => { setFormData((prev) => ({ ...prev, [field]: value })); }; // Render search status message const renderSearchStatus = () => { if (isSearching) { return ( {t("diary.crew.form.searching")} ); } if (searchStatus === "found") { return ( {t("diary.crew.form.crewFound")} ); } if (searchStatus === "not_found") { return ( {t("diary.crew.form.crewNotFound")} ); } return null; }; const themedStyles = { modalContainer: { backgroundColor: colors.card }, header: { borderBottomColor: colors.separator }, title: { color: colors.text }, label: { color: colors.text }, input: { backgroundColor: colors.backgroundSecondary, color: colors.text, borderColor: colors.separator, }, placeholder: { color: colors.textSecondary }, roleButton: { backgroundColor: colors.backgroundSecondary, borderColor: colors.separator, }, roleButtonActive: { backgroundColor: colors.primary + "20", borderColor: colors.primary, }, roleText: { color: colors.textSecondary }, roleTextActive: { color: colors.primary }, }; return ( {/* Header */} {mode === "add" ? t("diary.crew.form.addTitle") : t("diary.crew.form.editTitle")} {/* Content */} {/* Ảnh thuyền viên */} {isLoadingImage ? ( ) : newImageUri || imageUri ? ( ) : ( )} Nhấn để chọn ảnh {/* Personal ID */} {t("diary.crew.personalId")} * {/* Show waiting icon during debounce, spinner during search */} {(isWaitingDebounce || isSearching) && ( {isSearching ? ( ) : ( )} )} {mode === "add" && ( {isWaitingDebounce ? t("diary.crew.form.waitingSearch") : t("diary.crew.form.searchHint")} )} {renderSearchStatus()} {/* Name */} {t("diary.crew.form.name")} * updateField("name", v)} placeholder={t("diary.crew.form.namePlaceholder")} placeholderTextColor={themedStyles.placeholder.color} /> {/* Phone */} {t("diary.crew.phone")} updateField("phone", v)} placeholder={t("diary.crew.form.phonePlaceholder")} placeholderTextColor={themedStyles.placeholder.color} keyboardType="phone-pad" /> {/* Role */} {t("diary.crew.form.role")} {ROLES.map((role) => ( updateField("role", role)} > {t(`diary.crew.roles.${role}`)} ))} {/* Address */} {t("diary.crew.form.address")} updateField("address", v)} placeholder={t("diary.crew.form.addressPlaceholder")} placeholderTextColor={themedStyles.placeholder.color} /> {/* Note - Chỉ hiển thị khi edit, ẩn khi add */} {mode === "edit" && ( {t("diary.crew.note")} updateField("note", v)} placeholder={t("diary.crew.form.notePlaceholder")} placeholderTextColor={themedStyles.placeholder.color} multiline numberOfLines={3} /> )} {/* Footer */} {t("common.cancel")} {isSubmitting ? ( ) : ( {t("common.save")} )} ); } const styles = StyleSheet.create({ overlay: { flex: 1, backgroundColor: "rgba(0, 0, 0, 0.5)", justifyContent: "flex-end", }, modalContainer: { borderTopLeftRadius: 24, borderTopRightRadius: 24, maxHeight: "90%", }, header: { flexDirection: "row", alignItems: "center", justifyContent: "space-between", paddingHorizontal: 20, paddingVertical: 16, borderBottomWidth: 1, }, closeButton: { padding: 4, }, title: { fontSize: 18, fontWeight: "700", fontFamily: Platform.select({ ios: "System", android: "Roboto", default: "System", }), }, headerPlaceholder: { width: 32, }, content: { padding: 20, }, formGroup: { marginBottom: 16, }, label: { fontSize: 14, fontWeight: "600", marginBottom: 8, fontFamily: Platform.select({ ios: "System", android: "Roboto", default: "System", }), }, input: { height: 44, borderRadius: 10, borderWidth: 1, paddingHorizontal: 14, fontSize: 15, fontFamily: Platform.select({ ios: "System", android: "Roboto", default: "System", }), }, inputWithIcon: { position: "relative", }, inputWithIconInput: { paddingRight: 44, }, inputIcon: { position: "absolute", right: 12, top: 0, bottom: 0, justifyContent: "center", }, hint: { fontSize: 12, marginTop: 4, fontFamily: Platform.select({ ios: "System", android: "Roboto", default: "System", }), }, statusContainer: { flexDirection: "row", alignItems: "center", gap: 8, marginTop: 8, paddingHorizontal: 12, paddingVertical: 8, borderRadius: 8, }, statusSuccess: {}, statusWarning: {}, statusText: { fontSize: 13, fontFamily: Platform.select({ ios: "System", android: "Roboto", default: "System", }), }, textArea: { height: 80, paddingTop: 12, textAlignVertical: "top", }, roleContainer: { flexDirection: "row", flexWrap: "wrap", gap: 8, }, roleButton: { paddingHorizontal: 14, paddingVertical: 8, borderRadius: 20, borderWidth: 1, }, roleButtonText: { fontSize: 13, fontWeight: "500", fontFamily: Platform.select({ ios: "System", android: "Roboto", default: "System", }), }, footer: { flexDirection: "row", gap: 12, paddingHorizontal: 20, paddingVertical: 20, borderTopWidth: 1, }, cancelButton: { flex: 1, paddingVertical: 14, borderRadius: 12, alignItems: "center", }, cancelButtonText: { fontSize: 16, fontWeight: "600", fontFamily: Platform.select({ ios: "System", android: "Roboto", default: "System", }), }, saveButton: { flex: 1, paddingVertical: 14, borderRadius: 12, alignItems: "center", }, saveButtonText: { fontSize: 16, fontWeight: "600", color: "#FFFFFF", fontFamily: Platform.select({ ios: "System", android: "Roboto", default: "System", }), }, buttonDisabled: { opacity: 0.7, }, // Styles cho phần ảnh thuyền viên photoSection: { alignItems: "center", marginBottom: 20, }, avatarContainer: { width: 100, height: 100, borderRadius: 50, borderWidth: 2, justifyContent: "center", alignItems: "center", overflow: "hidden", position: "relative", }, avatar: { width: 100, height: 100, borderRadius: 50, }, avatarPlaceholder: { width: 100, height: 100, borderRadius: 50, justifyContent: "center", alignItems: "center", }, cameraIconOverlay: { position: "absolute", bottom: 0, right: 0, width: 32, height: 32, borderRadius: 16, justifyContent: "center", alignItems: "center", borderWidth: 2, borderColor: "#FFFFFF", }, photoHint: { fontSize: 12, marginTop: 8, }, });