Simplify and modernize entity components and translations for better performance and consistency

- Replace fragmented name rendering with unified `person.text` in Vue components.
- Migrate `GenderIconRenderBox` to use Bootstrap icons and TypeScript.
- Introduce `GenderTranslation` type and helper for gender rendering.
- Refactor `PersonRenderBox` to streamline rendering logic and improve maintainability. Migrate to typescript
- Update French translations for consistency with new gender rendering.
This commit is contained in:
2025-09-26 14:25:38 +02:00
parent ad2b6d63ac
commit 13b1c45271
7 changed files with 187 additions and 264 deletions

View File

@@ -0,0 +1,17 @@
import {Gender, GenderTranslation} from "ChillMainAssets/types";
/**
* Translates a given gender object into its corresponding gender translation string.
*
* @param {Gender|null} gender - The gender object to be translated, null values are also supported
* @return {GenderTranslation} Returns the gender translation string corresponding to the provided gender,
* or "unknown" if the gender is null.
*/
export function toGenderTranslation(gender: Gender|null): GenderTranslation
{
if (null === gender) {
return "unknown";
}
return gender.genderTranslation;
}

View File

@@ -33,11 +33,24 @@ export interface SetCivility {
id: number; id: number;
} }
/**
* Gender translation.
*
* Match the GenderEnum in PHP code.
*/
export type GenderTranslation = "male" | "female" | "neutral" | "unknown";
/**
* A gender
*
* See also
*/
export interface Gender { export interface Gender {
type: "chill_main_gender"; type: "chill_main_gender";
id: number; id: number;
label: string; label: string;
genderTranslation: string; genderTranslation: GenderTranslation;
} }
/** /**

View File

@@ -1,28 +1,28 @@
<template> <template>
<i :class="['fa', genderClass, 'px-1']" /> <i :class="['bi', genderClass]"></i>
</template> </template>
<script setup> <script setup lang="ts">
import { computed } from "vue"; import { computed } from "vue";
const props = defineProps({ import type { Gender } from "ChillMainAssets/types";
gender: { import {toGenderTranslation} from "ChillMainAssets/lib/api/genderHelper";
type: Object,
required: true,
},
});
const genderClass = computed(() => { interface GenderIconRenderBoxProps {
switch (props.gender.genderTranslation) { gender: Gender;
case "woman": }
return "fa-venus";
case "man": const props = defineProps<GenderIconRenderBoxProps>();
return "fa-mars";
case "both": const genderClass = computed<string>(() => {
return "fa-neuter"; switch (toGenderTranslation(props.gender)) {
case "female":
return "bi-gender-female";
case "male":
return "bi-gender-male";
case "neutral":
case "unknown": case "unknown":
return "fa-genderless";
default: default:
return "fa-genderless"; return "bi-gender-neuter";
} }
}); });
</script> </script>

View File

@@ -136,34 +136,6 @@ filter_order:
Search: Chercher dans la liste Search: Chercher dans la liste
By date: Filtrer par date By date: Filtrer par date
search_box: Filtrer par contenu search_box: Filtrer par contenu
renderbox:
person: "Usager"
birthday:
man: "Né le"
woman: "Née le"
neutral: "Né·e le"
unknown: "Né·e le"
deathdate: "Date de décès"
household_without_address: "Le ménage de l'usager est sans adresse"
no_data: "Aucune information renseignée"
type:
thirdparty: "Tiers"
person: "Usager"
holder: "Titulaire"
years_old: >-
{n, plural,
=0 {0 an}
one {1 an}
other {# ans}
}
residential_address: "Adresse de résidence"
located_at: "réside chez"
household_number: "Ménage n°{number}"
current_members: "Membres actuels"
no_current_address: "Sans adresse actuellement"
new_household: "Nouveau ménage"
no_members_yet: "Aucun membre actuellement"
pick_entity: pick_entity:
add: "Ajouter" add: "Ajouter"
modal_title: >- modal_title: >-

View File

@@ -5,46 +5,16 @@
<div class="item-col"> <div class="item-col">
<div class="entity-label"> <div class="entity-label">
<div :class="'denomination h' + options.hLevel"> <div :class="'denomination h' + options.hLevel">
<template v-if="options.addLink === true">
<a v-if="options.addLink === true" :href="getUrl"> <a v-if="options.addLink === true" :href="getUrl">
<!-- use person-text here to avoid code duplication ? TODO --> <span>{{ person.text }}</span>
<span class="firstname">{{ person.firstName }}</span>
<span class="lastname">{{ person.lastName }}</span>
<span v-if="person.suffixText" class="suffixtext"
>&nbsp;{{ person.suffixText }}</span
>
<span
v-if="person.altNames && options.addAltNames == true"
class="altnames"
>
<span :class="'altname altname-' + altNameKey">{{
altNameLabel
}}</span>
</span>
</a>
<!-- use person-text here to avoid code duplication ? TODO -->
<span class="firstname">{{ person.firstName + " " }}</span>
<span class="lastname">{{ person.lastName }}</span>
<span v-if="person.suffixText" class="suffixtext"
>&nbsp;{{ person.suffixText }}</span
>
<span v-if="person.deathdate" class="deathdate"> ()</span> <span v-if="person.deathdate" class="deathdate"> ()</span>
<span </a>
v-if="person.altNames && options.addAltNames == true" </template>
class="altnames" <template v-else>
> <span>{{ person.text }}</span>
<span :class="'altname altname-' + altNameKey">{{ <span v-if="person.deathdate" class="deathdate"> ()</span>
altNameLabel </template>
}}</span>
</span>
<span
v-if="options.addId == true"
class="id-number"
:title="'n° ' + person.id"
>{{ person.id }}</span
>
<badge-entity <badge-entity
v-if="options.addEntity === true" v-if="options.addEntity === true"
:entity="person" :entity="person"
@@ -52,61 +22,36 @@
/> />
</div> </div>
<p>
<span
v-if="options.addId == true"
:title="person.personId"
><i class="bi bi-info-circle"></i> {{ person.personId }}</span
>
</p>
<p v-if="options.addInfo === true" class="moreinfo"> <p v-if="options.addInfo === true" class="moreinfo">
<gender-icon-render-box <gender-icon-render-box
v-if="person.gender" v-if="person.gender"
:gender="person.gender" :gender="person.gender"
/> /> <span
<time v-if="person.birthdate"
v-if="person.birthdate && !person.deathdate"
:datetime="person.birthdate"
:title="birthdate"
> >
{{ {{ trans(RENDERBOX_BIRTHDAY_STATEMENT, {gender: toGenderTranslation(person.gender), birthdate: ISOToDate(person.birthdate?.datetime)}) }}
trans(birthdateTranslation) + </span>
" " +
new Intl.DateTimeFormat("fr-FR", {
dateStyle: "long",
}).format(birthdate)
}}
</time>
<time
v-else-if="person.birthdate && person.deathdate"
:datetime="person.deathdate"
:title="person.deathdate"
>
{{
new Intl.DateTimeFormat("fr-FR", {
dateStyle: "long",
}).format(birthdate)
}}
-
{{
new Intl.DateTimeFormat("fr-FR", {
dateStyle: "long",
}).format(deathdate)
}}
</time>
<time
v-else-if="person.deathdate"
:datetime="person.deathdate"
:title="person.deathdate"
>
{{
trans(RENDERBOX_DEATHDATE) +
" " +
new Intl.DateTimeFormat("fr-FR", {
dateStyle: "long",
}).format(deathdate)
}}
</time>
<span v-if="options.addAge && person.birthdate" class="age"> <span v-if="options.addAge && person.birthdate" class="age">
({{ trans(RENDERBOX_YEARS_OLD, {n: person.age}) }}) ({{ trans(RENDERBOX_YEARS_OLD, {n: person.age}) }})
</span> </span>
</p> </p>
<p>
<span
v-if="person.deathdate"
>
{{ trans(RENDERBOX_DEATHDATE_STATEMENT, {gender: toGenderTranslation(person.gender), deathdate: ISOToDate(person.deathdate?.datetime)}) }}
</span>
</p>
</div> </div>
</div> </div>
@@ -130,11 +75,6 @@
<a <a
v-if="options.addHouseholdLink === true" v-if="options.addHouseholdLink === true"
:href="getCurrentHouseholdUrl" :href="getCurrentHouseholdUrl"
:title="
trans(PERSONS_ASSOCIATED_SHOW_HOUSEHOLD_NUMBER, {
id: person.current_household_id,
})
"
> >
<span class="badge rounded-pill bg-chill-beige"> <span class="badge rounded-pill bg-chill-beige">
<i <i
@@ -180,6 +120,7 @@
:person="addr.hostPerson" :person="addr.hostPerson"
/> />
</span> </span>
<address-render-box <address-render-box
v-if="addr.hostPerson.address" v-if="addr.hostPerson.address"
:address="addr.hostPerson.address" :address="addr.hostPerson.address"
@@ -299,7 +240,7 @@
</span> </span>
</template> </template>
<script setup> <script setup lang="ts">
import {computed} from "vue"; import {computed} from "vue";
import {ISOToDate} from "ChillMainAssets/chill/js/date"; import {ISOToDate} from "ChillMainAssets/chill/js/date";
import AddressRenderBox from "ChillMainAssets/vuejs/_components/Entity/AddressRenderBox.vue"; import AddressRenderBox from "ChillMainAssets/vuejs/_components/Entity/AddressRenderBox.vue";
@@ -311,108 +252,69 @@ import {
trans, trans,
RENDERBOX_HOLDER, RENDERBOX_HOLDER,
RENDERBOX_NO_DATA, RENDERBOX_NO_DATA,
RENDERBOX_DEATHDATE, RENDERBOX_DEATHDATE_STATEMENT,
RENDERBOX_HOUSEHOLD_WITHOUT_ADDRESS, RENDERBOX_HOUSEHOLD_WITHOUT_ADDRESS,
RENDERBOX_RESIDENTIAL_ADDRESS, RENDERBOX_RESIDENTIAL_ADDRESS,
RENDERBOX_LOCATED_AT, RENDERBOX_LOCATED_AT,
RENDERBOX_BIRTHDAY_MAN, RENDERBOX_BIRTHDAY_STATEMENT,
RENDERBOX_BIRTHDAY_WOMAN, // PERSONS_ASSOCIATED_SHOW_HOUSEHOLD_NUMBER,
RENDERBOX_BIRTHDAY_UNKNOWN,
RENDERBOX_BIRTHDAY_NEUTRAL,
PERSONS_ASSOCIATED_SHOW_HOUSEHOLD_NUMBER,
RENDERBOX_YEARS_OLD, RENDERBOX_YEARS_OLD,
} from "translator"; } from "translator";
import {Person} from "ChillPersonAssets/types";
import {toGenderTranslation} from "ChillMainAssets/lib/api/genderHelper";
const props = defineProps({ interface RenderOptions {
person: { addInfo?: boolean;
required: true, addEntity?: boolean;
}, addAltNames?: boolean;
options: { addAge?: boolean;
type: Object, addId?: boolean;
required: false, addLink?: boolean;
}, hLevel?: number;
render: { entityDisplayLong?: boolean;
type: String, addCenter?: boolean;
}, addNoData?: boolean;
returnPath: { isMultiline?: boolean;
type: String, isHolder?: boolean;
}, addHouseholdLink?: boolean;
showResidentialAddresses: {
type: Boolean,
default: false,
},
});
const birthdateTranslation = computed(() => {
if (props.person.gender) {
const { genderTranslation } = props.person.gender;
switch (genderTranslation) {
case "man":
return RENDERBOX_BIRTHDAY_MAN;
case "woman":
return RENDERBOX_BIRTHDAY_WOMAN;
case "neutral":
return RENDERBOX_BIRTHDAY_NEUTRAL;
case "unknown":
return RENDERBOX_BIRTHDAY_UNKNOWN;
default:
return RENDERBOX_BIRTHDAY_UNKNOWN;
} }
} else {
return RENDERBOX_BIRTHDAY_UNKNOWN; interface Props {
person: Person;
options?: RenderOptions;
render?: "bloc" | "badge";
returnPath?: string;
showResidentialAddresses?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
render: "bloc", options: {
addInfo: true,
addEntity: false,
addAltNames: true,
addAge: true,
addId: true,
addLink: false,
hLevel: 3,
entityDisplayingLong: true,
addCenter: true,
addNoData: true,
isMultiline: true,
isHolder: false,
addHouseholdLink: true
} }
}); });
const isMultiline = computed(() => { const isMultiline = computed<boolean>(() => {
return props.options?.isMultiline || false; return props.options?.isMultiline || false;
}); });
const birthdate = computed(() => { const getUrl = computed<string>(() => {
if (
props.person.birthdate !== null &&
props.person.birthdate !== undefined &&
props.person.birthdate.datetime
) {
return ISOToDate(props.person.birthdate.datetime);
} else {
return "";
}
});
const deathdate = computed(() => {
if (
props.person.deathdate !== null &&
props.person.deathdate !== undefined &&
props.person.deathdate.datetime
) {
return new Date(props.person.deathdate.datetime);
} else {
return "";
}
});
const altNameLabel = computed(() => {
let altNameLabel = "";
(props.person.altNames || []).forEach(
(altName) => (altNameLabel += altName.label),
);
return altNameLabel;
});
const altNameKey = computed(() => {
let altNameKey = "";
(props.person.altNames || []).forEach(
(altName) => (altNameKey += altName.key),
);
return altNameKey;
});
const getUrl = computed(() => {
return `/fr/person/${props.person.id}/general`; return `/fr/person/${props.person.id}/general`;
}); });
const getCurrentHouseholdUrl = computed(() => { const getCurrentHouseholdUrl = computed<string>(() => {
let returnPath = props.returnPath ? `?returnPath=${props.returnPath}` : ``; const returnPath = props.returnPath ? `?returnPath=${props.returnPath}` : ``;
return `/fr/person/household/${props.person.current_household_id}/summary${returnPath}`; return `/fr/person/household/${props.person.current_household_id}/summary${returnPath}`;
}); });
</script> </script>

View File

@@ -1,13 +1,7 @@
<template> <template>
<span v-if="isCut">{{ cutText }}</span> <span v-if="isCut">{{ cutText }}</span>
<span v-else class="person-text"> <span v-else class="person-text">
<span class="firstname">{{ person.firstName }}</span> <span>{{ person.text }}</span>
<span class="lastname">&nbsp;{{ person.lastName }}</span>
<span v-if="person.altNames && person.altNames.length > 0" class="altnames">
<span :class="'altname altname-' + altNameKey"
>&nbsp;({{ altNameLabel }})</span
>
</span>
<span v-if="person.suffixText" class="suffixtext" <span v-if="person.suffixText" class="suffixtext"
>&nbsp;{{ person.suffixText }}</span >&nbsp;{{ person.suffixText }}</span
> >
@@ -33,16 +27,6 @@ const props = defineProps<{
const { person, isCut = false, addAge = true } = toRefs(props); const { person, isCut = false, addAge = true } = toRefs(props);
const altNameLabel = computed(() => {
if (!person.value.altNames) return "";
return person.value.altNames.map((a: AltName) => a.labels).join("");
});
const altNameKey = computed(() => {
if (!person.value.altNames) return "";
return person.value.altNames.map((a: AltName) => a.key).join("");
});
const cutText = computed(() => { const cutText = computed(() => {
if (!person.value.text) return ""; if (!person.value.text) return "";
const more = person.value.text.length > 15 ? "…" : ""; const more = person.value.text.length > 15 ? "…" : "";

View File

@@ -265,3 +265,38 @@ add_persons:
title: "Centre" title: "Centre"
error_only_one_person: "Une seule personne peut être sélectionnée !" error_only_one_person: "Une seule personne peut être sélectionnée !"
renderbox:
person: "Usager"
birthday_statement: >-
{gender, select,
man {Né le {birthdate, date}}
woman {Née le {birthdate, date}}
other {Né·e le {birthdate, date}}
}
deathdate_statement: >-
{gender, select,
man {Décédé le {deathdate, date}}
woman {Décédée le {deathdate, date}}
other {Décédé·e le {deathdate, date}}
}
household_without_address: "Le ménage de l'usager est sans adresse"
no_data: "Aucune information renseignée"
type:
thirdparty: "Tiers"
person: "Usager"
holder: "Titulaire"
years_old: >-
{n, plural,
=0 {0 an}
one {1 an}
other {# ans}
}
residential_address: "Adresse de résidence"
located_at: "réside chez"
household_number: "Ménage n°{number}"
current_members: "Membres actuels"
no_current_address: "Sans adresse actuellement"
new_household: "Nouveau ménage"
no_members_yet: "Aucun membre actuellement"