diff --git a/CHANGELOG.md b/CHANGELOG.md index cdd69e8..47ed982 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,22 @@ 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.30] — 2026-10-01 + +### Fiabilisation des dossiers et du suivi médical + +- correction de la fausse déclaration de décès de Lucci après vérification ASM3 et protection contre les fiches de décès résiduelles lors d’une correction de statut ; +- réparation de l’actualisation I-CAD après la suppression de l’ancienne adresse `/pro` ; +- exclusion uniforme des animaux adoptés dans les rappels et traitements du tableau de bord ; +- affichage de la date du test FIV/FeLV et possibilité d’en comptabiliser le tarif ; +- ajout direct d’un vaccin depuis sa liste, fabricants suggérés et tri alphabétique ; +- filtrage des tarifs par clinique pour les vaccins, tests et chirurgies ; +- propositions normalisées d’intitulés chirurgicaux et catalogue médical administrable ; +- lien direct entre l’adoptant et sa fiche d’annuaire, ainsi que de nouveaux rôles de professionnels animaliers ; +- amélioration du menu mobile, des zones de texte du site public et des longues listes de rappels ; +- ajout d’un script protégé permettant de réinitialiser automatiquement une instance de démonstration à heure fixe ; +- nouvelle icône Android issue du logo officiel de l’application. + ## [1.0.0-rc.29] — 2026-09-22 ### Corrections du suivi médical et du site public diff --git a/HISTORIQUE.md b/HISTORIQUE.md index 6fb9616..6cd62df 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.30` — Corrections issues de la validation terrain + +Une passe croisée avec les sauvegardes ASM3 a corrigé une collision historique entre Gribouille et Lucci. Le suivi médical gagne une date de test FIV/FeLV, des catalogues administrables et une meilleure association entre tarifs et cliniques. La synchronisation I-CAD, la navigation mobile et l’identité Android ont également été adaptées aux conditions réelles d’utilisation. + ## `1.0.0-rc.29` — Suivi médical et présentation Les traitements et les intitulés des produits médicaux peuvent être corrigés après leur saisie. Le formulaire des vermifuges s’ouvre de nouveau correctement. La fiche animale montre sa date de naissance et le CSS personnalisé du site public prend priorité sur les styles standard. diff --git a/README.md b/README.md index 4d069ee..c73a1ce 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.29`. 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.30`. 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 3dcfd46..370478c 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.0.0-rc.29 +1.0.0-rc.30 diff --git a/app/Controllers/AnimalBrowseActions.php b/app/Controllers/AnimalBrowseActions.php index bc14a65..3d4c912 100644 --- a/app/Controllers/AnimalBrowseActions.php +++ b/app/Controllers/AnimalBrowseActions.php @@ -518,7 +518,7 @@ trait AnimalBrowseActions // Adoptions $stmt = $db->prepare(' - SELECT adopter_name, adopter_address, adopter_postal_code, adopter_city, adopter_country, adopter_phone, adopter_email, adoption_date + SELECT adopter_name, adopter_address, adopter_postal_code, adopter_city, adopter_country, adopter_phone, adopter_email, adoption_date, adopter_contact_id FROM adoptions WHERE animal_id = :id ORDER BY id DESC @@ -650,7 +650,10 @@ trait AnimalBrowseActions $ref_med_administration = $db ->query('SELECT id, name, accro FROM ref_med_administration ORDER BY id') ->fetchAll(); - $vaccines = $db->query('SELECT id, name FROM ref_vaccines ORDER BY id')->fetchAll(); + $vaccines = $db->query('SELECT id, name FROM ref_vaccines ORDER BY name COLLATE NOCASE,id')->fetchAll(); + $vaccineManufacturers = $db + ->query('SELECT id,name FROM ref_vaccine_manufacturers WHERE active=1 ORDER BY name COLLATE NOCASE,id') + ->fetchAll(PDO::FETCH_ASSOC); $inventoryBatches = $db ->query( 'SELECT b.id,b.batch_number,b.expires_on,b.quantity,p.name,p.unit,p.category,p.reference_id FROM inventory_batches b JOIN inventory_products p ON p.id=b.product_id WHERE b.quantity>0 AND p.active=1 ORDER BY p.category,p.name COLLATE NOCASE,date(b.expires_on)', @@ -958,6 +961,7 @@ trait AnimalBrowseActions 'surgeries' => $surgeries, 'ref_med_administration' => $ref_med_administration, 'vaccines' => $vaccines, + 'vaccineManufacturers' => $vaccineManufacturers, 'inventoryBatches' => $inventoryBatches, 'activeUsers' => $activeUsers, 'clinics' => $clinics, diff --git a/app/Controllers/AnimalMedicalActions.php b/app/Controllers/AnimalMedicalActions.php index a8a2c4b..8564d5f 100644 --- a/app/Controllers/AnimalMedicalActions.php +++ b/app/Controllers/AnimalMedicalActions.php @@ -32,6 +32,8 @@ trait AnimalMedicalActions $testDate = date('Y-m-d'); } $testNotes = trim((string) ($_POST['test_notes'] ?? '')); + $testTariffId = (int) ($_POST['test_tariff_id'] ?? 0); + $testClinicId = (int) ($_POST['clinic_contact_id'] ?? 0); $conditions = is_array($_POST['conditions'] ?? null) ? $_POST['conditions'] : []; $db = DB::pdo(); @@ -53,6 +55,7 @@ trait AnimalMedicalActions UPDATE animals SET fiv_status = :fiv_status, felv_status = :felv_status, + fiv_felv_test_date = :test_date, fiv = :fiv, felv = :felv, updated_at = datetime('now') @@ -61,6 +64,7 @@ trait AnimalMedicalActions )->execute([ ':fiv_status' => $fivStatus, ':felv_status' => $felvStatus, + ':test_date' => $testDate, ':fiv' => $fivStatus === 'pos' ? 1 : 0, ':felv' => $felvStatus === 'pos' ? 1 : 0, ':id' => $animalId, @@ -98,6 +102,15 @@ trait AnimalMedicalActions } } + if ($testTariffId > 0) { + $tariffCheck = $db->prepare("SELECT 1 FROM clinic_tariffs WHERE id=? AND clinic_contact_id=? AND category='test' AND active=1"); + $tariffCheck->execute([$testTariffId, $testClinicId]); + if (!$testClinicId || !$tariffCheck->fetchColumn()) { + throw new RuntimeException(t('error.tariff_not_found')); + } + PricingService::record($db, $animalId, $testTariffId, 1, $testDate, $testNotes); + } + $conditionSelect = $db->prepare( 'SELECT status FROM animal_health_conditions WHERE id = :id AND animal_id = :animal_id', ); diff --git a/app/Controllers/AnimalRecordActions.php b/app/Controllers/AnimalRecordActions.php index a604f64..187e74e 100644 --- a/app/Controllers/AnimalRecordActions.php +++ b/app/Controllers/AnimalRecordActions.php @@ -746,6 +746,18 @@ trait AnimalRecordActions ':current_lng' => $currentLng, ]); + // Un décès est une donnée structurée distincte du statut. Lors d'une correction + // explicite d'un animal déclaré à tort décédé, ne pas laisser une ancienne fiche + // de décès continuer à alimenter la bannière et les statistiques. + $oldWasDeceased = in_array(strtolower((string) ($old['status'] ?? '')), ['decede', 'décédé'], true); + $isNowDeceased = in_array(strtolower($status), ['decede', 'décédé'], true); + if ($oldWasDeceased && !$isNowDeceased) { + $db->prepare('DELETE FROM animal_deaths WHERE animal_id=:id')->execute([':id' => $id]); + $db->prepare( + "INSERT INTO animal_history(animal_id,action,type,label,details,user_id) VALUES(:id,'death_correction','update','Décès annulé','La fiche de décès a été retirée après correction du statut de l’animal.',:user)", + )->execute([':id' => $id, ':user' => Auth::id()]); + } + if ($old) { LocationHistoryService::record( $db, diff --git a/app/Controllers/DashboardController.php b/app/Controllers/DashboardController.php index 098b7b6..08ed945 100644 --- a/app/Controllers/DashboardController.php +++ b/app/Controllers/DashboardController.php @@ -119,7 +119,7 @@ final class DashboardController JOIN ref_vaccines rv ON rv.id = v.vaccine_id WHERE v.due_date IS NOT NULL AND a.deleted_at IS NULL AND a.archived_at IS NULL - AND lower(a.status) NOT IN ('decede','décédé') + AND lower(a.status) NOT IN ('decede','décédé','adopte','adopté') AND date(v.due_date) < date('now') ORDER BY date(v.due_date) ASC LIMIT 50 @@ -135,7 +135,7 @@ final class DashboardController JOIN ref_vaccines rv ON rv.id = v.vaccine_id WHERE v.due_date IS NOT NULL AND a.deleted_at IS NULL AND a.archived_at IS NULL - AND lower(a.status) NOT IN ('decede','décédé') + AND lower(a.status) NOT IN ('decede','décédé','adopte','adopté') AND date(v.due_date) >= date('now') AND date(v.due_date) <= date('now', '+7 day') ORDER BY date(v.due_date) ASC @@ -152,7 +152,7 @@ final class DashboardController JOIN ref_vaccines rv ON rv.id = v.vaccine_id WHERE v.due_date IS NOT NULL AND a.deleted_at IS NULL AND a.archived_at IS NULL - AND lower(a.status) NOT IN ('decede','décédé') + AND lower(a.status) NOT IN ('decede','décédé','adopte','adopté') AND date(v.due_date) > date('now', '+7 day') AND date(v.due_date) <= date('now', '+30 day') ORDER BY date(v.due_date) ASC @@ -162,7 +162,7 @@ final class DashboardController $vaccines_30d = $stmt->fetchAll(); // Traitements en cours - $stmt = $db->prepare(' + $stmt = $db->prepare(" SELECT a.id, a.name, a.internal_code AS code, m.name AS medication_name, t.start_date, t.end_date, t.dose_text, t.route @@ -171,9 +171,10 @@ final class DashboardController JOIN ref_medications m ON m.id = t.medication_id WHERE t.ongoing = 1 AND a.deleted_at IS NULL AND a.archived_at IS NULL + AND lower(a.status) NOT IN ('decede','décédé','adopte','adopté') ORDER BY date(t.start_date) DESC, a.name ASC LIMIT 50 - '); + "); $stmt->execute(); $treatments = $stmt->fetchAll(); @@ -185,6 +186,7 @@ final class DashboardController JOIN ref_medications m ON m.id = t.medication_id WHERE t.ongoing = 1 AND a.deleted_at IS NULL AND a.archived_at IS NULL + AND lower(a.status) NOT IN ('decede','décédé','adopte','adopté') AND t.end_date IS NOT NULL AND date(t.end_date) < date('now') ORDER BY date(t.end_date) ASC diff --git a/app/Controllers/DirectoryController.php b/app/Controllers/DirectoryController.php index b52389f..4f96200 100644 --- a/app/Controllers/DirectoryController.php +++ b/app/Controllers/DirectoryController.php @@ -15,6 +15,10 @@ final class DirectoryController 'dirigeant' => t('role.dirigeant'), 'fa' => t('role.fa'), 'fourriere' => t('role.fourriere'), + 'pet_sitter' => t('role.pet_sitter'), + 'refuge_partenaire' => t('role.refuge_partenaire'), + 'toiletteur' => t('role.toiletteur'), + 'professionnel_autre' => t('role.professionnel_autre'), 'veterinaire' => t('role.veterinaire'), ]; } diff --git a/app/Controllers/SettingsController.php b/app/Controllers/SettingsController.php index 35d2ff4..e731eb4 100644 --- a/app/Controllers/SettingsController.php +++ b/app/Controllers/SettingsController.php @@ -1227,6 +1227,68 @@ final class SettingsController header('X-Content-Type-Options: nosniff'); readfile($path); } + public static function catalogs(): void + { + self::admin(); + $db = DB::pdo(); + render('settings_catalogs.php', [ + 'title' => t('settings.catalogs'), + 'catalogs' => [ + 'medication' => $db->query('SELECT id,name,molecule extra FROM ref_medications ORDER BY name COLLATE NOCASE')->fetchAll(PDO::FETCH_ASSOC), + 'vaccine' => $db->query('SELECT id,name,code extra FROM ref_vaccines ORDER BY name COLLATE NOCASE')->fetchAll(PDO::FETCH_ASSOC), + 'dewormer' => $db->query('SELECT id,name,active_ingredient extra FROM ref_dewormers ORDER BY name COLLATE NOCASE')->fetchAll(PDO::FETCH_ASSOC), + 'manufacturer' => $db->query('SELECT id,name,NULL extra FROM ref_vaccine_manufacturers ORDER BY name COLLATE NOCASE')->fetchAll(PDO::FETCH_ASSOC), + ], + 'saved' => isset($_GET['saved']), + 'error' => (string) ($_GET['error'] ?? ''), + ]); + } + public static function saveCatalogItem(): void + { + self::admin(); self::postOnly(); + $type = (string) ($_POST['type'] ?? ''); + $id = (int) ($_POST['id'] ?? 0); + $name = trim((string) ($_POST['name'] ?? '')); + $extra = trim((string) ($_POST['extra'] ?? '')) ?: null; + $map = [ + 'medication' => ['ref_medications','molecule'], 'vaccine' => ['ref_vaccines','code'], + 'dewormer' => ['ref_dewormers','active_ingredient'], 'manufacturer' => ['ref_vaccine_manufacturers',null], + ]; + if ($name === '' || !isset($map[$type])) { + header('Location: /settings/catalogs?error=' . rawurlencode(t('catalog.invalid'))); exit(); + } + [$table,$extraColumn] = $map[$type]; + try { + if ($id) { + $sql = "UPDATE $table SET name=?" . ($extraColumn ? ",$extraColumn=?" : '') . ' WHERE id=?'; + DB::pdo()->prepare($sql)->execute($extraColumn ? [$name,$extra,$id] : [$name,$id]); + } else { + $sql = "INSERT INTO $table(name" . ($extraColumn ? ",$extraColumn" : '') . ') VALUES(?' . ($extraColumn ? ',?' : '') . ')'; + DB::pdo()->prepare($sql)->execute($extraColumn ? [$name,$extra] : [$name]); + $id = (int) DB::pdo()->lastInsertId(); + } + AuditService::log('catalog_saved', '/settings/catalogs/save', 'Catalogue modifié : ' . $name, $type, $id); + header('Location: /settings/catalogs?saved=1'); + } catch (Throwable $e) { + header('Location: /settings/catalogs?error=' . rawurlencode(SecurityService::publicError($e))); + } + exit(); + } + public static function deleteCatalogItem(): void + { + self::admin(); self::postOnly(); + $type = (string) ($_POST['type'] ?? ''); $id = (int) ($_POST['id'] ?? 0); + $tables = ['medication'=>'ref_medications','vaccine'=>'ref_vaccines','dewormer'=>'ref_dewormers','manufacturer'=>'ref_vaccine_manufacturers']; + if (!$id || !isset($tables[$type])) { http_response_code(400); return; } + try { + DB::pdo()->prepare('DELETE FROM ' . $tables[$type] . ' WHERE id=?')->execute([$id]); + AuditService::log('catalog_deleted', '/settings/catalogs/delete', 'Élément de catalogue supprimé', $type, $id); + header('Location: /settings/catalogs?saved=1'); + } catch (Throwable) { + header('Location: /settings/catalogs?error=' . rawurlencode(t('catalog.in_use'))); + } + exit(); + } private static function postOnly(): void { if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') { diff --git a/app/Controllers/SurgeriesController.php b/app/Controllers/SurgeriesController.php index a0739d8..bc5c383 100644 --- a/app/Controllers/SurgeriesController.php +++ b/app/Controllers/SurgeriesController.php @@ -56,6 +56,16 @@ final class SurgeriesController } $db = DB::pdo(); $tariff = (int) ($_POST['tariff_id'] ?? 0) ?: null; + $clinic = (int) ($_POST['clinic_contact_id'] ?? 0) ?: null; + if ($tariff) { + $validTariff = $db->prepare('SELECT 1 FROM clinic_tariffs WHERE id=? AND clinic_contact_id=? AND active=1'); + $validTariff->execute([$tariff, $clinic]); + if (!$clinic || !$validTariff->fetchColumn()) { + http_response_code(400); + echo h(t('surgery.invalid_data')); + return; + } + } $amountRaw = str_replace(',', '.', trim((string) ($_POST['amount'] ?? ''))); $amount = $amountRaw === '' ? null : (int) round(max(0, (float) $amountRaw) * 100); if ($amount === null && $tariff) { @@ -77,7 +87,7 @@ final class SurgeriesController $name, $date, $status, - (int) ($_POST['clinic_contact_id'] ?? 0) ?: null, + $clinic, (int) ($_POST['veterinarian_contact_id'] ?? 0) ?: null, $tariff, $amount, @@ -92,7 +102,7 @@ final class SurgeriesController 'INSERT INTO animal_expenses(animal_id,clinic_contact_id,tariff_id,source_type,source_id,label,occurred_on,quantity,catalog_unit_cents,discount_percent,total_cents,notes,created_by) VALUES(?,?,?,\'surgery\',?,?,?,1,?,0,?,?,?)', )->execute([ $animal, - (int) ($_POST['clinic_contact_id'] ?? 0) ?: null, + $clinic, $tariff, $id, $name, diff --git a/app/Routes/web.php b/app/Routes/web.php index 81f4db3..ac1a2bf 100644 --- a/app/Routes/web.php +++ b/app/Routes/web.php @@ -338,6 +338,15 @@ switch ($path) { case '/settings/pricing': SettingsController::pricing(); break; + case '/settings/catalogs': + SettingsController::catalogs(); + break; + case '/settings/catalogs/save': + SettingsController::saveCatalogItem(); + break; + case '/settings/catalogs/delete': + SettingsController::deleteCatalogItem(); + break; case '/settings/pricing/tariff': SettingsController::saveTariff(); break; diff --git a/app/Services/IcadService.php b/app/Services/IcadService.php index ef381d3..eda60ea 100644 --- a/app/Services/IcadService.php +++ b/app/Services/IcadService.php @@ -112,8 +112,19 @@ final class IcadService ]); $this->assertSuccess($r2, t('service.icad.login_failed')); + // Le point d'entrée renvoie désormais du JSON et fournit l'adresse réelle + // de l'espace connecté. L'ancienne URL fixe /pro répond en 404 depuis 2026. + $loginResponse = json_decode(trim((string) $r2['body']), true); + if (is_array($loginResponse) && trim((string) ($loginResponse['error'] ?? '')) !== '') { + throw new RuntimeException(t('service.icad.login_failed')); + } + $accountPath = is_array($loginResponse) ? trim((string) ($loginResponse['address'] ?? '')) : ''; + if ($accountPath === '' || !str_starts_with($accountPath, '/')) { + $accountPath = '/acteur'; + } + // 3) Vérif session - $r2b = $this->req('https://www.i-cad.fr/pro', $cookie); + $r2b = $this->req('https://www.i-cad.fr' . $accountPath, $cookie); $this->assertSuccess($r2b, t('service.icad.login_failed')); if (stripos($r2b['body'], 'Se connecter') !== false || str_contains((string)$r2b['effective_url'], '/login')) { throw new RuntimeException(t('service.icad.login_failed')); diff --git a/app/Services/NotificationService.php b/app/Services/NotificationService.php index 6e56133..3684bcb 100644 --- a/app/Services/NotificationService.php +++ b/app/Services/NotificationService.php @@ -20,14 +20,14 @@ final class NotificationService 'treatments', 'danger', t('notification.treatments_overdue'), - "SELECT COUNT(*) FROM treatments t JOIN animals a ON a.id=t.animal_id WHERE t.ongoing=1 AND t.end_date IS NOT NULL AND date(t.end_date)date(d.administered_on))", + "SELECT COUNT(*) FROM dewormings d JOIN animals a ON a.id=d.animal_id WHERE d.next_due_date IS NOT NULL AND date(d.next_due_date)<=date('now','+7 day') AND a.deleted_at IS NULL AND a.archived_at IS NULL AND lower(a.status) NOT IN ('decede','décédé','adopte','adopté') AND NOT EXISTS(SELECT 1 FROM dewormings newer WHERE newer.animal_id=d.animal_id AND date(newer.administered_on)>date(d.administered_on))", '/dashboard', ], ]; diff --git a/app/Views/_animal_show_identity.php b/app/Views/_animal_show_identity.php index 3d9488c..d1d6b9a 100644 --- a/app/Views/_animal_show_identity.php +++ b/app/Views/_animal_show_identity.php @@ -4,7 +4,7 @@
- + DeathController::causeLabel($key), array_keys(DeathController::CAUSES))); $deathPlaceLabels = array_combine(array_keys(DeathController::PLACES), array_map(static fn ($key) => DeathController::placeLabel($key), array_keys(DeathController::PLACES))); @@ -221,11 +221,15 @@ $hasCurrent = ($currentLat !== null && $currentLng !== null);
- +
:
- + +
diff --git a/app/Views/_animal_show_medical.php b/app/Views/_animal_show_medical.php index 57e97f0..7835891 100644 --- a/app/Views/_animal_show_medical.php +++ b/app/Views/_animal_show_medical.php @@ -22,6 +22,10 @@
FeLV
+
+
+
+
@@ -423,6 +427,9 @@ ORDER BY created_at ASC
+
+ +
diff --git a/app/Views/_animal_show_modals.php b/app/Views/_animal_show_modals.php index 46b1474..7c58636 100644 --- a/app/Views/_animal_show_modals.php +++ b/app/Views/_animal_show_modals.php @@ -300,7 +300,11 @@ $currentTime = date('H:i');
- + + + +
@@ -619,13 +623,29 @@ document.addEventListener('DOMContentLoaded', () => {
- +
+
+ + +
+
+ + +

@@ -694,6 +714,22 @@ document.addEventListener('DOMContentLoaded', () => {
+
diff --git a/app/Views/dashboard.php b/app/Views/dashboard.php index dd8b24b..f2f4575 100644 --- a/app/Views/dashboard.php +++ b/app/Views/dashboard.php @@ -118,6 +118,28 @@ $evening = $careToday['evening'] ?? ['expected' => 0,'checked' => 0,'alerts' =>
+
@@ -264,7 +286,7 @@ if ($grantAlertTotal > 0): ?>
🚨
- +
@@ -290,7 +312,7 @@ if ($grantAlertTotal > 0): ?>
⏳
-
+
@@ -315,7 +337,7 @@ if ($grantAlertTotal > 0): ?>
📅
-
+
diff --git a/app/Views/evolutions.php b/app/Views/evolutions.php index 93df138..e4cef25 100644 --- a/app/Views/evolutions.php +++ b/app/Views/evolutions.php @@ -31,10 +31,10 @@
- 1.0.0-rc.28 -

Saisies médicales corrigibles

-

Vaccinations et traitements antiparasitaires peuvent être rectifiés sans - déclencher une seconde sortie de stock ou une nouvelle dépense.

+ 1.0.0-rc.30 +

Données fiabilisées sur le terrain

+

Décès historiques corrigibles, synchronisation I-CAD réparée, catalogues + médicaux administrables et tarifs filtrés par clinique.

@@ -43,8 +43,8 @@
Après la 1.0

Prochaines étapes

-

Tests HTTP plus complets, validation des paquets d’installation et amélioration - continue des parcours.

+

Réinitialisation planifiée des démonstrations, normalisation assistée des + contacts, validation des paquets YunoHost, ZimaOS et CasaOS, puis amélioration continue des parcours.

diff --git a/app/Views/settings_catalogs.php b/app/Views/settings_catalogs.php new file mode 100644 index 0000000..bcdae9b --- /dev/null +++ b/app/Views/settings_catalogs.php @@ -0,0 +1,17 @@ +t('animal.medication'),'vaccine'=>t('animal.vaccine'),'dewormer'=>t('animals.dewormer'),'manufacturer'=>t('animal.manufacturer')]; +$extras = ['medication'=>t('animal.molecule'),'vaccine'=>t('catalog.code'),'dewormer'=>t('catalog.active_ingredient'),'manufacturer'=>'']; ?> +
+
+

+
+ $rows): ?>
+

+
+
+
+
+
+
+
+ diff --git a/app/Views/settings_nav.php b/app/Views/settings_nav.php index ba4a8bb..49f5ea9 100644 --- a/app/Views/settings_nav.php +++ b/app/Views/settings_nav.php @@ -8,6 +8,7 @@ $settingsSection = match(true) { str_starts_with($settingsPath, '/settings/rooms') => 'rooms', str_starts_with($settingsPath, '/settings/pricing') => 'pricing', str_starts_with($settingsPath, '/settings/inventory') => 'inventory', + str_starts_with($settingsPath, '/settings/catalogs') => 'catalogs', str_starts_with($settingsPath, '/settings/lab-references') => 'lab-references', str_starts_with($settingsPath, '/settings/public-site') => 'public-site', str_starts_with($settingsPath, '/settings/backups') => 'backups', diff --git a/app/Views/settings_public_site.php b/app/Views/settings_public_site.php index d36c089..e875937 100644 --- a/app/Views/settings_public_site.php +++ b/app/Views/settings_public_site.php @@ -173,6 +173,14 @@ const insertImage = (button) => { textarea.setRangeText(`![${safeAlt}](${button.dataset.url})\n`, textarea.selectionStart, textarea.selectionEnd, 'end'); textarea.focus(); }; +document.querySelectorAll('textarea').forEach((textarea) => { + const resize = () => { + textarea.style.height = 'auto'; + textarea.style.height = Math.max(140, textarea.scrollHeight + 8) + 'px'; + }; + textarea.addEventListener('input', resize); + resize(); +}); document.querySelectorAll('.public-rich-text').forEach((textarea) => { textarea.addEventListener('focus', () => { activePublicEditor = textarea; }); const toolbar = document.createElement('div'); diff --git a/app/Views/surgery_form.php b/app/Views/surgery_form.php index ba39841..685a84b 100644 --- a/app/Views/surgery_form.php +++ b/app/Views/surgery_form.php @@ -8,7 +8,7 @@
+
+
Les tarifs affichés correspondent à la structure sélectionnée.