'Maladie', 'accident' => 'Accident', 'old_age' => 'Vieillesse', 'congenital' => 'Malformation ou affection congénitale', 'surgery_complication' => 'Complication médicale ou chirurgicale', 'poisoning' => 'Empoisonnement', 'unknown' => 'Cause inconnue', 'other' => 'Autre cause', ]; public const PLACES = [ 'refuge' => 'Au refuge', 'fa' => 'En famille d’accueil', 'veterinaire' => 'Chez le vétérinaire', 'exterieur' => 'À l’extérieur', 'inconnu' => 'Lieu inconnu', 'autre' => 'Autre lieu', ]; public const DISPOSITIONS = [ 'pending' => 'À organiser', 'collective_cremation' => 'Crémation collective', 'individual_cremation' => 'Crémation individuelle', 'returned_family' => 'Corps ou cendres remis à la famille', 'burial' => 'Inhumation', 'other' => 'Autre prise en charge', ]; public static function causeLabel(string $key): string { return t('death.cause.' . $key, [], self::CAUSES[$key] ?? $key); } public static function placeLabel(string $key): string { return t('death.place.' . $key, [], self::PLACES[$key] ?? $key); } public static function dispositionLabel(string $key): string { return t('death.disposition.' . $key, [], self::DISPOSITIONS[$key] ?? $key); } public static function form(): void { $db = DB::pdo(); $id = (int) ($_GET['id'] ?? 0); $animal = self::animal($db, $id); $stmt = $db->prepare( 'SELECT ad.*,vet.name veterinarian_name,crem.name crematorium_name FROM animal_deaths ad LEFT JOIN directory_contacts vet ON vet.id=ad.veterinarian_contact_id LEFT JOIN directory_contacts crem ON crem.id=ad.crematorium_contact_id WHERE ad.animal_id=:id', ); $stmt->execute([':id' => $id]); $death = $stmt->fetch(PDO::FETCH_ASSOC) ?: [ 'animal_id' => $id, 'deceased_date' => date('Y-m-d'), 'cause_code' => 'disease', 'cause_details' => '', 'occurred_in_care' => !in_array($animal['status'], ['adopte', 'adopté'], true) ? 1 : 0, 'place_type' => 'refuge', 'place_details' => '', 'euthanized' => 0, 'veterinarian_contact_id' => '', 'crematorium_contact_id' => '', 'body_disposition' => 'pending', 'cremation_date' => '', 'notes' => '', ]; $veterinarians = self::contacts($db, 'veterinaire'); $crematoriums = self::contacts($db, 'crematorium'); render('animal_death_form.php', [ 'title' => t('death.title', ['name' => $animal['name']]), 'animal' => $animal, 'death' => $death, 'causes' => self::CAUSES, 'places' => self::PLACES, 'dispositions' => self::DISPOSITIONS, 'veterinarians' => $veterinarians, 'crematoriums' => $crematoriums, ]); } public static function save(): void { if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST') { http_response_code(405); return; } $db = DB::pdo(); $id = (int) ($_POST['animal_id'] ?? 0); $animal = self::animal($db, $id); $date = trim((string) ($_POST['deceased_date'] ?? '')); $cause = (string) ($_POST['cause_code'] ?? ''); $place = (string) ($_POST['place_type'] ?? ''); $disposition = (string) ($_POST['body_disposition'] ?? 'pending'); if ( !self::validDate($date) || $date > date('Y-m-d') || !isset(self::CAUSES[$cause]) || !isset(self::PLACES[$place]) || !isset(self::DISPOSITIONS[$disposition]) ) { http_response_code(400); echo h(t('death.invalid')); return; } if ($cause === 'other' && trim((string) ($_POST['cause_details'] ?? '')) === '') { http_response_code(400); echo h(t('death.cause_required')); return; } $vet = self::validContact($db, (int) ($_POST['veterinarian_contact_id'] ?? 0), 'veterinaire'); $crem = self::validContact($db, (int) ($_POST['crematorium_contact_id'] ?? 0), 'crematorium'); $cremationDate = trim((string) ($_POST['cremation_date'] ?? '')); if ($cremationDate !== '' && (!self::validDate($cremationDate) || $cremationDate < $date)) { http_response_code(400); echo h(t('death.cremation_after')); return; } $check = $db->prepare('SELECT 1 FROM animal_deaths WHERE animal_id=:id'); $check->execute([':id' => $id]); $isUpdate = (bool) $check->fetchColumn(); $beforeLocation = LocationHistoryService::snapshot($animal); $params = [ ':animal' => $id, ':date' => $date, ':cause' => $cause, ':details' => trim((string) ($_POST['cause_details'] ?? '')) ?: null, ':care' => isset($_POST['occurred_in_care']) ? 1 : 0, ':place' => $place, ':place_details' => trim((string) ($_POST['place_details'] ?? '')) ?: null, ':euthanized' => isset($_POST['euthanized']) ? 1 : 0, ':vet' => $vet, ':crem' => $crem, ':disposition' => $disposition, ':cremation_date' => $cremationDate ?: null, ':notes' => trim((string) ($_POST['notes'] ?? '')) ?: null, ':user' => Auth::id(), ]; $db->beginTransaction(); try { $db->prepare( "INSERT INTO animal_deaths(animal_id,deceased_date,cause_code,cause_details,occurred_in_care,place_type,place_details,euthanized,veterinarian_contact_id,crematorium_contact_id,body_disposition,cremation_date,notes,created_by,updated_by) VALUES(:animal,:date,:cause,:details,:care,:place,:place_details,:euthanized,:vet,:crem,:disposition,:cremation_date,:notes,:user,:user) ON CONFLICT(animal_id) DO UPDATE SET deceased_date=excluded.deceased_date,cause_code=excluded.cause_code,cause_details=excluded.cause_details,occurred_in_care=excluded.occurred_in_care,place_type=excluded.place_type,place_details=excluded.place_details,euthanized=excluded.euthanized,veterinarian_contact_id=excluded.veterinarian_contact_id,crematorium_contact_id=excluded.crematorium_contact_id,body_disposition=excluded.body_disposition,cremation_date=excluded.cremation_date,notes=excluded.notes,updated_by=excluded.updated_by,updated_at=datetime('now')", )->execute($params); $db->prepare( "UPDATE animals SET status='decede',refuge_room=NULL,care_box_key=NULL,updated_at=datetime('now') WHERE id=:id", )->execute([':id' => $id]); LocationHistoryService::record( $db, $id, $beforeLocation, [ 'status' => 'decede', 'refuge_room' => null, 'care_box_key' => null, 'current_address' => $animal['current_address'] ?? null, ], 'death', 'Décès enregistré', $date . ' 12:00:00', ); $db->prepare( 'UPDATE treatments SET ongoing=0,end_date=COALESCE(end_date,:date) WHERE animal_id=:id AND ongoing=1', )->execute([':date' => $date, ':id' => $id]); $details = 'Date : ' . self::frDate($date) . '\nCause : ' . self::CAUSES[$cause] . '\nContexte : ' . ($params[':care'] ? 'Sous la responsabilité du refuge' : 'Après la prise en charge du refuge'); if ($params[':euthanized']) { $details .= '\nEuthanasie : oui'; } $db->prepare( "INSERT INTO animal_history(animal_id,type,label,details,user_id) VALUES(:id,'death',:label,:details,:user)", )->execute([ ':id' => $id, ':label' => $isUpdate ? 'Informations de décès modifiées' : 'Décès enregistré', ':details' => $details, ':user' => Auth::id(), ]); self::syncMovement( $db, $id, $date, $cause, $params[':details'], $place, $params[':place_details'], $params[':euthanized'] === 1, $vet, $crem, $disposition, ); $db->commit(); header('Location: /animal?id=' . $id); exit(); } catch (Throwable $e) { if ($db->inTransaction()) { $db->rollBack(); } throw $e; } } private static function animal(PDO $db, int $id): array { $s = $db->prepare('SELECT * FROM animals WHERE id=:id AND deleted_at IS NULL'); $s->execute([':id' => $id]); $a = $s->fetch(PDO::FETCH_ASSOC); if (!$a) { http_response_code(404); echo h(t('error.animal_not_found')); exit(); } return $a; } private static function contacts(PDO $db, string $role): array { $s = $db->prepare( 'SELECT dc.id,dc.name,dc.city FROM directory_contacts dc JOIN directory_contact_roles r ON r.contact_id=dc.id WHERE r.role=:role AND dc.deleted_at IS NULL ORDER BY dc.name COLLATE NOCASE', ); $s->execute([':role' => $role]); return $s->fetchAll(PDO::FETCH_ASSOC); } private static function validContact(PDO $db, int $id, string $role): ?int { if (!$id) { return null; } $s = $db->prepare( 'SELECT 1 FROM directory_contacts dc JOIN directory_contact_roles r ON r.contact_id=dc.id WHERE dc.id=:id AND r.role=:role AND dc.deleted_at IS NULL', ); $s->execute([':id' => $id, ':role' => $role]); return $s->fetchColumn() ? $id : null; } private static function syncMovement( PDO $db, int $animalId, string $date, string $cause, ?string $causeDetails, string $place, ?string $placeDetails, bool $euthanized, ?int $vetId, ?int $cremId, string $disposition, ): void { $names = []; if ($vetId || $cremId) { $s = $db->prepare('SELECT id,name FROM directory_contacts WHERE id IN (:vet,:crem)'); $s->execute([':vet' => $vetId ?? 0, ':crem' => $cremId ?? 0]); foreach ($s->fetchAll(PDO::FETCH_ASSOC) as $row) { $names[(int) $row['id']] = $row['name']; } } $noteParts = ['Cause : ' . self::CAUSES[$cause] . ($causeDetails ? ' — ' . $causeDetails : '')]; if ($euthanized) { $noteParts[] = 'Euthanasie : oui'; } if ($cremId && isset($names[$cremId])) { $noteParts[] = 'Crématorium : ' . $names[$cremId]; } $noteParts[] = 'Prise en charge : ' . self::DISPOSITIONS[$disposition]; $s = $db->prepare( "SELECT id FROM animal_movements WHERE animal_id=:animal AND kind='exit' AND lower(lieu) IN ('décès','deces','décès de l''animal','deces de l''animal') ORDER BY id DESC LIMIT 1", ); $s->execute([':animal' => $animalId]); $movementId = (int) $s->fetchColumn(); $values = [ ':animal' => $animalId, ':place' => self::PLACES[$place] . ($placeDetails ? ' — ' . $placeDetails : ''), ':contact' => $vetId && isset($names[$vetId]) ? $names[$vetId] : null, ':note' => implode("\n", $noteParts), ':at' => $date . ' 12:00:00', ]; if ($movementId) { $values[':movement'] = $movementId; $db->prepare( "UPDATE animal_movements SET place=:place,lieu='Décès de l''animal',contact_name=:contact,note=:note,created_at=:at WHERE id=:movement AND animal_id=:animal", )->execute($values); } else { $db->prepare( "INSERT INTO animal_movements(animal_id,kind,place,lieu,contact_name,note,created_at) VALUES(:animal,'exit',:place,'Décès de l''animal',:contact,:note,:at)", )->execute($values); } } private static function validDate(string $date): bool { $d = DateTimeImmutable::createFromFormat('!Y-m-d', $date); $errors = DateTimeImmutable::getLastErrors(); return $d !== false && ($errors === false || ((int) $errors['warning_count'] === 0 && (int) $errors['error_count'] === 0)) && $d->format('Y-m-d') === $date; } private static function frDate(string $date): string { $d = DateTimeImmutable::createFromFormat('Y-m-d', $date); return $d ? $d->format('d/m/Y') : $date; } }