From 81f259ea6ef722de7e3b6da071c092fdd81630a6 Mon Sep 17 00:00:00 2001 From: oversu <95a7002b-481b-4ef3-b578-f9c421550aa4@localhost> Date: Thu, 10 Sep 2026 00:35:23 +0200 Subject: [PATCH 01/18] =?UTF-8?q?Am=C3=A9liore=20l'=C3=A9dition=20du=20sit?= =?UTF-8?q?e=20public?= 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/Controllers/SettingsController.php | 14 ++++++ app/Views/evolutions.php | 8 +-- app/Views/public_site_contact.php | 13 +++-- app/Views/public_site_layout.php | 24 +++++++++ app/Views/settings_public_site.php | 69 +++++++++++++++++++++----- resources/lang/en.php | 2 +- resources/lang/fr.php | 2 +- 11 files changed, 123 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d1a7860..8d4d7d8 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.18] — 2026-09-09 + +### Finitions du site public + +- présence des mentions légales et de la politique de confidentialité dans le pied de chaque page publique ; +- insertion guidée des images de la bibliothèque dans les textes, avec description alternative ; +- ajout d’une image sans rechargement de la page ni perte des textes en cours de saisie ; +- présentation des réseaux sociaux sous forme de liens identifiables, colorés et accessibles. + ## [1.0.0-rc.17] — 2026-09-08 ### Interface mobile et médias publics diff --git a/HISTORIQUE.md b/HISTORIQUE.md index 57479cd..bfbf5b0 100644 --- a/HISTORIQUE.md +++ b/HISTORIQUE.md @@ -77,3 +77,7 @@ Création d’un parcours de mise à jour commun à l’interface et à la ligne ## `1.0.0-rc.17` — Utilisation mobile Remplacement de la barre de navigation horizontale par un menu mobile repliable et fiabilisation des permissions appliquées aux nouvelles photos publiques. + +## `1.0.0-rc.18` — Finitions du site public + +Déplacement des informations légales dans le pied de page, insertion guidée des images sans perdre les textes en cours d’édition et identification visuelle des réseaux sociaux. diff --git a/README.md b/README.md index aa7ab98..aa0a1fb 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.17`. 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.18`. La branche 1.0 est gelée fonctionnellement : seuls les défauts bloquants découverts pendant les validations terrain sont corrigés avant `1.0.0`. 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 17e3421..4744cd3 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.0.0-rc.17 +1.0.0-rc.18 diff --git a/app/Controllers/SettingsController.php b/app/Controllers/SettingsController.php index bac8d80..35d2ff4 100644 --- a/app/Controllers/SettingsController.php +++ b/app/Controllers/SettingsController.php @@ -775,8 +775,22 @@ final class SettingsController $filename = ImageService::storeUploaded((string) $upload['tmp_name'], $directory, 'site', 'photo'); @chmod($directory, 0755); AuditService::log('public_site_image_uploaded', '/settings/public-site/image', 'Image du site public ajoutée'); + if (str_contains((string) ($_SERVER['HTTP_ACCEPT'] ?? ''), 'application/json')) { + header('Content-Type: application/json; charset=utf-8'); + echo json_encode([ + 'filename' => $filename, + 'url' => '/media/site/' . rawurlencode($filename), + ], JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR); + exit(); + } header('Location: /settings/public-site?image=' . rawurlencode($filename)); } catch (Throwable $e) { + if (str_contains((string) ($_SERVER['HTTP_ACCEPT'] ?? ''), 'application/json')) { + http_response_code(422); + header('Content-Type: application/json; charset=utf-8'); + echo json_encode(['error' => SecurityService::publicError($e)], JSON_THROW_ON_ERROR); + exit(); + } header('Location: /settings/public-site?error=' . rawurlencode(SecurityService::publicError($e))); } exit(); diff --git a/app/Views/evolutions.php b/app/Views/evolutions.php index 93ed841..00b7f17 100644 --- a/app/Views/evolutions.php +++ b/app/Views/evolutions.php @@ -31,10 +31,10 @@
- 1.0.0-rc.17 -

Utilisation mobile

-

Navigation repliable sur les petits écrans et publication fiable des nouvelles - photographies, y compris depuis l’application Android.

+ 1.0.0-rc.18 +

Site public

+

Informations légales accessibles sur chaque page, insertion guidée des images + dans les contenus et liens sociaux plus lisibles.

diff --git a/app/Views/public_site_contact.php b/app/Views/public_site_contact.php index 5599cbe..f2a29a9 100644 --- a/app/Views/public_site_contact.php +++ b/app/Views/public_site_contact.php @@ -13,11 +13,16 @@ $obfuscate = static fn (string$value): string => implode('', array_reverse(mb_st
✉
-
- 'Facebook','instagram' => 'Instagram','youtube' => 'YouTube','tiktok' => 'TikTok','whatsapp' => 'WhatsApp','linkedin' => 'LinkedIn','x' => 'X','snapchat' => 'Snapchat'] as $network => $label): +

@@ -27,8 +32,6 @@ $obfuscate = static fn (string$value): string => implode('', array_reverse(mb_st

-
· -
diff --git a/resources/lang/en.php b/resources/lang/en.php index 2fbc98d..9626db6 100644 --- a/resources/lang/en.php +++ b/resources/lang/en.php @@ -120,7 +120,7 @@ return [ 'accounting.document_save_failed'=>'The document could not be saved.', 'search.title'=>'Global search','search.placeholder'=>'Animal, contact, appointment or invoice…','search.action'=>'Search','search.hint'=>'Enter at least two characters.','search.none'=>'No accessible result.','search.open'=>'Open global search','search.shortcut'=>'Ctrl K','search.group.animals'=>'Animals','search.group.directory'=>'Directory','search.group.agenda'=>'Calendar','search.group.accounting'=>'Accounting','search.loading'=>'Searching…','search.all_results'=>'Show all results','search.error'=>'Search is temporarily unavailable.', 'public_site.settings_heading'=>'Public website','public_site.theme_custom'=>'Custom','public_site.site_name'=>'Website name','public_site.custom_css'=>'Custom CSS','public_site.custom_css_help'=>'Used by the custom theme. Imports and external resources are blocked.','public_site.invalid_css'=>'The stylesheet contains a forbidden instruction.','public_site.routing_management_help'=>'Management stays at / and the public website at /site.','public_site.publish_animal'=>'Publish this animal on the public website','public_site.publish_animal_help'=>'The profile will only be visible when the animal is also available for adoption. Only public photos and the text below are used.','lab_reference.add_species'=>'Add a species to lab reports','lab_reference.copy_from'=>'Copy reference ranges from','lab_reference.target_species'=>'New species','lab_reference.copy_help'=>'Ranges are copied as a starting point and remain editable.','lab_reference.invalid_species'=>'Choose two different valid species.','lab_reference.nothing_copied'=>'This species already has these reference ranges.','statistics.hair_types'=>'Hair types','animals.filtered_by'=>'Filter: :value','service.icad.home_unavailable'=>'The I-CAD website is unavailable','service.icad.search_unavailable'=>'I-CAD search is unavailable','service.icad.record_unavailable'=>'The I-CAD record is unavailable', - 'public_site.image_library'=>'Image library','public_site.image_file'=>'Image to add','public_site.image_upload'=>'Upload image','public_site.image_help'=>'Upload an image, place the cursor in a text field, then choose “Insert”. Accepted formats: JPEG, PNG and WebP.','public_site.image_none'=>'No image uploaded.','public_site.image_insert'=>'Insert','public_site.image_invalid'=>'Choose a valid image.', + 'public_site.image_library'=>'Image library','public_site.image_file'=>'Image to add','public_site.image_upload'=>'Upload image','public_site.image_help'=>'From the text you want to enrich, choose “Insert”, then select an image. Accepted formats: JPEG, PNG and WebP.','public_site.image_none'=>'No image uploaded.','public_site.image_insert'=>'Insert','public_site.image_invalid'=>'Choose a valid image.','public_site.image_alt_prompt'=>'Briefly describe the image for people who cannot see it:','public_site.image_default_alt'=>'Association image','public_site.image_target_selected'=>'Text selected: now choose an image from the library.', 'rooms.duplicate'=>'A room with this name or code already exists.','settings.species_duplicate'=>'A species with this name or code already exists.', 'settings.invalid_rna'=>'The RNA number must start with W followed by 9 digits.','settings.invalid_siret'=>'The SIRET number must contain 14 digits and be valid.','rooms.name_too_long'=>'A room name cannot exceed 100 characters.','rooms.box_name_too_long'=>'A box name cannot exceed 100 characters.','animal.name_too_long'=>'An animal name cannot exceed 150 characters.', 'pricing.adoption_fees_help'=>'Indicative amounts shared by all animals, according to their sterilisation status.','rooms.inactive_help'=>'An unchecked room becomes inactive, while its previous assignments remain in the history. Its internal code never changes.', diff --git a/resources/lang/fr.php b/resources/lang/fr.php index 4762674..febc145 100644 --- a/resources/lang/fr.php +++ b/resources/lang/fr.php @@ -120,7 +120,7 @@ return [ 'accounting.document_save_failed'=>'Impossible d’enregistrer le document.', 'search.title'=>'Recherche globale','search.placeholder'=>'Animal, contact, rendez-vous ou facture…','search.action'=>'Rechercher','search.hint'=>'Saisissez au moins deux caractères.','search.none'=>'Aucun résultat accessible.','search.open'=>'Ouvrir la recherche globale','search.shortcut'=>'Ctrl K','search.group.animals'=>'Animaux','search.group.directory'=>'Annuaire','search.group.agenda'=>'Agenda','search.group.accounting'=>'Comptabilité','search.loading'=>'Recherche…','search.all_results'=>'Afficher tous les résultats','search.error'=>'La recherche est momentanément indisponible.', 'public_site.settings_heading'=>'Site public','public_site.theme_custom'=>'Personnalisé','public_site.site_name'=>'Nom du site','public_site.custom_css'=>'CSS personnalisé','public_site.custom_css_help'=>'Actif avec le thème personnalisé. Les imports et les ressources externes sont bloqués.','public_site.invalid_css'=>'La feuille de style contient une instruction interdite.','public_site.routing_management_help'=>'La gestion reste sur / et le site public sur /site.','public_site.publish_animal'=>'Publier cet animal sur le site public','public_site.publish_animal_help'=>'La fiche ne sera visible que si l’animal est également disponible à l’adoption. Seules les photos publiques et le texte ci-dessous seront utilisés.','lab_reference.add_species'=>'Ajouter une espèce aux bilans','lab_reference.copy_from'=>'Copier les références de','lab_reference.target_species'=>'Nouvelle espèce','lab_reference.copy_help'=>'Les intervalles sont copiés comme point de départ, puis restent modifiables.','lab_reference.invalid_species'=>'Choisissez deux espèces différentes et valides.','lab_reference.nothing_copied'=>'Cette espèce possède déjà ces références.','statistics.hair_types'=>'Types de poil','animals.filtered_by'=>'Filtre : :value','service.icad.home_unavailable'=>'Le site I-CAD est indisponible','service.icad.search_unavailable'=>'La recherche I-CAD est indisponible','service.icad.record_unavailable'=>'La fiche I-CAD est indisponible', - 'public_site.image_library'=>'Bibliothèque d’images','public_site.image_file'=>'Image à ajouter','public_site.image_upload'=>'Ajouter l’image','public_site.image_help'=>'Ajoutez une image, placez le curseur dans un texte puis choisissez « Insérer ». Formats acceptés : JPEG, PNG et WebP.','public_site.image_none'=>'Aucune image ajoutée.','public_site.image_insert'=>'Insérer','public_site.image_invalid'=>'Choisissez une image valide.', + 'public_site.image_library'=>'Bibliothèque d’images','public_site.image_file'=>'Image à ajouter','public_site.image_upload'=>'Ajouter l’image','public_site.image_help'=>'Depuis le texte à enrichir, choisissez « Insérer » puis sélectionnez une image. Formats acceptés : JPEG, PNG et WebP.','public_site.image_none'=>'Aucune image ajoutée.','public_site.image_insert'=>'Insérer','public_site.image_invalid'=>'Choisissez une image valide.','public_site.image_alt_prompt'=>'Décrivez brièvement l’image pour les personnes qui ne peuvent pas la voir :','public_site.image_default_alt'=>'Image de l’association','public_site.image_target_selected'=>'Texte sélectionné : choisissez maintenant une image dans la bibliothèque.', 'rooms.duplicate'=>'Une salle portant ce nom ou ce code existe déjà.','settings.species_duplicate'=>'Une espèce portant ce nom ou ce code existe déjà.', 'settings.invalid_rna'=>'Le numéro RNA doit commencer par W et être suivi de 9 chiffres.','settings.invalid_siret'=>'Le numéro SIRET doit contenir 14 chiffres et être valide.','rooms.name_too_long'=>'Le nom d’une salle ne peut pas dépasser 100 caractères.','rooms.box_name_too_long'=>'Le nom d’un box ne peut pas dépasser 100 caractères.','animal.name_too_long'=>'Le nom d’un animal ne peut pas dépasser 150 caractères.', 'pricing.adoption_fees_help'=>'Montants indicatifs communs à tous les animaux, selon leur état de stérilisation.','rooms.inactive_help'=>'Une salle décochée devient inactive, mais ses anciennes affectations restent dans l’historique. Son code interne ne change jamais.', From a0ea338292ed1be0bc0bcb12e42080544c1efc45 Mon Sep 17 00:00:00 2001 From: oversu <95a7002b-481b-4ef3-b578-f9c421550aa4@localhost> Date: Sun, 13 Sep 2026 14:28:12 +0200 Subject: [PATCH 02/18] =?UTF-8?q?Horodate=20pr=C3=A9cis=C3=A9ment=20les=20?= =?UTF-8?q?contr=C3=B4les=20de=20soins?= 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/AnimalMedicalActions.php | 25 +++++++++++++++++++----- app/Views/_animal_show_modals.php | 13 +++++++++++- app/Views/evolutions.php | 8 ++++---- resources/lang/en.php | 1 + resources/lang/fr.php | 1 + tests/run.php | 8 ++++++++ 10 files changed, 60 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d4d7d8..c3aa7e5 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.19] — 2026-09-13 + +### Horodatage des contrôles de soins + +- ajout d’une date et d’une heure aux notes médicales, proposées à l’instant de la saisie et librement modifiables ; +- utilisation du même horodatage pour la note, la mesure de poids, le tarif associé et la date d’une éventuelle stérilisation ; +- validation stricte des dates et heures transmises avant enregistrement. + ## [1.0.0-rc.18] — 2026-09-09 ### Finitions du site public diff --git a/HISTORIQUE.md b/HISTORIQUE.md index bfbf5b0..e7efa40 100644 --- a/HISTORIQUE.md +++ b/HISTORIQUE.md @@ -81,3 +81,7 @@ Remplacement de la barre de navigation horizontale par un menu mobile repliable ## `1.0.0-rc.18` — Finitions du site public Déplacement des informations légales dans le pied de page, insertion guidée des images sans perdre les textes en cours d’édition et identification visuelle des réseaux sociaux. + +## `1.0.0-rc.19` — Contrôles de soins horodatés + +Ajout d’une date et d’une heure modifiables aux contrôles médicaux, avec reprise cohérente de l’instant choisi dans les mesures de poids et les données associées. diff --git a/README.md b/README.md index aa0a1fb..1800b24 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.18`. La branche 1.0 est gelée fonctionnellement : seuls les défauts bloquants découverts pendant les validations terrain sont corrigés avant `1.0.0`. +La candidate actuelle est `1.0.0-rc.19`. La branche 1.0 est gelée fonctionnellement : seuls les défauts bloquants découverts pendant les validations terrain sont corrigés avant `1.0.0`. Le contrôle de stabilisation est disponible dans [docs/AUDIT-1.0.md](docs/AUDIT-1.0.md). diff --git a/VERSION b/VERSION index 4744cd3..784e940 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.0.0-rc.18 +1.0.0-rc.19 diff --git a/app/Controllers/AnimalMedicalActions.php b/app/Controllers/AnimalMedicalActions.php index 25452ab..3cd6172 100644 --- a/app/Controllers/AnimalMedicalActions.php +++ b/app/Controllers/AnimalMedicalActions.php @@ -221,6 +221,20 @@ trait AnimalMedicalActions $temperature = $_POST['temperature_c'] ?? null; $weight = $_POST['weight_kg'] ?? null; + $notedDate = trim((string) ($_POST['noted_date'] ?? date('Y-m-d'))); + $notedTime = trim((string) ($_POST['noted_time'] ?? date('H:i'))); + $observedAt = DateTimeImmutable::createFromFormat('!Y-m-d H:i', $notedDate . ' ' . $notedTime); + $dateErrors = DateTimeImmutable::getLastErrors(); + if ( + !$observedAt || + ($dateErrors !== false && ($dateErrors['warning_count'] > 0 || $dateErrors['error_count'] > 0)) || + $observedAt->format('Y-m-d H:i') !== $notedDate . ' ' . $notedTime + ) { + http_response_code(400); + echo h(t('error.invalid_medical_datetime')); + return; + } + $observedAtSql = $observedAt->format('Y-m-d H:i:s'); $clinicId = (int) ($_POST['clinic_id'] ?? 0); $clinicId = $clinicId > 0 ? $clinicId : null; @@ -251,13 +265,14 @@ trait AnimalMedicalActions // Enregistre la note médicale avant ses éventuelles pièces jointes. $stmt = $db->prepare(' INSERT INTO medical_notes - (animal_id, kind, reason, symptoms, exam, diagnosis, plan, temperature_c, weight_kg, clinic_id, vet_name, clinic_contact_id, vet_contact_id, created_by, created_at) + (animal_id, noted_at, kind, reason, symptoms, exam, diagnosis, plan, temperature_c, weight_kg, clinic_id, vet_name, clinic_contact_id, vet_contact_id, created_by, created_at) VALUES - (:aid, :kind, :reason, :symptoms, :exam, :diagnosis, :plan, :temp, :weight, :clinic, :vet, :clinic_contact, :vet_contact, :created_by, :created) + (:aid, :noted_at, :kind, :reason, :symptoms, :exam, :diagnosis, :plan, :temp, :weight, :clinic, :vet, :clinic_contact, :vet_contact, :created_by, :created) '); $stmt->execute([ ':aid' => $animalId, + ':noted_at' => $observedAtSql, ':kind' => $kind, ':reason' => $reason, ':symptoms' => $symptoms, @@ -282,7 +297,7 @@ trait AnimalMedicalActions $animalId, (int) $_POST['tariff_id'], 1, - substr(self::now(), 0, 10), + $observedAt->format('Y-m-d'), 'Ajoutée avec la note médicale', ); } @@ -344,7 +359,7 @@ trait AnimalMedicalActions ", )->execute([ ':aid' => $animalId, - ':at' => self::now(), + ':at' => $observedAtSql, ':v' => (float) $weight, ':created_by' => Auth::id(), ]); @@ -358,7 +373,7 @@ trait AnimalMedicalActions $db->prepare( "UPDATE animals SET sterilized=1,sterilization_status='yes',sterilization_date=COALESCE(sterilization_date,date(:date)),sterilization_vet_contact_id=COALESCE(:vet,sterilization_vet_contact_id),sterilization_clinic_contact_id=COALESCE(:clinic,sterilization_clinic_contact_id) WHERE id=:id", )->execute([ - ':date' => self::now(), + ':date' => $observedAtSql, ':vet' => $vetContact['id'] ?? null, ':clinic' => $clinicContact['id'] ?? null, ':id' => $animalId, diff --git a/app/Views/_animal_show_modals.php b/app/Views/_animal_show_modals.php index 4792870..7df107c 100644 --- a/app/Views/_animal_show_modals.php +++ b/app/Views/_animal_show_modals.php @@ -7,6 +7,7 @@ /** @var array $vaccines */ $defaultDue = date('Y-m-d', strtotime('+1 year')); $today = date('Y-m-d'); +$currentTime = date('H:i'); ?> @@ -612,6 +613,16 @@ $today = date('Y-m-d');
+
+ + +
+ +
+ + +
+
-
+
diff --git a/app/Views/evolutions.php b/app/Views/evolutions.php index 00b7f17..6df0f1c 100644 --- a/app/Views/evolutions.php +++ b/app/Views/evolutions.php @@ -31,10 +31,10 @@
- 1.0.0-rc.18 -

Site public

-

Informations légales accessibles sur chaque page, insertion guidée des images - dans les contenus et liens sociaux plus lisibles.

+ 1.0.0-rc.19 +

Suivi des soins

+

Date et heure modifiables pour distinguer précisément plusieurs contrôles et + mesures réalisés au cours d’une même journée.

diff --git a/resources/lang/en.php b/resources/lang/en.php index 9626db6..85d8c68 100644 --- a/resources/lang/en.php +++ b/resources/lang/en.php @@ -198,4 +198,5 @@ return [ 'updates.directory_failed'=>'Unable to prepare directory :path.', 'updates.download_log'=>'Download technical log', 'updates.log_missing'=>'No update log is available.', + 'error.invalid_medical_datetime'=>'Enter a valid check-up date and time.', ]; diff --git a/resources/lang/fr.php b/resources/lang/fr.php index febc145..928c849 100644 --- a/resources/lang/fr.php +++ b/resources/lang/fr.php @@ -198,4 +198,5 @@ return [ 'updates.directory_failed'=>'Impossible de préparer le dossier :path.', 'updates.download_log'=>'Télécharger le journal technique', 'updates.log_missing'=>'Aucun journal de mise à jour n’est disponible.', + 'error.invalid_medical_datetime'=>'Indiquez une date et une heure de contrôle valides.', ]; diff --git a/tests/run.php b/tests/run.php index 33457c5..2d29660 100644 --- a/tests/run.php +++ b/tests/run.php @@ -360,6 +360,14 @@ try { $assert((int) $db->query('SELECT COUNT(*) FROM adoptions')->fetchColumn() === 1); $assert(!$db->query('PRAGMA foreign_key_check')->fetchAll()); }); + $test('Les contrôles médicaux conservent leur date et leur heure', function () use ($assert): void { + $view = file_get_contents(__DIR__ . '/../app/Views/_animal_show_modals.php'); + $controller = file_get_contents(__DIR__ . '/../app/Controllers/AnimalMedicalActions.php'); + $assert(str_contains($view, 'name="noted_date"'), 'Le champ de date du contrôle est absent'); + $assert(str_contains($view, 'name="noted_time"'), 'Le champ d’heure du contrôle est absent'); + $assert(str_contains($controller, "':noted_at' => \$observedAtSql"), 'La note ne reprend pas l’instant choisi'); + $assert(str_contains($controller, "':at' => \$observedAtSql"), 'La courbe de poids ne reprend pas l’instant choisi'); + }); $test('La fiche annuaire agrège adoptions et dépenses', function () use ($db, $assert): void { $db->exec("INSERT INTO directory_contacts(kind,name) VALUES('organization','Structure test')"); $contact = (int) $db->lastInsertId(); From 68590eddbb5687196477bcdee48667004beb6a13 Mon Sep 17 00:00:00 2001 From: oversu <95a7002b-481b-4ef3-b578-f9c421550aa4@localhost> Date: Sun, 13 Sep 2026 15:57:57 +0200 Subject: [PATCH 03/18] Corrige la localisation actuelle des animaux --- CHANGELOG.md | 9 ++++++ HISTORIQUE.md | 4 +++ README.md | 2 +- VERSION | 2 +- app/Controllers/AnimalBrowseActions.php | 29 +++++++++++++++++++ app/Controllers/PlacementsController.php | 19 ++++++++++-- app/Views/_animal_show_identity.php | 6 ++-- app/Views/animal_form.php | 37 ++++++++---------------- app/Views/evolutions.php | 8 ++--- 9 files changed, 80 insertions(+), 36 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c3aa7e5..e29c827 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.20] — 2026-09-13 + +### Localisation des animaux + +- regroupement des informations de récupération et de lieu actuel dans une seule section sans anciens champs en double ; +- conservation de l’adresse actuelle lors de la modification d’une fiche ; +- géocodage du nouveau lieu lors d’un placement ou d’un retour au refuge ; +- réparation ponctuelle des coordonnées manquantes des animaux déjà importés afin d’afficher leur lieu actuel sur la carte. + ## [1.0.0-rc.19] — 2026-09-13 ### Horodatage des contrôles de soins diff --git a/HISTORIQUE.md b/HISTORIQUE.md index e7efa40..29736fe 100644 --- a/HISTORIQUE.md +++ b/HISTORIQUE.md @@ -85,3 +85,7 @@ Déplacement des informations légales dans le pied de page, insertion guidée d ## `1.0.0-rc.19` — Contrôles de soins horodatés Ajout d’une date et d’une heure modifiables aux contrôles médicaux, avec reprise cohérente de l’instant choisi dans les mesures de poids et les données associées. + +## `1.0.0-rc.20` — Localisations cohérentes + +Simplification des lieux affichés dans la fiche, conservation de l’adresse actuelle et réparation des coordonnées manquantes après un import ou un placement. diff --git a/README.md b/README.md index 1800b24..914bc81 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.19`. La branche 1.0 est gelée fonctionnellement : seuls les défauts bloquants découverts pendant les validations terrain sont corrigés avant `1.0.0`. +La candidate actuelle est `1.0.0-rc.20`. 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 784e940..dca2922 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.0.0-rc.19 +1.0.0-rc.20 diff --git a/app/Controllers/AnimalBrowseActions.php b/app/Controllers/AnimalBrowseActions.php index 1e34209..86e657b 100644 --- a/app/Controllers/AnimalBrowseActions.php +++ b/app/Controllers/AnimalBrowseActions.php @@ -453,6 +453,35 @@ trait AnimalBrowseActions echo h(t('error.animal_not_found')); return; } + + // Les imports historiques peuvent contenir une adresse sans ses coordonnées. + // On complète alors la fiche une seule fois, au premier affichage de la carte. + $coordinateUpdates = []; + foreach ([ + 'rescue' => trim((string) ($animal['rescue_address'] ?: $animal['rescue_location_name'] ?? '')), + 'current' => trim((string) ($animal['current_address'] ?? '')), + ] as $kind => $address) { + if ( + $address !== '' && + ($animal[$kind . '_lat'] === null || $animal[$kind . '_lng'] === null) + ) { + $coordinates = GeoService::geocode($db, $address); + if ($coordinates) { + $coordinateUpdates[$kind . '_lat'] = $coordinates['lat']; + $coordinateUpdates[$kind . '_lng'] = $coordinates['lng']; + } + } + } + if ($coordinateUpdates) { + $assignments = []; + $parameters = [':id' => $id]; + foreach ($coordinateUpdates as $column => $value) { + $assignments[] = $column . '=:' . $column; + $parameters[':' . $column] = $value; + $animal[$column] = $value; + } + $db->prepare('UPDATE animals SET ' . implode(',', $assignments) . ' WHERE id=:id')->execute($parameters); + } if (!empty($animal['intake_owner_care_home'])) { $animal['intake_reason'] = 'owner_care_home'; } diff --git a/app/Controllers/PlacementsController.php b/app/Controllers/PlacementsController.php index 7255d35..4366c64 100644 --- a/app/Controllers/PlacementsController.php +++ b/app/Controllers/PlacementsController.php @@ -72,6 +72,15 @@ final class PlacementsController ]), ); } + $currentLat = null; + $currentLng = null; + if ($currentAddress !== '') { + $coordinates = GeoService::geocode($db, $currentAddress); + if ($coordinates) { + $currentLat = $coordinates['lat']; + $currentLng = $coordinates['lng']; + } + } $notes = trim((string) ($_POST['notes'] ?? '')); $db->beginTransaction(); try { @@ -87,8 +96,14 @@ final class PlacementsController ':user' => Auth::id(), ]); $db->prepare( - 'UPDATE animals SET status=:status,refuge_room=NULL,care_box_key=NULL,current_address=:address,current_lat=NULL,current_lng=NULL,updated_at=datetime(\'now\') WHERE id=:id', - )->execute([':status' => $status, ':address' => $currentAddress ?: null, ':id' => $animalId]); + 'UPDATE animals SET status=:status,refuge_room=NULL,care_box_key=NULL,current_address=:address,current_lat=:lat,current_lng=:lng,updated_at=datetime(\'now\') WHERE id=:id', + )->execute([ + ':status' => $status, + ':address' => $currentAddress ?: null, + ':lat' => $currentLat, + ':lng' => $currentLng, + ':id' => $animalId, + ]); $after = $animal; $after['status'] = $status; $after['refuge_room'] = null; diff --git a/app/Views/_animal_show_identity.php b/app/Views/_animal_show_identity.php index 10efc84..3d9488c 100644 --- a/app/Views/_animal_show_identity.php +++ b/app/Views/_animal_show_identity.php @@ -178,11 +178,11 @@
-
+
diff --git a/app/Views/animal_form.php b/app/Views/animal_form.php index 02203ab..a63b15b 100644 --- a/app/Views/animal_form.php +++ b/app/Views/animal_form.php @@ -422,22 +422,6 @@ $estMonths = $birth_estimated_months > 0 ? ($birth_estimated_months % 12) : '';
-
-
-
- -
-
- -
-
- disabled> -
-
@@ -534,7 +518,7 @@ $estMonths = $birth_estimated_months > 0 ? ($birth_estimated_months % 12) : '';
-
+
0 ? ($birth_estimated_months % 12) : ''; value="">
-
+
+ + +
+
+ +
-
- - -
diff --git a/app/Views/evolutions.php b/app/Views/evolutions.php index 6df0f1c..d89f344 100644 --- a/app/Views/evolutions.php +++ b/app/Views/evolutions.php @@ -31,10 +31,10 @@
- 1.0.0-rc.19 -

Suivi des soins

-

Date et heure modifiables pour distinguer précisément plusieurs contrôles et - mesures réalisés au cours d’une même journée.

+ 1.0.0-rc.20 +

Localisations cohérentes

+

Lieu de récupération et lieu actuel clairement distingués, avec réparation + des coordonnées manquantes après un import ou un placement.

From d35434c52250678a661f5d8e494d72c00d2eb0dc Mon Sep 17 00:00:00 2001 From: oversu <95a7002b-481b-4ef3-b578-f9c421550aa4@localhost> Date: Tue, 15 Sep 2026 15:43:00 +0200 Subject: [PATCH 04/18] =?UTF-8?q?feat:=20suivre=20la=20temp=C3=A9rature=20?= =?UTF-8?q?pendant=20les=20tourn=C3=A9es?= 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/Controllers/AnimalBrowseActions.php | 16 ++++++ app/Controllers/CareRoundsController.php | 15 +++++- app/Views/_animal_show_medical.php | 8 +-- app/Views/_animal_show_modals.php | 59 ++++++++++++++++++----- app/Views/animal_show.php | 1 + app/Views/care_round.php | 10 ++++ app/Views/evolutions.php | 8 +-- migrations/095_care_round_temperature.sql | 1 + resources/lang/en.php | 5 ++ resources/lang/fr.php | 5 ++ tests/run.php | 7 +++ 15 files changed, 129 insertions(+), 23 deletions(-) create mode 100644 migrations/095_care_round_temperature.sql diff --git a/CHANGELOG.md b/CHANGELOG.md index e29c827..7a4a30a 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.21] — 2026-09-15 + +### Suivi de la température + +- ajout de la température à chaque contrôle d’animal pendant une tournée, préremplie à 38,5 °C et modifiable ; +- validation des températures enregistrées entre 30 et 45 °C ; +- regroupement du poids et de la température dans un graphique médical commun, avec une échelle dédiée à chaque unité ; +- reprise dans la courbe des températures saisies dans les notes médicales et pendant les tournées. + ## [1.0.0-rc.20] — 2026-09-13 ### Localisation des animaux diff --git a/HISTORIQUE.md b/HISTORIQUE.md index 29736fe..e2934d6 100644 --- a/HISTORIQUE.md +++ b/HISTORIQUE.md @@ -89,3 +89,7 @@ Ajout d’une date et d’une heure modifiables aux contrôles médicaux, avec r ## `1.0.0-rc.20` — Localisations cohérentes Simplification des lieux affichés dans la fiche, conservation de l’adresse actuelle et réparation des coordonnées manquantes après un import ou un placement. + +## `1.0.0-rc.21` — Température pendant les tournées + +Ajout d’une température préremplie à 38,5 °C lors du contrôle de chaque animal et affichage conjoint des courbes de poids et de température dans son dossier médical. diff --git a/README.md b/README.md index 914bc81..45914f2 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.20`. 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.21`. 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 dca2922..fee2ed6 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.0.0-rc.20 +1.0.0-rc.21 diff --git a/app/Controllers/AnimalBrowseActions.php b/app/Controllers/AnimalBrowseActions.php index 86e657b..845e5f9 100644 --- a/app/Controllers/AnimalBrowseActions.php +++ b/app/Controllers/AnimalBrowseActions.php @@ -704,6 +704,21 @@ trait AnimalBrowseActions $weightsStmt->execute([':id' => $id]); $weights = $weightsStmt->fetchAll(); + $temperaturesStmt = $db->prepare(" + SELECT measured_at, value FROM ( + SELECT noted_at AS measured_at, temperature_c AS value + FROM medical_notes + WHERE animal_id = :medical_id AND temperature_c IS NOT NULL + UNION ALL + SELECT observation.checked_at AS measured_at, observation.temperature_c AS value + FROM care_round_observations observation + WHERE observation.animal_id = :round_id AND observation.temperature_c IS NOT NULL + ) + ORDER BY datetime(measured_at) ASC + "); + $temperaturesStmt->execute([':medical_id' => $id, ':round_id' => $id]); + $temperatures = $temperaturesStmt->fetchAll(PDO::FETCH_ASSOC); + $stmt = $db->prepare(" SELECT value FROM measurements @@ -952,6 +967,7 @@ trait AnimalBrowseActions 'adopters' => $adopters, 'death' => $death, 'weights' => $weights, + 'temperatures' => $temperatures, 'lastWeight' => $lastWeight, 'prevWeight' => $prevWeight, 'photos' => $photos, diff --git a/app/Controllers/CareRoundsController.php b/app/Controllers/CareRoundsController.php index 64d74ad..5e899b1 100644 --- a/app/Controllers/CareRoundsController.php +++ b/app/Controllers/CareRoundsController.php @@ -139,6 +139,18 @@ final class CareRoundsController $value = (string) ($_POST[$field] ?? $opts[0]); $values[$field] = in_array($value, $opts, true) ? $value : $opts[0]; } + $temperatureInput = str_replace(',', '.', trim((string) ($_POST['temperature_c'] ?? ''))); + if (!is_numeric($temperatureInput)) { + http_response_code(400); + echo h(t('care.invalid_temperature')); + return; + } + $temperature = (float) $temperatureInput; + if ($temperature < 30 || $temperature > 45) { + http_response_code(400); + echo h(t('care.invalid_temperature')); + return; + } $db->prepare( 'UPDATE care_rounds SET performed_by_user_id=COALESCE(performed_by_user_id,:user) WHERE id=:id', )->execute([':user' => Auth::id(), ':id' => $roundId]); @@ -220,7 +232,7 @@ final class CareRoundsController $db->beginTransaction(); try { $stmt = $db->prepare( - "INSERT INTO care_round_observations(round_id,animal_id,food,water,urine,stool,general_state,comment,alert_signature) VALUES(:round,:animal,:food,:water,:urine,:stool,:general,:comment,:signature) ON CONFLICT(round_id,animal_id) DO UPDATE SET food=excluded.food,water=excluded.water,urine=excluded.urine,stool=excluded.stool,general_state=excluded.general_state,comment=excluded.comment,alert_signature=excluded.alert_signature,checked_at=datetime('now')", + "INSERT INTO care_round_observations(round_id,animal_id,food,water,urine,stool,general_state,temperature_c,comment,alert_signature) VALUES(:round,:animal,:food,:water,:urine,:stool,:general,:temperature,:comment,:signature) ON CONFLICT(round_id,animal_id) DO UPDATE SET food=excluded.food,water=excluded.water,urine=excluded.urine,stool=excluded.stool,general_state=excluded.general_state,temperature_c=excluded.temperature_c,comment=excluded.comment,alert_signature=excluded.alert_signature,checked_at=datetime('now')", ); $stmt->execute([ ':round' => $roundId, @@ -230,6 +242,7 @@ final class CareRoundsController ':urine' => $values['urine'], ':stool' => $values['stool'], ':general' => $values['general_state'], + ':temperature' => $temperature, ':comment' => $comment ?: null, ':signature' => $signature, ]); diff --git a/app/Views/_animal_show_medical.php b/app/Views/_animal_show_medical.php index 4bb698e..75a4211 100644 --- a/app/Views/_animal_show_medical.php +++ b/app/Views/_animal_show_medical.php @@ -235,14 +235,14 @@
-

+

- -
+ +
- +
diff --git a/app/Views/_animal_show_modals.php b/app/Views/_animal_show_modals.php index 7df107c..972364f 100644 --- a/app/Views/_animal_show_modals.php +++ b/app/Views/_animal_show_modals.php @@ -944,25 +944,47 @@ $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 10/18] =?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 11/18] 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="">