Corriger les dossiers animaux et le suivi médical

This commit is contained in:
Alexandre NOEL 2026-10-01 15:22:09 +02:00
commit 7b89fd5dfa
34 changed files with 440 additions and 39 deletions

View file

@ -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,

View file

@ -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',
);

View file

@ -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,

View file

@ -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

View file

@ -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'),
];
}

View file

@ -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') {

View file

@ -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,