Enhance PersonEdit form: Add birthdate input with validation, improve field error handling using hasValidationError, refactor birthDate to respect timezone offsets, and update translations for better user feedback. Replace DateTimeCreate with DateTimeWrite across types and components.

This commit is contained in:
2025-09-18 11:32:36 +02:00
parent 806f709d80
commit b6145b2e5f
6 changed files with 80 additions and 14 deletions

View File

@@ -158,3 +158,18 @@ export const intervalISOToDays = (str: string | null): number | null => {
return days; return days;
}; };
export function getTimezoneOffsetString(date: Date, timeZone: string): string {
const utcDate = new Date(date.toLocaleString("en-US", { timeZone: "UTC" }));
const tzDate = new Date(date.toLocaleString("en-US", { timeZone }));
const offsetMinutes = (utcDate.getTime() - tzDate.getTime()) / (60 * 1000);
// Inverser le signe pour avoir la convention ±HH:MM
const sign = offsetMinutes <= 0 ? "+" : "-";
const absMinutes = Math.abs(offsetMinutes);
const hours = String(Math.floor(absMinutes / 60)).padStart(2, "0");
const minutes = String(absMinutes % 60).padStart(2, "0");
return `${sign}${hours}:${minutes}`;
}

View File

@@ -8,9 +8,9 @@ export interface DateTime {
} }
/** /**
* A date representation to use when we create an instance * A date representation to use when we create or update a date
*/ */
export interface DateTimeCreate { export interface DateTimeWrite {
/** /**
* Must be a string in format Y-m-d\TH:i:sO * Must be a string in format Y-m-d\TH:i:sO
*/ */

View File

@@ -11,7 +11,7 @@ import {
Job, Job,
PrivateCommentEmbeddable, PrivateCommentEmbeddable,
TranslatableString, TranslatableString,
DateTimeCreate, DateTimeWrite,
SetGender, SetGender,
SetCenter, SetCenter,
SetCivility, SetCivility,
@@ -67,8 +67,8 @@ export interface PersonWrite {
lastName: string; lastName: string;
altNames: AltNameWrite[]; altNames: AltNameWrite[];
// address: number | null; // address: number | null;
birthdate: DateTimeCreate | null; birthdate: DateTimeWrite | null;
deathdate: DateTimeCreate | null; deathdate: DateTimeWrite | null;
phonenumber: string; phonenumber: string;
mobilenumber: string; mobilenumber: string;
email: string; email: string;

View File

@@ -72,6 +72,7 @@ export interface WritePersonViolationMap
civility: { civility: {
"{{ value }}": string | null; "{{ value }}": string | null;
}; };
birthdate: {};
} }
export const createPerson = async (person: PersonWrite): Promise<Person> => { export const createPerson = async (person: PersonWrite): Promise<Person> => {
return makeFetch<PersonWrite, Person, WritePersonViolationMap>( return makeFetch<PersonWrite, Person, WritePersonViolationMap>(

View File

@@ -140,6 +140,7 @@
<div class="form-floating"> <div class="form-floating">
<select <select
class="form-select form-select-lg" class="form-select form-select-lg"
:class="{ 'is-invalid': hasValidationError('center') }"
id="center" id="center"
v-model="center" v-model="center"
> >
@@ -150,7 +151,7 @@
{{ c.name }} {{ c.name }}
</option> </option>
</select> </select>
<label>{{ trans(PERSON_MESSAGES_PERSON_CENTER_TITLE) }}</label> <label for="center">{{ trans(PERSON_MESSAGES_PERSON_CENTER_TITLE) }}</label>
</div> </div>
<div <div
v-for="err in validationError('center')" v-for="err in validationError('center')"
@@ -166,6 +167,7 @@
<div class="form-floating"> <div class="form-floating">
<select <select
class="form-select form-select-lg" class="form-select form-select-lg"
:class="{ 'is-invalid': hasValidationError('civility') }"
id="civility" id="civility"
v-model="civility" v-model="civility"
> >
@@ -176,7 +178,7 @@
{{ localizeString(c.name) }} {{ localizeString(c.name) }}
</option> </option>
</select> </select>
<label>{{ trans(PERSON_MESSAGES_PERSON_CIVILITY_TITLE) }}</label> <label for="civility">{{ trans(PERSON_MESSAGES_PERSON_CIVILITY_TITLE) }}</label>
</div> </div>
<div <div
v-for="err in validationError('civility')" v-for="err in validationError('civility')"
@@ -187,6 +189,32 @@
</div> </div>
</div> </div>
<div class="mb-3">
<div class="input-group has-validation">
<span class="input-group-text">
<i class="bi bi-cake2-fill"></i>
</span>
<div class="form-floating">
<input
class="form-control form-control-lg"
:class="{ 'is-invalid': hasValidationError('birthdate') }"
name="birthdate"
type="date"
v-model="birthDate"
:placeholder="trans(BIRTHDATE)"
:aria-label="trans(BIRTHDATE)"
/>
<label for="birthdate">{{ trans(BIRTHDATE) }}</label>
</div>
<div
v-for="err in validationError('birthdate')"
class="invalid-feedback was-validated-force"
>
{{ err }}
</div>
</div>
</div>
<div class="mb-3"> <div class="mb-3">
<div class="input-group has-validation"> <div class="input-group has-validation">
<span class="input-group-text" id="phonenumber"> <span class="input-group-text" id="phonenumber">
@@ -195,11 +223,13 @@
<div class="form-floating"> <div class="form-floating">
<input <input
class="form-control form-control-lg" class="form-control form-control-lg"
:class="{ 'is-invalid': hasValidationError('phonenumber') }"
v-model="phonenumber" v-model="phonenumber"
:placeholder="trans(PERSON_MESSAGES_PERSON_PHONENUMBER)" :placeholder="trans(PERSON_MESSAGES_PERSON_PHONENUMBER)"
:aria-label="trans(PERSON_MESSAGES_PERSON_PHONENUMBER)" :aria-label="trans(PERSON_MESSAGES_PERSON_PHONENUMBER)"
aria-describedby="phonenumber" aria-describedby="phonenumber"
/> />
<label for="phonenumber">{{ trans(PERSON_MESSAGES_PERSON_PHONENUMBER) }}</label>
</div> </div>
<div <div
v-for="err in validationError('phonenumber')" v-for="err in validationError('phonenumber')"
@@ -218,11 +248,13 @@
<div class="form-floating"> <div class="form-floating">
<input <input
class="form-control form-control-lg" class="form-control form-control-lg"
:class="{ 'is-invalid': hasValidationError('mobilenumber') }"
v-model="mobilenumber" v-model="mobilenumber"
:placeholder="trans(PERSON_MESSAGES_PERSON_MOBILENUMBER)" :placeholder="trans(PERSON_MESSAGES_PERSON_MOBILENUMBER)"
:aria-label="trans(PERSON_MESSAGES_PERSON_MOBILENUMBER)" :aria-label="trans(PERSON_MESSAGES_PERSON_MOBILENUMBER)"
aria-describedby="mobilenumber" aria-describedby="mobilenumber"
/> />
<label for="mobilenumber">{{ trans(PERSON_MESSAGES_PERSON_MOBILENUMBER) }}</label>
</div> </div>
<div <div
v-for="err in validationError('mobilenumber')" v-for="err in validationError('mobilenumber')"
@@ -241,11 +273,13 @@
<div class="form-floating"> <div class="form-floating">
<input <input
class="form-control form-control-lg" class="form-control form-control-lg"
:class="{ 'is-invalid': hasValidationError('email') }"
v-model="email" v-model="email"
:placeholder="trans(PERSON_MESSAGES_PERSON_EMAIL)" :placeholder="trans(PERSON_MESSAGES_PERSON_EMAIL)"
:aria-label="trans(PERSON_MESSAGES_PERSON_EMAIL)" :aria-label="trans(PERSON_MESSAGES_PERSON_EMAIL)"
aria-describedby="email" aria-describedby="email"
/> />
<label for="email">{{ trans(PERSON_MESSAGES_PERSON_EMAIL) }}</label>
</div> </div>
<div <div
v-for="err in validationError('email')" v-for="err in validationError('email')"
@@ -305,6 +339,8 @@ import {
} from "../../_api/OnTheFly"; } from "../../_api/OnTheFly";
import { import {
trans, trans,
BIRTHDATE,
PERSON_EDIT_ERROR_WHILE_SAVING,
PERSON_MESSAGES_PERSON_LASTNAME, PERSON_MESSAGES_PERSON_LASTNAME,
PERSON_MESSAGES_PERSON_FIRSTNAME, PERSON_MESSAGES_PERSON_FIRSTNAME,
PERSON_MESSAGES_PERSON_GENDER_PLACEHOLDER, PERSON_MESSAGES_PERSON_GENDER_PLACEHOLDER,
@@ -323,7 +359,7 @@ import {
Center, Center,
Civility, Civility,
Gender, Gender,
DateTimeCreate, DateTimeWrite,
} from "ChillMainAssets/types"; } from "ChillMainAssets/types";
import { import {
AltName, AltName,
@@ -337,6 +373,8 @@ import {
isValidationException, isValidationException,
ValidationExceptionInterface, ValidationExceptionInterface,
} from "ChillMainAssets/lib/api/apiMethods"; } from "ChillMainAssets/lib/api/apiMethods";
import {useToast} from "vue-toast-notification";
import {getTimezoneOffsetString, ISOToDate} from "ChillMainAssets/chill/js/date";
interface PersonEditComponentConfig { interface PersonEditComponentConfig {
id?: number | null; id?: number | null;
@@ -355,6 +393,8 @@ const emit =
defineExpose({ postPerson }); defineExpose({ postPerson });
const toast = useToast();
const person = reactive<PersonWrite>({ const person = reactive<PersonWrite>({
type: "person", type: "person",
firstName: "", firstName: "",
@@ -436,12 +476,18 @@ const civility = computed({
const birthDate = computed({ const birthDate = computed({
get: () => (person.birthdate ? person.birthdate.datetime.split("T")[0] : ""), get: () => (person.birthdate ? person.birthdate.datetime.split("T")[0] : ""),
set: (value: string) => { set: (value: string) => {
if (person.birthdate) { const date = ISOToDate(value);
person.birthdate.datetime = value + "T00:00:00+0100"; if (null === date) {
} else { person.birthdate = null;
person.birthdate = { datetime: value + "T00:00:00+0100" }; return;
} }
}, const offset = getTimezoneOffsetString(date, Intl.DateTimeFormat().resolvedOptions().timeZone);
if (person.birthdate) {
person.birthdate.datetime = value + "T00:00:00" + offset;
} else {
person.birthdate = { datetime: value + "T00:00:00" + offset };
}
}
}); });
const phonenumber = computed({ const phonenumber = computed({
get: () => person.phonenumber, get: () => person.phonenumber,
@@ -586,6 +632,8 @@ async function postPerson(): Promise<void> {
if (isValidationException<WritePersonViolationMap>(e)) { if (isValidationException<WritePersonViolationMap>(e)) {
console.log(e.byProperty); console.log(e.byProperty);
validationErrors.value = e.byProperty; validationErrors.value = e.byProperty;
} else {
toast.error(trans(PERSON_EDIT_ERROR_WHILE_SAVING));
} }
} }
} }

View File

@@ -105,6 +105,8 @@ Administrative status: Situation administrative
person: person:
Identifiers: Identifiants Identifiers: Identifiants
person_edit:
Error while saving: Erreur lors de l'enregistrement
# dédoublonnage # dédoublonnage
Old person: Doublon Old person: Doublon
@@ -1547,7 +1549,7 @@ person_messages:
center_id: "Identifiant du centre" center_id: "Identifiant du centre"
center_type: "Type de centre" center_type: "Type de centre"
center_name: "Territoire" center_name: "Territoire"
phonenumber: "Téléphone" phonenumber: "Téléphone fixe"
mobilenumber: "Mobile" mobilenumber: "Mobile"
altnames: "Autres noms" altnames: "Autres noms"
email: "Courriel" email: "Courriel"