Horodate précisément les contrôles de soins

This commit is contained in:
Alexandre NOEL 2026-09-13 14:28:12 +02:00
commit a0ea338292
10 changed files with 60 additions and 12 deletions

View file

@ -2,6 +2,14 @@
Toutes les modifications importantes du projet sont consignées ici. Le format s’inspire de [Keep a Changelog](https://keepachangelog.com/fr/1.1.0/) et le projet suit le [versionnage sémantique](https://semver.org/lang/fr/).
## [1.0.0-rc.19] — 2026-09-13
### Horodatage des contrôles de soins
- ajout d’une date et d’une heure aux notes médicales, proposées à l’instant de la saisie et librement modifiables ;
- utilisation du même horodatage pour la note, la mesure de poids, le tarif associé et la date d’une éventuelle stérilisation ;
- validation stricte des dates et heures transmises avant enregistrement.
## [1.0.0-rc.18] — 2026-09-09
### Finitions du site public

View file

@ -81,3 +81,7 @@ Remplacement de la barre de navigation horizontale par un menu mobile repliable
## `1.0.0-rc.18` — Finitions du site public
Déplacement des informations légales dans le pied de page, insertion guidée des images sans perdre les textes en cours d’édition et identification visuelle des réseaux sociaux.
## `1.0.0-rc.19` — Contrôles de soins horodatés
Ajout d’une date et d’une heure modifiables aux contrôles médicaux, avec reprise cohérente de l’instant choisi dans les mesures de poids et les données associées.

View file

@ -200,7 +200,7 @@ L’option `--apply` ne doit être utilisée qu’après lecture des rapports. L
## État du projet
La candidate actuelle est `1.0.0-rc.18`. La branche 1.0 est gelée fonctionnellement : seuls les défauts bloquants découverts pendant les validations terrain sont corrigés avant `1.0.0`.
La candidate actuelle est `1.0.0-rc.19`. La branche 1.0 est gelée fonctionnellement : seuls les défauts bloquants découverts pendant les validations terrain sont corrigés avant `1.0.0`.
Le contrôle de stabilisation est disponible dans [docs/AUDIT-1.0.md](docs/AUDIT-1.0.md).

View file

@ -1 +1 @@
1.0.0-rc.18
1.0.0-rc.19

View file

@ -221,6 +221,20 @@ trait AnimalMedicalActions
$temperature = $_POST['temperature_c'] ?? null;
$weight = $_POST['weight_kg'] ?? null;
$notedDate = trim((string) ($_POST['noted_date'] ?? date('Y-m-d')));
$notedTime = trim((string) ($_POST['noted_time'] ?? date('H:i')));
$observedAt = DateTimeImmutable::createFromFormat('!Y-m-d H:i', $notedDate . ' ' . $notedTime);
$dateErrors = DateTimeImmutable::getLastErrors();
if (
!$observedAt ||
($dateErrors !== false && ($dateErrors['warning_count'] > 0 || $dateErrors['error_count'] > 0)) ||
$observedAt->format('Y-m-d H:i') !== $notedDate . ' ' . $notedTime
) {
http_response_code(400);
echo h(t('error.invalid_medical_datetime'));
return;
}
$observedAtSql = $observedAt->format('Y-m-d H:i:s');
$clinicId = (int) ($_POST['clinic_id'] ?? 0);
$clinicId = $clinicId > 0 ? $clinicId : null;
@ -251,13 +265,14 @@ trait AnimalMedicalActions
// Enregistre la note médicale avant ses éventuelles pièces jointes.
$stmt = $db->prepare('
INSERT INTO medical_notes
(animal_id, kind, reason, symptoms, exam, diagnosis, plan, temperature_c, weight_kg, clinic_id, vet_name, clinic_contact_id, vet_contact_id, created_by, created_at)
(animal_id, noted_at, kind, reason, symptoms, exam, diagnosis, plan, temperature_c, weight_kg, clinic_id, vet_name, clinic_contact_id, vet_contact_id, created_by, created_at)
VALUES
(:aid, :kind, :reason, :symptoms, :exam, :diagnosis, :plan, :temp, :weight, :clinic, :vet, :clinic_contact, :vet_contact, :created_by, :created)
(:aid, :noted_at, :kind, :reason, :symptoms, :exam, :diagnosis, :plan, :temp, :weight, :clinic, :vet, :clinic_contact, :vet_contact, :created_by, :created)
');
$stmt->execute([
':aid' => $animalId,
':noted_at' => $observedAtSql,
':kind' => $kind,
':reason' => $reason,
':symptoms' => $symptoms,
@ -282,7 +297,7 @@ trait AnimalMedicalActions
$animalId,
(int) $_POST['tariff_id'],
1,
substr(self::now(), 0, 10),
$observedAt->format('Y-m-d'),
'Ajoutée avec la note médicale',
);
}
@ -344,7 +359,7 @@ trait AnimalMedicalActions
",
)->execute([
':aid' => $animalId,
':at' => self::now(),
':at' => $observedAtSql,
':v' => (float) $weight,
':created_by' => Auth::id(),
]);
@ -358,7 +373,7 @@ trait AnimalMedicalActions
$db->prepare(
"UPDATE animals SET sterilized=1,sterilization_status='yes',sterilization_date=COALESCE(sterilization_date,date(:date)),sterilization_vet_contact_id=COALESCE(:vet,sterilization_vet_contact_id),sterilization_clinic_contact_id=COALESCE(:clinic,sterilization_clinic_contact_id) WHERE id=:id",
)->execute([
':date' => self::now(),
':date' => $observedAtSql,
':vet' => $vetContact['id'] ?? null,
':clinic' => $clinicContact['id'] ?? null,
':id' => $animalId,

View file

@ -7,6 +7,7 @@
/** @var array $vaccines */
$defaultDue = date('Y-m-d', strtotime('+1 year'));
$today = date('Y-m-d');
$currentTime = date('H:i');
?>
<?php if ($canViewMedical): ?>
@ -612,6 +613,16 @@ $today = date('Y-m-d');
<input type="hidden" name="animal_id" value="<?= (int)$animal['id'] ?>">
<div class="row g-2">
<div class="col-md-4">
<label class="form-label"><?= h(t('common.date')) ?></label>
<input class="form-control" type="date" name="noted_date" value="<?= h($today) ?>" required>
</div>
<div class="col-md-4">
<label class="form-label"><?= h(t('agenda.time')) ?></label>
<input class="form-control" type="time" name="noted_time" value="<?= h($currentTime) ?>" required>
</div>
<div class="col-md-4">
<label class="form-label"><?= h(t('animal.form.type')) ?></label>
<select class="form-select" name="kind">
@ -625,7 +636,7 @@ $today = date('Y-m-d');
</select>
</div>
<div class="col-md-8">
<div class="col-12">
<label class="form-label"><?= h(t('animal.reason')) ?></label>
<input class="form-control" name="reason" id="medicalReason"
placeholder="<?= h(t('animal.reason_placeholder')) ?>">

View file

@ -31,10 +31,10 @@
<div class="col-lg-6">
<section class="card h-100">
<div class="card-body">
<span class="badge bg-blue-lt mb-2">1.0.0-rc.18</span>
<h3>Site public</h3>
<p class="text-muted">Informations légales accessibles sur chaque page, insertion guidée des images
dans les contenus et liens sociaux plus lisibles.</p>
<span class="badge bg-blue-lt mb-2">1.0.0-rc.19</span>
<h3>Suivi des soins</h3>
<p class="text-muted">Date et heure modifiables pour distinguer précisément plusieurs contrôles et
mesures réalisés au cours d’une même journée.</p>
</div>
</section>
</div>

View file

@ -198,4 +198,5 @@ return [
'updates.directory_failed'=>'Unable to prepare directory :path.',
'updates.download_log'=>'Download technical log',
'updates.log_missing'=>'No update log is available.',
'error.invalid_medical_datetime'=>'Enter a valid check-up date and time.',
];

View file

@ -198,4 +198,5 @@ return [
'updates.directory_failed'=>'Impossible de préparer le dossier :path.',
'updates.download_log'=>'Télécharger le journal technique',
'updates.log_missing'=>'Aucun journal de mise à jour n’est disponible.',
'error.invalid_medical_datetime'=>'Indiquez une date et une heure de contrôle valides.',
];

View file

@ -360,6 +360,14 @@ try {
$assert((int) $db->query('SELECT COUNT(*) FROM adoptions')->fetchColumn() === 1);
$assert(!$db->query('PRAGMA foreign_key_check')->fetchAll());
});
$test('Les contrôles médicaux conservent leur date et leur heure', function () use ($assert): void {
$view = file_get_contents(__DIR__ . '/../app/Views/_animal_show_modals.php');
$controller = file_get_contents(__DIR__ . '/../app/Controllers/AnimalMedicalActions.php');
$assert(str_contains($view, 'name="noted_date"'), 'Le champ de date du contrôle est absent');
$assert(str_contains($view, 'name="noted_time"'), 'Le champ d’heure du contrôle est absent');
$assert(str_contains($controller, "':noted_at' => \$observedAtSql"), 'La note ne reprend pas l’instant choisi');
$assert(str_contains($controller, "':at' => \$observedAtSql"), 'La courbe de poids ne reprend pas l’instant choisi');
});
$test('La fiche annuaire agrège adoptions et dépenses', function () use ($db, $assert): void {
$db->exec("INSERT INTO directory_contacts(kind,name) VALUES('organization','Structure test')");
$contact = (int) $db->lastInsertId();