import React, { useState, useEffect } from 'react' import { useTranslation } from 'react-i18next' import { db } from '../services/db.js' import { getActiveMasterKey } from '../services/auth.js' import { getLogbookKey } from '../services/logbookKeys.js' import { encryptJson, decryptJson } from '../services/crypto.js' import { syncLogbook } from '../services/sync.js' import { PlausibleEvents, trackPlausibleEvent } from '../services/analytics.js' import { useDialog } from './ModalDialog.tsx' import { Users, User, Plus, Trash2, Edit2, Save, X, Check, Camera } from 'lucide-react' interface CrewFormProps { logbookId: string readOnly?: boolean skipperReadOnly?: boolean preloadedData?: any[] } interface CrewMemberData { name: string address: string birthDate: string phone: string nationality: string passportNumber: string bloodType: string allergies: string diseases: string role: 'skipper' | 'crew' photo?: string | null } interface DecryptedCrew { payloadId: string data: CrewMemberData } export default function CrewForm({ logbookId, readOnly = false, skipperReadOnly = false, preloadedData }: CrewFormProps) { const { t } = useTranslation() const { showConfirm } = useDialog() const skipperFormReadOnly = readOnly || skipperReadOnly // Skipper profile state const [skipName, setSkipName] = useState('') const [skipAddress, setSkipAddress] = useState('') const [skipBirthDate, setSkipBirthDate] = useState('') const [skipPhone, setSkipPhone] = useState('') const [skipNationality, setSkipNationality] = useState('') const [skipPassport, setSkipPassport] = useState('') const [skipBloodType, setSkipBloodType] = useState('') const [skipAllergies, setSkipAllergies] = useState('') const [skipDiseases, setSkipDiseases] = useState('') const skipFileInputRef = React.useRef(null) const [skipPhoto, setSkipPhoto] = useState(null) const [skipPhotoError, setSkipPhotoError] = useState(null) // Crew list state const [crewList, setCrewList] = useState([]) // Inline editor modal/form state const [showMemberForm, setShowMemberForm] = useState(false) const [editingId, setEditingId] = useState(null) // null = adding, string = editing const [memName, setMemName] = useState('') const [memAddress, setMemAddress] = useState('') const [memBirthDate, setMemBirthDate] = useState('') const [memPhone, setMemPhone] = useState('') const [memNationality, setMemNationality] = useState('') const [memPassport, setMemPassport] = useState('') const [memBloodType, setMemBloodType] = useState('') const [memAllergies, setMemAllergies] = useState('') const [memDiseases, setMemDiseases] = useState('') const memFileInputRef = React.useRef(null) const [memPhoto, setMemPhoto] = useState(null) const [memPhotoError, setMemPhotoError] = useState(null) const [loading, setLoading] = useState(false) const [savingSkipper, setSavingSkipper] = useState(false) const [savingMember, setSavingMember] = useState(false) const [skipperSuccess, setSkipperSuccess] = useState(false) const [error, setError] = useState(null) useEffect(() => { loadCrewData() }, [logbookId, preloadedData]) const resizeImageFile = (file: File): Promise => { return new Promise((resolve, reject) => { const reader = new FileReader() reader.onload = (event) => { const img = new Image() img.onload = () => { try { const canvas = document.createElement('canvas') const ctx = canvas.getContext('2d') if (!ctx) throw new Error('Could not get canvas context') let width = img.width let height = img.height const MAX_WIDTH = 800 const MAX_HEIGHT = 600 if (width > MAX_WIDTH || height > MAX_HEIGHT) { const ratio = Math.min(MAX_WIDTH / width, MAX_HEIGHT / height) width = Math.round(width * ratio) height = Math.round(height * ratio) } canvas.width = width canvas.height = height ctx.drawImage(img, 0, 0, width, height) const compressedBase64 = canvas.toDataURL('image/jpeg', 0.7) resolve(compressedBase64) } catch (err) { reject(err) } } img.onerror = () => reject(new Error('Invalid image file')) img.src = event.target?.result as string } reader.onerror = () => reject(new Error('Failed to read file')) reader.readAsDataURL(file) }) } const loadCrewData = async () => { setLoading(true) setError(null) try { if (readOnly && preloadedData) { const decryptedCrews: DecryptedCrew[] = [] for (const c of preloadedData) { if (c.payloadId === 'skipper') { setSkipName(c.data.name || '') setSkipAddress(c.data.address || '') setSkipBirthDate(c.data.birthDate || '') setSkipPhone(c.data.phone || '') setSkipNationality(c.data.nationality || '') setSkipPassport(c.data.passportNumber || '') setSkipBloodType(c.data.bloodType || '') setSkipAllergies(c.data.allergies || '') setSkipDiseases(c.data.diseases || '') setSkipPhoto(c.data.photo || null) } else { decryptedCrews.push({ payloadId: c.payloadId, data: c.data }) } } setCrewList(decryptedCrews) return } const masterKey = await getLogbookKey(logbookId) || getActiveMasterKey() if (!masterKey) throw new Error('Encryption key not found. Please log in.') const localCrews = await db.crews.where({ logbookId }).toArray() const decryptedCrews: DecryptedCrew[] = [] for (const c of localCrews) { const decrypted = await decryptJson(c.encryptedData, c.iv, c.tag, masterKey) if (decrypted) { if (c.payloadId === 'skipper') { setSkipName(decrypted.name || '') setSkipAddress(decrypted.address || '') setSkipBirthDate(decrypted.birthDate || '') setSkipPhone(decrypted.phone || '') setSkipNationality(decrypted.nationality || '') setSkipPassport(decrypted.passportNumber || '') setSkipBloodType(decrypted.bloodType || '') setSkipAllergies(decrypted.allergies || '') setSkipDiseases(decrypted.diseases || '') setSkipPhoto(decrypted.photo || null) } else { decryptedCrews.push({ payloadId: c.payloadId, data: decrypted }) } } } setCrewList(decryptedCrews) } catch (err: any) { console.error('Failed to load crew files:', err) setError(err.message || 'Decryption failed. Could not load crew profiles.') } finally { setLoading(false) } } const handleSaveSkipper = async (e: React.FormEvent) => { e.preventDefault() if (skipperFormReadOnly) return setSavingSkipper(true) setError(null) setSkipperSuccess(false) try { const masterKey = await getLogbookKey(logbookId) || getActiveMasterKey() if (!masterKey) throw new Error('Encryption key not found. Please log in.') const skipperData: CrewMemberData = { name: skipName.trim(), address: skipAddress.trim(), birthDate: skipBirthDate, phone: skipPhone.trim(), nationality: skipNationality.trim(), passportNumber: skipPassport.trim(), bloodType: skipBloodType.trim(), allergies: skipAllergies.trim(), diseases: skipDiseases.trim(), role: 'skipper', photo: skipPhoto } const encrypted = await encryptJson(skipperData, masterKey) const now = new Date().toISOString() await db.crews.put({ payloadId: 'skipper', logbookId, encryptedData: encrypted.ciphertext, iv: encrypted.iv, tag: encrypted.tag, updatedAt: now }) await db.syncQueue.put({ action: 'update', type: 'crew', payloadId: 'skipper', logbookId, data: JSON.stringify(encrypted), updatedAt: now }) setSkipperSuccess(true) trackPlausibleEvent(PlausibleEvents.CREW_SAVED, { role: 'skipper', action: 'update' }) setTimeout(() => setSkipperSuccess(false), 3000) syncLogbook(logbookId).catch((err) => console.warn('Background sync failed:', err)) } catch (err: any) { console.error('Failed to save skipper profile:', err) setError(err.message || 'Failed to save skipper profile.') } finally { setSavingSkipper(false) } } const openAddMember = () => { setEditingId(null) setMemName('') setMemAddress('') setMemBirthDate('') setMemPhone('') setMemNationality('') setMemPassport('') setMemBloodType('') setMemAllergies('') setMemDiseases('') setMemPhoto(null) setMemPhotoError(null) setShowMemberForm(true) } const openEditMember = (member: DecryptedCrew) => { setEditingId(member.payloadId) setMemName(member.data.name) setMemAddress(member.data.address) setMemBirthDate(member.data.birthDate) setMemPhone(member.data.phone) setMemNationality(member.data.nationality) setMemPassport(member.data.passportNumber) setMemBloodType(member.data.bloodType) setMemAllergies(member.data.allergies) setMemDiseases(member.data.diseases) setMemPhoto(member.data.photo || null) setMemPhotoError(null) setShowMemberForm(true) } const handleSaveMember = async (e: React.FormEvent) => { e.preventDefault() if (readOnly || !memName.trim()) return setSavingMember(true) setError(null) try { const masterKey = await getLogbookKey(logbookId) || getActiveMasterKey() if (!masterKey) throw new Error('Encryption key not found. Please log in.') const memberData: CrewMemberData = { name: memName.trim(), address: memAddress.trim(), birthDate: memBirthDate, phone: memPhone.trim(), nationality: memNationality.trim(), passportNumber: memPassport.trim(), bloodType: memBloodType.trim(), allergies: memAllergies.trim(), diseases: memDiseases.trim(), role: 'crew', photo: memPhoto } const encrypted = await encryptJson(memberData, masterKey) const now = new Date().toISOString() const memberId = editingId || window.crypto.randomUUID() const isNew = !editingId await db.crews.put({ payloadId: memberId, logbookId, encryptedData: encrypted.ciphertext, iv: encrypted.iv, tag: encrypted.tag, updatedAt: now }) await db.syncQueue.put({ action: isNew ? 'create' : 'update', type: 'crew', payloadId: memberId, logbookId, data: JSON.stringify(encrypted), updatedAt: now }) // Update state local list if (isNew) { setCrewList((prev) => [...prev, { payloadId: memberId, data: memberData }]) } else { setCrewList((prev) => prev.map((item) => (item.payloadId === memberId ? { payloadId: memberId, data: memberData } : item)) ) } setShowMemberForm(false) trackPlausibleEvent(PlausibleEvents.CREW_SAVED, { role: 'crew', action: isNew ? 'create' : 'update' }) syncLogbook(logbookId).catch((err) => console.warn('Background sync failed:', err)) } catch (err: any) { console.error('Failed to save crew member:', err) setError(err.message || 'Failed to save crew member.') } finally { setSavingMember(false) } } const handleDeleteMember = async (memberId: string) => { if (readOnly) return if (await showConfirm(t('crew.delete_confirm'), t('crew.title'), t('logs.confirm_yes'), t('logs.confirm_no'))) { setError(null) try { const now = new Date().toISOString() await db.crews.delete(memberId) await db.syncQueue.put({ action: 'delete', type: 'crew', payloadId: memberId, logbookId, data: '', updatedAt: now }) setCrewList((prev) => prev.filter((item) => item.payloadId !== memberId)) syncLogbook(logbookId).catch((err) => console.warn('Background sync failed:', err)) } catch (err: any) { console.error('Failed to delete crew member:', err) setError(err.message || 'Failed to delete crew member.') } } } if (loading) { return (

{t('crew.loading')}

) } return (
{/* Skipper Section */}

{t('crew.skipper_section')}

{error &&
{error}
} {skipperReadOnly && !readOnly && (

{t('crew.skipper_read_only_hint')}

)}
skipFileInputRef.current?.click()} style={{ cursor: skipperFormReadOnly ? 'default' : 'pointer' }}> {skipPhoto ? ( {skipName ) : (
)} {!skipperFormReadOnly && (
{skipPhoto ? t('vessel.photo_change') : t('vessel.photo_add')}
)}
{!skipperFormReadOnly && (
{skipPhoto && ( )}
)} { const file = e.target.files?.[0] if (!file) return setSkipPhotoError(null) try { const resized = await resizeImageFile(file) setSkipPhoto(resized) trackPlausibleEvent(PlausibleEvents.PHOTO_UPLOADED, { context: 'crew', role: 'skipper' }) } catch (err: any) { setSkipPhotoError(err.message || 'Failed to process image') } }} accept="image/*" style={{ display: 'none' }} /> {skipPhotoError &&
{skipPhotoError}
}
setSkipName(e.target.value)} disabled={savingSkipper || skipperFormReadOnly} required />
setSkipAddress(e.target.value)} disabled={savingSkipper || skipperFormReadOnly} />
setSkipBirthDate(e.target.value)} disabled={savingSkipper || skipperFormReadOnly} />
setSkipPhone(e.target.value)} disabled={savingSkipper || skipperFormReadOnly} />
setSkipNationality(e.target.value)} disabled={savingSkipper || skipperFormReadOnly} />
setSkipPassport(e.target.value)} disabled={savingSkipper || skipperFormReadOnly} />
setSkipBloodType(e.target.value)} disabled={savingSkipper || skipperFormReadOnly} />
setSkipAllergies(e.target.value)} disabled={savingSkipper || skipperFormReadOnly} />
setSkipDiseases(e.target.value)} disabled={savingSkipper || skipperFormReadOnly} />
{!skipperFormReadOnly && (
{skipperSuccess && (
{t('crew.saved')}
)}
)}
{/* Crew list section */}

{t('crew.crew_section')}

{!readOnly && crewList.length < 5 && !showMemberForm && ( )}
{/* Add/Edit member form */} {showMemberForm && (

{editingId ? t('crew.edit_crew') : t('crew.add_crew')}

memFileInputRef.current?.click()}> {memPhoto ? ( {memName ) : (
)}
{memPhoto ? t('vessel.photo_change') : t('vessel.photo_add')}
{memPhoto && ( )}
{ const file = e.target.files?.[0] if (!file) return setMemPhotoError(null) try { const resized = await resizeImageFile(file) setMemPhoto(resized) trackPlausibleEvent(PlausibleEvents.PHOTO_UPLOADED, { context: 'crew', role: 'crew' }) } catch (err: any) { setMemPhotoError(err.message || 'Failed to process image') } }} accept="image/*" style={{ display: 'none' }} /> {memPhotoError &&
{memPhotoError}
}
setMemName(e.target.value)} disabled={savingMember} required />
setMemAddress(e.target.value)} disabled={savingMember} />
setMemBirthDate(e.target.value)} disabled={savingMember} />
setMemPhone(e.target.value)} disabled={savingMember} />
setMemNationality(e.target.value)} disabled={savingMember} />
setMemPassport(e.target.value)} disabled={savingMember} />
setMemBloodType(e.target.value)} disabled={savingMember} />
setMemAllergies(e.target.value)} disabled={savingMember} />
setMemDiseases(e.target.value)} disabled={savingMember} />
)} {crewList.length === 0 ? (
{t('crew.no_crew')}
) : (
{crewList.map((m) => (
{m.data.photo ? ( {m.data.name} ) : (
)}

{m.data.name}

{!readOnly && (
)}
{m.data.birthDate &&

{t('crew.birthdate')}: {m.data.birthDate}

} {m.data.phone &&

{t('crew.phone')}: {m.data.phone}

} {m.data.nationality &&

{t('crew.nationality')}: {m.data.nationality}

} {m.data.passportNumber &&

{t('crew.passport')}: {m.data.passportNumber}

} {m.data.bloodType &&

{t('crew.bloodtype')}: {m.data.bloodType}

} {m.data.allergies &&

{t('crew.allergies')}: {m.data.allergies}

} {m.data.diseases &&

{t('crew.diseases')}: {m.data.diseases}

}
))}
)}
) }