import { useState, useEffect, useRef } from 'react' import DateField from './DateField.jsx' import { UserPlus, X, Search, ChevronDown } from 'lucide-react' import './AddPersonPanel.css' import PzBusy from './PzBusy.jsx' import { t } from '../i18n/index.js' import { firstNameLabel } from '../utils/personName.js' import { SEX_CHOICES } from '../utils/personSex.js' // Titre traduit d'un bloc : le passage entre et garde son italique function withEmphasis(text) { const [before, emphasized = '', after = ''] = text.split(/<\/?em>/) return <>{before}{emphasized}{after}> } /** * Panneau slide-in gauche pour ajouter une personne * Visible uniquement en mode édition * * Features: * - Slide-in depuis la gauche (400px desktop, 90% mobile) * - Focus auto sur Prénom à l'ouverture * - Validation temps-réel (required) * - Section Relations avec dropdown searchable * - Reste ouvert après ajout réussi (reset form) * * @param {Object} props * @param {boolean} props.visible - Afficher le panneau * @param {Array} props.persons - Liste des personnes existantes pour les relations * @param {boolean} props.loading - Soumission en cours * @param {string} props.error - Message d'erreur * @param {Function} props.onSubmit - Handler soumission (formData) * @param {Function} props.onClose - Handler fermeture */ function AddPersonPanel({ visible, persons = [], loading = false, error = '', onSubmit, onClose }) { const [firstName, setFirstName] = useState('') // Prénom inconnu : enregistré vide, affiché « Prénom inconnu » dans l'arbre et la fiche const [unknownFirstName, setUnknownFirstName] = useState(false) const [lastName, setLastName] = useState('') const [birthName, setBirthName] = useState('') const [nickname, setNickname] = useState('') const [sex, setSex] = useState(null) const [birthDate, setBirthDate] = useState('') const [deathDate, setDeathDate] = useState('') // Décédé(e) sans date connue (issue #77) const [isDeceased, setIsDeceased] = useState(false) // Lien d'enfant ou de parent : la personne qui est l'enfant du lien a été adoptée const [adopted, setAdopted] = useState(false) // Les champs de date gardent leur texte : on les recrée après chaque ajout const [dateFieldsKey, setDateFieldsKey] = useState(0) const [relations, setRelations] = useState([]) const [relationSearch, setRelationSearch] = useState('') const [relationDropdownOpen, setRelationDropdownOpen] = useState(false) const [selectedRelationType, setSelectedRelationType] = useState('parent') const [touched, setTouched] = useState({}) // Prénom inconnu sans nom de famille : la personne n'aurait aucun nom const [lastNameMissing, setLastNameMissing] = useState(false) const [justAdded, setJustAdded] = useState(false) const firstNameRef = useRef(null) const panelRef = useRef(null) // Focus sur Prénom à l'ouverture useEffect(() => { if (visible) { setTimeout(() => firstNameRef.current?.focus(), 300) } }, [visible]) // ESC pour fermer useEffect(() => { if (!visible) return function handleKeyDown(e) { if (e.key === 'Escape') { onClose() } } document.addEventListener('keydown', handleKeyDown) return () => document.removeEventListener('keydown', handleKeyDown) }, [visible, onClose]) // Fermer dropdown relation au clic extérieur useEffect(() => { if (!relationDropdownOpen) return function handleClick(e) { if (!e.target.closest('.relation-dropdown-wrapper')) { setRelationDropdownOpen(false) } } document.addEventListener('mousedown', handleClick) return () => document.removeEventListener('mousedown', handleClick) }, [relationDropdownOpen]) // Reset form après ajout réussi useEffect(() => { if (justAdded && !loading && !error) { setFirstName('') setUnknownFirstName(false) setLastName('') setBirthName('') setNickname('') setSex(null) setBirthDate('') setDeathDate('') setIsDeceased(false) setDateFieldsKey((k) => k + 1) setRelations([]) setTouched({}) setLastNameMissing(false) setJustAdded(false) firstNameRef.current?.focus() } }, [justAdded, loading, error]) const filteredPersons = persons.filter((p) => { if (!relationSearch) return true const fullName = `${p.firstName || ''} ${p.lastName || ''}`.toLowerCase() return fullName.includes(relationSearch.toLowerCase()) }) const handleAddRelation = (person) => { // Éviter les doublons if (relations.some((r) => r.personId === person.id)) return setRelations((prev) => [ ...prev, { personId: person.id, personName: `${firstNameLabel(person)} ${person.lastName || ''}`.trim(), type: selectedRelationType }, ]) setRelationSearch('') setRelationDropdownOpen(false) } const handleRemoveRelation = (personId) => { setRelations((prev) => prev.filter((r) => r.personId !== personId)) } const handleSubmit = (e) => { e.preventDefault() if (!unknownFirstName && !firstName.trim()) { setTouched({ firstName: true }) firstNameRef.current?.focus() return } if (unknownFirstName && !lastName.trim()) { setLastNameMissing(true) return } setJustAdded(true) onSubmit({ firstName: unknownFirstName ? '' : firstName.trim(), lastName: lastName.trim(), birthName: birthName.trim() || null, nickname: nickname.trim() || null, sex, birthDate: birthDate || null, deathDate: deathDate || null, isDeceased: !deathDate && isDeceased, // Adopté(e) : s'applique aux liens d'enfant ou de parent de cette fiche relations: relations.map((rel) => ({ ...rel, adopted: adopted && (rel.type === 'child' || rel.type === 'parent') })), }) } const relationTypeLabels = { parent: t('Parent de'), child: t('Enfant de'), spouse: t('Conjoint·e de'), sibling: t('Frère·Sœur de'), } const relationTypeTabs = { parent: t('Parent'), child: t('Enfant'), spouse: t('Conjoint·e'), sibling: t('Frère·Sœur'), } return (