mirror of
https://github.com/IliesAllali/parenthese.git
synced 2026-11-06 23:32:08 +00:00
Onglet Vie : « En couple avec » sauf mariage dit, date et lieu d'un couple, prénoms en liens
Un couple naît « mariage » dès qu'un enfant a deux parents : la frise disait « Épouse X » pour tous. Elle dit maintenant « En couple avec X », et « Mariage avec X » seulement quand c'est dit (date, lieu ou documents de mariage, case cochée). Toucher la ligne d'un couple (administrateur) ouvre sa fiche avec « C'était un mariage », la date (même approximative) et le lieu ; PATCH /unions accepte startPlace et startDatePrecision. Le prénom d'un proche (conjoint, enfant, frère ou sœur, parent) ouvre sa fiche et amène la caméra sur lui. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
cdb75655dd
commit
1fae061a2d
10 files changed
+152
-38
No files matched your search
@@ -143,6 +143,9 @@ const patchUnionSchema = z
|
||||
partner2PersonId: z.string().min(1).optional().nullable(),
|
||||
unionType: z.string().max(50).optional().nullable(),
|
||||
startDate: z.string().regex(isoDateRegex).optional().nullable(),
|
||||
// Lieu et précision de la date du couple, saisis dans l'onglet Vie (« vers 1980 »)
|
||||
startPlace: z.string().trim().max(300).optional().nullable(),
|
||||
startDatePrecision: z.enum(['about', 'estimated', 'calculated', 'before', 'after']).optional().nullable(),
|
||||
endDate: z.string().regex(isoDateRegex).optional().nullable(),
|
||||
displayOrder: z.number().int().positive().optional(),
|
||||
})
|
||||
@@ -1780,6 +1783,8 @@ export const treeRoutes: FastifyPluginAsync = async (app) => {
|
||||
},
|
||||
data: {
|
||||
...unionPrecisionResets(existingUnion, { startDate }),
|
||||
...(payload.data.startDatePrecision !== undefined ? { startDatePrecision: payload.data.startDatePrecision } : {}),
|
||||
startPlace: payload.data.startPlace !== undefined ? payload.data.startPlace || null : undefined,
|
||||
partner1PersonId: payload.data.partner1PersonId,
|
||||
partner2PersonId: payload.data.partner2PersonId,
|
||||
unionType: payload.data.unionType,
|
||||
|
||||
@@ -3542,6 +3542,11 @@ function App() {
|
||||
onMoveSibling={(delta) => galaxyZoomApiRef.current?.moveSibling?.(tree.selectedPerson.id, delta)}
|
||||
onUploadMomentFile={tree.canEditCurrentTree || tree.canSubmitContribution ? media.uploadMomentFile : undefined}
|
||||
onMomentSaved={media.refreshMomentMedia}
|
||||
onOpenPerson={async (personId) => {
|
||||
const target = persons.find((p) => String(p.id) === String(personId))
|
||||
if (!target || !(await chooseSelectedPerson(target))) return
|
||||
galaxyZoomApiRef.current?.focusPerson?.(target.id)
|
||||
}}
|
||||
onQuickEdit={tree.canEditCurrentTree && userRole.isAdmin && !isDemoMode ? handleEditToggle : undefined}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -216,6 +216,19 @@ export async function createParentChildLink(treeId, payload, token) {
|
||||
return data.link
|
||||
}
|
||||
|
||||
// Type, date et lieu d'un couple, depuis l'onglet Vie
|
||||
export async function updateUnion(treeId, unionId, payload, token) {
|
||||
const data = await apiRequest(`/trees/${encodeURIComponent(treeId)}/unions/${encodeURIComponent(unionId)}`, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
|
||||
return data.union
|
||||
}
|
||||
|
||||
export async function deleteUnion(treeId, unionId, token) {
|
||||
const data = await apiRequest(`/trees/${encodeURIComponent(treeId)}/unions/${encodeURIComponent(unionId)}`, {
|
||||
method: 'DELETE',
|
||||
|
||||
@@ -79,3 +79,13 @@
|
||||
.lt-notice { margin: 0 0 10px; }
|
||||
.lt-add { margin-top: 14px; }
|
||||
.pc-empty .lt-add { margin-top: 12px; }
|
||||
|
||||
/* Prénom d'un proche dans la frise : ouvre sa fiche (Patch, 28/10) */
|
||||
.lt-person {
|
||||
appearance: none; border: 0; background: none; padding: 0; margin: 0; cursor: pointer;
|
||||
font: inherit; color: var(--pz-a700); border-radius: 4px;
|
||||
text-decoration: underline; text-decoration-color: var(--pz-a200); text-underline-offset: 3px;
|
||||
transition: text-decoration-color 160ms ease;
|
||||
}
|
||||
@media (hover: hover) and (pointer: fine) { .lt-person:hover { text-decoration-color: var(--pz-a500); } }
|
||||
.lt-person:focus-visible { outline: 2px solid var(--pz-a500); outline-offset: 2px; }
|
||||
@@ -26,6 +26,31 @@ function Sources({ text }) {
|
||||
)
|
||||
}
|
||||
|
||||
// Titre d'une ligne : le prénom d'un proche ouvre sa fiche ; le reste du titre ouvre la modification du moment
|
||||
// (administrateur). Un bouton ne peut pas en contenir un autre : les deux sont côte à côte.
|
||||
function Title({ entry, editable, onEdit, onOpenPerson }) {
|
||||
const editLabel = t('Modifier « {title} »', { title: entry.title })
|
||||
const link = onOpenPerson ? entry.link : null
|
||||
const at = link ? entry.title.indexOf(link.name) : -1
|
||||
if (at < 0) {
|
||||
return editable ? (
|
||||
<button type="button" className="lt-title lt-title--edit" onClick={() => onEdit?.(entry)} aria-label={editLabel}>{entry.title}</button>
|
||||
) : <div className="lt-title">{entry.title}</div>
|
||||
}
|
||||
const before = entry.title.slice(0, at)
|
||||
const after = entry.title.slice(at + link.name.length)
|
||||
const edit = (text) => (editable && text.trim()
|
||||
? <button type="button" className="lt-title--edit" onClick={() => onEdit?.(entry)} aria-label={editLabel}>{text}</button>
|
||||
: text)
|
||||
return (
|
||||
<div className="lt-title">
|
||||
{edit(before.trimEnd())}{/\s$/.test(before) ? ' ' : ''}
|
||||
<button type="button" className="lt-person" onClick={() => onOpenPerson(link.personId)}>{link.name}</button>
|
||||
{before.trim() ? after : edit(after)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const FILE_LABELS = { video: t('Vidéo'), audio: t('Enregistrement'), document: t('Document') }
|
||||
|
||||
// Souvenirs rattachés au moment : les photos en vignettes, le reste (vidéo, acte, voix) en pastilles
|
||||
@@ -59,7 +84,7 @@ function Attachments({ medias, onOpen }) {
|
||||
|
||||
export default function LifeTimeline({
|
||||
person, family, life, loading = false, failed = false, medias = [],
|
||||
canAdd = false, canEdit = false, notice = '', onAdd, onEdit, onOpenMedia,
|
||||
canAdd = false, canEdit = false, notice = '', onAdd, onEdit, onOpenMedia, onOpenPerson,
|
||||
}) {
|
||||
const entries = buildLifeTimeline(person, family, life)
|
||||
const mediaById = new Map(medias.map((m) => [String(m.id), m]))
|
||||
@@ -96,13 +121,7 @@ export default function LifeTimeline({
|
||||
<li key={entry.key} className={`lt-ev lt-ev--${entry.kind}`}>
|
||||
<span className="lt-year">{entry.year ?? ''}</span>
|
||||
<div className="lt-body">
|
||||
{editable ? (
|
||||
<button type="button" className="lt-title lt-title--edit" onClick={() => onEdit?.(entry)} aria-label={t('Modifier « {title} »', { title: entry.title })}>
|
||||
{entry.title}
|
||||
</button>
|
||||
) : (
|
||||
<div className="lt-title">{entry.title}</div>
|
||||
)}
|
||||
<Title entry={entry} editable={editable} onEdit={onEdit} onOpenPerson={onOpenPerson} />
|
||||
{entry.meta && <div className="lt-meta">{entry.meta}</div>}
|
||||
{entry.note && <p className="lt-note">{entry.note}</p>}
|
||||
{entry.sources && <Sources text={entry.sources} />}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { createPortal } from 'react-dom'
|
||||
import { X, Paperclip, Images, FileText, Mic, Play, Check } from 'lucide-react'
|
||||
import { t } from '../i18n/index.js'
|
||||
import { firstNameLabel } from '../utils/personName.js'
|
||||
import { createLifeEvent, updateLifeEvent, deleteLifeEvent } from '../api/treeApi.js'
|
||||
import { createLifeEvent, updateLifeEvent, deleteLifeEvent, updateUnion } from '../api/treeApi.js'
|
||||
import { DESCRIBED_TYPES, eventTitle } from '../utils/lifeTimeline.js'
|
||||
import { inferMediaTypeFromMimeType } from '../utils/mediaUpload.js'
|
||||
import { readContributorName, saveContributorName } from '../utils/contributorName.js'
|
||||
@@ -14,7 +14,8 @@ import { fetchImmichConfig } from '../api/immichApi.js'
|
||||
// Fiche « Ajouter un moment » de l'onglet Vie : quoi, quand, où, et des souvenirs (photo, vidéo, acte en
|
||||
// PDF, enregistrement). La date exacte, la fin, la source et la note restent dans « Plus de détails ».
|
||||
// Trois usages : ajouter (administrateur ou famille), modifier un moment (administrateur), et compléter une
|
||||
// naissance, un mariage ou un décès avec ses documents (administrateur, la date se change dans Infos).
|
||||
// naissance ou un décès avec ses documents (administrateur, la date se change dans Infos). Pour un couple, la
|
||||
// fiche dit aussi si c'est un mariage, et en garde la date et le lieu (onglet Vie, 28/10).
|
||||
|
||||
// Suggestions touchables : le texte exact devient le type correspondant (exporté en GEDCOM sous sa balise)
|
||||
const SUGGESTIONS = [
|
||||
@@ -36,15 +37,19 @@ function FileThumb({ type, url }) {
|
||||
return <span className="ms-file-icon"><Icon size={18} strokeWidth={1.8} /></span>
|
||||
}
|
||||
|
||||
export default function MomentSheet({ person, entry = null, treeId, token, medias = [], isAdmin = false, onUploadFile, onClose, onSaved, onDelete }) {
|
||||
export default function MomentSheet({ person, entry = null, treeId, token, medias = [], isAdmin = false, onUploadFile, onClose, onSaved, onDelete, onUnionSaved }) {
|
||||
const milestone = entry?.milestone || null
|
||||
const event = entry?.event || null
|
||||
// Couple : sa date et son lieu vivent sur le couple, pas sur le moment
|
||||
const union = milestone && entry?.union ? entry.union : null
|
||||
const editing = Boolean(event)
|
||||
const dated = union ? { date: union.startDate, datePrecision: union.startDatePrecision, place: union.startPlace } : event
|
||||
const [what, setWhat] = useState(() => (event && !milestone ? eventTitle(event) : ''))
|
||||
const [year, setYear] = useState(() => yearOf(event?.date))
|
||||
const [approx, setApprox] = useState(() => ['about', 'estimated', 'calculated', 'between'].includes(event?.datePrecision))
|
||||
const [place, setPlace] = useState(event?.place || '')
|
||||
const [day, setDay] = useState(() => exactDay(event?.date))
|
||||
const [married, setMarried] = useState(() => Boolean(union?.married))
|
||||
const [year, setYear] = useState(() => yearOf(dated?.date))
|
||||
const [approx, setApprox] = useState(() => ['about', 'estimated', 'calculated', 'between'].includes(dated?.datePrecision))
|
||||
const [place, setPlace] = useState(dated?.place || '')
|
||||
const [day, setDay] = useState(() => exactDay(dated?.date))
|
||||
const [endYear, setEndYear] = useState(() => yearOf(event?.dateEnd))
|
||||
const [sources, setSources] = useState(event?.sources || '')
|
||||
const [notes, setNotes] = useState(event?.notes || '')
|
||||
@@ -68,6 +73,8 @@ export default function MomentSheet({ person, entry = null, treeId, token, media
|
||||
const created = useRef(null)
|
||||
const fileRef = useRef(null)
|
||||
const hasDetails = Boolean(day || endYear || sources || notes)
|
||||
// Les champs « Quand ? » et « Où ? » : un moment à soi, ou un couple
|
||||
const withWhen = !milestone || Boolean(union)
|
||||
|
||||
useEffect(() => { firstFieldRef.current?.focus() }, [])
|
||||
useEffect(() => {
|
||||
@@ -151,9 +158,17 @@ export default function MomentSheet({ person, entry = null, treeId, token, media
|
||||
// Un fichier refusé au milieu de l'envoi : le moment est déjà créé, on ne le recrée pas en réessayant
|
||||
let eventId = event?.id || created.current?.id || null
|
||||
let status = created.current?.status || 'approved'
|
||||
if (union) {
|
||||
const start = day || (year ? `${year}-01-01` : null)
|
||||
const unionType = married ? 'mariage' : 'union'
|
||||
await updateUnion(treeId, union.id, { unionType, startDate: start, startDatePrecision: start && approx && !day ? 'about' : null, startPlace: place.trim() || null }, token)
|
||||
onUnionSaved?.(union.id, { unionType, startYear: start ? Number(start.slice(0, 4)) : null })
|
||||
}
|
||||
// Un couple qui n'est pas un mariage, sans source, note ni souvenir : rien à garder à côté de lui
|
||||
const needsRow = !union || married || Boolean(sources.trim() || notes.trim() || linkedIds.length || files.length)
|
||||
if (editing) {
|
||||
await updateLifeEvent(treeId, eventId, buildPayload(), token)
|
||||
} else if (!created.current) {
|
||||
} else if (!created.current && needsRow) {
|
||||
const response = await createLifeEvent(treeId, person.id, buildPayload(), token)
|
||||
eventId = response?.event?.id
|
||||
status = response?.event?.status || 'approved'
|
||||
@@ -204,14 +219,20 @@ export default function MomentSheet({ person, entry = null, treeId, token, media
|
||||
</button>
|
||||
<div className="pz-modal-head">
|
||||
<h2 id="ms-title" className="pz-title ms-title">{title}</h2>
|
||||
{milestone && <p className="pz-small">{t('Ajoutez ses documents, un acte en PDF ou une photo, et la source. La date et le lieu se changent dans l’onglet Infos.')}</p>}
|
||||
{milestone && !union && <p className="pz-small">{t('Ajoutez ses documents, un acte en PDF ou une photo, et la source. La date et le lieu se changent dans l’onglet Infos.')}</p>}
|
||||
{!isAdmin && <p className="pz-small">{t('Ce que vous ajoutez apparaîtra dans sa vie quand la personne qui gère l’arbre l’aura accepté.')}</p>}
|
||||
</div>
|
||||
|
||||
<form className="ms-form" onSubmit={(e) => { e.preventDefault(); save() }}>
|
||||
{!milestone && (
|
||||
{union && (
|
||||
<label className="ms-check ms-married">
|
||||
<input type="checkbox" checked={married} onChange={(e) => setMarried(e.target.checked)} />
|
||||
{t('C’était un mariage')}
|
||||
</label>
|
||||
)}
|
||||
{withWhen && (
|
||||
<>
|
||||
<div className="pz-field">
|
||||
{!milestone && <div className="pz-field">
|
||||
<label htmlFor="ms-what">{t('Quoi ?')}</label>
|
||||
<input id="ms-what" ref={firstFieldRef} value={what} maxLength={120} onChange={(e) => setWhat(e.target.value)} placeholder={t('Service militaire, premier emploi, départ pour Paris…')} />
|
||||
{!editing && (
|
||||
@@ -221,12 +242,12 @@ export default function MomentSheet({ person, entry = null, treeId, token, media
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>}
|
||||
|
||||
<div className="ms-row">
|
||||
<div className="pz-field ms-year">
|
||||
<label htmlFor="ms-year">{t('Quand ?')}</label>
|
||||
<input id="ms-year" inputMode="numeric" maxLength={4} value={year} onChange={(e) => setYear(e.target.value.replace(/\D/g, ''))} placeholder={t('Année')} aria-invalid={!yearValid || undefined} disabled={Boolean(day)} />
|
||||
<input id="ms-year" ref={union ? firstFieldRef : undefined} inputMode="numeric" maxLength={4} value={year} onChange={(e) => setYear(e.target.value.replace(/\D/g, ''))} placeholder={t('Année')} aria-invalid={!yearValid || undefined} disabled={Boolean(day)} />
|
||||
</div>
|
||||
<label className="ms-check">
|
||||
<input type="checkbox" checked={approx} onChange={(e) => setApprox(e.target.checked)} disabled={Boolean(day) && !endYear} />
|
||||
@@ -264,7 +285,7 @@ export default function MomentSheet({ person, entry = null, treeId, token, media
|
||||
)}
|
||||
<div className="ms-file-actions">
|
||||
{isAdmin && immichConfigured && <button type="button" className="ms-file-add" disabled={busy} onClick={() => setPickingImmich((value) => !value)}>{t('Depuis Immich')}</button>}
|
||||
<button type="button" ref={milestone ? firstFieldRef : undefined} className="ms-file-add" onClick={() => fileRef.current?.click()} disabled={busy || !onUploadFile}>
|
||||
<button type="button" ref={milestone && !union ? firstFieldRef : undefined} className="ms-file-add" onClick={() => fileRef.current?.click()} disabled={busy || !onUploadFile}>
|
||||
<Paperclip size={15} strokeWidth={1.8} />
|
||||
{t('Ajouter un fichier')}
|
||||
</button>
|
||||
@@ -299,19 +320,19 @@ export default function MomentSheet({ person, entry = null, treeId, token, media
|
||||
</div>
|
||||
)}
|
||||
|
||||
<details className="ms-more" open={hasDetails || milestone ? true : undefined}>
|
||||
<summary>{milestone ? t('Source et note') : t('Plus de détails')}</summary>
|
||||
<details className="ms-more" open={hasDetails || (milestone && !union) ? true : undefined}>
|
||||
<summary>{milestone && !union ? t('Source et note') : t('Plus de détails')}</summary>
|
||||
<div className="ms-more-body">
|
||||
{!milestone && (
|
||||
{withWhen && (
|
||||
<div className="ms-row">
|
||||
<div className="pz-field">
|
||||
<label htmlFor="ms-day">{t('Date exacte')}</label>
|
||||
<input id="ms-day" type="date" value={day} onChange={(e) => { setDay(e.target.value); if (e.target.value) setYear(e.target.value.slice(0, 4)) }} />
|
||||
</div>
|
||||
<div className="pz-field ms-year">
|
||||
{!union && <div className="pz-field ms-year">
|
||||
<label htmlFor="ms-end">{t('Jusqu’en')}</label>
|
||||
<input id="ms-end" inputMode="numeric" maxLength={4} value={endYear} onChange={(e) => setEndYear(e.target.value.replace(/\D/g, ''))} placeholder={t('Année')} aria-invalid={!endValid || undefined} />
|
||||
</div>
|
||||
</div>}
|
||||
</div>
|
||||
)}
|
||||
<div className="pz-field">
|
||||
|
||||
@@ -82,6 +82,8 @@ const PersonCard = ({
|
||||
onMoveSibling,
|
||||
onUploadMomentFile,
|
||||
onMomentSaved,
|
||||
// Prénom d'un proche touché dans l'onglet Vie : sa fiche s'ouvre
|
||||
onOpenPerson,
|
||||
// Suppression annulable (App, hooks/useUndoable.js)
|
||||
onUndoable,
|
||||
saveOnLeaveRef = null,
|
||||
@@ -951,6 +953,7 @@ const PersonCard = ({
|
||||
onAdd={() => { setLifeNotice(''); setMomentSheet({ entry: null }) }}
|
||||
onEdit={(entry) => { setLifeNotice(''); setMomentSheet({ entry }) }}
|
||||
onOpenMedia={(media) => onMediaSelect?.(media, person)}
|
||||
onOpenPerson={onOpenPerson}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -1291,6 +1294,11 @@ const PersonCard = ({
|
||||
undo: () => setLifeRevision((r) => r + 1),
|
||||
})
|
||||
} : undefined}
|
||||
onUnionSaved={(unionId, { unionType, startYear }) => {
|
||||
// Le couple du graphe suit tout de suite (onglet Infos, frise), sans recharger l'arbre
|
||||
const union = personUnions.find((u) => String(u.id) === String(unionId))
|
||||
if (union) Object.assign(union, { unionType, startYear })
|
||||
}}
|
||||
onSaved={({ pending, uploaded }) => {
|
||||
setMomentSheet(null)
|
||||
if (pending) setLifeNotice(t('Merci, c’est envoyé. Le moment apparaîtra dans sa vie quand la personne qui gère l’arbre l’aura accepté.'))
|
||||
|
||||
@@ -428,8 +428,9 @@ export default {
|
||||
'Naissance de {name}, sa fratrie': 'Birth of a sibling, {name}',
|
||||
'Naissance à {place}': 'Born in {place}',
|
||||
'Conjoint inconnu': 'Unknown partner',
|
||||
'Union avec {name}': 'Partnership with {name}',
|
||||
'Épouse {name}': 'Marries {name}',
|
||||
'Mariage avec {name}': 'Married {name}',
|
||||
'En couple avec {name}': 'Together with {name}',
|
||||
'C’était un mariage': 'They were married',
|
||||
'Fin de l’union avec {name}': 'End of the relationship with {name}',
|
||||
'Décès de {name}': 'Death of {name}',
|
||||
'Événement': 'Event',
|
||||
|
||||
@@ -93,6 +93,9 @@ export function describeDate(date, end, precision) {
|
||||
const ageAt = (birthYear, year) => (birthYear && year && year > birthYear ? t('{age} ans', { age: year - birthYear }) : '')
|
||||
const joinMeta = (...parts) => parts.filter(Boolean).join(' · ')
|
||||
|
||||
// Le prénom d'un proche, cliquable dans la frise : il ouvre sa fiche
|
||||
const linkTo = (p, name = firstNameLabel(p)) => (p?.id != null && name ? { personId: p.id, name } : null)
|
||||
|
||||
const parentWord = (p) => {
|
||||
if (p.sex === 'male' || p.sex === 'M') return t('Décès de son père {name}', { name: firstNameLabel(p) })
|
||||
if (p.sex === 'female' || p.sex === 'F') return t('Décès de sa mère {name}', { name: firstNameLabel(p) })
|
||||
@@ -150,9 +153,17 @@ export function buildLifeTimeline(person, family = {}, life = null) {
|
||||
const childYears = (family.children || []).filter((c) => childIds.has(String(c.id))).map((c) => yearOf(c.birthDate) ?? c.birthYear).filter(within)
|
||||
const partnerDeath = union.partner ? yearOf(union.partner.deathDate) ?? union.partner.deathYear : null
|
||||
const anchor = year ? null : (childYears.length ? Math.min(...childYears) : null) ?? union.endYear ?? (within(partnerDeath) ? partnerDeath : null)
|
||||
// Un couple naît « mariage » par défaut dès qu'un enfant a deux parents : « Mariage » seulement quand c'est
|
||||
// dit (une date, un lieu, des documents de mariage), sinon « En couple avec », qui vaut pour toutes les familles
|
||||
const married = union.unionType !== 'union' && Boolean(year || detail.startPlace || extra)
|
||||
out.push({
|
||||
key: `union-${union.id}`, year, sortYear: anchor, order: year ? 2 : 1, kind: 'mile',
|
||||
title: union.unionType === 'union' ? t('Union avec {name}', { name: partnerName }) : t('Épouse {name}', { name: partnerName }),
|
||||
title: married ? t('Mariage avec {name}', { name: partnerName }) : t('En couple avec {name}', { name: partnerName }),
|
||||
link: union.partner ? linkTo(union.partner, partnerName) : null,
|
||||
union: {
|
||||
id: union.id, married, startDate: detail.startDate || (year ? String(year) : ''),
|
||||
startDatePrecision: detail.startDatePrecision || null, startPlace: detail.startPlace || '',
|
||||
},
|
||||
meta: year
|
||||
? joinMeta(place, detail.startDatePrecision ? describeDate(detail.startDate, null, detail.startDatePrecision) : '', ageAt(birthYear, year))
|
||||
: joinMeta(place, t('date inconnue')),
|
||||
@@ -165,7 +176,7 @@ export function buildLifeTimeline(person, family = {}, life = null) {
|
||||
}
|
||||
if (union.partner && within(yearOf(union.partner.deathDate) ?? union.partner.deathYear)) {
|
||||
const year2 = yearOf(union.partner.deathDate) ?? union.partner.deathYear
|
||||
out.push({ key: `partner-death-${union.id}`, year: year2, order: 4, kind: 'fam', title: t('Décès de {name}', { name: union.partner.firstName }) })
|
||||
out.push({ key: `partner-death-${union.id}`, year: year2, order: 4, kind: 'fam', title: t('Décès de {name}', { name: union.partner.firstName }), link: linkTo(union.partner, union.partner.firstName) })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,17 +200,17 @@ export function buildLifeTimeline(person, family = {}, life = null) {
|
||||
|
||||
for (const child of family.children || []) {
|
||||
const year = yearOf(child.birthDate) ?? child.birthYear
|
||||
if (within(year)) out.push({ key: `child-${child.id}`, year, order: 5, kind: 'fam', title: t('Naissance de {name}', { name: child.firstName }) })
|
||||
if (within(year)) out.push({ key: `child-${child.id}`, year, order: 5, kind: 'fam', title: t('Naissance de {name}', { name: child.firstName }), link: linkTo(child, child.firstName) })
|
||||
const death = yearOf(child.deathDate) ?? child.deathYear
|
||||
if (within(death) && death !== deathYear) out.push({ key: `child-death-${child.id}`, year: death, order: 6, kind: 'fam', title: t('Décès de {name}', { name: child.firstName }) })
|
||||
if (within(death) && death !== deathYear) out.push({ key: `child-death-${child.id}`, year: death, order: 6, kind: 'fam', title: t('Décès de {name}', { name: child.firstName }), link: linkTo(child, child.firstName) })
|
||||
}
|
||||
for (const sibling of family.siblings || []) {
|
||||
const year = yearOf(sibling.birthDate) ?? sibling.birthYear
|
||||
if (within(year) && year !== birthYear) out.push({ key: `sibling-${sibling.id}`, year, order: 5, kind: 'fam', title: siblingWord(sibling) })
|
||||
if (within(year) && year !== birthYear) out.push({ key: `sibling-${sibling.id}`, year, order: 5, kind: 'fam', title: siblingWord(sibling), link: linkTo(sibling) })
|
||||
}
|
||||
for (const parent of family.parents || []) {
|
||||
const year = yearOf(parent.deathDate) ?? parent.deathYear
|
||||
if (within(year)) out.push({ key: `parent-death-${parent.id}`, year, order: 6, kind: 'fam', title: parentWord(parent) })
|
||||
if (within(year)) out.push({ key: `parent-death-${parent.id}`, year, order: 6, kind: 'fam', title: parentWord(parent), link: linkTo(parent) })
|
||||
}
|
||||
|
||||
if (deathYear || person.isAlive === false) {
|
||||
|
||||
@@ -26,7 +26,7 @@ describe('frise de vie', () => {
|
||||
|
||||
it('range tout par date, les entrées sans date en bas', () => {
|
||||
expect(titles).toEqual([
|
||||
'Naissance à Limoges', 'Naissance de son frère André', 'Militaire', 'Épouse Simone Petit', 'Naissance de Jacques',
|
||||
'Naissance à Limoges', 'Naissance de son frère André', 'Militaire', 'Mariage avec Simone Petit', 'Naissance de Jacques',
|
||||
'Instituteur', 'Fin de l’union avec Simone', 'Décès de son père Jean', 'Décès à Brive-la-Gaillarde', 'Résidence',
|
||||
])
|
||||
})
|
||||
@@ -36,7 +36,7 @@ describe('frise de vie', () => {
|
||||
expect(birth).toMatchObject({ kind: 'mile', year: 1920, sources: 'Registre A\nRegistre B' })
|
||||
expect(entries.find((e) => e.title === 'Instituteur').meta).toBe('jusqu’en 1981 · 26 ans')
|
||||
expect(entries.find((e) => e.title === 'Militaire')).toMatchObject({ kind: 'minor', meta: 'entre 1940 et 1942 · 20 ans', note: 'Verdun' })
|
||||
expect(entries.find((e) => e.title === 'Épouse Simone Petit')).toMatchObject({ kind: 'mile', meta: 'Limoges · 23 ans' })
|
||||
expect(entries.find((e) => e.title === 'Mariage avec Simone Petit')).toMatchObject({ kind: 'mile', meta: 'Limoges · 23 ans' })
|
||||
})
|
||||
|
||||
it('famille limitée à sa vie : frère aîné, mère morte après lui, enfant mort après lui écartés', () => {
|
||||
@@ -77,7 +77,7 @@ describe('frise de vie', () => {
|
||||
],
|
||||
}, null)
|
||||
expect(entries.map((e) => e.title)).toEqual([
|
||||
'Naissance', 'Épouse Marc Roy', 'Naissance de Hugo', 'Naissance de Léa', 'Décès de Marc', 'Épouse Paul Vidal', 'Décès de Paul', 'Épouse Yves Mallet',
|
||||
'Naissance', 'En couple avec Marc Roy', 'Naissance de Hugo', 'Naissance de Léa', 'Décès de Marc', 'En couple avec Paul Vidal', 'Décès de Paul', 'En couple avec Yves Mallet',
|
||||
])
|
||||
// Aucune année inventée : colonne vide, « date inconnue » dit pourquoi
|
||||
for (const key of ['union-u1', 'union-u2', 'union-u3']) {
|
||||
@@ -86,6 +86,27 @@ describe('frise de vie', () => {
|
||||
expect(entries.find((e) => e.key === 'union-u1')).not.toHaveProperty('sortYear')
|
||||
})
|
||||
|
||||
it('couple : « Mariage » seulement quand c’est dit, sinon « En couple avec »', () => {
|
||||
const lucie = { id: 1, firstName: 'Lucie', birthYear: 1940 }
|
||||
const marc = { id: 2, firstName: 'Marc', lastName: 'Roy' }
|
||||
const title = (union, life = null) => buildLifeTimeline(lucie, { unions: [{ id: 'u1', partner: marc, ...union }] }, life).find((e) => e.key === 'union-u1').title
|
||||
// Le type « mariage » posé par défaut, sans date ni lieu ni document : pas un mariage
|
||||
expect(title({ unionType: 'mariage' })).toBe('En couple avec Marc Roy')
|
||||
expect(title({ unionType: 'mariage', startYear: 1962 })).toBe('Mariage avec Marc Roy')
|
||||
expect(title({ unionType: 'mariage' }, { unions: [{ id: 'u1', startPlace: 'Lille' }] })).toBe('Mariage avec Marc Roy')
|
||||
expect(title({ unionType: 'mariage' }, { events: [{ id: 'm', type: 'marriage', unionId: 'u1' }] })).toBe('Mariage avec Marc Roy')
|
||||
// « Pas un mariage » dit explicitement, même daté
|
||||
expect(title({ unionType: 'union', startYear: 1962 })).toBe('En couple avec Marc Roy')
|
||||
})
|
||||
|
||||
it('les proches sont des liens vers leur fiche', () => {
|
||||
expect(entries.find((e) => e.key === 'union-u2').link).toEqual({ personId: 5, name: 'Simone Petit' })
|
||||
expect(entries.find((e) => e.key === 'child-6').link).toEqual({ personId: 6, name: 'Jacques' })
|
||||
expect(entries.find((e) => e.key === 'sibling-4').link).toEqual({ personId: 4, name: 'André' })
|
||||
expect(entries.find((e) => e.key === 'parent-death-1').link).toEqual({ personId: 1, name: 'Jean' })
|
||||
expect(entries.find((e) => e.key === 'birth').link).toBeUndefined()
|
||||
})
|
||||
|
||||
it('un moment sans date dit « date inconnue »', () => {
|
||||
expect(entries.find((e) => e.title === 'Résidence').meta).toBe('Rue des Écoles · date inconnue')
|
||||
})
|
||||
|
||||
Reference in new issue
Block a user