305 lines
13 KiB
PHP
305 lines
13 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
final class DeathController
|
||
{
|
||
public const CAUSES = [
|
||
'disease' => '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;
|
||
}
|
||
}
|