463 lines
21 KiB
PHP
463 lines
21 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
final class CareRoundsController
|
||
{
|
||
public static function index(): void
|
||
{
|
||
$db = DB::pdo();
|
||
$period = ($_GET['period'] ?? 'morning') === 'evening' ? 'evening' : 'morning';
|
||
$date = preg_match('/^\d{4}-\d{2}-\d{2}$/', (string) ($_GET['date'] ?? ''))
|
||
? (string) $_GET['date']
|
||
: date('Y-m-d');
|
||
$stmt = $db->prepare('SELECT * FROM care_rounds WHERE round_date=:date AND period=:period');
|
||
$stmt->execute([':date' => $date, ':period' => $period]);
|
||
$round = $stmt->fetch();
|
||
if (!$round) {
|
||
$round = ['id' => 0, 'round_date' => $date, 'period' => $period, 'performed_by_user_id' => null];
|
||
}
|
||
$layouts = [];
|
||
foreach ($db->query('SELECT * FROM care_room_layouts')->fetchAll() as $l) {
|
||
$layouts[$l['room']] = $l;
|
||
}
|
||
$animals = $db
|
||
->query(
|
||
"
|
||
SELECT a.*, p.filename AS primary_photo
|
||
FROM animals a LEFT JOIN animal_photos p ON p.animal_id=a.id AND p.is_primary=1
|
||
WHERE a.deleted_at IS NULL AND a.archived_at IS NULL
|
||
AND lower(a.status) IN ('refuge','soin','quarantaine')
|
||
ORDER BY a.name COLLATE NOCASE
|
||
",
|
||
)
|
||
->fetchAll();
|
||
$observations = [];
|
||
$administrations = [];
|
||
if ((int) $round['id'] > 0) {
|
||
$stmt = $db->prepare('SELECT * FROM care_round_observations WHERE round_id=:id');
|
||
$stmt->execute([':id' => $round['id']]);
|
||
foreach ($stmt->fetchAll() as $o) {
|
||
$observations[(int) $o['animal_id']] = $o;
|
||
}
|
||
$stmt = $db->prepare('SELECT treatment_id,status FROM treatment_administrations WHERE round_id=:id');
|
||
$stmt->execute([':id' => $round['id']]);
|
||
foreach ($stmt->fetchAll() as $a) {
|
||
$administrations[(int) $a['treatment_id']] = $a['status'];
|
||
}
|
||
}
|
||
$treatments = [];
|
||
$stmt = $db->prepare("
|
||
SELECT t.*,m.name AS medication_name FROM treatments t JOIN ref_medications m ON m.id=t.medication_id
|
||
WHERE t.ongoing=1 AND ((:period='morning' AND t.give_morning=1) OR (:period='evening' AND t.give_evening=1))
|
||
ORDER BY m.name
|
||
");
|
||
$stmt->execute([':period' => $period]);
|
||
foreach ($stmt->fetchAll() as $t) {
|
||
$treatments[(int) $t['animal_id']][] = $t;
|
||
}
|
||
$unconfigured = [];
|
||
$rows = $db
|
||
->query(
|
||
'SELECT t.id,t.animal_id,t.dose_text,m.name medication_name FROM treatments t JOIN ref_medications m ON m.id=t.medication_id WHERE t.ongoing=1 AND t.give_morning=0 AND t.give_evening=0 AND t.give_as_needed=0 ORDER BY m.name',
|
||
)
|
||
->fetchAll();
|
||
foreach ($rows as $r) {
|
||
$unconfigured[(int) $r['animal_id']][] = $r;
|
||
}
|
||
render('care_round.php', [
|
||
'title' => t('care.title'),
|
||
'pageDescription' => t('care.description'),
|
||
'period' => $period,
|
||
'date' => $date,
|
||
'round' => $round,
|
||
'layouts' => $layouts,
|
||
'rooms' => ShelterRoomService::all(true),
|
||
'animals' => $animals,
|
||
'treatmentsByAnimal' => $treatments,
|
||
'unconfiguredTreatments' => $unconfigured,
|
||
'observations' => $observations,
|
||
'administrations' => $administrations,
|
||
]);
|
||
}
|
||
|
||
public static function saveObservation(): void
|
||
{
|
||
if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST') {
|
||
http_response_code(405);
|
||
return;
|
||
}
|
||
$db = DB::pdo();
|
||
$roundId = self::ensureRound($db);
|
||
$animalId = (int) ($_POST['animal_id'] ?? 0);
|
||
if (!$roundId || !$animalId) {
|
||
http_response_code(400);
|
||
return;
|
||
}
|
||
$animalCheck = $db->prepare(
|
||
"SELECT 1 FROM animals WHERE id=? AND deleted_at IS NULL AND archived_at IS NULL AND lower(status) IN ('refuge','soin','quarantaine')",
|
||
);
|
||
$animalCheck->execute([$animalId]);
|
||
if (!$animalCheck->fetchColumn()) {
|
||
http_response_code(404);
|
||
return;
|
||
}
|
||
$allowed = [
|
||
'food' => ['normal', 'little', 'refused'],
|
||
'water' => ['normal', 'renewed', 'abnormal'],
|
||
'urine' => ['normal', 'absent', 'abnormal'],
|
||
'stool' => ['normal', 'absent', 'abnormal'],
|
||
'general_state' => ['good', 'watch', 'alert'],
|
||
];
|
||
$values = [];
|
||
foreach ($allowed as $field => $opts) {
|
||
$value = (string) ($_POST[$field] ?? $opts[0]);
|
||
$values[$field] = in_array($value, $opts, true) ? $value : $opts[0];
|
||
}
|
||
$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]);
|
||
$comment = trim((string) ($_POST['comment'] ?? ''));
|
||
$photoComment = trim((string) ($_POST['photo_comment'] ?? ''));
|
||
$photo = $_FILES['care_photo'] ?? null;
|
||
$hasPhoto = is_array($photo) && ($photo['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_NO_FILE;
|
||
if ($hasPhoto) {
|
||
if (
|
||
($photo['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK ||
|
||
!is_uploaded_file((string) $photo['tmp_name']) ||
|
||
($photo['size'] ?? 0) > 20 * 1024 * 1024 ||
|
||
$photoComment === ''
|
||
) {
|
||
http_response_code(400);
|
||
echo h(t('care.invalid_photo'));
|
||
return;
|
||
}
|
||
$imageInfo = @getimagesize((string) $photo['tmp_name']);
|
||
$mime = (string) ($imageInfo['mime'] ?? '');
|
||
$extensions = ['image/jpeg' => 'jpg', 'image/png' => 'png', 'image/webp' => 'webp', 'image/gif' => 'gif'];
|
||
if (!isset($extensions[$mime])) {
|
||
http_response_code(400);
|
||
echo h(t('care.invalid_photo_format'));
|
||
return;
|
||
}
|
||
}
|
||
$admins = is_array($_POST['treatments'] ?? null) ? $_POST['treatments'] : [];
|
||
$skippedIds = [];
|
||
foreach ($admins as $treatmentId => $status) {
|
||
if ($status === 'skipped') {
|
||
$skippedIds[] = (int) $treatmentId;
|
||
}
|
||
}
|
||
$alerts = [];
|
||
if ($values['food'] === 'little') {
|
||
$alerts[] = 'A peu mangé';
|
||
} elseif ($values['food'] === 'refused') {
|
||
$alerts[] = 'A refusé de manger';
|
||
}
|
||
if ($values['water'] === 'abnormal') {
|
||
$alerts[] = 'Eau anormale';
|
||
}
|
||
if ($values['urine'] === 'absent') {
|
||
$alerts[] = 'Urines absentes';
|
||
} elseif ($values['urine'] === 'abnormal') {
|
||
$alerts[] = 'Urines anormales';
|
||
}
|
||
if ($values['stool'] === 'absent') {
|
||
$alerts[] = 'Selles absentes';
|
||
} elseif ($values['stool'] === 'abnormal') {
|
||
$alerts[] = 'Selles anormales';
|
||
}
|
||
if ($values['general_state'] === 'watch') {
|
||
$alerts[] = 'État général à surveiller';
|
||
} elseif ($values['general_state'] === 'alert') {
|
||
$alerts[] = 'Alerte sur l’état général';
|
||
}
|
||
if ($comment !== '') {
|
||
$alerts[] = 'Commentaire : ' . $comment;
|
||
}
|
||
foreach ($skippedIds as $treatmentId) {
|
||
$nameStmt = $db->prepare(
|
||
'SELECT m.name FROM treatments t LEFT JOIN ref_medications m ON m.id=t.medication_id WHERE t.id=:id AND t.animal_id=:animal',
|
||
);
|
||
$nameStmt->execute([':id' => $treatmentId, ':animal' => $animalId]);
|
||
$alerts[] = 'Traitement non donné : ' . ($nameStmt->fetchColumn() ?: 'traitement #' . $treatmentId);
|
||
}
|
||
$signature = $alerts
|
||
? hash('sha256', json_encode([$values, $comment, $skippedIds], JSON_UNESCAPED_UNICODE))
|
||
: null;
|
||
$previousStmt = $db->prepare(
|
||
'SELECT alert_signature FROM care_round_observations WHERE round_id=:round AND animal_id=:animal',
|
||
);
|
||
$previousStmt->execute([':round' => $roundId, ':animal' => $animalId]);
|
||
$previousSignature = $previousStmt->fetchColumn() ?: null;
|
||
|
||
$savedPhotoPath = null;
|
||
$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')",
|
||
);
|
||
$stmt->execute([
|
||
':round' => $roundId,
|
||
':animal' => $animalId,
|
||
':food' => $values['food'],
|
||
':water' => $values['water'],
|
||
':urine' => $values['urine'],
|
||
':stool' => $values['stool'],
|
||
':general' => $values['general_state'],
|
||
':comment' => $comment ?: null,
|
||
':signature' => $signature,
|
||
]);
|
||
$ins = $db->prepare(
|
||
"INSERT INTO treatment_administrations(round_id,treatment_id,animal_id,status,comment) VALUES(:round,:treatment,:animal,:status,:comment) ON CONFLICT(round_id,treatment_id) DO UPDATE SET status=excluded.status,comment=excluded.comment,administered_at=datetime('now')",
|
||
);
|
||
$treatmentCheck = $db->prepare('SELECT 1 FROM treatments WHERE id=? AND animal_id=? AND ongoing=1');
|
||
foreach ($admins as $treatmentId => $status) {
|
||
if (!in_array($status, ['given', 'skipped'], true)) {
|
||
continue;
|
||
}
|
||
$treatmentCheck->execute([(int) $treatmentId, $animalId]);
|
||
if (!$treatmentCheck->fetchColumn()) {
|
||
continue;
|
||
}
|
||
$ins->execute([
|
||
':round' => $roundId,
|
||
':treatment' => (int) $treatmentId,
|
||
':animal' => $animalId,
|
||
':status' => $status,
|
||
':comment' => null,
|
||
]);
|
||
}
|
||
if ($hasPhoto) {
|
||
$dir = PrivateMediaService::animalDir($animalId);
|
||
PrivateMediaService::ensure($dir);
|
||
$stored = ImageService::storeUploaded((string) $photo['tmp_name'], $dir, 'tournee', 'medical');
|
||
$filename = $stored['filename'];
|
||
$savedPhotoPath = $stored['path'];
|
||
$mime = $stored['mime'];
|
||
@chmod($savedPhotoPath, 0600);
|
||
$photoStmt = $db->prepare(
|
||
'INSERT INTO animal_photos(animal_id,filename,original_name,mime,size_bytes,is_primary,is_public,caption,care_round_id) VALUES(:animal,:filename,:original,:mime,:size,0,0,:caption,:round)',
|
||
);
|
||
$photoStmt->execute([
|
||
':animal' => $animalId,
|
||
':filename' => $filename,
|
||
':original' => (string) ($photo['name'] ?? 'photo'),
|
||
':mime' => $mime,
|
||
':size' => $stored['size'],
|
||
':caption' => $photoComment,
|
||
':round' => $roundId,
|
||
]);
|
||
$alerts[] = 'Photo privée ajoutée : ' . $photoComment;
|
||
}
|
||
if (($signature !== null && $signature !== $previousSignature) || $hasPhoto) {
|
||
$roundStmt = $db->prepare('SELECT round_date,period FROM care_rounds WHERE id=:id');
|
||
$roundStmt->execute([':id' => $roundId]);
|
||
$roundInfo = $roundStmt->fetch();
|
||
$roundDate = $roundInfo ? date('d/m/Y', strtotime((string) $roundInfo['round_date'])) : date('d/m/Y');
|
||
$roundPeriod = ($roundInfo['period'] ?? 'morning') === 'evening' ? 'soir' : 'matin';
|
||
$history = $db->prepare(
|
||
"INSERT INTO animal_history(animal_id,type,label,details,user_id) VALUES(:animal,'care_round',:label,:details,:user)",
|
||
);
|
||
$history->execute([
|
||
':animal' => $animalId,
|
||
':label' => 'Anomalie durant la tournée du ' . $roundPeriod . ' — ' . $roundDate,
|
||
':details' => implode("\n", $alerts),
|
||
':user' => Auth::id(),
|
||
]);
|
||
}
|
||
$db->prepare("UPDATE animals SET updated_at=datetime('now') WHERE id=:id")->execute([':id' => $animalId]);
|
||
$db->commit();
|
||
} catch (Throwable $e) {
|
||
if ($db->inTransaction()) {
|
||
$db->rollBack();
|
||
}
|
||
if ($savedPhotoPath && is_file($savedPhotoPath)) {
|
||
@unlink($savedPhotoPath);
|
||
}
|
||
throw $e;
|
||
}
|
||
$period = ($_POST['period'] ?? 'morning') === 'evening' ? 'evening' : 'morning';
|
||
$date = (string) ($_POST['date'] ?? date('Y-m-d'));
|
||
header('Location: /care-round?period=' . $period . '&date=' . rawurlencode($date) . '#animal-' . $animalId);
|
||
exit();
|
||
}
|
||
|
||
public static function saveRoom(): void
|
||
{
|
||
if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST') {
|
||
http_response_code(405);
|
||
return;
|
||
}
|
||
$db = DB::pdo();
|
||
$roundId = self::ensureRound($db);
|
||
$room = (string) ($_POST['room'] ?? '');
|
||
$rooms = ShelterRoomService::byCode(true);
|
||
if (!$roundId || !isset($rooms[$room]) || empty($rooms[$room]['bulk_validation'])) {
|
||
http_response_code(400);
|
||
return;
|
||
}
|
||
|
||
$animals = $db
|
||
->query(
|
||
"SELECT id,status,refuge_room FROM animals WHERE deleted_at IS NULL AND archived_at IS NULL AND lower(status) IN ('refuge','soin','quarantaine')",
|
||
)
|
||
->fetchAll(PDO::FETCH_ASSOC);
|
||
$stmt = $db->prepare(
|
||
"INSERT OR IGNORE INTO care_round_observations(round_id,animal_id,food,water,urine,stool,general_state) VALUES(:round,:animal,'normal','normal','normal','normal','good')",
|
||
);
|
||
foreach ($animals as $animal) {
|
||
if (ShelterRoomService::resolveAnimal($animal, $rooms) === $room) {
|
||
$stmt->execute([':round' => $roundId, ':animal' => $animal['id']]);
|
||
}
|
||
}
|
||
|
||
$period = ($_POST['period'] ?? 'morning') === 'evening' ? 'evening' : 'morning';
|
||
$date = preg_match('/^\d{4}-\d{2}-\d{2}$/', (string) ($_POST['date'] ?? ''))
|
||
? (string) $_POST['date']
|
||
: date('Y-m-d');
|
||
header('Location: /care-round?period=' . $period . '&date=' . rawurlencode($date) . '#room-' . $room);
|
||
exit();
|
||
}
|
||
|
||
public static function setup(): void
|
||
{
|
||
$db = DB::pdo();
|
||
$layouts = [];
|
||
foreach ($db->query('SELECT * FROM care_room_layouts')->fetchAll() as $l) {
|
||
$layouts[$l['room']] = $l;
|
||
}
|
||
$animals = $db
|
||
->query(
|
||
"SELECT id,name,status,refuge_room,care_box_key,current_address FROM animals WHERE deleted_at IS NULL AND archived_at IS NULL AND lower(status) IN ('refuge','soin','quarantaine') ORDER BY name COLLATE NOCASE",
|
||
)
|
||
->fetchAll();
|
||
render('care_setup.php', [
|
||
'title' => t('care.setup_title'),
|
||
'layouts' => $layouts,
|
||
'rooms' => ShelterRoomService::all(true),
|
||
'animals' => $animals,
|
||
]);
|
||
}
|
||
|
||
public static function saveSetup(): void
|
||
{
|
||
if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST') {
|
||
http_response_code(405);
|
||
return;
|
||
}
|
||
$db = DB::pdo();
|
||
$topOpts = ['split', 'left2', 'right2', 'merged'];
|
||
$bottomOpts = ['split', 'merged'];
|
||
foreach (['care', 'quarantine'] as $room) {
|
||
$top = (string) ($_POST[$room . '_top'] ?? 'split');
|
||
$bottom = (string) ($_POST[$room . '_bottom'] ?? 'split');
|
||
if (!in_array($top, $topOpts, true)) {
|
||
$top = 'split';
|
||
}
|
||
if (!in_array($bottom, $bottomOpts, true)) {
|
||
$bottom = 'split';
|
||
}
|
||
$db->prepare(
|
||
"UPDATE care_room_layouts SET top_layout=:top,bottom_layout=:bottom,updated_at=datetime('now') WHERE room=:room",
|
||
)->execute([':top' => $top, ':bottom' => $bottom, ':room' => $room]);
|
||
}
|
||
$assignments = is_array($_POST['box'] ?? null) ? $_POST['box'] : [];
|
||
$roomAssignments = is_array($_POST['room'] ?? null) ? $_POST['room'] : [];
|
||
$rooms = ShelterRoomService::byCode(true);
|
||
$locationReason = trim((string) ($_POST['location_change_reason'] ?? ''));
|
||
$select = $db->prepare(
|
||
'SELECT status,refuge_room,care_box_key,current_address FROM animals WHERE id=:id AND deleted_at IS NULL AND archived_at IS NULL',
|
||
);
|
||
$stmt = $db->prepare(
|
||
"UPDATE animals SET status=:status,refuge_room=:room,care_box_key=:box,updated_at=datetime('now') WHERE id=:id AND deleted_at IS NULL AND archived_at IS NULL",
|
||
);
|
||
foreach ($roomAssignments as $id => $roomCode) {
|
||
$animalId = (int) $id;
|
||
$select->execute([':id' => $animalId]);
|
||
$before = $select->fetch(PDO::FETCH_ASSOC);
|
||
if (!$before) {
|
||
continue;
|
||
}
|
||
$roomCode = (string) $roomCode;
|
||
if (!isset($rooms[$roomCode])) {
|
||
continue;
|
||
}
|
||
$room = $rooms[$roomCode];
|
||
$allowedBoxes = array_column(
|
||
array_filter($room['boxes'], static fn($box) => (int) $box['active'] === 1),
|
||
'code',
|
||
);
|
||
if (in_array($roomCode, ['care', 'quarantine'], true)) {
|
||
$allowedBoxes = array_values(
|
||
array_unique(array_merge($allowedBoxes, ['top_ab', 'top_bc', 'top_all', 'bottom_all'])),
|
||
);
|
||
}
|
||
$newBox = trim((string) ($assignments[$id] ?? ''));
|
||
if ($room['room_type'] !== 'boxes' || !in_array($newBox, $allowedBoxes, true)) {
|
||
$newBox = null;
|
||
}
|
||
$status = (string) $room['status_code'];
|
||
$stmt->execute([':status' => $status, ':room' => $roomCode, ':box' => $newBox, ':id' => $animalId]);
|
||
LocationHistoryService::record(
|
||
$db,
|
||
$animalId,
|
||
$before,
|
||
array_merge($before, ['status' => $status, 'refuge_room' => $roomCode, 'care_box_key' => $newBox]),
|
||
'care_setup',
|
||
$locationReason ?: 'Attribution de la salle ou du box depuis la tournée',
|
||
);
|
||
}
|
||
header('Location: /care-round');
|
||
exit();
|
||
}
|
||
|
||
public static function saveTreatmentSchedule(): void
|
||
{
|
||
if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST') {
|
||
http_response_code(405);
|
||
return;
|
||
}
|
||
$id = (int) ($_POST['treatment_id'] ?? 0);
|
||
$schedule = (string) ($_POST['schedule'] ?? '');
|
||
$values = match ($schedule) {
|
||
'morning' => [1, 0, 0],
|
||
'evening' => [0, 1, 0],
|
||
'both' => [1, 1, 0],
|
||
'needed' => [0, 0, 1],
|
||
default => null,
|
||
};
|
||
if (!$id || !$values) {
|
||
http_response_code(400);
|
||
return;
|
||
}
|
||
DB::pdo()
|
||
->prepare('UPDATE treatments SET give_morning=?,give_evening=?,give_as_needed=? WHERE id=?')
|
||
->execute([$values[0], $values[1], $values[2], $id]);
|
||
$period = ($_POST['period'] ?? 'morning') === 'evening' ? 'evening' : 'morning';
|
||
header('Location: /care-round?period=' . $period);
|
||
exit();
|
||
}
|
||
|
||
private static function ensureRound(PDO $db): int
|
||
{
|
||
$date = preg_match('/^\d{4}-\d{2}-\d{2}$/', (string) ($_POST['date'] ?? ''))
|
||
? (string) $_POST['date']
|
||
: date('Y-m-d');
|
||
$period = ($_POST['period'] ?? 'morning') === 'evening' ? 'evening' : 'morning';
|
||
$requested = (int) ($_POST['round_id'] ?? 0);
|
||
if ($requested) {
|
||
$s = $db->prepare('SELECT id FROM care_rounds WHERE id=? AND round_date=? AND period=?');
|
||
$s->execute([$requested, $date, $period]);
|
||
if ($s->fetchColumn()) {
|
||
return $requested;
|
||
}
|
||
}
|
||
$db->prepare(
|
||
'INSERT OR IGNORE INTO care_rounds(round_date,period,performed_by_user_id) VALUES(?,?,?)',
|
||
)->execute([$date, $period, Auth::id()]);
|
||
$s = $db->prepare('SELECT id FROM care_rounds WHERE round_date=? AND period=?');
|
||
$s->execute([$date, $period]);
|
||
return (int) $s->fetchColumn();
|
||
}
|
||
}
|