exec(" UPDATE animals SET intake_date = date(updated_at) WHERE lower(trim(status))='quarantaine' AND deleted_at IS NULL AND archived_at IS NULL AND (intake_date IS NULL OR trim(intake_date)='') AND updated_at IS NOT NULL AND trim(updated_at)<>'' "); $quarantineDays = AppSettings::int('quarantine_days', 1, 90); $stmt = $db->prepare(" UPDATE animals SET quarantine_until = date(intake_date, '+' || :days || ' day') WHERE lower(trim(status))='quarantaine' AND deleted_at IS NULL AND archived_at IS NULL AND (quarantine_until IS NULL OR trim(quarantine_until)='') AND intake_date IS NOT NULL AND trim(intake_date)<>'' "); $stmt->execute([':days' => $quarantineDays]); // Stats par statut (avec tolérance accents) $stats = $db ->query( " SELECT SUM(CASE WHEN lower(status) NOT IN ('adopte','adopté','decede','décédé') THEN 1 ELSE 0 END) AS total, SUM(CASE WHEN lower(status) NOT IN ('adopte','adopté','decede','décédé') AND birth_date IS NOT NULL AND trim(birth_date) != '' AND date(birth_date) > date('now','-12 months') THEN 1 ELSE 0 END) AS kittens, SUM(CASE WHEN lower(status) NOT IN ('adopte','adopté','decede','décédé') AND birth_date IS NOT NULL AND trim(birth_date) != '' AND date(birth_date) <= date('now','-12 months') THEN 1 ELSE 0 END) AS adults, SUM(CASE WHEN lower(status) NOT IN ('adopte','adopté','decede','décédé') AND (birth_date IS NULL OR trim(birth_date) = '') THEN 1 ELSE 0 END) AS age_unknown, SUM(CASE WHEN status='refuge' THEN 1 ELSE 0 END) AS refuge, SUM(CASE WHEN status='refuge' AND refuge_room='silver' THEN 1 ELSE 0 END) AS refuge_silver, SUM(CASE WHEN status='refuge' AND refuge_room='twix' THEN 1 ELSE 0 END) AS refuge_twix, SUM(CASE WHEN status='refuge' AND (refuge_room IS NULL OR trim(refuge_room)='') THEN 1 ELSE 0 END) AS refuge_unassigned, SUM(CASE WHEN status='fa' THEN 1 ELSE 0 END) AS fa, SUM(CASE WHEN status='fa_permanente' THEN 1 ELSE 0 END) AS fa_permanente, SUM(CASE WHEN status='quarantaine' THEN 1 ELSE 0 END) AS quarantaine, SUM(CASE WHEN status='soin' THEN 1 ELSE 0 END) AS soin, SUM(CASE WHEN status='isolation' THEN 1 ELSE 0 END) AS isolation, SUM(CASE WHEN status='hospitalise' OR status='hospitalisé' THEN 1 ELSE 0 END) AS hospitalise, SUM(CASE WHEN status='reserve' OR status='réservé' THEN 1 ELSE 0 END) AS reserve, SUM(CASE WHEN status='adopte' OR status='adopté' THEN 1 ELSE 0 END) AS adopte, SUM(CASE WHEN status='decede' OR status='décédé' THEN 1 ELSE 0 END) AS dece, SUM(CASE WHEN status='enfui' THEN 1 ELSE 0 END) AS enfui FROM animals WHERE deleted_at IS NULL AND archived_at IS NULL ", ) ->fetch(); $shelterRoomCounts = []; $configuredRooms = ShelterRoomService::byCode(true); $roomAnimals = $db ->query( "SELECT status,refuge_room FROM animals WHERE deleted_at IS NULL AND archived_at IS NULL AND status IN ('refuge','soin','quarantaine')", ) ->fetchAll(PDO::FETCH_ASSOC); foreach ($configuredRooms as $code => $room) { $shelterRoomCounts[$code] = [ 'name' => $room['name'], 'color' => $room['color'], 'status' => $room['status_code'], 'count' => 0, ]; } foreach ($roomAnimals as $roomAnimal) { $code = ShelterRoomService::resolveAnimal($roomAnimal, $configuredRooms); if (isset($shelterRoomCounts[$code])) { $shelterRoomCounts[$code]['count']++; } } $quality = $db ->query( "SELECT COUNT(*) total, SUM(CASE WHEN birth_date IS NULL OR trim(birth_date)='' THEN 1 ELSE 0 END) missing_birth_date, SUM(CASE WHEN sex IS NULL OR trim(sex)='' OR sex='U' THEN 1 ELSE 0 END) missing_sex, SUM(CASE WHEN color IS NULL OR trim(color)='' THEN 1 ELSE 0 END) missing_color, SUM(CASE WHEN breed IS NULL OR trim(breed)='' THEN 1 ELSE 0 END) missing_breed, SUM(CASE WHEN (rescue_location_name IS NULL OR trim(rescue_location_name)='') AND (rescue_address IS NULL OR trim(rescue_address)='') THEN 1 ELSE 0 END) missing_origin, SUM(CASE WHEN (chip_id IS NULL OR trim(chip_id)='') AND (chip IS NULL OR trim(chip)='') THEN 1 ELSE 0 END) missing_chip, SUM(CASE WHEN COALESCE(NULLIF(identification_registration_status,''),'unknown')='unknown' THEN 1 ELSE 0 END) unknown_identification_registration, SUM(CASE WHEN COALESCE(NULLIF(adoption_availability,''),'unknown')='unknown' THEN 1 ELSE 0 END) unknown_adoption_availability, SUM(CASE WHEN sterilization_status='no' THEN 1 ELSE 0 END) unsterilized, SUM(CASE WHEN COALESCE(NULLIF(sterilization_status,''),'unknown')='unknown' THEN 1 ELSE 0 END) unknown_sterilization, SUM(CASE WHEN birth_date IS NULL OR trim(birth_date)='' OR sex IS NULL OR trim(sex)='' OR sex='U' OR color IS NULL OR trim(color)='' OR breed IS NULL OR trim(breed)='' OR ((rescue_location_name IS NULL OR trim(rescue_location_name)='') AND (rescue_address IS NULL OR trim(rescue_address)='')) OR ((chip_id IS NULL OR trim(chip_id)='') AND (chip IS NULL OR trim(chip)='')) OR COALESCE(NULLIF(identification_registration_status,''),'unknown')='unknown' OR COALESCE(NULLIF(adoption_availability,''),'unknown')='unknown' OR COALESCE(NULLIF(sterilization_status,''),'unknown')='unknown' THEN 1 ELSE 0 END) incomplete FROM animals WHERE deleted_at IS NULL AND archived_at IS NULL AND lower(status) NOT IN ('adopte','adopté','decede','décédé','enfui','fugue')", ) ->fetch(PDO::FETCH_ASSOC) ?: []; // Activité $activity = $db ->query( " SELECT (SELECT COUNT(*) FROM medical_notes mn JOIN animals a ON a.id=mn.animal_id WHERE a.deleted_at IS NULL AND a.archived_at IS NULL AND date(mn.noted_at) >= date('now','-7 day')) AS notes_7d, (SELECT COUNT(*) FROM vaccinations v JOIN animals a ON a.id=v.animal_id WHERE a.deleted_at IS NULL AND a.archived_at IS NULL AND v.done_date IS NOT NULL AND date(v.done_date) >= date('now','-30 day')) AS vaccines_30d, (SELECT COUNT(*) FROM treatments t JOIN animals a ON a.id=t.animal_id WHERE a.deleted_at IS NULL AND a.archived_at IS NULL AND t.start_date IS NOT NULL AND date(t.start_date) >= date('now','-30 day')) AS treatments_30d ", ) ->fetch(); // Vaccins en retard $stmt = $db->prepare(" SELECT a.id, a.name, a.internal_code AS code, rv.name AS vaccine_name, v.due_date FROM vaccinations v JOIN animals a ON a.id = v.animal_id JOIN ref_vaccines rv ON rv.id = v.vaccine_id WHERE v.due_date IS NOT NULL AND a.deleted_at IS NULL AND a.archived_at IS NULL AND lower(a.status) NOT IN ('decede','décédé') AND date(v.due_date) < date('now') ORDER BY date(v.due_date) ASC LIMIT 50 "); $stmt->execute(); $vaccines_overdue = $stmt->fetchAll(); // Vaccins ≤ 7 jours $stmt = $db->prepare(" SELECT a.id, a.name, a.internal_code AS code, rv.name AS vaccine_name, v.due_date FROM vaccinations v JOIN animals a ON a.id = v.animal_id JOIN ref_vaccines rv ON rv.id = v.vaccine_id WHERE v.due_date IS NOT NULL AND a.deleted_at IS NULL AND a.archived_at IS NULL AND lower(a.status) NOT IN ('decede','décédé') AND date(v.due_date) >= date('now') AND date(v.due_date) <= date('now', '+7 day') ORDER BY date(v.due_date) ASC LIMIT 50 "); $stmt->execute(); $vaccines_7d = $stmt->fetchAll(); // Vaccins ≤ 30 jours $stmt = $db->prepare(" SELECT a.id, a.name, a.internal_code AS code, rv.name AS vaccine_name, v.due_date FROM vaccinations v JOIN animals a ON a.id = v.animal_id JOIN ref_vaccines rv ON rv.id = v.vaccine_id WHERE v.due_date IS NOT NULL AND a.deleted_at IS NULL AND a.archived_at IS NULL AND lower(a.status) NOT IN ('decede','décédé') AND date(v.due_date) > date('now', '+7 day') AND date(v.due_date) <= date('now', '+30 day') ORDER BY date(v.due_date) ASC LIMIT 50 "); $stmt->execute(); $vaccines_30d = $stmt->fetchAll(); // Traitements en cours $stmt = $db->prepare(' SELECT a.id, a.name, a.internal_code AS code, m.name AS medication_name, t.start_date, t.end_date, t.dose_text, t.route FROM treatments t JOIN animals a ON a.id = t.animal_id JOIN ref_medications m ON m.id = t.medication_id WHERE t.ongoing = 1 AND a.deleted_at IS NULL AND a.archived_at IS NULL ORDER BY date(t.start_date) DESC, a.name ASC LIMIT 50 '); $stmt->execute(); $treatments = $stmt->fetchAll(); // Traitements à clôturer (fin dépassée mais ongoing=1) $stmt = $db->prepare(" SELECT a.id, a.name, a.internal_code AS code, m.name AS medication_name, t.end_date FROM treatments t JOIN animals a ON a.id = t.animal_id JOIN ref_medications m ON m.id = t.medication_id WHERE t.ongoing = 1 AND a.deleted_at IS NULL AND a.archived_at IS NULL AND t.end_date IS NOT NULL AND date(t.end_date) < date('now') ORDER BY date(t.end_date) ASC LIMIT 50 "); $stmt->execute(); $treatments_overdue = $stmt->fetchAll(); // Dernières notes médicales $stmt = $db->prepare(' SELECT mn.noted_at, mn.kind, mn.reason, a.id AS animal_id, a.name, a.internal_code AS code FROM medical_notes mn JOIN animals a ON a.id = mn.animal_id WHERE a.deleted_at IS NULL AND a.archived_at IS NULL ORDER BY datetime(mn.noted_at) DESC, mn.id DESC LIMIT 30 '); $stmt->execute(); $recent_notes = $stmt->fetchAll(); // Quarantaine : liste + J-... (si dates renseignées) $stmt = $db->prepare(" SELECT id, name, internal_code AS code, intake_date, quarantine_until, CASE WHEN quarantine_until IS NULL OR quarantine_until = '' THEN NULL ELSE CAST(julianday(date(quarantine_until)) - julianday(date('now','localtime')) AS INT) END AS days_left FROM animals WHERE lower(trim(status))='quarantaine' AND deleted_at IS NULL AND archived_at IS NULL ORDER BY CASE WHEN quarantine_until IS NULL OR quarantine_until = '' THEN 9999 ELSE days_left END ASC, name ASC LIMIT 100 "); $stmt->execute(); $quarantine = $stmt->fetchAll(); $stmt = $db->prepare(' SELECT a.id, a.name, a.internal_code AS code, wa.last_weight, wa.w30_weight, wa.w30_date, ROUND(((wa.last_weight - wa.w30_weight) / wa.w30_weight) * 100, 1) AS pct FROM v_animals_weight_alerts wa JOIN animals a ON a.id = wa.animal_id WHERE wa.last_weight IS NOT NULL AND a.deleted_at IS NULL AND a.archived_at IS NULL AND wa.w30_weight IS NOT NULL AND wa.w30_weight > 0 AND ABS((wa.last_weight - wa.w30_weight) / wa.w30_weight) >= 0.15 ORDER BY ABS((wa.last_weight - wa.w30_weight) / wa.w30_weight) DESC LIMIT 50 '); $stmt->execute(); $weight_alerts = $stmt->fetchAll(); $careToday = []; $rows = $db ->query( "SELECT periods.period, (SELECT COUNT(*) FROM animals a WHERE a.deleted_at IS NULL AND a.archived_at IS NULL AND lower(a.status) IN ('refuge','soin','quarantaine')) expected, COUNT(o.id) checked, SUM(CASE WHEN o.alert_signature IS NOT NULL THEN 1 ELSE 0 END) alerts FROM (SELECT 'morning' period UNION ALL SELECT 'evening') periods LEFT JOIN care_rounds r ON r.round_date=date('now','localtime') AND r.period=periods.period LEFT JOIN care_round_observations o ON o.round_id=r.id GROUP BY periods.period ORDER BY periods.period DESC", ) ->fetchAll(); foreach ($rows as $row) { $careToday[$row['period']] = $row; } $stmt = $db->query( "SELECT a.id,a.name,r.period,o.comment FROM care_round_observations o JOIN care_rounds r ON r.id=o.round_id JOIN animals a ON a.id=o.animal_id WHERE r.round_date=date('now','localtime') AND o.alert_signature IS NOT NULL AND a.deleted_at IS NULL ORDER BY o.checked_at DESC LIMIT 10", ); $careAlertsToday = $stmt->fetchAll(); $grantAlerts = PermissionService::can('grants') ? GrantService::alerts((int) date('Y')) : []; $agendaDue = PermissionService::can('agenda') ? AgendaService::due() : []; render('dashboard.php', [ 'title' => 'Dashboard', 'stats' => $stats, 'shelterRoomCounts' => $shelterRoomCounts, 'quality' => $quality, 'activity' => $activity, 'vaccines_overdue' => $vaccines_overdue, 'vaccines_7d' => $vaccines_7d, 'vaccines_30d' => $vaccines_30d, 'treatments' => $treatments, 'treatments_overdue' => $treatments_overdue, 'recent_notes' => $recent_notes, 'quarantine' => $quarantine, 'weight_alerts' => $weight_alerts, 'careToday' => $careToday, 'careAlertsToday' => $careAlertsToday, 'grantAlerts' => $grantAlerts, 'agendaDue' => $agendaDue, ]); } }