From f20d7cc965cb57b099911e95744c35a0951e6845 Mon Sep 17 00:00:00 2001 From: oversu <95a7002b-481b-4ef3-b578-f9c421550aa4@localhost> Date: Tue, 15 Sep 2026 22:15:59 +0200 Subject: [PATCH 1/8] feat: permettre la correction des soins antiparasitaires --- CHANGELOG.md | 11 +++++ HISTORIQUE.md | 4 ++ README.md | 2 +- VERSION | 2 +- app/Controllers/AnimalMedicalActions.php | 62 +++++++++++++++++------- app/Controllers/AnimalRecordActions.php | 1 + app/Controllers/DewormingController.php | 44 +++++++++++++++-- app/Services/PricingService.php | 2 +- app/Views/_animal_show_medical.php | 22 +++++++++ app/Views/_animal_show_modals.php | 55 ++++++++++++++++++--- app/Views/evolutions.php | 8 +-- resources/lang/en.php | 18 +++++++ resources/lang/fr.php | 18 +++++++ tests/run.php | 17 +++++++ 14 files changed, 233 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5dc5dc9..e211222 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,17 @@ 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.28] — 2026-09-15 + +### Saisies médicales corrigibles + +- possibilité de modifier une vaccination ou une administration de vermifuge / anti-puce directement depuis la fiche animale ; +- préremplissage du formulaire et contrôle de l’appartenance de l’entrée à l’animal ; +- absence de nouveau déstockage, de nouvelle dépense ou d’application à la portée pendant une correction ; +- ajout d’une trace de correction dans l’historique ; +- harmonisation du libellé « Vermifuge / anti-puce » dans toute l’interface ; +- sélection de « Chat Européen » comme race initiale lors de la création d’une fiche de chat. + ## [1.0.0-rc.27] — 2026-09-15 ### Recherche globale des animaux diff --git a/HISTORIQUE.md b/HISTORIQUE.md index f228dfa..558fd31 100644 --- a/HISTORIQUE.md +++ b/HISTORIQUE.md @@ -10,6 +10,10 @@ L’import depuis Animal Shelter Manager a été conçu comme un parcours pruden Avant la version 1.0, le projet a fait l’objet d’un nettoyage structurel et d’un découpage de ses fichiers les plus volumineux. La recherche universelle, les salles configurables, les traductions française et anglaise et une suite de contrôles isolés ont complété cette phase de stabilisation. +## `1.0.0-rc.28` — Corrections des saisies médicales + +Les vaccinations et administrations de vermifuges ou d’anti-puces peuvent être corrigées sans provoquer une seconde sortie de stock ni une nouvelle dépense. Les nouvelles fiches de chats proposent également la race Chat Européen par défaut. + ## `1.0.0-rc.27` — Recherche tolérante La recherche globale retrouve les animaux par leur numéro de puce ou de tatouage et rapproche les termes indépendamment de leur casse et de leurs accents. diff --git a/README.md b/README.md index 7b6120d..1796654 100644 --- a/README.md +++ b/README.md @@ -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.27`. 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.28`. 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). diff --git a/VERSION b/VERSION index d2e3cec..90a7420 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.0.0-rc.27 +1.0.0-rc.28 diff --git a/app/Controllers/AnimalMedicalActions.php b/app/Controllers/AnimalMedicalActions.php index 3f8a04e..3846c5f 100644 --- a/app/Controllers/AnimalMedicalActions.php +++ b/app/Controllers/AnimalMedicalActions.php @@ -562,15 +562,30 @@ trait AnimalMedicalActions $db = DB::pdo(); $id = (int) ($_POST['animal_id'] ?? 0); + $entryId = (int) ($_POST['entry_id'] ?? 0); $vacId = (int) ($_POST['vaccine_id'] ?? 0); if ($id <= 0 || $vacId <= 0) { http_response_code(400); echo h(t('error.bad_request')); return; } + if ($entryId > 0) { + $stmt = $db->prepare('SELECT 1 FROM vaccinations WHERE id=:entry AND animal_id=:animal'); + $stmt->execute([':entry' => $entryId, ':animal' => $id]); + if (!$stmt->fetchColumn()) { + http_response_code(404); + echo h(t('error.not_found')); + return; + } + } $done = trim((string) ($_POST['done_date'] ?? date('Y-m-d'))); $due = trim((string) ($_POST['due_date'] ?? '')); + if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $done) || ($due !== '' && !preg_match('/^\d{4}-\d{2}-\d{2}$/', $due))) { + http_response_code(400); + echo h(t('error.bad_request')); + return; + } // Si due_date est fourni, on respecte. // Sinon auto selon protocole. if ($due === '') { @@ -602,9 +617,9 @@ trait AnimalMedicalActions SELECT COUNT(*) FROM vaccinations v JOIN ref_vaccines rv ON rv.id = v.vaccine_id - WHERE v.animal_id = :aid AND upper(coalesce(rv.family,'')) = 'L' + WHERE v.animal_id = :aid AND v.id <> :entry AND upper(coalesce(rv.family,'')) = 'L' "); - $stmt->execute([':aid' => $id]); + $stmt->execute([':aid' => $id, ':entry' => $entryId]); $countL = (int) $stmt->fetchColumn(); $isPrimoL = $countL === 0; } @@ -658,11 +673,7 @@ trait AnimalMedicalActions } $vet_name = $vetContact['name'] ?? $vet_name; - $stmt = $db->prepare(' - INSERT INTO vaccinations(animal_id, vaccine_id, done_date, due_date, lot,manufacturer,batch_expires_on, notes, clinic_id, vet_name, clinic_contact_id, vet_contact_id, created_by) - VALUES(:aid,:vid,:done,:due,:lot,:manufacturer,:batch_expiry,:notes,:clinic_id,:vet_name,:clinic_contact_id,:vet_contact_id,:created_by) - '); - $stmt->execute([ + $values = [ ':aid' => $id, ':vid' => $vacId, ':done' => $done, @@ -676,21 +687,38 @@ trait AnimalMedicalActions ':clinic_contact_id' => $clinicContact['id'] ?? null, ':vet_contact_id' => $vetContact['id'] ?? null, ':created_by' => Auth::id(), - ]); - InventoryService::consumeBatch( - (int) ($_POST['inventory_batch_id'] ?? 0), - (float) str_replace(',', '.', (string) ($_POST['inventory_quantity'] ?? 0)), - $id, - 'Vaccination du ' . $done, - ); - if ((int) ($_POST['tariff_id'] ?? 0) > 0) { - PricingService::record($db, $id, (int) $_POST['tariff_id'], 1, $done, 'Ajoutée avec la vaccination'); + ]; + if ($entryId > 0) { + unset($values[':aid'], $values[':created_by']); + $values[':entry'] = $entryId; + $stmt = $db->prepare(' + UPDATE vaccinations SET vaccine_id=:vid,done_date=:done,due_date=:due,lot=:lot, + manufacturer=:manufacturer,batch_expires_on=:batch_expiry,notes=:notes,clinic_id=:clinic_id, + vet_name=:vet_name,clinic_contact_id=:clinic_contact_id,vet_contact_id=:vet_contact_id + WHERE id=:entry + '); + $stmt->execute($values); + } else { + $stmt = $db->prepare(' + INSERT INTO vaccinations(animal_id, vaccine_id, done_date, due_date, lot,manufacturer,batch_expires_on, notes, clinic_id, vet_name, clinic_contact_id, vet_contact_id, created_by) + VALUES(:aid,:vid,:done,:due,:lot,:manufacturer,:batch_expiry,:notes,:clinic_id,:vet_name,:clinic_contact_id,:vet_contact_id,:created_by) + '); + $stmt->execute($values); + InventoryService::consumeBatch( + (int) ($_POST['inventory_batch_id'] ?? 0), + (float) str_replace(',', '.', (string) ($_POST['inventory_quantity'] ?? 0)), + $id, + 'Vaccination du ' . $done, + ); + if ((int) ($_POST['tariff_id'] ?? 0) > 0) { + PricingService::record($db, $id, (int) $_POST['tariff_id'], 1, $done, 'Ajoutée avec la vaccination'); + } } self::logHistory( $id, 'vaccine', - 'Vaccin ajouté', + $entryId > 0 ? 'Vaccin corrigé' : 'Vaccin ajouté', "Vaccin #$vacId\nFait le : $done" . ($due !== '' ? "\nRappel : $due" : '') . ($manufacturer !== '' ? "\nFabricant : $manufacturer" : '') . diff --git a/app/Controllers/AnimalRecordActions.php b/app/Controllers/AnimalRecordActions.php index 462fb8b..a604f64 100644 --- a/app/Controllers/AnimalRecordActions.php +++ b/app/Controllers/AnimalRecordActions.php @@ -182,6 +182,7 @@ trait AnimalRecordActions 'adoption_unavailability_reason' => '', 'website_published' => 0, 'website_description' => '', + 'breed' => 'Chat Européen', 'color' => '', 'hair_type' => '', 'birth_date' => '', diff --git a/app/Controllers/DewormingController.php b/app/Controllers/DewormingController.php index 3ce8082..5757a83 100644 --- a/app/Controllers/DewormingController.php +++ b/app/Controllers/DewormingController.php @@ -17,6 +17,7 @@ final class DewormingController } $db = DB::pdo(); $animalId = (int) ($_POST['animal_id'] ?? 0); + $entryId = (int) ($_POST['entry_id'] ?? 0); $date = trim((string) ($_POST['administered_on'] ?? '')); $dewormer = (int) ($_POST['dewormer_id'] ?? 0); if (!$animalId || !self::date($date)) { @@ -31,6 +32,15 @@ final class DewormingController echo h(t('error.animal_not_found')); return; } + if ($entryId > 0) { + $s = $db->prepare('SELECT 1 FROM dewormings WHERE id=? AND animal_id=?'); + $s->execute([$entryId, $animalId]); + if (!$s->fetchColumn()) { + http_response_code(404); + echo h(t('error.not_found')); + return; + } + } if (isset($_POST['create_dewormer'])) { $name = trim((string) ($_POST['new_dewormer_name'] ?? '')); if ($name === '') { @@ -59,7 +69,7 @@ final class DewormingController } $ids = [$animalId]; $litterId = null; - if (isset($_POST['apply_litter'])) { + if ($entryId === 0 && isset($_POST['apply_litter'])) { $s = $db->prepare( 'SELECT l.id FROM litters l LEFT JOIN litter_kittens lk ON lk.litter_id=l.id WHERE l.mother_id=? OR lk.animal_id=? ORDER BY l.id DESC LIMIT 1', ); @@ -85,8 +95,36 @@ final class DewormingController 'INSERT INTO dewormings(animal_id,dewormer_id,administered_on,dose_text,weight_kg,next_due_date,notes,source_litter_id,created_by) VALUES(?,?,?,?,?,?,?,?,?)', ); $history = $db->prepare( - "INSERT INTO animal_history(animal_id,action,type,label,details,user_id) VALUES(?,'deworming','deworming','Vermifuge administré',?,?)", + "INSERT INTO animal_history(animal_id,action,type,label,details,user_id) VALUES(?,'deworming','deworming','Vermifuge / anti-puce administré',?,?)", ); + if ($entryId > 0) { + $s = $db->prepare('SELECT birth_date FROM animals WHERE id=?'); + $s->execute([$animalId]); + $birthDate = $s->fetchColumn() ?: null; + $next = $manual ?: self::nextDue(is_string($birthDate) ? $birthDate : null, $date); + $db->prepare( + "UPDATE dewormings SET dewormer_id=?,administered_on=?,dose_text=?,weight_kg=?,next_due_date=?,notes=?,updated_at=datetime('now') WHERE id=? AND animal_id=?", + )->execute([ + $dewormer ?: null, + $date, + trim((string) ($_POST['dose_text'] ?? '')) ?: null, + ($_POST['weight_kg'] ?? '') !== '' ? (float) $_POST['weight_kg'] : null, + $next, + trim((string) ($_POST['notes'] ?? '')) ?: null, + $entryId, + $animalId, + ]); + $db->prepare( + "INSERT INTO animal_history(animal_id,action,type,label,details,user_id) VALUES(?,'deworming_updated','deworming','Vermifuge / anti-puce corrigé',?,?)", + )->execute([ + $animalId, + 'Administration : ' . self::fr($date) . ($next ? ' · Prochaine échéance : ' . self::fr($next) : ''), + Auth::id(), + ]); + $db->commit(); + header('Location: /animal?id=' . $animalId . '#tab-med'); + exit(); + } foreach ($ids as $id) { $s = $db->prepare('SELECT name,birth_date FROM animals WHERE id=? AND deleted_at IS NULL'); $s->execute([$id]); @@ -117,7 +155,7 @@ final class DewormingController (int) ($_POST['inventory_batch_id'] ?? 0), $perAnimal * count($ids), $animalId, - 'Vermifuge du ' . $date . ($litterId ? ' · portée #' . $litterId : ''), + 'Vermifuge / anti-puce du ' . $date . ($litterId ? ' · portée #' . $litterId : ''), ); $db->commit(); header('Location: /animal?id=' . $animalId . '#tab-med'); diff --git a/app/Services/PricingService.php b/app/Services/PricingService.php index 9b41b5b..e773377 100644 --- a/app/Services/PricingService.php +++ b/app/Services/PricingService.php @@ -12,7 +12,7 @@ final class PricingService 'euthanasia' => 'Euthanasie', 'cremation' => 'Crémation', 'medication' => 'Médicament', - 'dewormer' => 'Vermifuge', + 'dewormer' => 'Vermifuge / anti-puce', 'other' => 'Autre', ]; public const GROUPS = [ diff --git a/app/Views/_animal_show_medical.php b/app/Views/_animal_show_medical.php index 31b8b95..9eaa17c 100644 --- a/app/Views/_animal_show_medical.php +++ b/app/Views/_animal_show_medical.php @@ -218,6 +218,7 @@ + @@ -227,6 +228,16 @@ + @@ -362,6 +373,7 @@ ORDER BY created_at ASC + @@ -426,6 +438,16 @@ ORDER BY created_at ASC + diff --git a/app/Views/_animal_show_modals.php b/app/Views/_animal_show_modals.php index 82c27a8..d56aad7 100644 --- a/app/Views/_animal_show_modals.php +++ b/app/Views/_animal_show_modals.php @@ -156,6 +156,7 @@ $currentTime = date('H:i'); class="btn-close" data-bs-dismiss="modal" aria-label="">