diff --git a/frontend/src/components/PersonCard.jsx b/frontend/src/components/PersonCard.jsx index b4cd7c0..1b8e4ba 100644 --- a/frontend/src/components/PersonCard.jsx +++ b/frontend/src/components/PersonCard.jsx @@ -436,6 +436,8 @@ const PersonCard = ({ unions: personUnions.map((union) => ({ ...union, partner: persons.find((p) => p.id === (union.partner1Id === person.id ? union.partner2Id : union.partner1Id)) || null, + // Enfants du couple : placent dans la frise une union dont la date manque + childIds: filiations.filter((f) => f.unionId && String(f.unionId) === String(union.id)).map((f) => f.childId), })), } diff --git a/frontend/src/utils/lifeTimeline.js b/frontend/src/utils/lifeTimeline.js index ed684f9..5bb3078 100644 --- a/frontend/src/utils/lifeTimeline.js +++ b/frontend/src/utils/lifeTimeline.js @@ -107,7 +107,7 @@ const siblingWord = (p) => { /** * @param person personne de la fiche (birthDate, deathDate, birthPlace, birthYear, deathYear) - * @param family { parents, children, siblings, unions: [{ id, partner, startYear, endYear, unionType }] } + * @param family { parents, children, siblings, unions: [{ id, partner, startYear, endYear, unionType, childIds }] } * @param life réponse de GET /trees/:id/persons/:personId/life, ou null tant qu'elle n'est pas là * @returns entrées triées { key, year, kind: 'mile' | 'minor' | 'fam', title, meta, note, sources, mediaIds, * event (moment modifiable), milestone ({ type, unionId } d'une naissance, d'un mariage, d'un décès) } @@ -144,10 +144,18 @@ export function buildLifeTimeline(person, family = {}, life = null) { const partnerName = union.partner ? fullName(union.partner) : t('Conjoint inconnu') const place = formatPlace(detail.startPlace || '') const extra = sourcesOf('marriage', union.id) + // Union sans date : à sa place logique, juste avant le premier enfant du couple, sinon avant la fin de + // l'union ou le décès du conjoint. Aucune année n'est inventée, la colonne reste vide. + const childIds = new Set((union.childIds || []).map(String)) + 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) out.push({ - key: `union-${union.id}`, year, order: 2, kind: 'mile', + 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 }), - meta: joinMeta(place, detail.startDatePrecision ? describeDate(detail.startDate, null, detail.startDatePrecision) : '', ageAt(birthYear, year)), + meta: year + ? joinMeta(place, detail.startDatePrecision ? describeDate(detail.startDate, null, detail.startDatePrecision) : '', ageAt(birthYear, year)) + : joinMeta(place, t('date inconnue')), note: extra?.notes || '', sources: extra?.sources || '', ...milestoneFields(extra, { type: 'marriage', unionId: union.id }), }) @@ -171,7 +179,7 @@ export function buildLifeTimeline(person, family = {}, life = null) { } out.push({ key: `event-${event.id}`, year, order: 1, kind: 'minor', title, - meta: joinMeta(formatPlace(event.place || ''), describeDate(event.date, event.dateEnd, event.datePrecision), ageAt(birthYear, year)), + meta: joinMeta(formatPlace(event.place || ''), year ? describeDate(event.date, event.dateEnd, event.datePrecision) : t('date inconnue'), ageAt(birthYear, year)), note: event.notes || '', sources: event.sources || '', mediaIds: event.mediaIds || [], // Seuls les moments propres à la personne se modifient depuis l'onglet (pas ceux d'une union) @@ -206,9 +214,9 @@ export function buildLifeTimeline(person, family = {}, life = null) { }) } - // Les entrées sans date restent en bas, dans l'ordre où elles sont arrivées + // Une union sans date se range à sa place logique (sortYear) ; le reste sans date va en bas, dans l'ordre d'arrivée return out .map((entry, index) => ({ ...entry, index })) - .sort((a, b) => (a.year ?? Infinity) - (b.year ?? Infinity) || a.order - b.order || a.index - b.index) - .map(({ order, index, ...entry }) => entry) // eslint-disable-line no-unused-vars + .sort((a, b) => (a.year ?? a.sortYear ?? Infinity) - (b.year ?? b.sortYear ?? Infinity) || a.order - b.order || a.index - b.index) + .map(({ order, index, sortYear, ...entry }) => entry) // eslint-disable-line no-unused-vars } diff --git a/frontend/src/utils/lifeTimeline.test.js b/frontend/src/utils/lifeTimeline.test.js index a4b0105..d99a320 100644 --- a/frontend/src/utils/lifeTimeline.test.js +++ b/frontend/src/utils/lifeTimeline.test.js @@ -66,6 +66,30 @@ describe('frise de vie', () => { expect(entries.map((e) => [e.title, e.year, e.meta])).toEqual([['Naissance', 1900, ''], ['Décès', null, 'date inconnue']]) }) + it('union sans date : avant le premier enfant du couple, sinon avant le décès du conjoint, sinon en bas', () => { + const lucie = { id: 1, firstName: 'Lucie', birthYear: 1940 } + const entries = buildLifeTimeline(lucie, { + children: [{ id: 7, firstName: 'Hugo', birthYear: 1966 }, { id: 8, firstName: 'Léa', birthYear: 1981 }], + unions: [ + { id: 'u1', partner: { id: 2, firstName: 'Marc', lastName: 'Roy', deathYear: 2013 }, unionType: 'mariage', childIds: [7] }, + { id: 'u2', partner: { id: 3, firstName: 'Paul', lastName: 'Vidal', deathYear: 2016 }, unionType: 'mariage', childIds: [] }, + { id: 'u3', partner: { id: 4, firstName: 'Yves', lastName: 'Mallet' }, unionType: 'mariage' }, + ], + }, 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', + ]) + // Aucune année inventée : colonne vide, « date inconnue » dit pourquoi + for (const key of ['union-u1', 'union-u2', 'union-u3']) { + expect(entries.find((e) => e.key === key)).toMatchObject({ year: undefined, meta: 'date inconnue' }) + } + expect(entries.find((e) => e.key === 'union-u1')).not.toHaveProperty('sortYear') + }) + + it('un moment sans date dit « date inconnue »', () => { + expect(entries.find((e) => e.title === 'Résidence').meta).toBe('Rue des Écoles · date inconnue') + }) + it('dit les dates comme on les dit', () => { expect(describeDate('1879-01-01', null, 'about')).toBe('vers 1879') expect(describeDate('1879-01-01', null, 'before')).toBe('avant 1879')