Globinours/app/Controllers/CareRoundsController.php

463 lines
21 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?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();
}
}