From 2029849f18278abd9994ef7b82af10451de93c3e Mon Sep 17 00:00:00 2001 From: oversu <95a7002b-481b-4ef3-b578-f9c421550aa4@localhost> Date: Tue, 15 Sep 2026 18:27:40 +0200 Subject: [PATCH 01/13] =?UTF-8?q?fix:=20r=C3=A9tablir=20la=20cr=C3=A9ation?= =?UTF-8?q?=20des=20port=C3=A9es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 8 ++++++++ HISTORIQUE.md | 4 ++++ README.md | 2 +- VERSION | 2 +- app/Controllers/AnimalRelationshipActions.php | 7 ------- app/Controllers/AnimalsController.php | 4 ++-- app/Controllers/CareRoundsController.php | 2 +- app/Controllers/DeathController.php | 2 +- app/Controllers/DewormingController.php | 2 +- app/Controllers/MedicalDocumentsController.php | 2 +- app/Controllers/PlacementsController.php | 2 +- app/Controllers/SurgeriesController.php | 2 +- app/Views/evolutions.php | 8 ++++---- tests/run.php | 9 +++++++++ 14 files changed, 35 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 85b35b6..6c4bbcb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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.23] — 2026-09-15 + +### Création des portées + +- retrait d’un bloc de téléversement résiduel qui interrompait systématiquement l’enregistrement d’une portée ; +- correction du message trompeur « Le téléversement a échoué » rencontré sans aucun fichier envoyé ; +- compatibilité des événements de portée, de soins, de décès, de chirurgie et de placement avec l’historique des bases existantes. + ## [1.0.0-rc.22] — 2026-09-15 ### Disponibilité après adoption diff --git a/HISTORIQUE.md b/HISTORIQUE.md index 16a2e1b..e08f0e6 100644 --- a/HISTORIQUE.md +++ b/HISTORIQUE.md @@ -97,3 +97,7 @@ Ajout d’une température préremplie à 38,5 °C lors du contrôle de chaque a ## `1.0.0-rc.22` — Disponibilité après adoption Une adoption finalisée rend désormais automatiquement l’animal indisponible à l’adoption afin que son état administratif et sa visibilité restent cohérents. + +## `1.0.0-rc.23` — Fiabilité des portées + +Suppression d’un ancien bloc de téléversement qui bloquait la création des fratries et harmonisation de leur écriture dans l’historique des animaux. diff --git a/README.md b/README.md index fd19de9..6840add 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.22`. 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.23`. 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 49eb7f4..96b536f 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.0.0-rc.22 +1.0.0-rc.23 diff --git a/app/Controllers/AnimalRelationshipActions.php b/app/Controllers/AnimalRelationshipActions.php index e799555..f3eb7c9 100644 --- a/app/Controllers/AnimalRelationshipActions.php +++ b/app/Controllers/AnimalRelationshipActions.php @@ -139,13 +139,6 @@ trait AnimalRelationshipActions $baseName = trim((string) ($parents[$motherId]['name'] ?? ($parents[$fatherId]['name'] ?? 'Portée'))); $allowedSexes = ['F', 'M', 'U']; - try { - PrivateMediaService::moveVisibility($animalId, (string) $photo['filename'], $isPublic === 1); - } catch (Throwable $e) { - http_response_code(500); - echo h(t('error.upload_failed')); - return; - } $db->beginTransaction(); try { $stmt = $db->prepare( diff --git a/app/Controllers/AnimalsController.php b/app/Controllers/AnimalsController.php index 58e5757..209c6dd 100644 --- a/app/Controllers/AnimalsController.php +++ b/app/Controllers/AnimalsController.php @@ -116,8 +116,8 @@ final class AnimalsController $db = DB::pdo(); $db->prepare( ' - INSERT INTO animal_history (animal_id, type, label, details, user_id) - VALUES (:aid, :type, :label, :details, :user) + INSERT INTO animal_history (animal_id, action, type, label, details, user_id) + VALUES (:aid, :type, :type, :label, :details, :user) ', )->execute([ ':aid' => $animalId, diff --git a/app/Controllers/CareRoundsController.php b/app/Controllers/CareRoundsController.php index 5e899b1..1cbf058 100644 --- a/app/Controllers/CareRoundsController.php +++ b/app/Controllers/CareRoundsController.php @@ -295,7 +295,7 @@ final class CareRoundsController $roundDate = $roundInfo ? date('d/m/Y', strtotime((string) $roundInfo['round_date'])) : date('d/m/Y'); $roundPeriod = t('care.' . self::period($roundInfo['period'] ?? 'morning')); $history = $db->prepare( - "INSERT INTO animal_history(animal_id,type,label,details,user_id) VALUES(:animal,'care_round',:label,:details,:user)", + "INSERT INTO animal_history(animal_id,action,type,label,details,user_id) VALUES(:animal,'care_round','care_round',:label,:details,:user)", ); $history->execute([ ':animal' => $animalId, diff --git a/app/Controllers/DeathController.php b/app/Controllers/DeathController.php index 4518524..dd7a2e7 100644 --- a/app/Controllers/DeathController.php +++ b/app/Controllers/DeathController.php @@ -175,7 +175,7 @@ final class DeathController $details .= '\nEuthanasie : oui'; } $db->prepare( - "INSERT INTO animal_history(animal_id,type,label,details,user_id) VALUES(:id,'death',:label,:details,:user)", + "INSERT INTO animal_history(animal_id,action,type,label,details,user_id) VALUES(:id,'death','death',:label,:details,:user)", )->execute([ ':id' => $id, ':label' => $isUpdate ? 'Informations de décès modifiées' : 'Décès enregistré', diff --git a/app/Controllers/DewormingController.php b/app/Controllers/DewormingController.php index dfe0e1a..3ce8082 100644 --- a/app/Controllers/DewormingController.php +++ b/app/Controllers/DewormingController.php @@ -85,7 +85,7 @@ 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,type,label,details,user_id) VALUES(?,'deworming','Vermifuge administré',?,?)", + "INSERT INTO animal_history(animal_id,action,type,label,details,user_id) VALUES(?,'deworming','deworming','Vermifuge administré',?,?)", ); foreach ($ids as $id) { $s = $db->prepare('SELECT name,birth_date FROM animals WHERE id=? AND deleted_at IS NULL'); diff --git a/app/Controllers/MedicalDocumentsController.php b/app/Controllers/MedicalDocumentsController.php index 698af5b..a331e87 100644 --- a/app/Controllers/MedicalDocumentsController.php +++ b/app/Controllers/MedicalDocumentsController.php @@ -314,7 +314,7 @@ final class MedicalDocumentsController private static function history(PDO $db, int $animalId, string $label, string $details): void { $db->prepare( - "INSERT INTO animal_history(animal_id,type,label,details,user_id) VALUES(?,'medical',?,?,?)", + "INSERT INTO animal_history(animal_id,action,type,label,details,user_id) VALUES(?,'medical','medical',?,?,?)", )->execute([$animalId, $label, $details, Auth::id()]); } private static function fr(string $date): string diff --git a/app/Controllers/PlacementsController.php b/app/Controllers/PlacementsController.php index 4366c64..4fa878a 100644 --- a/app/Controllers/PlacementsController.php +++ b/app/Controllers/PlacementsController.php @@ -126,7 +126,7 @@ final class PlacementsController $details[] = 'Notes : ' . $notes; } $db->prepare( - 'INSERT INTO animal_history(animal_id,type,label,details,user_id,created_at) VALUES(:animal,\'placement\',:label,:details,:user,:date)', + 'INSERT INTO animal_history(animal_id,action,type,label,details,user_id,created_at) VALUES(:animal,\'placement\',\'placement\',:label,:details,:user,:date)', )->execute([ ':animal' => $animalId, ':label' => self::TYPES[$type], diff --git a/app/Controllers/SurgeriesController.php b/app/Controllers/SurgeriesController.php index 01fab0d..a0739d8 100644 --- a/app/Controllers/SurgeriesController.php +++ b/app/Controllers/SurgeriesController.php @@ -104,7 +104,7 @@ final class SurgeriesController ]); } $db->prepare( - "INSERT INTO animal_history(animal_id,type,label,details,user_id) VALUES(?,'medical','Chirurgie enregistrée',?,?)", + "INSERT INTO animal_history(animal_id,action,type,label,details,user_id) VALUES(?,'medical','medical','Chirurgie enregistrée',?,?)", )->execute([$animal, $name . ' — ' . $date, Auth::id()]); $db->commit(); header('Location: /animal?id=' . $animal . '#tab-med'); diff --git a/app/Views/evolutions.php b/app/Views/evolutions.php index 5ed43da..983078b 100644 --- a/app/Views/evolutions.php +++ b/app/Views/evolutions.php @@ -31,10 +31,10 @@
Une adoption finalisée rend automatiquement l’animal indisponible afin de - conserver un état administratif cohérent.
+ 1.0.0-rc.23 +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.
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/migrations/096_animal_history_compatibility.sql b/migrations/096_animal_history_compatibility.sql new file mode 100644 index 0000000..4857199 --- /dev/null +++ b/migrations/096_animal_history_compatibility.sql @@ -0,0 +1,3 @@ +-- La structure est harmonisée conditionnellement par Migrations::run afin de +-- prendre en charge les deux schémas historiques sans recréer la table. +SELECT 1; diff --git a/tests/run.php b/tests/run.php index 729c36c..db76a7b 100644 --- a/tests/run.php +++ b/tests/run.php @@ -87,6 +87,15 @@ try { 'L’historique des portées ne renseigne pas son action', ); }); + $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'); + $updates = file_get_contents(__DIR__ . '/../app/Services/UpdateService.php'); + $assert(str_contains($security, "@chmod(\$root . '/data', 0710)"), 'La racine des données ne reste pas traversable'); + $assert(str_contains($security, "hardenPublicTree(\$root . '/public/media')"), 'Les médias publics ne sont pas réparés'); + $assert(str_contains($images, 'preparePublicDirectory($directory)'), 'Les nouveaux dossiers publics ne sont pas préparés'); + $assert(str_contains($updates, "normalizePublicCodePermissions(\$root . '/public')"), 'Le code public n’est pas normalisé après mise à jour'); + }); $test("Les voies d’administration des médicaments sont initialisées", function () use ($db, $assert): void { $routes = $db ->query('SELECT accro FROM ref_med_administration ORDER BY id') @@ -97,7 +106,7 @@ try { $db->query('PRAGMA table_info(animal_history)')->fetchAll(PDO::FETCH_ASSOC), 'name', ); - foreach (['type', 'label', 'details'] as $column) { + foreach (['action', 'field', 'old_value', 'new_value', 'meta', 'type', 'label', 'details', 'user_id'] as $column) { $assert(in_array($column, $historyColumns, true), 'Colonne historique absente : ' . $column); } $assert( 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 03/13] =?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 @@ = $litter['father_id'] ? '' . h($litter['father_name']) . '' : h(t('animal.unknown_male')) ?>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.
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 +Les salles configurables, les états I-CAD et le suivi vaccinal restent + cohérents sur les installations neuves comme sur les bases historiques.
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 +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.