- {!milestone && (
+ {withWhen && (
{ setDay(e.target.value); if (e.target.value) setYear(e.target.value.slice(0, 4)) }} />
-
+ {!union &&
setEndYear(e.target.value.replace(/\D/g, ''))} placeholder={t('Année')} aria-invalid={!endValid || undefined} />
-
+
}
)}
diff --git a/frontend/src/components/PersonCard.jsx b/frontend/src/components/PersonCard.jsx
index 5663ad8..ce681d5 100644
--- a/frontend/src/components/PersonCard.jsx
+++ b/frontend/src/components/PersonCard.jsx
@@ -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é.'))
diff --git a/frontend/src/i18n/en/fiche.js b/frontend/src/i18n/en/fiche.js
index 61867ee..e06de91 100644
--- a/frontend/src/i18n/en/fiche.js
+++ b/frontend/src/i18n/en/fiche.js
@@ -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',
diff --git a/frontend/src/utils/lifeTimeline.js b/frontend/src/utils/lifeTimeline.js
index 5bb3078..b3a3a36 100644
--- a/frontend/src/utils/lifeTimeline.js
+++ b/frontend/src/utils/lifeTimeline.js
@@ -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) {
diff --git a/frontend/src/utils/lifeTimeline.test.js b/frontend/src/utils/lifeTimeline.test.js
index d99a320..df0d576 100644
--- a/frontend/src/utils/lifeTimeline.test.js
+++ b/frontend/src/utils/lifeTimeline.test.js
@@ -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')
})