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;
}
/**
* Gender translation.
*
* Match the GenderEnum in PHP code.
*/
export type GenderTranslation = "male" | "female" | "neutral" | "unknown";
/**
* A gender
*
* See also
*/
export interface Gender {
type: "chill_main_gender";
id: number;
label: string;
genderTranslation: string;
genderTranslation: GenderTranslation;
}
/**

View File

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

View File

@@ -136,34 +136,6 @@ filter_order:
Search: Chercher dans la liste
By date: Filtrer par date
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:
add: "Ajouter"
modal_title: >-

View File

@@ -5,46 +5,16 @@
<div class="item-col">
<div class="entity-label">
<div :class="'denomination h' + options.hLevel">
<a v-if="options.addLink === true" :href="getUrl">
<!-- 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.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.altNames && options.addAltNames == true"
class="altnames"
>
<span :class="'altname altname-' + altNameKey">{{
altNameLabel
}}</span>
</span>
<span
v-if="options.addId == true"
class="id-number"
:title="'n° ' + person.id"
>{{ person.id }}</span
>
<template v-if="options.addLink === true">
<a v-if="options.addLink === true" :href="getUrl">
<span>{{ person.text }}</span>
<span v-if="person.deathdate" class="deathdate"> ()</span>
</a>
</template>
<template v-else>
<span>{{ person.text }}</span>
<span v-if="person.deathdate" class="deathdate"> ()</span>
</template>
<badge-entity
v-if="options.addEntity === true"
:entity="person"
@@ -52,61 +22,36 @@
/>
</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">
<gender-icon-render-box
v-if="person.gender"
:gender="person.gender"
/>
<time
v-if="person.birthdate && !person.deathdate"
:datetime="person.birthdate"
:title="birthdate"
/> <span
v-if="person.birthdate"
>
{{
trans(birthdateTranslation) +
" " +
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>
{{ trans(RENDERBOX_BIRTHDAY_STATEMENT, {gender: toGenderTranslation(person.gender), birthdate: ISOToDate(person.birthdate?.datetime)}) }}
</span>
<span v-if="options.addAge && person.birthdate" class="age">
({{ trans(RENDERBOX_YEARS_OLD, { n: person.age }) }})
({{ trans(RENDERBOX_YEARS_OLD, {n: person.age}) }})
</span>
</p>
<p>
<span
v-if="person.deathdate"
>
{{ trans(RENDERBOX_DEATHDATE_STATEMENT, {gender: toGenderTranslation(person.gender), deathdate: ISOToDate(person.deathdate?.datetime)}) }}
</span>
</p>
</div>
</div>
@@ -114,11 +59,11 @@
<div class="float-button bottom">
<div class="box">
<div class="action">
<slot name="record-actions" />
<slot name="record-actions"/>
</div>
<ul class="list-content fa-ul">
<li v-if="person.current_household_id">
<i class="fa fa-li fa-map-marker" />
<i class="fa fa-li fa-map-marker"/>
<address-render-box
v-if="person.current_household_address"
:address="person.current_household_address"
@@ -130,11 +75,6 @@
<a
v-if="options.addHouseholdLink === true"
:href="getCurrentHouseholdUrl"
:title="
trans(PERSONS_ASSOCIATED_SHOW_HOUSEHOLD_NUMBER, {
id: person.current_household_id,
})
"
>
<span class="badge rounded-pill bg-chill-beige">
<i
@@ -144,7 +84,7 @@
</a>
</li>
<li v-else-if="options.addNoData">
<i class="fa fa-li fa-map-marker" />
<i class="fa fa-li fa-map-marker"/>
<p class="chill-no-data-statement">
{{ trans(RENDERBOX_NO_DATA) }}
</p>
@@ -160,7 +100,7 @@
v-for="(addr, i) in person.current_residential_addresses"
:key="i"
>
<i class="fa fa-li fa-map-marker" />
<i class="fa fa-li fa-map-marker"/>
<div v-if="addr.address">
<span class="item-key">
{{ trans(RENDERBOX_RESIDENTIAL_ADDRESS) }}:
@@ -180,6 +120,7 @@
:person="addr.hostPerson"
/>
</span>
<address-render-box
v-if="addr.hostPerson.address"
:address="addr.hostPerson.address"
@@ -204,36 +145,36 @@
</template>
<li v-if="person.email">
<i class="fa fa-li fa-envelope-o" />
<i class="fa fa-li fa-envelope-o"/>
<a :href="'mailto: ' + person.email">{{ person.email }}</a>
</li>
<li v-else-if="options.addNoData">
<i class="fa fa-li fa-envelope-o" />
<i class="fa fa-li fa-envelope-o"/>
<p class="chill-no-data-statement">
{{ trans(RENDERBOX_NO_DATA) }}
</p>
</li>
<li v-if="person.mobilenumber">
<i class="fa fa-li fa-mobile" />
<i class="fa fa-li fa-mobile"/>
<a :href="'tel: ' + person.mobilenumber">
{{ person.mobilenumber }}
</a>
</li>
<li v-else-if="options.addNoData">
<i class="fa fa-li fa-mobile" />
<i class="fa fa-li fa-mobile"/>
<p class="chill-no-data-statement">
{{ trans(RENDERBOX_NO_DATA) }}
</p>
</li>
<li v-if="person.phonenumber">
<i class="fa fa-li fa-phone" />
<i class="fa fa-li fa-phone"/>
<a :href="'tel: ' + person.phonenumber">
{{ person.phonenumber }}
</a>
</li>
<li v-else-if="options.addNoData">
<i class="fa fa-li fa-phone" />
<i class="fa fa-li fa-phone"/>
<p class="chill-no-data-statement">
{{ trans(RENDERBOX_NO_DATA) }}
</p>
@@ -246,25 +187,25 @@
options.addCenter
"
>
<i class="fa fa-li fa-long-arrow-right" />
<i class="fa fa-li fa-long-arrow-right"/>
<template v-for="c in person.centers">
{{ c.name }}
</template>
</li>
<li v-else-if="options.addNoData">
<i class="fa fa-li fa-long-arrow-right" />
<i class="fa fa-li fa-long-arrow-right"/>
<p class="chill-no-data-statement">
{{ trans(RENDERBOX_NO_DATA) }}
</p>
</li>
<slot name="custom-zone" />
<slot name="custom-zone"/>
</ul>
</div>
</div>
</div>
</div>
<slot name="end-bloc" />
<slot name="end-bloc"/>
</section>
</div>
@@ -278,11 +219,11 @@
class="fa-stack fa-holder"
:title="trans(RENDERBOX_HOLDER)"
>
<i class="fa fa-circle fa-stack-1x text-success" />
<i class="fa fa-circle fa-stack-1x text-success"/>
<i class="fa fa-stack-1x">T</i>
</span>
<person-text :person="person" />
<person-text :person="person"/>
</a>
<span v-else>
<span
@@ -290,18 +231,18 @@
class="fa-stack fa-holder"
:title="trans(RENDERBOX_HOLDER)"
>
<i class="fa fa-circle fa-stack-1x text-success" />
<i class="fa fa-circle fa-stack-1x text-success"/>
<i class="fa fa-stack-1x">T</i>
</span>
<person-text :person="person" />
<person-text :person="person"/>
</span>
<slot name="post-badge" />
<slot name="post-badge"/>
</span>
</template>
<script setup>
import { computed } from "vue";
import { ISOToDate } from "ChillMainAssets/chill/js/date";
<script setup lang="ts">
import {computed} from "vue";
import {ISOToDate} from "ChillMainAssets/chill/js/date";
import AddressRenderBox from "ChillMainAssets/vuejs/_components/Entity/AddressRenderBox.vue";
import GenderIconRenderBox from "ChillMainAssets/vuejs/_components/Entity/GenderIconRenderBox.vue";
import BadgeEntity from "ChillMainAssets/vuejs/_components/BadgeEntity.vue";
@@ -311,108 +252,69 @@ import {
trans,
RENDERBOX_HOLDER,
RENDERBOX_NO_DATA,
RENDERBOX_DEATHDATE,
RENDERBOX_DEATHDATE_STATEMENT,
RENDERBOX_HOUSEHOLD_WITHOUT_ADDRESS,
RENDERBOX_RESIDENTIAL_ADDRESS,
RENDERBOX_LOCATED_AT,
RENDERBOX_BIRTHDAY_MAN,
RENDERBOX_BIRTHDAY_WOMAN,
RENDERBOX_BIRTHDAY_UNKNOWN,
RENDERBOX_BIRTHDAY_NEUTRAL,
PERSONS_ASSOCIATED_SHOW_HOUSEHOLD_NUMBER,
RENDERBOX_BIRTHDAY_STATEMENT,
// PERSONS_ASSOCIATED_SHOW_HOUSEHOLD_NUMBER,
RENDERBOX_YEARS_OLD,
} from "translator";
import {Person} from "ChillPersonAssets/types";
import {toGenderTranslation} from "ChillMainAssets/lib/api/genderHelper";
const props = defineProps({
person: {
required: true,
},
options: {
type: Object,
required: false,
},
render: {
type: String,
},
returnPath: {
type: String,
},
showResidentialAddresses: {
type: Boolean,
default: false,
},
});
interface RenderOptions {
addInfo?: boolean;
addEntity?: boolean;
addAltNames?: boolean;
addAge?: boolean;
addId?: boolean;
addLink?: boolean;
hLevel?: number;
entityDisplayLong?: boolean;
addCenter?: boolean;
addNoData?: boolean;
isMultiline?: boolean;
isHolder?: boolean;
addHouseholdLink?: boolean;
}
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;
});
const birthdate = computed(() => {
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(() => {
const getUrl = computed<string>(() => {
return `/fr/person/${props.person.id}/general`;
});
const getCurrentHouseholdUrl = computed(() => {
let returnPath = props.returnPath ? `?returnPath=${props.returnPath}` : ``;
const getCurrentHouseholdUrl = computed<string>(() => {
const returnPath = props.returnPath ? `?returnPath=${props.returnPath}` : ``;
return `/fr/person/household/${props.person.current_household_id}/summary${returnPath}`;
});
</script>

View File

@@ -1,13 +1,7 @@
<template>
<span v-if="isCut">{{ cutText }}</span>
<span v-else class="person-text">
<span class="firstname">{{ person.firstName }}</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>{{ person.text }}</span>
<span v-if="person.suffixText" class="suffixtext"
>&nbsp;{{ person.suffixText }}</span
>
@@ -33,16 +27,6 @@ const props = defineProps<{
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(() => {
if (!person.value.text) return "";
const more = person.value.text.length > 15 ? "…" : "";

View File

@@ -265,3 +265,38 @@ add_persons:
title: "Centre"
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"