From f45cfc40f25e73871f4660234f2575863e2ddaf4 Mon Sep 17 00:00:00 2001 From: oversu <95a7002b-481b-4ef3-b578-f9c421550aa4@localhost> Date: Tue, 15 Sep 2026 18:48:25 +0200 Subject: [PATCH 01/11] =?UTF-8?q?feat:=20permettre=20la=20modification=20d?= =?UTF-8?q?es=20port=C3=A9es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 10 ++ HISTORIQUE.md | 4 + README.md | 2 +- VERSION | 2 +- app/Controllers/AnimalRelationshipActions.php | 154 +++++++++++++++++- app/Routes/web.php | 9 + app/Services/PermissionService.php | 3 + app/Views/_animal_show_litters.php | 3 +- app/Views/evolutions.php | 4 +- app/Views/layout.php | 2 +- app/Views/litter_edit.php | 13 ++ app/Views/litter_form.php | 3 +- resources/lang/en.php | 6 + resources/lang/fr.php | 6 + tests/run.php | 11 ++ 15 files changed, 218 insertions(+), 14 deletions(-) create mode 100644 app/Views/litter_edit.php diff --git a/CHANGELOG.md b/CHANGELOG.md index b580187..839e322 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,16 @@ 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.25] — 2026-09-15 + +### Modification des portées + +- ajout d’un écran permettant de corriger les parents et la date de naissance d’une portée ; +- ajout de nouvelles fiches ou de chats existants à une portée déjà enregistrée ; +- accès direct à la modification de chaque chaton depuis la portée ; +- possibilité de détacher un chaton sans supprimer sa fiche ni aucune de ses données ; +- contrôles empêchant de rattacher un parent comme chaton de la même portée. + ## [1.0.0-rc.24] — 2026-09-15 ### Permissions et bases historiques diff --git a/HISTORIQUE.md b/HISTORIQUE.md index 5751a2d..9cb5fa6 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.25` — Portées modifiables + +Les portées ne sont plus figées après leur création : parents et date peuvent être corrigés, tandis que les chatons peuvent être ajoutés, modifiés ou détachés sans perte de leur fiche. + ## `1.0.0-rc.24` — Permissions durables Séparation plus stricte entre les données privées et les médias publics afin que les réparations de sécurité et les mises à jour conservent l’accès de Nginx aux images et aux ressources de l’interface. Les deux structures d’historique rencontrées sur les installations anciennes sont également réunifiées pour sécuriser les portées et les autres écritures métier. diff --git a/README.md b/README.md index 9920a92..0da8de5 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.24`. 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.25`. 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 debcfd1..07923b2 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.0.0-rc.24 +1.0.0-rc.25 diff --git a/app/Controllers/AnimalRelationshipActions.php b/app/Controllers/AnimalRelationshipActions.php index f3eb7c9..43cc1d4 100644 --- a/app/Controllers/AnimalRelationshipActions.php +++ b/app/Controllers/AnimalRelationshipActions.php @@ -8,8 +8,10 @@ trait AnimalRelationshipActions { $db = DB::pdo(); $sourceAnimalId = (int) ($_GET['animal_id'] ?? 0); + $existingLitterId = (int) ($_GET['litter_id'] ?? 0); $motherId = 0; $fatherId = 0; + $birthDate = ''; if ($sourceAnimalId > 0) { $stmt = $db->prepare( @@ -30,6 +32,20 @@ trait AnimalRelationshipActions } } + if ($existingLitterId > 0) { + $stmt = $db->prepare('SELECT mother_id,father_id,birth_date FROM litters WHERE id=:id'); + $stmt->execute([':id' => $existingLitterId]); + $existingLitter = $stmt->fetch(PDO::FETCH_ASSOC); + if (!$existingLitter) { + http_response_code(404); + echo h(t('litter.not_found')); + return; + } + $motherId = (int) ($existingLitter['mother_id'] ?? 0); + $fatherId = (int) ($existingLitter['father_id'] ?? 0); + $birthDate = (string) ($existingLitter['birth_date'] ?? ''); + } + $eligibleSql = " SELECT id, name FROM animals WHERE deleted_at IS NULL AND archived_at IS NULL AND lower(species) IN ('chat', 'cat') AND sex = :sex @@ -53,6 +69,8 @@ trait AnimalRelationshipActions 'sourceAnimalId' => $sourceAnimalId, 'motherId' => $motherId, 'fatherId' => $fatherId, + 'birthDate' => $birthDate, + 'existingLitterId' => $existingLitterId, 'mothers' => $mothers, 'fathers' => $fathers, 'existingKittens' => $existingKittens, @@ -66,6 +84,7 @@ trait AnimalRelationshipActions return; } $db = DB::pdo(); + $existingLitterId = (int) ($_POST['existing_litter_id'] ?? 0); $motherId = (int) ($_POST['mother_id'] ?? 0) ?: null; $fatherId = (int) ($_POST['father_id'] ?? 0) ?: null; $birthDate = trim((string) ($_POST['birth_date'] ?? '')) ?: null; @@ -141,11 +160,22 @@ trait AnimalRelationshipActions $allowedSexes = ['F', 'M', 'U']; $db->beginTransaction(); try { - $stmt = $db->prepare( - 'INSERT INTO litters(mother_id, father_id, birth_date) VALUES(:mother, :father, :birth)', - ); - $stmt->execute([':mother' => $motherId, ':father' => $fatherId, ':birth' => $birthDate]); - $litterId = (int) $db->lastInsertId(); + if ($existingLitterId > 0) { + $stmt = $db->prepare('SELECT id,mother_id,father_id,birth_date FROM litters WHERE id=:id'); + $stmt->execute([':id' => $existingLitterId]); + $existingLitter = $stmt->fetch(PDO::FETCH_ASSOC); + if (!$existingLitter) { + throw new RuntimeException(t('litter.not_found')); + } + $litterId = (int) $existingLitter['id']; + $motherId = (int) ($existingLitter['mother_id'] ?? 0) ?: null; + $fatherId = (int) ($existingLitter['father_id'] ?? 0) ?: null; + $birthDate = (string) ($existingLitter['birth_date'] ?? '') ?: null; + } else { + $stmt = $db->prepare('INSERT INTO litters(mother_id, father_id, birth_date) VALUES(:mother, :father, :birth)'); + $stmt->execute([':mother' => $motherId, ':father' => $fatherId, ':birth' => $birthDate]); + $litterId = (int) $db->lastInsertId(); + } $insertAnimal = $db->prepare(" INSERT INTO animals(name, internal_code, status, species, sex, breed, color, birth_date, birth_is_estimated,intake_date,intake_type,intake_reason, created_at, updated_at) VALUES(:name, :code, 'refuge', 'chat', :sex, 'Chat Européen', :color, :birth, 0,:birth,'born_in_care','birth', datetime('now'), datetime('now')) @@ -153,11 +183,14 @@ trait AnimalRelationshipActions $link = $db->prepare( 'INSERT INTO litter_kittens(litter_id, animal_id, position) VALUES(:litter, :animal, :position)', ); + $positionBase = $existingLitterId > 0 + ? (int) $db->query('SELECT COALESCE(MAX(position),0) FROM litter_kittens WHERE litter_id=' . $litterId)->fetchColumn() + : 0; foreach ($kittens as $index => $kitten) { if (!is_array($kitten)) { continue; } - $position = $index + 1; + $position = $positionBase + $index + 1; $name = trim((string) ($kitten['name'] ?? '')); if ($name === '') { $name = $baseName . '-Bébé-' . $position; @@ -198,7 +231,7 @@ trait AnimalRelationshipActions ); self::logHistory($animalId, 'create', 'Chaton ajouté avec sa portée', 'Portée n°' . $litterId); } - $position = count($kittens); + $position = $positionBase + count($kittens); foreach ($existingKittenIds as $animalId) { $link->execute([':litter' => $litterId, ':animal' => $animalId, ':position' => ++$position]); self::logHistory($animalId, 'update', 'Rattaché à une nouvelle portée', 'Portée n°' . $litterId); @@ -214,6 +247,113 @@ trait AnimalRelationshipActions } } + public static function litterEditForm(): void + { + $db = DB::pdo(); + $litterId = (int) ($_GET['id'] ?? 0); + $sourceAnimalId = (int) ($_GET['animal_id'] ?? 0); + $stmt = $db->prepare('SELECT l.*,m.name mother_name,f.name father_name FROM litters l LEFT JOIN animals m ON m.id=l.mother_id LEFT JOIN animals f ON f.id=l.father_id WHERE l.id=:id'); + $stmt->execute([':id' => $litterId]); + $litter = $stmt->fetch(PDO::FETCH_ASSOC); + if (!$litter) { + http_response_code(404); + echo h(t('litter.not_found')); + return; + } + $parents = $db->query("SELECT id,name,sex FROM animals WHERE deleted_at IS NULL AND lower(species) IN ('chat','cat') ORDER BY name COLLATE NOCASE")->fetchAll(PDO::FETCH_ASSOC); + $stmt = $db->prepare('SELECT a.id,a.name,a.internal_code,a.sex,a.color,a.status,a.archived_at FROM litter_kittens lk JOIN animals a ON a.id=lk.animal_id WHERE lk.litter_id=:id ORDER BY lk.position,a.name COLLATE NOCASE'); + $stmt->execute([':id' => $litterId]); + render('litter_edit.php', [ + 'title' => t('litter.edit'), + 'pageDescription' => t('litter.edit_help'), + 'litter' => $litter, + 'mothers' => array_values(array_filter($parents, static fn(array $animal): bool => $animal['sex'] === 'F')), + 'fathers' => array_values(array_filter($parents, static fn(array $animal): bool => $animal['sex'] === 'M')), + 'kittens' => $stmt->fetchAll(PDO::FETCH_ASSOC), + 'sourceAnimalId' => $sourceAnimalId, + ]); + } + + public static function updateLitter(): void + { + if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST') { + http_response_code(405); + return; + } + $db = DB::pdo(); + $litterId = (int) ($_POST['litter_id'] ?? 0); + $sourceAnimalId = (int) ($_POST['source_animal_id'] ?? 0); + $motherId = (int) ($_POST['mother_id'] ?? 0) ?: null; + $fatherId = (int) ($_POST['father_id'] ?? 0) ?: null; + $birthDate = trim((string) ($_POST['birth_date'] ?? '')) ?: null; + if ($litterId <= 0 || ($motherId !== null && $motherId === $fatherId)) { + http_response_code(400); + echo h(t('litter.invalid_parent')); + return; + } + if ($birthDate !== null && !preg_match('/^\d{4}-\d{2}-\d{2}$/', $birthDate)) { + http_response_code(400); + echo h(t('error.invalid_birth_date')); + return; + } + $ids = array_values(array_filter([$motherId, $fatherId])); + if ($ids) { + $placeholders = implode(',', array_fill(0, count($ids), '?')); + $stmt = $db->prepare("SELECT id,sex FROM animals WHERE id IN ($placeholders) AND deleted_at IS NULL AND lower(species) IN ('chat','cat')"); + $stmt->execute($ids); + $valid = []; + foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $animal) { + $valid[(int) $animal['id']] = $animal['sex']; + } + if (($motherId && ($valid[$motherId] ?? '') !== 'F') || ($fatherId && ($valid[$fatherId] ?? '') !== 'M')) { + http_response_code(400); + echo h(t('litter.invalid_parent')); + return; + } + $stmt = $db->prepare('SELECT COUNT(*) FROM litter_kittens WHERE litter_id=? AND animal_id IN (' . $placeholders . ')'); + $stmt->execute([$litterId, ...$ids]); + if ((int) $stmt->fetchColumn() > 0) { + http_response_code(400); + echo h(t('litter.parent_is_kitten')); + return; + } + } + $exists = $db->prepare('SELECT 1 FROM litters WHERE id=:id'); + $exists->execute([':id' => $litterId]); + if (!$exists->fetchColumn()) { + http_response_code(404); + return; + } + $db->prepare('UPDATE litters SET mother_id=:mother,father_id=:father,birth_date=:birth WHERE id=:id')->execute([':mother' => $motherId, ':father' => $fatherId, ':birth' => $birthDate, ':id' => $litterId]); + header('Location: /animal?id=' . ($sourceAnimalId ?: ($motherId ?: ($fatherId ?: 0))) . '#tab-litters'); + exit(); + } + + public static function detachLitterKitten(): void + { + if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST') { + http_response_code(405); + return; + } + $litterId = (int) ($_POST['litter_id'] ?? 0); + $animalId = (int) ($_POST['animal_id'] ?? 0); + $sourceAnimalId = (int) ($_POST['source_animal_id'] ?? 0); + $stmt = DB::pdo()->prepare('DELETE FROM litter_kittens WHERE litter_id=:litter AND animal_id=:animal'); + $stmt->execute([':litter' => $litterId, ':animal' => $animalId]); + if ($stmt->rowCount() > 0) { + self::logHistory($animalId, 'update', 'Retiré d’une portée', 'Ancienne portée n°' . $litterId); + } + $remaining = DB::pdo()->prepare('SELECT COUNT(*) FROM litter_kittens WHERE litter_id=:id'); + $remaining->execute([':id' => $litterId]); + if ((int) $remaining->fetchColumn() === 0) { + DB::pdo()->prepare('DELETE FROM litters WHERE id=:id')->execute([':id' => $litterId]); + header('Location: /animal?id=' . ($sourceAnimalId ?: $animalId) . '#tab-litters'); + exit(); + } + header('Location: /litter/edit?id=' . $litterId . '&animal_id=' . $sourceAnimalId); + exit(); + } + public static function litterAddExistingForm(): void { $db = DB::pdo(); diff --git a/app/Routes/web.php b/app/Routes/web.php index 552b61c..9a12b0e 100644 --- a/app/Routes/web.php +++ b/app/Routes/web.php @@ -189,6 +189,15 @@ switch ($path) { case '/litter/save': AnimalsController::saveLitter(); break; + case '/litter/edit': + AnimalsController::litterEditForm(); + break; + case '/litter/update': + AnimalsController::updateLitter(); + break; + case '/litter/kitten/detach': + AnimalsController::detachLitterKitten(); + break; case '/litter/add-existing': AnimalsController::litterAddExistingForm(); break; diff --git a/app/Services/PermissionService.php b/app/Services/PermissionService.php index a7b8959..9de5203 100644 --- a/app/Services/PermissionService.php +++ b/app/Services/PermissionService.php @@ -211,6 +211,9 @@ final class PermissionService '/animal/death', '/animal/surgery/new', '/litter/new', + '/litter/edit', + '/litter/update', + '/litter/kitten/detach', '/litter/add-existing', '/litter/add-existing/save', '/bonded/new', diff --git a/app/Views/_animal_show_litters.php b/app/Views/_animal_show_litters.php index 82414cc..e611923 100644 --- a/app/Views/_animal_show_litters.php +++ b/app/Views/_animal_show_litters.php @@ -39,7 +39,8 @@ ' . h($litter['father_name']) . '' : h(t('animal.unknown_male')) ?> - ✏️ + + diff --git a/app/Views/evolutions.php b/app/Views/evolutions.php index a722d84..7883136 100644 --- a/app/Views/evolutions.php +++ b/app/Views/evolutions.php @@ -31,8 +31,8 @@
- 1.0.0-rc.24 -

Fiabilité des portées

+ 1.0.0-rc.25 +

Portées modifiables et fiables

La création d’une fratrie ne dépend plus d’un ancien traitement de photo et son historique reste compatible avec les bases existantes.

diff --git a/app/Views/layout.php b/app/Views/layout.php index 098b27e..5bf40e1 100644 --- a/app/Views/layout.php +++ b/app/Views/layout.php @@ -271,7 +271,7 @@ if ($pageDescription === '') { if (path.startsWith('/admin') || path.startsWith('/documents')) return 'administrative'; return null; }; - const permissionEditPages = new Set(['/animal/new', '/animal/edit', '/animal/death', '/litter/new', + const permissionEditPages = new Set(['/animal/new', '/animal/edit', '/animal/death', '/litter/new', '/litter/edit', '/litter/update', '/litter/kitten/detach', '/litter/add-existing', '/litter/add-existing/save', '/bonded/new', '/directory/new', '/directory/edit', '/care-round/setup' ]); diff --git a/app/Views/litter_edit.php b/app/Views/litter_edit.php new file mode 100644 index 0000000..d9e5dbb --- /dev/null +++ b/app/Views/litter_edit.php @@ -0,0 +1,13 @@ + +
+ +

+
+
+
+
+
+
+
+
+
diff --git a/app/Views/litter_form.php b/app/Views/litter_form.php index babc8e5..19bda54 100644 --- a/app/Views/litter_form.php +++ b/app/Views/litter_form.php @@ -8,6 +8,7 @@ $prefillCount = max(0, min(20, (int)($_GET['count'] ?? 4))); ?>
+
diff --git a/resources/lang/en.php b/resources/lang/en.php index f00c501..b947b62 100644 --- a/resources/lang/en.php +++ b/resources/lang/en.php @@ -204,4 +204,10 @@ return [ 'care.temperature'=>'Temperature', 'care.invalid_temperature'=>'Temperature must be between 30 and 45 °C.', 'chart.temperature'=>'Temperature (°C)', + 'litter.edit'=>'Edit litter', + 'litter.edit_help'=>'Correct the parents, birth date or linked kittens.', + 'litter.kittens'=>'Kittens in this litter', + 'litter.detach'=>'Detach', + 'litter.detach_confirm'=>'Remove this cat from the litter? Its record and data will be preserved.', + 'litter.parent_is_kitten'=>'A parent cannot also be a kitten in this litter.', ]; diff --git a/resources/lang/fr.php b/resources/lang/fr.php index 375c96c..12f4b89 100644 --- a/resources/lang/fr.php +++ b/resources/lang/fr.php @@ -204,4 +204,10 @@ return [ 'care.temperature'=>'Température', 'care.invalid_temperature'=>'La température doit être comprise entre 30 et 45 °C.', 'chart.temperature'=>'Température (°C)', + 'litter.edit'=>'Modifier la portée', + 'litter.edit_help'=>'Corriger les parents, la date ou les chatons rattachés.', + 'litter.kittens'=>'Chatons de la portée', + 'litter.detach'=>'Détacher', + 'litter.detach_confirm'=>'Retirer ce chat de la portée ? Sa fiche et ses données seront conservées.', + 'litter.parent_is_kitten'=>'Un parent ne peut pas être également chaton de cette portée.', ]; diff --git a/tests/run.php b/tests/run.php index db76a7b..55b5a7d 100644 --- a/tests/run.php +++ b/tests/run.php @@ -87,6 +87,17 @@ try { 'L’historique des portées ne renseigne pas son action', ); }); + $test('Une portée peut être corrigée sans supprimer ses animaux', function () use ($assert): void { + $routes = file_get_contents(__DIR__ . '/../app/Routes/web.php'); + $controller = file_get_contents(__DIR__ . '/../app/Controllers/AnimalRelationshipActions.php'); + $view = file_get_contents(__DIR__ . '/../app/Views/litter_edit.php'); + foreach (['/litter/edit', '/litter/update', '/litter/kitten/detach'] as $route) { + $assert(str_contains($routes, $route), 'Route de portée absente : ' . $route); + } + $assert(str_contains($controller, 'DELETE FROM litter_kittens'), 'Le détachement du chaton est absent'); + $assert(!str_contains($controller, 'DELETE FROM animals'), 'Le détachement supprimerait une fiche animale'); + $assert(str_contains($view, '/animal/edit?id='), 'La fiche du chaton n’est pas modifiable depuis la portée'); + }); $test('Les médias publics restent accessibles au serveur web', function () use ($assert): void { $security = file_get_contents(__DIR__ . '/../app/Services/SecurityService.php'); $images = file_get_contents(__DIR__ . '/../app/Services/ImageService.php'); From 361bef8561b90e74fc2c881f23e09944b20dff1d Mon Sep 17 00:00:00 2001 From: oversu <95a7002b-481b-4ef3-b578-f9c421550aa4@localhost> Date: Tue, 15 Sep 2026 21:11:21 +0200 Subject: [PATCH 02/11] fix: fiabiliser les fiches animales et vaccinations --- CHANGELOG.md | 12 ++++ HISTORIQUE.md | 5 ++ README.md | 2 +- VERSION | 2 +- app/Controllers/AnimalBrowseActions.php | 5 +- app/Controllers/AnimalMedicalActions.php | 20 +----- app/Controllers/AnimalRecordActions.php | 5 +- app/Services/Asm3MigrationPlanner.php | 2 +- app/Services/ExportService.php | 2 +- app/Services/ImageService.php | 2 +- app/Services/Migrations.php | 70 ++++++++++++++++++- app/Views/_animal_show_medical.php | 4 +- app/Views/_animal_show_modals.php | 26 ------- app/Views/animal_form.php | 9 ++- app/Views/animal_show.php | 4 +- app/Views/evolutions.php | 8 +-- .../097_remove_legacy_room_constraint.sql | 3 + resources/lang/en.php | 5 ++ resources/lang/fr.php | 5 ++ tests/run.php | 31 ++++++++ 20 files changed, 157 insertions(+), 65 deletions(-) create mode 100644 migrations/097_remove_legacy_room_constraint.sql diff --git a/CHANGELOG.md b/CHANGELOG.md index 839e322..20fb4c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,18 @@ 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.26] — 2026-09-15 + +### Données animales et suivi vétérinaire + +- suppression sûre de l’ancienne contrainte SQLite limitant les salles à « silver » et « twix » ; +- reconstruction transactionnelle de la table des animaux avec conservation de toutes les fiches, relations, vues et index ; +- ajout d’un test de sauvegarde réelle contrôlant le nombre d’animaux, l’intégrité SQLite et les clés étrangères ; +- suppression des avertissements `open_basedir` pendant la détection de l’outil d’optimisation des images. +- remplacement des anciens états I-CAD par « Inconnu », « Enregistré », « En attente de changement » et « Propriétaire changé » ; +- retrait du champ redondant « Administré par » lors de la saisie d’un vaccin ; +- affichage du cabinet vétérinaire dans le récapitulatif des vaccinations et son export. + ## [1.0.0-rc.25] — 2026-09-15 ### Modification des portées diff --git a/HISTORIQUE.md b/HISTORIQUE.md index 9cb5fa6..8d1f156 100644 --- a/HISTORIQUE.md +++ b/HISTORIQUE.md @@ -10,6 +10,11 @@ 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.26` — Salles réellement configurables + +Retrait de la dernière contrainte héritée qui limitait encore les fiches animales aux deux premières salles historiques. La migration conserve les animaux et toutes leurs relations tout en autorisant les salles définies dans la configuration. +Les états d’enregistrement I-CAD suivent désormais le changement de propriétaire, tandis que le suivi vaccinal s’appuie sans doublon sur le cabinet et le vétérinaire de l’annuaire. + ## `1.0.0-rc.25` — Portées modifiables Les portées ne sont plus figées après leur création : parents et date peuvent être corrigés, tandis que les chatons peuvent être ajoutés, modifiés ou détachés sans perte de leur fiche. diff --git a/README.md b/README.md index 0da8de5..4aedc46 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.25`. 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.26`. 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 07923b2..f7b1136 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.0.0-rc.25 +1.0.0-rc.26 diff --git a/app/Controllers/AnimalBrowseActions.php b/app/Controllers/AnimalBrowseActions.php index 845e5f9..bc14a65 100644 --- a/app/Controllers/AnimalBrowseActions.php +++ b/app/Controllers/AnimalBrowseActions.php @@ -612,12 +612,11 @@ trait AnimalBrowseActions // Vaccins $stmt = $db->prepare(' - SELECT v.*, rv.name AS vaccine_name, COALESCE(dc.name, c.name) AS clinic_name,COALESCE(v.administered_by_name,au.display_name,au.username) AS administrator_name + SELECT v.*, rv.name AS vaccine_name, COALESCE(dc.name, c.name) AS clinic_name FROM vaccinations v JOIN ref_vaccines rv ON rv.id = v.vaccine_id LEFT JOIN clinics c ON c.id = v.clinic_id LEFT JOIN directory_contacts dc ON dc.id = v.clinic_contact_id - LEFT JOIN users au ON au.id=v.administered_by_user_id WHERE v.animal_id = :id ORDER BY v.done_date DESC, v.id DESC LIMIT 50 @@ -833,7 +832,7 @@ trait AnimalBrowseActions CASE WHEN COALESCE(v.lot,'')!='' THEN '\nLot : ' || v.lot ELSE '' END || CASE WHEN COALESCE(v.manufacturer,'')!='' THEN '\nFabricant : ' || v.manufacturer ELSE '' END || CASE WHEN COALESCE(v.batch_expires_on,'')!='' THEN '\nExpiration du lot : ' || v.batch_expires_on ELSE '' END || - CASE WHEN COALESCE(v.administered_by_name,'')!='' THEN '\nAdministré par : ' || v.administered_by_name WHEN v.administered_by_user_id IS NOT NULL THEN '\nAdministré par : ' || COALESCE((SELECT display_name FROM users WHERE id=v.administered_by_user_id),(SELECT username FROM users WHERE id=v.administered_by_user_id),'Compte inconnu') ELSE '' END || + CASE WHEN v.clinic_contact_id IS NOT NULL THEN '\nCabinet vétérinaire : ' || COALESCE((SELECT name FROM directory_contacts WHERE id=v.clinic_contact_id),'Cabinet inconnu') WHEN v.clinic_id IS NOT NULL THEN '\nCabinet vétérinaire : ' || COALESCE((SELECT name FROM clinics WHERE id=v.clinic_id),'Cabinet inconnu') ELSE '' END || CASE WHEN COALESCE(v.notes,'')!='' THEN '\nNotes : ' || v.notes ELSE '' END ) AS details, datetime(v.done_date || ' 12:00:00') AS at diff --git a/app/Controllers/AnimalMedicalActions.php b/app/Controllers/AnimalMedicalActions.php index 3cd6172..3f8a04e 100644 --- a/app/Controllers/AnimalMedicalActions.php +++ b/app/Controllers/AnimalMedicalActions.php @@ -630,15 +630,6 @@ trait AnimalMedicalActions echo h(t('error.invalid_batch_expiry')); return; } - $administratorUserId = (int) ($_POST['administered_by_user_id'] ?? 0) ?: null; - $administratorName = trim((string) ($_POST['administered_by_name'] ?? '')); - if ($administratorUserId) { - $check = $db->prepare('SELECT 1 FROM users WHERE id=:id AND active=1'); - $check->execute([':id' => $administratorUserId]); - if (!$check->fetchColumn()) { - $administratorUserId = null; - } - } $notes = trim((string) ($_POST['notes'] ?? '')); $clinic_id = (int) ($_POST['clinic_id'] ?? 0); $clinic_id = $clinic_id > 0 ? $clinic_id : null; @@ -668,8 +659,8 @@ 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,administered_by_user_id,administered_by_name, notes, clinic_id, vet_name, clinic_contact_id, vet_contact_id, created_by) - VALUES(:aid,:vid,:done,:due,:lot,:manufacturer,:batch_expiry,:administrator_user,:administrator_name,:notes,:clinic_id,:vet_name,:clinic_contact_id,:vet_contact_id,:created_by) + 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([ ':aid' => $id, @@ -679,8 +670,6 @@ trait AnimalMedicalActions ':lot' => $lot !== '' ? $lot : null, ':manufacturer' => $manufacturer ?: null, ':batch_expiry' => $batchExpiresOn ?: null, - ':administrator_user' => $administratorUserId, - ':administrator_name' => $administratorName ?: null, ':notes' => $notes !== '' ? $notes : null, ':clinic_id' => $clinic_id, ':vet_name' => $vet_name, @@ -707,10 +696,7 @@ trait AnimalMedicalActions ($manufacturer !== '' ? "\nFabricant : $manufacturer" : '') . ($lot !== '' ? "\nLot : $lot" : '') . ($batchExpiresOn !== '' ? "\nExpiration du lot : $batchExpiresOn" : '') . - ($administratorName !== '' || $administratorUserId - ? "\nAdministré par : " . - ($administratorName !== '' ? $administratorName : 'utilisateur #' . $administratorUserId) - : ''), + "\nCabinet vétérinaire : " . ($clinicContact['name'] ?? ''), ); $db->prepare("UPDATE animals SET updated_at = datetime('now') WHERE id = :id")->execute([':id' => $id]); diff --git a/app/Controllers/AnimalRecordActions.php b/app/Controllers/AnimalRecordActions.php index 9935cb6..462fb8b 100644 --- a/app/Controllers/AnimalRecordActions.php +++ b/app/Controllers/AnimalRecordActions.php @@ -376,7 +376,10 @@ trait AnimalRecordActions } $identificationDate = trim((string) ($_POST['identification_date'] ?? '')); $identificationRegistration = (string) ($_POST['identification_registration_status'] ?? 'unknown'); - if (!in_array($identificationRegistration, ['unknown', 'pending', 'registered'], true)) { + if ($identificationRegistration === 'pending') { + $identificationRegistration = 'change_pending'; + } + if (!in_array($identificationRegistration, ['unknown', 'registered', 'change_pending', 'owner_changed'], true)) { $identificationRegistration = 'unknown'; } if ($chip_id === '') { diff --git a/app/Services/Asm3MigrationPlanner.php b/app/Services/Asm3MigrationPlanner.php index 90edbc2..b3233f4 100644 --- a/app/Services/Asm3MigrationPlanner.php +++ b/app/Services/Asm3MigrationPlanner.php @@ -152,7 +152,7 @@ final class Asm3MigrationPlanner 'identification_date' => $date($chip !== '' ? $r['IDENTICHIPDATE'] ?? null : $r['TATTOODATE'] ?? null), 'identification_registration_status' => match ((string) ($r['IDENTICHIPSTATUS'] ?? '')) { '1' => 'registered', - '0' => 'pending', + '0' => 'change_pending', default => 'unknown', }, 'sterilization_status' => $yes($r['NEUTERED'] ?? 0) ? 'yes' : 'unknown', diff --git a/app/Services/ExportService.php b/app/Services/ExportService.php index df454b8..1ad5d5b 100644 --- a/app/Services/ExportService.php +++ b/app/Services/ExportService.php @@ -38,7 +38,7 @@ final class ExportService 'vaccinations' => [ 'Vaccinations', 'vaccinations.csv', - 'SELECT v.id,a.internal_code code_animal,a.name animal,rv.name vaccin,v.done_date date_realisation,v.due_date prochain_rappel,v.manufacturer fabricant,v.lot,v.batch_expires_on expiration_lot,COALESCE(v.administered_by_name,au.display_name,au.username) administre_par,COALESCE(cu.display_name,cu.username) saisi_par,vet.name veterinaire,clinic.name cabinet,v.notes FROM vaccinations v JOIN animals a ON a.id=v.animal_id JOIN ref_vaccines rv ON rv.id=v.vaccine_id LEFT JOIN directory_contacts vet ON vet.id=v.vet_contact_id LEFT JOIN directory_contacts clinic ON clinic.id=v.clinic_contact_id LEFT JOIN users au ON au.id=v.administered_by_user_id LEFT JOIN users cu ON cu.id=v.created_by ORDER BY v.done_date,v.id', + 'SELECT v.id,a.internal_code code_animal,a.name animal,rv.name vaccin,v.done_date date_realisation,v.due_date prochain_rappel,v.manufacturer fabricant,v.lot,v.batch_expires_on expiration_lot,clinic.name administre_par,COALESCE(cu.display_name,cu.username) saisi_par,vet.name veterinaire,clinic.name cabinet,v.notes FROM vaccinations v JOIN animals a ON a.id=v.animal_id JOIN ref_vaccines rv ON rv.id=v.vaccine_id LEFT JOIN directory_contacts vet ON vet.id=v.vet_contact_id LEFT JOIN directory_contacts clinic ON clinic.id=v.clinic_contact_id LEFT JOIN users cu ON cu.id=v.created_by ORDER BY v.done_date,v.id', ], 'deaths' => [ 'Décès', diff --git a/app/Services/ImageService.php b/app/Services/ImageService.php index d214ef3..89682d6 100644 --- a/app/Services/ImageService.php +++ b/app/Services/ImageService.php @@ -131,7 +131,7 @@ final class ImageService ['/usr/bin/magick', '/usr/local/bin/magick', '/usr/bin/convert', '/usr/local/bin/convert'] as $binary ) { - if (is_executable($binary)) { + if (@is_executable($binary)) { return $binary; } } diff --git a/app/Services/Migrations.php b/app/Services/Migrations.php index 1476068..860a0c6 100644 --- a/app/Services/Migrations.php +++ b/app/Services/Migrations.php @@ -91,13 +91,21 @@ final class Migrations } } - $relaxesForeignKeys = $name === '093_room_rules_and_care_periods.sql'; + $relaxesForeignKeys = in_array( + $name, + ['093_room_rules_and_care_periods.sql', '097_remove_legacy_room_constraint.sql'], + true, + ); if ($relaxesForeignKeys) { $db->exec('PRAGMA foreign_keys = OFF'); } $db->beginTransaction(); try { - $db->exec($sql); + if ($name === '097_remove_legacy_room_constraint.sql') { + self::modernizeAnimalConstraints($db); + } else { + $db->exec($sql); + } $ins = $db->prepare("INSERT INTO _migrations(name, applied_at) VALUES(:n, datetime('now'))"); $ins->execute([':n' => $name]); if ($relaxesForeignKeys && $db->query('PRAGMA foreign_key_check')->fetchColumn() !== false) { @@ -116,4 +124,62 @@ final class Migrations } } } + + private static function modernizeAnimalConstraints(PDO $db): void + { + $createSql = (string) $db + ->query("SELECT sql FROM sqlite_master WHERE type='table' AND name='animals'") + ->fetchColumn(); + $constraint = "/refuge_room\\s+TEXT\\s+CHECK\\s*\\(\\s*refuge_room\\s+IN\\s*\\(\\s*'silver'\\s*,\\s*'twix'\\s*\\)\\s+OR\\s+refuge_room\\s+IS\\s+NULL\\s*\\)/i"; + $registrationConstraint = "/identification_registration_status\\s+TEXT\\s+NOT\\s+NULL\\s+DEFAULT\\s+'unknown'\\s+CHECK\\s*\\(\\s*identification_registration_status\\s+IN\\s*\\(\\s*'unknown'\\s*,\\s*'pending'\\s*,\\s*'registered'\\s*\\)\\s*\\)/i"; + if ( + $createSql === '' + || (!preg_match($constraint, $createSql) && !preg_match($registrationConstraint, $createSql)) + ) { + return; + } + + $newSql = preg_replace( + '/^CREATE TABLE\\s+(?:IF NOT EXISTS\\s+)?["`\[]?animals["`\]]?/i', + 'CREATE TABLE animals_room_unrestricted', + $createSql, + 1, + ); + $newSql = preg_replace($constraint, 'refuge_room TEXT', (string) $newSql, 1); + $newSql = preg_replace( + $registrationConstraint, + "identification_registration_status TEXT NOT NULL DEFAULT 'unknown' " . + "CHECK(identification_registration_status IN ('unknown','registered','change_pending','owner_changed','pending'))", + (string) $newSql, + 1, + ); + if (!$newSql || preg_match($constraint, $newSql)) { + throw new RuntimeException('Unable to remove the legacy shelter-room constraint.'); + } + + $objects = $db + ->query("SELECT sql FROM sqlite_master WHERE tbl_name='animals' AND type IN ('index','trigger') AND sql IS NOT NULL") + ->fetchAll(PDO::FETCH_COLUMN); + $views = $db + ->query("SELECT name,sql FROM sqlite_master WHERE type='view' AND sql IS NOT NULL ORDER BY rowid") + ->fetchAll(PDO::FETCH_ASSOC); + $columns = array_column($db->query('PRAGMA table_info(animals)')->fetchAll(PDO::FETCH_ASSOC), 'name'); + $quotedColumns = implode(',', array_map(static fn(string $column): string => '"' . str_replace('"', '""', $column) . '"', $columns)); + + foreach ($views as $view) { + $viewName = '"' . str_replace('"', '""', (string) $view['name']) . '"'; + $db->exec('DROP VIEW ' . $viewName); + } + $db->exec($newSql); + $db->exec("INSERT INTO animals_room_unrestricted ($quotedColumns) SELECT $quotedColumns FROM animals"); + $db->exec("UPDATE animals_room_unrestricted SET identification_registration_status='change_pending' WHERE identification_registration_status='pending'"); + $db->exec('DROP TABLE animals'); + $db->exec('ALTER TABLE animals_room_unrestricted RENAME TO animals'); + foreach ($objects as $objectSql) { + $db->exec((string) $objectSql); + } + foreach ($views as $view) { + $db->exec((string) $view['sql']); + } + } } diff --git a/app/Views/_animal_show_medical.php b/app/Views/_animal_show_medical.php index 75a4211..31b8b95 100644 --- a/app/Views/_animal_show_medical.php +++ b/app/Views/_animal_show_medical.php @@ -410,7 +410,7 @@ ORDER BY created_at ASC - + @@ -424,7 +424,7 @@ ORDER BY created_at ASC
date('d/m/Y', strtotime((string)$v['batch_expires_on']))])) ?>
- + diff --git a/app/Views/_animal_show_modals.php b/app/Views/_animal_show_modals.php index 972364f..82c27a8 100644 --- a/app/Views/_animal_show_modals.php +++ b/app/Views/_animal_show_modals.php @@ -312,38 +312,12 @@ $currentTime = date('H:i');
-
- - -
- -
- - -
-
-
diff --git a/app/Views/animal_show.php b/app/Views/animal_show.php index 05c5b34..fc6b553 100644 --- a/app/Views/animal_show.php +++ b/app/Views/animal_show.php @@ -296,9 +296,9 @@ if ($felvPos) { [t('animal.availability_define'),'bg-secondary-lt'],'not_available' => [t('animals.unavailable_title'),'bg-orange-lt'],'available' => [t('animals.available_title'),'bg-green-lt']]; [$availabilityLabel,$availabilityClass] = $availabilityMeta[$availability] ?? $availabilityMeta['unknown']; -$registrationLabels = ['unknown' => t('animal.registration_unknown'),'pending' => t('animal.registration_pending'),'registered' => t('animal.registration_done')]; +$registrationLabels = ['unknown' => t('animal.registration_unknown'),'pending' => t('animal.registration_change_pending'),'change_pending' => t('animal.registration_change_pending'),'registered' => t('animal.registration_done'),'owner_changed' => t('animal.registration_owner_changed')]; $registration = $animal['identification_registration_status'] ?? 'unknown'; -$registrationClass = $registration === 'registered' ? 'bg-green-lt' : ($registration === 'pending' ? 'bg-yellow-lt' : 'bg-secondary-lt'); ?> +$registrationClass = in_array($registration, ['registered', 'owner_changed'], true) ? 'bg-green-lt' : (in_array($registration, ['pending', 'change_pending'], true) ? 'bg-yellow-lt' : 'bg-secondary-lt'); ?>
- 1.0.0-rc.25 -

Portées modifiables et fiables

-

La création d’une fratrie ne dépend plus d’un ancien traitement de photo et - son historique reste compatible avec les bases existantes.

+ 1.0.0-rc.26 +

Données animales fiabilisées

+

Les salles configurables, les états I-CAD et le suivi vaccinal restent + cohérents sur les installations neuves comme sur les bases historiques.

diff --git a/migrations/097_remove_legacy_room_constraint.sql b/migrations/097_remove_legacy_room_constraint.sql new file mode 100644 index 0000000..0291a07 --- /dev/null +++ b/migrations/097_remove_legacy_room_constraint.sql @@ -0,0 +1,3 @@ +-- La reconstruction conditionnelle est réalisée par Migrations::run afin de +-- préserver toutes les colonnes des bases historiques. +SELECT 1; diff --git a/resources/lang/en.php b/resources/lang/en.php index b947b62..76d8dfd 100644 --- a/resources/lang/en.php +++ b/resources/lang/en.php @@ -210,4 +210,9 @@ return [ 'litter.detach'=>'Detach', 'litter.detach_confirm'=>'Remove this cat from the litter? Its record and data will be preserved.', 'litter.parent_is_kitten'=>'A parent cannot also be a kitten in this litter.', + 'animal.form.change_pending'=>'Ownership change pending', + 'animal.form.owner_changed'=>'Owner changed', + 'animal.registration_change_pending'=>'I-CAD: ownership change pending', + 'animal.registration_owner_changed'=>'I-CAD: owner changed', + 'animal.veterinary_clinic'=>'Veterinary clinic', ]; diff --git a/resources/lang/fr.php b/resources/lang/fr.php index 12f4b89..75b6400 100644 --- a/resources/lang/fr.php +++ b/resources/lang/fr.php @@ -210,4 +210,9 @@ return [ 'litter.detach'=>'Détacher', 'litter.detach_confirm'=>'Retirer ce chat de la portée ? Sa fiche et ses données seront conservées.', 'litter.parent_is_kitten'=>'Un parent ne peut pas être également chaton de cette portée.', + 'animal.form.change_pending'=>'En attente de changement', + 'animal.form.owner_changed'=>'Propriétaire changé', + 'animal.registration_change_pending'=>'I-CAD : en attente de changement', + 'animal.registration_owner_changed'=>'I-CAD : propriétaire changé', + 'animal.veterinary_clinic'=>'Clinique vétérinaire', ]; diff --git a/tests/run.php b/tests/run.php index 55b5a7d..8b7051d 100644 --- a/tests/run.php +++ b/tests/run.php @@ -167,6 +167,37 @@ try { $assert(!ShelterRoomService::accepts($room, ['species' => 'chat', 'sex' => 'F', 'fiv' => 1])); $assert(!ShelterRoomService::accepts($room, ['species' => 'chat', 'sex' => 'M', 'fiv' => 0])); }); + $test('Une salle configurable peut être enregistrée sur un animal', function () use ($db, $assert): void { + $schema = (string) $db->query("SELECT sql FROM sqlite_master WHERE type='table' AND name='animals'")->fetchColumn(); + $assert(!str_contains($schema, "refuge_room IN ('silver', 'twix')"), 'Ancienne contrainte de salle encore active'); + $code = 'TEST-ROOM-' . bin2hex(random_bytes(3)); + $db->prepare("INSERT INTO animals(name,internal_code,refuge_room) VALUES('Test salle',:code,'nouvelle-salle')")->execute([':code' => $code]); + $id = (int) $db->lastInsertId(); + $assert((string) $db->query('SELECT refuge_room FROM animals WHERE id=' . $id)->fetchColumn() === 'nouvelle-salle'); + $db->prepare('DELETE FROM animals WHERE id=:id')->execute([':id' => $id]); + }); + $test('Les quatre états I-CAD sont acceptés', function () use ($db, $assert): void { + $code = 'TEST-ICAD-' . bin2hex(random_bytes(3)); + $db->prepare("INSERT INTO animals(name,internal_code,identification_registration_status) VALUES('Test I-CAD',:code,'unknown')") + ->execute([':code' => $code]); + $id = (int) $db->lastInsertId(); + foreach (['unknown', 'registered', 'change_pending', 'owner_changed'] as $status) { + $db->prepare('UPDATE animals SET identification_registration_status=:status WHERE id=:id') + ->execute([':status' => $status, ':id' => $id]); + $assert( + (string) $db->query('SELECT identification_registration_status FROM animals WHERE id=' . $id)->fetchColumn() === $status, + 'État I-CAD refusé : ' . $status, + ); + } + $db->prepare('DELETE FROM animals WHERE id=:id')->execute([':id' => $id]); + }); + $test('La vaccination repose sur le cabinet vétérinaire', function () use ($assert): void { + $modal = (string) file_get_contents(dirname(__DIR__) . '/app/Views/_animal_show_modals.php'); + $summary = (string) file_get_contents(dirname(__DIR__) . '/app/Views/_animal_show_medical.php'); + $assert(!str_contains($modal, 'name="administered_by_user_id"')); + $assert(!str_contains($modal, 'name="administered_by_name"')); + $assert(str_contains($summary, "\$v['clinic_name']")); + }); $test('Les paramètres sont enregistrés et relus', function () use ($assert): void { AppSettings::save(['association_name' => 'Refuge Test', 'app_language' => 'en'], null); $assert(AppSettings::get('association_name') === 'Refuge Test'); From a932db0ace64113067958dc042e547acc1866dc3 Mon Sep 17 00:00:00 2001 From: oversu <95a7002b-481b-4ef3-b578-f9c421550aa4@localhost> Date: Tue, 15 Sep 2026 21:50:34 +0200 Subject: [PATCH 03/11] =?UTF-8?q?fix:=20=C3=A9tendre=20la=20recherche=20au?= =?UTF-8?q?x=20identifications?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 9 +++++++++ HISTORIQUE.md | 4 ++++ README.md | 2 +- VERSION | 2 +- app/Services/DB.php | 11 +++++++++++ app/Services/GlobalSearchService.php | 24 ++++++++++++++++++------ app/Views/evolutions.php | 8 ++++---- tests/run.php | 18 ++++++++++++++++++ 8 files changed, 66 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 20fb4c5..5dc5dc9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ 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.27] — 2026-09-15 + +### Recherche globale des animaux + +- ajout des numéros de puce et de tatouage actuels à la recherche, avec maintien du champ historique ; +- recherche insensible aux majuscules, minuscules et accents dans tous les groupes de résultats ; +- affichage du numéro d’identification dans le résultat animal correspondant ; +- ajout de tests couvrant notamment `Bréhat`, `Brehat`, `BREHAT`, `bReHaT` et un numéro de puce. + ## [1.0.0-rc.26] — 2026-09-15 ### Données animales et suivi vétérinaire diff --git a/HISTORIQUE.md b/HISTORIQUE.md index 8d1f156..f228dfa 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.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. + ## `1.0.0-rc.26` — Salles réellement configurables Retrait de la dernière contrainte héritée qui limitait encore les fiches animales aux deux premières salles historiques. La migration conserve les animaux et toutes leurs relations tout en autorisant les salles définies dans la configuration. diff --git a/README.md b/README.md index 4aedc46..7b6120d 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.26`. 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.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`. 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 f7b1136..d2e3cec 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.0.0-rc.26 +1.0.0-rc.27 diff --git a/app/Services/DB.php b/app/Services/DB.php index 9995146..65c648a 100644 --- a/app/Services/DB.php +++ b/app/Services/DB.php @@ -29,6 +29,17 @@ final class DB $pdo->exec('PRAGMA busy_timeout = 3000;'); @chmod($dbPath, 0600); $pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC); + $pdo->sqliteCreateFunction( + 'search_fold', + static function (mixed $value): string { + $text = mb_strtolower(trim((string) $value), 'UTF-8'); + $text = strtr($text, ['œ' => 'oe', 'æ' => 'ae', 'ø' => 'o', 'ß' => 'ss']); + $ascii = @iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $text); + return $ascii === false ? $text : strtolower($ascii); + }, + 1, + PDO::SQLITE_DETERMINISTIC, + ); self::$pdo = $pdo; return $pdo; diff --git a/app/Services/GlobalSearchService.php b/app/Services/GlobalSearchService.php index 84a847a..5230094 100644 --- a/app/Services/GlobalSearchService.php +++ b/app/Services/GlobalSearchService.php @@ -11,17 +11,21 @@ final class GlobalSearchService return []; } $db = DB::pdo(); - $like = '%' . str_replace(['\\', '%', '_'], ['\\\\', '\\%', '\\_'], $query) . '%'; + $foldedQuery = self::fold($query); + $like = '%' . str_replace(['\\', '%', '_'], ['\\\\', '\\%', '\\_'], $foldedQuery) . '%'; $out = []; if (PermissionService::can('animals')) { $out['animals'] = self::rows( $db, - "SELECT id,name,internal_code,status,species FROM animals WHERE deleted_at IS NULL AND (name LIKE :q ESCAPE '\\' OR internal_code LIKE :q ESCAPE '\\' OR COALESCE(chip,'') LIKE :q ESCAPE '\\') ORDER BY name COLLATE NOCASE LIMIT :lim", + "SELECT id,name,internal_code,status,species,COALESCE(NULLIF(chip_id,''),NULLIF(chip,'')) identification_number FROM animals WHERE deleted_at IS NULL AND (search_fold(name) LIKE :q ESCAPE '\\' OR search_fold(internal_code) LIKE :q ESCAPE '\\' OR search_fold(COALESCE(chip_id,'')) LIKE :q ESCAPE '\\' OR search_fold(COALESCE(chip,'')) LIKE :q ESCAPE '\\') ORDER BY name COLLATE NOCASE LIMIT :lim", $like, $limit, static fn($r) => [ 'title' => $r['name'], - 'detail' => implode(' · ', array_filter([$r['internal_code'], $r['species'], $r['status']])), + 'detail' => implode( + ' · ', + array_filter([$r['internal_code'], $r['identification_number'], $r['species'], $r['status']]), + ), 'url' => '/animal?id=' . (int) $r['id'], ], ); @@ -29,7 +33,7 @@ final class GlobalSearchService if (PermissionService::can('directory')) { $out['directory'] = self::rows( $db, - "SELECT id,name,kind,phone,email,city FROM directory_contacts WHERE deleted_at IS NULL AND (name LIKE :q ESCAPE '\\' OR COALESCE(phone,'') LIKE :q ESCAPE '\\' OR COALESCE(email,'') LIKE :q ESCAPE '\\' OR COALESCE(city,'') LIKE :q ESCAPE '\\') ORDER BY name COLLATE NOCASE LIMIT :lim", + "SELECT id,name,kind,phone,email,city FROM directory_contacts WHERE deleted_at IS NULL AND (search_fold(name) LIKE :q ESCAPE '\\' OR search_fold(COALESCE(phone,'')) LIKE :q ESCAPE '\\' OR search_fold(COALESCE(email,'')) LIKE :q ESCAPE '\\' OR search_fold(COALESCE(city,'')) LIKE :q ESCAPE '\\') ORDER BY name COLLATE NOCASE LIMIT :lim", $like, $limit, static fn($r) => [ @@ -42,7 +46,7 @@ final class GlobalSearchService if (PermissionService::can('agenda')) { $out['agenda'] = self::rows( $db, - "SELECT id,title,starts_at,location,status FROM agenda_items WHERE title LIKE :q ESCAPE '\\' OR COALESCE(description,'') LIKE :q ESCAPE '\\' OR COALESCE(location,'') LIKE :q ESCAPE '\\' ORDER BY starts_at DESC LIMIT :lim", + "SELECT id,title,starts_at,location,status FROM agenda_items WHERE search_fold(title) LIKE :q ESCAPE '\\' OR search_fold(COALESCE(description,'')) LIKE :q ESCAPE '\\' OR search_fold(COALESCE(location,'')) LIKE :q ESCAPE '\\' ORDER BY starts_at DESC LIMIT :lim", $like, $limit, static fn($r) => [ @@ -58,7 +62,7 @@ final class GlobalSearchService if (PermissionService::can('accounting')) { $out['accounting'] = self::rows( $db, - "SELECT ai.id,ai.reference,ai.invoice_date,ai.status,dc.name vendor FROM accounting_invoices ai LEFT JOIN directory_contacts dc ON dc.id=ai.vendor_contact_id WHERE ai.deleted_at IS NULL AND (ai.reference LIKE :q ESCAPE '\\' OR COALESCE(ai.notes,'') LIKE :q ESCAPE '\\' OR COALESCE(dc.name,'') LIKE :q ESCAPE '\\') ORDER BY ai.invoice_date DESC LIMIT :lim", + "SELECT ai.id,ai.reference,ai.invoice_date,ai.status,dc.name vendor FROM accounting_invoices ai LEFT JOIN directory_contacts dc ON dc.id=ai.vendor_contact_id WHERE ai.deleted_at IS NULL AND (search_fold(ai.reference) LIKE :q ESCAPE '\\' OR search_fold(COALESCE(ai.notes,'')) LIKE :q ESCAPE '\\' OR search_fold(COALESCE(dc.name,'')) LIKE :q ESCAPE '\\') ORDER BY ai.invoice_date DESC LIMIT :lim", $like, $limit, static fn($r) => [ @@ -71,6 +75,14 @@ final class GlobalSearchService return array_filter($out); } + private static function fold(string $value): string + { + $value = mb_strtolower(trim($value), 'UTF-8'); + $value = strtr($value, ['œ' => 'oe', 'æ' => 'ae', 'ø' => 'o', 'ß' => 'ss']); + $ascii = @iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $value); + return $ascii === false ? $value : strtolower($ascii); + } + private static function rows(PDO $db, string $sql, string $like, int $limit, callable $map): array { $s = $db->prepare($sql); diff --git a/app/Views/evolutions.php b/app/Views/evolutions.php index b268467..e9e2c8c 100644 --- a/app/Views/evolutions.php +++ b/app/Views/evolutions.php @@ -31,10 +31,10 @@
- 1.0.0-rc.26 -

Données animales fiabilisées

-

Les salles configurables, les états I-CAD et le suivi vaccinal restent - cohérents sur les installations neuves comme sur les bases historiques.

+ 1.0.0-rc.27 +

Recherche tolérante

+

Les fiches sont retrouvées par nom, code ou identification sans que les + accents ni les différences de casse empêchent le résultat.

diff --git a/tests/run.php b/tests/run.php index 8b7051d..93af42e 100644 --- a/tests/run.php +++ b/tests/run.php @@ -481,6 +481,24 @@ try { $agenda = GlobalSearchService::search('Mission bénévole'); $assert(!empty($agenda['agenda']), 'Événement absent de la recherche'); }); + $test('La recherche ignore la casse et les accents et retrouve les identifiants', function () use ($db, $assert): void { + $code = 'TEST-SEARCH-' . bin2hex(random_bytes(3)); + $chip = '250269610987654'; + $db->prepare("INSERT INTO animals(name,internal_code,chip_id,identification_type) VALUES('Bréhat',:code,:chip,'microchip')") + ->execute([':code' => $code, ':chip' => $chip]); + $id = (int) $db->lastInsertId(); + foreach (['Bréhat', 'Brehat', 'BREHAT', 'bReHaT', $chip] as $query) { + $results = GlobalSearchService::search($query); + $ids = array_map( + static fn(array $row): int => (int) filter_var($row['url'], FILTER_SANITIZE_NUMBER_INT), + $results['animals'] ?? [], + ); + $assert(in_array($id, $ids, true), 'Recherche infructueuse : ' . $query); + } + $result = GlobalSearchService::search($chip)['animals'][0] ?? []; + $assert(str_contains((string) ($result['detail'] ?? ''), $chip), 'Numéro absent du résultat'); + $db->prepare('DELETE FROM animals WHERE id=:id')->execute([':id' => $id]); + }); $test('Une sauvegarde ZIP contient une base et un manifeste', function () use (&$createdBackup, $assert): void { if (!class_exists('ZipArchive')) { return; 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 04/11] 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="">