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