Globinours/app/Controllers/AnimalRecordActions.php

1061 lines
50 KiB
PHP
Raw 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);
trait AnimalRecordActions
{
use AnimalRelationshipActions;
public static function pdf(): void
{
$id = (int) ($_GET['id'] ?? 0);
if ($id <= 0) {
http_response_code(400);
echo h(t('error.bad_request'));
return;
}
$db = DB::pdo();
// Animal
$stmt = $db->prepare('SELECT * FROM animals WHERE id=:id');
$stmt->execute([':id' => $id]);
$animal = $stmt->fetch();
if (!$animal) {
http_response_code(404);
echo h(t('error.not_found'));
return;
}
$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) ?: null;
// Notes médicales + clinique
$stmt = $db->prepare('
SELECT mn.*, COALESCE(dc.name, c.name) AS clinic_name
FROM medical_notes mn
LEFT JOIN clinics c ON c.id = mn.clinic_id
LEFT JOIN directory_contacts dc ON dc.id = mn.clinic_contact_id
WHERE mn.animal_id=:id
ORDER BY mn.noted_at DESC, mn.id DESC
');
$stmt->execute([':id' => $id]);
$medical_notes = $stmt->fetchAll();
// Photos médicales indexées par note
$stmt = $db->prepare('
SELECT id, medical_note_id, filename
FROM medical_photos
WHERE medical_note_id IN (
SELECT id FROM medical_notes WHERE animal_id = :id
)
ORDER BY medical_note_id ASC, id ASC
');
$stmt->execute([':id' => $id]);
$allMedicalPhotos = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Regroupe par note
$medicalPhotosByNote = [];
foreach ($allMedicalPhotos as $p) {
$medicalPhotosByNote[$p['medical_note_id']][] = $p;
}
// Traitements (tous, y compris terminés)
$stmt = $db->prepare('
SELECT t.*, m.name AS medication_name, m.molecule, m.form, COALESCE(dc.name, c.name) AS clinic_name
FROM treatments t
JOIN ref_medications m ON m.id = t.medication_id
LEFT JOIN clinics c ON c.id = t.clinic_id
LEFT JOIN directory_contacts dc ON dc.id = t.clinic_contact_id
WHERE t.animal_id=:id
ORDER BY t.ongoing DESC, t.start_date DESC, t.id DESC
');
$stmt->execute([':id' => $id]);
$treatments = $stmt->fetchAll();
// Vaccins
$stmt = $db->prepare('
SELECT v.*, rv.name AS vaccine_name, COALESCE(dc.name, c.name) AS clinic_name
FROM vaccinations v
JOIN ref_vaccines rv ON rv.id = v.vaccine_id
LEFT JOIN clinics c ON c.id = v.clinic_id
LEFT JOIN directory_contacts dc ON dc.id = v.clinic_contact_id
WHERE v.animal_id=:id
ORDER BY v.done_date DESC, v.id DESC
');
$stmt->execute([':id' => $id]);
$vaccinations = $stmt->fetchAll();
// Photos
$stmt = $db->prepare('SELECT * FROM animal_photos WHERE animal_id=:id ORDER BY is_primary DESC, id DESC');
$stmt->execute([':id' => $id]);
$photos = $stmt->fetchAll();
// Génère le HTML PDF via une vue dédiée
//$viewFile = __DIR__ . '/../Views/animal_pdf.php';
$viewFile = __DIR__ . '/../Views/animal_pdf_v2.php';
if (!file_exists($viewFile)) {
http_response_code(500);
echo h(t('error.missing_pdf_view'));
return;
}
// variables pour la vue
$title = 'Dossier - ' . ($animal['name'] ?? 'Animal #' . $id);
// Photo principale
$stmt = $db->prepare('
SELECT filename
FROM animal_photos
WHERE animal_id=:id AND is_primary=1
ORDER BY id DESC
LIMIT 1
');
$stmt->execute([':id' => $id]);
$primary_photo = $stmt->fetchColumn() ?: null;
// Dernière note médicale (résumé)
$stmt = $db->prepare('
SELECT noted_at, kind, reason, weight_kg, temperature_c, diagnosis, plan
FROM medical_notes
WHERE animal_id=:id
ORDER BY datetime(noted_at) DESC, id DESC
LIMIT 1
');
$stmt->execute([':id' => $id]);
$last_med = $stmt->fetch() ?: null;
// Prochain vaccin (due_date la plus proche)
$stmt = $db->prepare('
SELECT rv.name AS vaccine_name, v.due_date
FROM vaccinations v
JOIN ref_vaccines rv ON rv.id = v.vaccine_id
WHERE v.animal_id=:id AND v.due_date IS NOT NULL
ORDER BY date(v.due_date) ASC, v.id ASC
LIMIT 1
');
$stmt->execute([':id' => $id]);
$next_vaccine = $stmt->fetch() ?: null;
$generatedAt = new DateTimeImmutable('now', new DateTimeZone('Europe/Paris'));
$footerText =
'Généré par ' .
AppSettings::get('association_name') .
', le ' .
$generatedAt->format('d/m/Y') .
' à ' .
$generatedAt->format('H\\hi');
$internalCode = trim((string) ($animal['internal_code'] ?? ''));
if ($internalCode !== '') {
$footerText .= ' · Dossier ' . $internalCode;
}
header('Content-Type: text/html; charset=UTF-8');
require $viewFile;
}
public static function form(): void
{
$db = DB::pdo();
$id = (int) ($_GET['id'] ?? 0);
$animal = [
'id' => 0,
'name' => '',
'internal_code' => '',
'status' => 'refuge',
'species' => 'chat',
'sex' => 'U',
'sterilized' => 0,
'sterilization_status' => 'unknown',
'sterilization_date' => '',
'sterilization_vet_contact_id' => '',
'sterilization_clinic_contact_id' => '',
'refuge_room' => '',
'chip_id' => '',
'identification_type' => 'unknown',
'identification_date' => '',
'identification_registration_status' => 'unknown',
'adoption_availability' => 'unknown',
'adoption_available_from' => '',
'adoption_unavailability_reason' => '',
'website_published' => 0,
'website_description' => '',
'breed' => 'Chat Européen',
'color' => '',
'hair_type' => '',
'birth_date' => '',
'notes' => '',
'intake_date' => '',
'intake_type' => 'unknown',
'intake_reason' => 'unknown',
'depositor_contact_id' => '',
'intake_circumstances' => '',
'intake_owner_care_home' => 0,
'quarantine_until' => '',
'compatibility_dogs' => 'unknown',
'compatibility_cats' => 'unknown',
'compatibility_children' => 'unknown',
'house_trained' => 'unknown',
'free_cat_site_name' => '',
'free_cat_city' => '',
'free_cat_address' => '',
'free_cat_caretaker_contact_id' => '',
'free_cat_captured_at' => '',
'free_cat_released_at' => '',
'free_cat_tracking_status' => 'present',
// Normalise les statuts FIV et FeLV enregistrés sur la fiche.
'fiv' => '',
'felv' => '',
];
if ($id > 0) {
$stmt = $db->prepare('SELECT * FROM animals WHERE id=:id');
$stmt->execute([':id' => $id]);
$row = $stmt->fetch();
if (!$row) {
http_response_code(404);
echo h(t('error.not_found'));
return;
}
$animal = array_merge($animal, $row);
if (!empty($animal['intake_owner_care_home'])) {
$animal['intake_reason'] = 'owner_care_home';
}
}
$fosterContacts = $db
->query(
"SELECT DISTINCT dc.id,dc.name,dc.address,dc.postal_code,dc.city FROM directory_contacts dc JOIN directory_contact_roles r ON r.contact_id=dc.id AND r.role='fa' WHERE dc.deleted_at IS NULL ORDER BY dc.name COLLATE NOCASE",
)
->fetchAll(PDO::FETCH_ASSOC);
$currentFosterId = 0;
if ($id > 0) {
$stmt = $db->prepare(
"SELECT contact_id FROM animal_placements WHERE animal_id=:id AND event_type='foster' ORDER BY date(event_date) DESC,id DESC LIMIT 1",
);
$stmt->execute([':id' => $id]);
$currentFosterId = (int) $stmt->fetchColumn();
}
$depositors = $db
->query(
"SELECT dc.id,dc.name,dc.phone,dc.city FROM directory_contacts dc JOIN directory_contact_roles r ON r.contact_id=dc.id AND r.role='deposant' WHERE dc.deleted_at IS NULL ORDER BY dc.name COLLATE NOCASE",
)
->fetchAll(PDO::FETCH_ASSOC);
$sterilizationVets = $db
->query(
"SELECT dc.id,dc.name FROM directory_contacts dc JOIN directory_contact_roles r ON r.contact_id=dc.id AND r.role='veterinaire' WHERE dc.deleted_at IS NULL ORDER BY dc.name COLLATE NOCASE",
)
->fetchAll(PDO::FETCH_ASSOC);
$sterilizationClinics = $db
->query(
"SELECT dc.id,dc.name FROM directory_contacts dc JOIN directory_contact_roles r ON r.contact_id=dc.id AND r.role='cabinet' WHERE dc.deleted_at IS NULL ORDER BY dc.name COLLATE NOCASE",
)
->fetchAll(PDO::FETCH_ASSOC);
$freeCatCaretakers = $db
->query(
"SELECT id,name,city FROM directory_contacts WHERE deleted_at IS NULL AND kind='person' ORDER BY name COLLATE NOCASE",
)
->fetchAll(PDO::FETCH_ASSOC);
render('animal_form.php', [
'title' => $id > 0 ? 'Modifier ' . $animal['name'] : 'Ajouter un animal',
'pageDescription' =>
$id > 0
? 'Modification de la fiche de ' . $animal['name'] . '.'
: 'Création d’une nouvelle fiche animal dans Globinours.',
'animal' => $animal,
'intakeTypes' => self::intakeTypes(),
'intakeReasons' => self::intakeReasons(),
'depositors' => $depositors,
'sterilizationVets' => $sterilizationVets,
'sterilizationClinics' => $sterilizationClinics,
'fosterContacts' => $fosterContacts,
'currentFosterId' => $currentFosterId,
'freeCatCaretakers' => $freeCatCaretakers,
'speciesOptions' => SpeciesService::activeWithCurrent((string) ($animal['species'] ?? 'chat')),
'shelterRooms' => ShelterRoomService::all(true),
]);
}
private static function nextInternalCode(string $name): string
{
$year = date('Y');
$base = str_replace('-', '_', Ids::slugifyUpper($name));
$db = DB::pdo();
$stmt = $db->prepare('
SELECT COUNT(*) FROM animals
WHERE internal_code LIKE :p
');
$stmt->execute([':p' => $year . '-%']);
$n = (int) $stmt->fetchColumn();
$seq = str_pad((string) ($n + 1), 3, '0', STR_PAD_LEFT);
return $year . '-' . $seq . '-' . $base;
}
public static function save(): void
{
require_once __DIR__ . '/../Services/GeocodeService.php';
require_once __DIR__ . '/../Services/GeoService.php';
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
return;
}
$db = DB::pdo();
$id = (int) ($_POST['id'] ?? 0);
$old = null;
if ($id > 0) {
$st = $db->prepare('SELECT * FROM animals WHERE id=:id');
$st->execute([':id' => $id]);
$old = $st->fetch(PDO::FETCH_ASSOC) ?: null;
}
$isArchivedCorrection = $old && !empty($old['archived_at']);
$name = trim((string) ($_POST['name'] ?? ''));
$status = trim((string) ($_POST['status'] ?? 'refuge'));
if ($isArchivedCorrection) {
$status = (string) $old['status'];
}
if (in_array(strtolower($status), ['decede', 'décédé'], true)) {
$deathCheck = $db->prepare('SELECT 1 FROM animal_deaths WHERE animal_id=:id');
$deathCheck->execute([':id' => $id]);
if (!$deathCheck->fetchColumn()) {
http_response_code(400);
echo h(t('error.use_death_action'));
return;
}
}
$refugeRoom = trim((string) ($_POST['refuge_room'] ?? ''));
$locationChangeReason = trim((string) ($_POST['location_change_reason'] ?? ''));
$availableRooms = ShelterRoomService::byCode(true);
if (!isset($availableRooms[$refugeRoom]) || (string) $availableRooms[$refugeRoom]['status_code'] !== $status) {
$refugeRoom = null;
}
$species = SpeciesService::normalize((string) ($_POST['species'] ?? 'chat'));
if (!SpeciesService::exists($species, $id <= 0)) {
http_response_code(400);
echo h(t('error.valid_species'));
return;
}
if ($status === 'chat_libre' && !SpeciesService::isCat($species)) {
http_response_code(400);
echo h(t('error.freecat_cats_only'));
return;
}
$sex = trim((string) ($_POST['sex'] ?? 'U'));
$sterilizationStatus = (string) ($_POST['sterilization_status'] ?? 'unknown');
if (!in_array($sterilizationStatus, ['unknown', 'no', 'yes'], true)) {
$sterilizationStatus = 'unknown';
}
$sterilized = $sterilizationStatus === 'yes' ? 1 : 0;
$sterilizationDate = trim((string) ($_POST['sterilization_date'] ?? ''));
if ($sterilizationDate !== '' && !preg_match('/^\d{4}-\d{2}-\d{2}$/', $sterilizationDate)) {
http_response_code(400);
echo h(t('error.invalid_sterilization_date'));
return;
}
$sterilizationVetId = (int) ($_POST['sterilization_vet_contact_id'] ?? 0) ?: null;
$sterilizationClinicId = (int) ($_POST['sterilization_clinic_contact_id'] ?? 0) ?: null;
if ($sterilizationStatus !== 'yes') {
$sterilizationDate = '';
$sterilizationVetId = null;
$sterilizationClinicId = null;
}
$chip_id = trim((string) ($_POST['chip_id'] ?? ''));
$identificationType = (string) ($_POST['identification_type'] ?? 'unknown');
if (!in_array($identificationType, ['unknown', 'microchip', 'tattoo'], true)) {
$identificationType = 'unknown';
}
$identificationDate = trim((string) ($_POST['identification_date'] ?? ''));
$identificationRegistration = (string) ($_POST['identification_registration_status'] ?? 'unknown');
if ($identificationRegistration === 'pending') {
$identificationRegistration = 'change_pending';
}
if (!in_array($identificationRegistration, ['unknown', 'registered', 'change_pending', 'owner_changed'], true)) {
$identificationRegistration = 'unknown';
}
if ($chip_id === '') {
$identificationType = 'unknown';
$identificationDate = '';
$identificationRegistration = 'unknown';
}
$adoptionAvailability = (string) ($_POST['adoption_availability'] ?? 'unknown');
if (!in_array($adoptionAvailability, ['unknown', 'not_available', 'available'], true)) {
$adoptionAvailability = 'unknown';
}
$adoptionAvailableFrom = trim((string) ($_POST['adoption_available_from'] ?? ''));
$adoptionUnavailabilityReason = trim((string) ($_POST['adoption_unavailability_reason'] ?? ''));
foreach ([$identificationDate, $adoptionAvailableFrom] as $dateValue) {
if ($dateValue !== '' && !preg_match('/^\d{4}-\d{2}-\d{2}$/', $dateValue)) {
http_response_code(400);
echo h(t('error.invalid_date'));
return;
}
}
if ($adoptionAvailability !== 'available') {
$adoptionAvailableFrom = '';
}
if ($adoptionAvailability !== 'not_available') {
$adoptionUnavailabilityReason = '';
}
$websitePublished = isset($_POST['website_published']) ? 1 : 0;
$websiteDescription = mb_substr(trim((string) ($_POST['website_description'] ?? '')), 0, 5000);
$breed = trim((string) ($_POST['breed'] ?? ''));
$color = trim((string) ($_POST['color'] ?? ''));
$hairType = trim((string) ($_POST['hair_type'] ?? ''));
$profileValue = static function (string $field): string {
$value = (string) ($_POST[$field] ?? 'unknown');
return in_array($value, ['unknown', 'yes', 'no'], true) ? $value : 'unknown';
};
$compatibilityDogs = $profileValue('compatibility_dogs');
$compatibilityCats = $profileValue('compatibility_cats');
$compatibilityChildren = $profileValue('compatibility_children');
$houseTrained = $profileValue('house_trained');
$freeCatSiteName = trim((string) ($_POST['free_cat_site_name'] ?? ''));
$freeCatCity = trim((string) ($_POST['free_cat_city'] ?? ''));
$freeCatAddress = trim((string) ($_POST['free_cat_address'] ?? ''));
$freeCatCaretakerId = (int) ($_POST['free_cat_caretaker_contact_id'] ?? 0) ?: null;
$freeCatCapturedAt = trim((string) ($_POST['free_cat_captured_at'] ?? ''));
$freeCatReleasedAt = trim((string) ($_POST['free_cat_released_at'] ?? ''));
$freeCatTrackingStatus = (string) ($_POST['free_cat_tracking_status'] ?? 'present');
if (!in_array($freeCatTrackingStatus, ['present', 'missing', 'moved', 'deceased'], true)) {
$freeCatTrackingStatus = 'present';
}
foreach ([$freeCatCapturedAt, $freeCatReleasedAt] as $freeCatDate) {
if ($freeCatDate !== '' && !preg_match('/^\d{4}-\d{2}-\d{2}$/', $freeCatDate)) {
http_response_code(400);
echo h(t('error.invalid_freecat_date'));
return;
}
}
if ($status !== 'chat_libre') {
$freeCatSiteName = '';
$freeCatCity = '';
$freeCatAddress = '';
$freeCatCaretakerId = null;
$freeCatCapturedAt = '';
$freeCatReleasedAt = '';
$freeCatTrackingStatus = 'present';
} elseif ($freeCatCaretakerId) {
$check = $db->prepare(
"SELECT 1 FROM directory_contacts WHERE id=:id AND deleted_at IS NULL AND kind='person'",
);
$check->execute([':id' => $freeCatCaretakerId]);
if (!$check->fetchColumn()) {
$freeCatCaretakerId = null;
}
}
$notes = trim((string) ($_POST['notes'] ?? ''));
$intake_date = trim((string) ($_POST['intake_date'] ?? '')); // YYYY-MM-DD
$intakeType = (string) ($_POST['intake_type'] ?? 'unknown');
if (!isset(self::INTAKE_TYPES[$intakeType])) {
$intakeType = 'unknown';
}
$submittedIntakeReason = (string) ($_POST['intake_reason'] ?? 'unknown');
if (!isset(self::INTAKE_REASONS[$submittedIntakeReason])) {
$submittedIntakeReason = 'unknown';
}
$intakeOwnerCareHome = $submittedIntakeReason === 'owner_care_home' ? 1 : 0;
$intakeReason = $intakeOwnerCareHome ? 'owner_health' : $submittedIntakeReason;
$intakeCircumstances = trim((string) ($_POST['intake_circumstances'] ?? ''));
// Naissance (date + estimation)
$birth_date = trim((string) ($_POST['birth_date'] ?? ''));
$birth_is_estimated = (int) ($_POST['birth_is_estimated'] ?? 0 ? 1 : 0);
$birth_est_years = trim((string) ($_POST['birth_est_years'] ?? ''));
$birth_est_months = trim((string) ($_POST['birth_est_months'] ?? ''));
$birth_est_years = $birth_est_years === '' ? 0 : (int) $birth_est_years;
$birth_est_months = $birth_est_months === '' ? 0 : (int) $birth_est_months;
$birth_estimated_months = $birth_est_years * 12 + $birth_est_months;
if ($birth_estimated_months === 0) {
$birth_estimated_months = null;
}
// Statuts FIV et FeLV enregistrés sur la fiche.
$fiv = array_key_exists('fiv', $_POST) ? (isset($_POST['fiv']) ? 1 : 0) : null;
$felv = array_key_exists('felv', $_POST) ? (isset($_POST['felv']) ? 1 : 0) : null;
// Validations minimales
if ($name === '' || mb_strlen($name) > 150) {
http_response_code(400);
echo h($name === '' ? t('error.name_required') : t('animal.name_too_long'));
return;
}
if ($breed === '' && SpeciesService::isCat($species)) {
$breed = 'Chat Européen';
}
if ($sex === '') {
$sex = 'U';
}
// Si naissance estimée et date vide => calcul auto depuis "mois"
if ($birth_is_estimated === 1 && $birth_date === '' && $birth_estimated_months !== null) {
$dt = new DateTime('now');
$dt->modify('-' . $birth_estimated_months . ' months');
$birth_date = $dt->format('Y-m-d');
}
// Normaliser dates vides en NULL (SQLite aime mieux)
$birth_date = $birth_date === '' ? null : $birth_date;
$intake_date = $intake_date === '' ? null : $intake_date;
// Génère le code interne lorsqu'il est absent.
$internal_code = trim((string) ($_POST['internal_code'] ?? ''));
if ($internal_code === '' && $old && !empty($old['internal_code'])) {
$internal_code = (string) $old['internal_code'];
if (preg_match('/^(\d{4}-\d{3})-(.+)$/', $internal_code, $parts)) {
$legacyBase = strtoupper(trim($name));
$legacyBase = trim((string) preg_replace('/[^A-Z0-9]+/u', '_', $legacyBase), '_');
$normalizedBase = str_replace('-', '_', Ids::slugifyUpper($name));
if ($parts[2] === $legacyBase && $legacyBase !== $normalizedBase) {
$candidate = $parts[1] . '-' . $normalizedBase;
$duplicate = $db->prepare('SELECT 1 FROM animals WHERE internal_code=:code AND id<>:id');
$duplicate->execute([':code' => $candidate, ':id' => $id]);
if (!$duplicate->fetchColumn()) {
$internal_code = $candidate;
}
}
}
} elseif ($internal_code === '') {
$internal_code = self::nextInternalCode($name);
}
// Quarantaine : durée issue des paramètres généraux.
$quarantine_until = null;
if (strtolower($status) === 'quarantaine' && $intake_date !== null) {
$ts = strtotime($intake_date);
if ($ts !== false) {
$quarantine_until = date('Y-m-d', $ts + AppSettings::int('quarantine_days', 1, 90) * 86400);
}
} else {
// La date de sortie n'a de sens que pour un animal en quarantaine.
$quarantine_until = null;
}
$rescueAddress = trim((string) ($_POST['rescue_address'] ?? ''));
$rescueLocationName = trim((string) ($_POST['rescue_location_name'] ?? ''));
$currentAddress = trim((string) ($_POST['current_address'] ?? ''));
$fosterContactId = (int) ($_POST['foster_contact_id'] ?? 0) ?: null;
$fosterContact = null;
if (!$isArchivedCorrection && in_array($status, ['fa', 'fa_permanente'], true) && $fosterContactId) {
$st = $db->prepare(
"SELECT dc.* FROM directory_contacts dc JOIN directory_contact_roles r ON r.contact_id=dc.id AND r.role='fa' WHERE dc.id=:id AND dc.deleted_at IS NULL",
);
$st->execute([':id' => $fosterContactId]);
$fosterContact = $st->fetch(PDO::FETCH_ASSOC) ?: null;
if (!$fosterContact) {
http_response_code(400);
echo h(t('error.invalid_foster'));
return;
}
$currentAddress =
implode(
' ',
array_filter([
$fosterContact['address'] ?? '',
$fosterContact['postal_code'] ?? '',
$fosterContact['city'] ?? '',
]),
) ?:
'Chez ' . $fosterContact['name'];
}
if ($status === 'chat_libre') {
$currentAddress =
trim(implode(' ', array_filter([$freeCatAddress, $freeCatCity]))) ?:
($freeCatSiteName ?:
$currentAddress);
}
$depositor = self::resolveDirectoryContact(
$db,
(int) ($_POST['depositor_contact_id'] ?? 0),
trim((string) ($_POST['new_depositor_name'] ?? '')),
'person',
'deposant',
);
$depositorId = $depositor ? (int) $depositor['id'] : null;
// Le lieu actuel est conservé lorsque la fiche archivée est corrigée.
if ($isArchivedCorrection) {
$currentAddress = (string) ($old['current_address'] ?? '');
}
if ($currentAddress === '' && !$isArchivedCorrection) {
$currentAddress = '5 RUE NOTRE DAME 29260 LESNEVEN';
}
// Géocode rescue si adresse présente
$rescueLat = null;
$rescueLng = null;
if ($rescueAddress !== '') {
$geo = GeoService::geocode($db, $rescueAddress);
if ($geo) {
$rescueLat = $geo['lat'];
$rescueLng = $geo['lng'];
}
}
// Géocode current (toujours)
$currentLat = null;
$currentLng = null;
if ($currentAddress !== '' && !$isArchivedCorrection) {
$geo2 = GeoService::geocode($db, $currentAddress);
if ($geo2) {
$currentLat = $geo2['lat'];
$currentLng = $geo2['lng'];
}
}
$fiv = $fiv ?? (int) ($old['fiv'] ?? 0);
$felv = $felv ?? (int) ($old['felv'] ?? 0);
if ($refugeRoom !== null && !ShelterRoomService::accepts($availableRooms[$refugeRoom], [
'species' => $species,
'sex' => $sex,
'fiv' => $fiv,
])) {
http_response_code(400);
echo h(t('rooms.animal_not_allowed'));
return;
}
$careBox = in_array($status, ['soin', 'quarantaine'], true) ? $old['care_box_key'] ?? null : null;
if ($isArchivedCorrection) {
$refugeRoom = $old['refuge_room'] ?? null;
$careBox = $old['care_box_key'] ?? null;
$quarantine_until = $old['quarantine_until'] ?? null;
$currentLat = $old['current_lat'] ?? null;
$currentLng = $old['current_lng'] ?? null;
}
if ($id > 0) {
$stmt = $db->prepare("
UPDATE animals SET
name=:name,
internal_code=:internal_code,
status=:status,
refuge_room=:refuge_room,
care_box_key=:care_box_key,
species=:species,
sex=:sex,
sterilized=:sterilized,
sterilization_status=:sterilization_status,sterilization_date=:sterilization_date,sterilization_vet_contact_id=:sterilization_vet,sterilization_clinic_contact_id=:sterilization_clinic,
chip_id=:chip_id,
identification_type=:identification_type,identification_date=:identification_date,identification_registration_status=:identification_registration_status,
adoption_availability=:adoption_availability,adoption_available_from=:adoption_available_from,adoption_unavailability_reason=:adoption_unavailability_reason,
website_published=:website_published,website_description=:website_description,
breed=:breed,
color=:color,
hair_type=:hair_type,
compatibility_dogs=:compatibility_dogs,
compatibility_cats=:compatibility_cats,
compatibility_children=:compatibility_children,
house_trained=:house_trained,
free_cat_site_name=:free_cat_site_name,free_cat_city=:free_cat_city,free_cat_address=:free_cat_address,
free_cat_caretaker_contact_id=:free_cat_caretaker_contact_id,free_cat_captured_at=:free_cat_captured_at,
free_cat_released_at=:free_cat_released_at,free_cat_tracking_status=:free_cat_tracking_status,
birth_date=:birth_date,
birth_is_estimated=:birth_is_estimated,
birth_estimated_months=:birth_estimated_months,
notes=:notes,
intake_date=:intake_date,
intake_type=:intake_type,
intake_reason=:intake_reason,
intake_owner_care_home=:intake_owner_care_home,
depositor_contact_id=:depositor_contact_id,
intake_circumstances=:intake_circumstances,
quarantine_until=:quarantine_until,
fiv=:fiv,
felv=:felv,
rescue_address=:rescue_address,rescue_location_name=:rescue_location_name,
rescue_location_confidence=CASE WHEN COALESCE(:rescue_location_name,'')=COALESCE(rescue_location_name,'') AND COALESCE(:rescue_address,'')=COALESCE(rescue_address,'') THEN rescue_location_confidence WHEN TRIM(COALESCE(:rescue_location_name,''))<>'' OR TRIM(COALESCE(:rescue_address,''))<>'' THEN 'documented' ELSE NULL END,
rescue_location_evidence=CASE WHEN COALESCE(:rescue_location_name,'')=COALESCE(rescue_location_name,'') AND COALESCE(:rescue_address,'')=COALESCE(rescue_address,'') THEN rescue_location_evidence WHEN TRIM(COALESCE(:rescue_location_name,''))<>'' OR TRIM(COALESCE(:rescue_address,''))<>'' THEN 'Lieu renseigné depuis la fiche animale' ELSE NULL END,
rescue_lat=:rescue_lat, rescue_lng=:rescue_lng,
current_address=:current_address, current_lat=:current_lat, current_lng=:current_lng,
updated_at=datetime('now')
WHERE id=:id
");
$stmt->execute([
':id' => $id,
':name' => $name,
':internal_code' => $internal_code,
':status' => $status,
':refuge_room' => $refugeRoom,
':care_box_key' => $careBox,
':species' => $species,
':sex' => $sex,
':sterilized' => $sterilized,
':sterilization_status' => $sterilizationStatus,
':sterilization_date' => $sterilizationDate ?: null,
':sterilization_vet' => $sterilizationVetId,
':sterilization_clinic' => $sterilizationClinicId,
':chip_id' => $chip_id,
':identification_type' => $identificationType,
':identification_date' => $identificationDate ?: null,
':identification_registration_status' => $identificationRegistration,
':adoption_availability' => $adoptionAvailability,
':adoption_available_from' => $adoptionAvailableFrom ?: null,
':adoption_unavailability_reason' => $adoptionUnavailabilityReason ?: null,
':website_published' => $websitePublished,
':website_description' => $websiteDescription ?: null,
':breed' => $breed,
':color' => $color !== '' ? $color : null,
':hair_type' => $hairType !== '' ? $hairType : null,
':compatibility_dogs' => $compatibilityDogs,
':compatibility_cats' => $compatibilityCats,
':compatibility_children' => $compatibilityChildren,
':house_trained' => $houseTrained,
':free_cat_site_name' => $freeCatSiteName ?: null,
':free_cat_city' => $freeCatCity ?: null,
':free_cat_address' => $freeCatAddress ?: null,
':free_cat_caretaker_contact_id' => $freeCatCaretakerId,
':free_cat_captured_at' => $freeCatCapturedAt ?: null,
':free_cat_released_at' => $freeCatReleasedAt ?: null,
':free_cat_tracking_status' => $freeCatTrackingStatus,
':birth_date' => $birth_date,
':birth_is_estimated' => $birth_is_estimated,
':birth_estimated_months' => $birth_estimated_months,
':notes' => $notes,
':intake_date' => $intake_date,
':intake_type' => $intakeType,
':intake_reason' => $intakeReason,
':intake_owner_care_home' => $intakeOwnerCareHome,
':depositor_contact_id' => $depositorId,
':intake_circumstances' => $intakeCircumstances ?: null,
':quarantine_until' => $quarantine_until,
':fiv' => $fiv,
':felv' => $felv,
':rescue_address' => $rescueAddress,
':rescue_location_name' => $rescueLocationName ?: null,
':rescue_lat' => $rescueLat,
':rescue_lng' => $rescueLng,
':current_address' => $currentAddress,
':current_lat' => $currentLat,
':current_lng' => $currentLng,
]);
if ($old) {
LocationHistoryService::record(
$db,
$id,
$old,
[
'status' => $status,
'refuge_room' => $refugeRoom,
'care_box_key' => $careBox,
'current_address' => $currentAddress,
],
'animal_form',
$locationChangeReason ?: 'Emplacement modifié depuis la fiche animale',
);
}
//self::logHistory(
// $id,
// 'update',
// 'Fiche modifiée'
//);
// Code interne : en édition, on ne le regénère PAS
$internal_code = trim((string) ($_POST['internal_code'] ?? ''));
if ($id > 0) {
if ($internal_code === '' && $old && !empty($old['internal_code'])) {
$internal_code = (string) $old['internal_code'];
}
} else {
if ($internal_code === '') {
$internal_code = self::nextInternalCode($name);
}
}
if ($old) {
// Valeurs persistées à comparer avec l'état précédent de la fiche.
$new = [
'name' => $name,
'internal_code' => $internal_code,
'status' => $status,
'refuge_room' => $refugeRoom,
'species' => $species,
'sex' => $sex,
'sterilized' => $sterilized,
'sterilization_status' => $sterilizationStatus,
'sterilization_date' => $sterilizationDate,
'sterilization_vet_contact_id' => $sterilizationVetId,
'sterilization_clinic_contact_id' => $sterilizationClinicId,
'chip_id' => $chip_id,
'identification_type' => $identificationType,
'identification_date' => $identificationDate,
'identification_registration_status' => $identificationRegistration,
'adoption_availability' => $adoptionAvailability,
'adoption_available_from' => $adoptionAvailableFrom,
'adoption_unavailability_reason' => $adoptionUnavailabilityReason,
'website_published' => $websitePublished,
'website_description' => $websiteDescription,
'breed' => $breed,
'color' => $color,
'hair_type' => $hairType,
'compatibility_dogs' => $compatibilityDogs,
'compatibility_cats' => $compatibilityCats,
'compatibility_children' => $compatibilityChildren,
'house_trained' => $houseTrained,
'birth_date' => $birth_date,
'notes' => $notes,
'intake_date' => $intake_date,
'intake_type' => $intakeType,
'intake_reason' => $intakeReason,
'intake_owner_care_home' => $intakeOwnerCareHome,
'depositor_contact_id' => $depositorId,
'intake_circumstances' => $intakeCircumstances,
'rescue_location_name' => $rescueLocationName,
'quarantine_until' => $quarantine_until,
'fiv' => $fiv,
'felv' => $felv,
// Cette liste doit rester alignée sur les champs écrits par la requête UPDATE.
];
// Champs à ignorer (bruit)
$ignore = ['id', 'created_at', 'updated_at', 'internal_code'];
$changes = [];
foreach ($new as $field => $newVal) {
if (in_array($field, $ignore, true)) {
continue;
}
$oldVal = $old[$field] ?? null;
// Normalisation légère pour éviter les faux positifs
$o = is_null($oldVal) ? '' : (string) $oldVal;
$n = is_null($newVal) ? '' : (string) $newVal;
if ($o !== $n) {
$label = match ($field) {
'name' => 'Nom',
'status' => 'Statut',
'refuge_room' => 'Salle au refuge',
'species' => 'Espèce',
'sex' => 'Sexe',
'sterilized' => 'Stérilisation',
'sterilization_status' => 'État de stérilisation',
'sterilization_date' => 'Date de stérilisation',
'sterilization_vet_contact_id' => 'Vétérinaire de stérilisation',
'sterilization_clinic_contact_id' => 'Cabinet de stérilisation',
'chip_id' => 'Puce',
'identification_type' => 'Type d’identification',
'identification_date' => 'Date d’identification',
'identification_registration_status' => 'Enregistrement I-CAD',
'adoption_availability' => 'Disponibilité à l’adoption',
'adoption_available_from' => 'Disponible à partir du',
'adoption_unavailability_reason' => 'Motif d’indisponibilité',
'website_published' => 'Publication sur le site',
'website_description' => 'Présentation publique',
'breed' => 'Race',
'color' => 'Couleur / robe',
'hair_type' => 'Type de poil',
'compatibility_dogs' => 'Compatibilité chiens',
'compatibility_cats' => 'Compatibilité chats',
'compatibility_children' => 'Compatibilité enfants',
'house_trained' => 'Propreté',
'birth_date' => 'Naissance',
'intake_date' => 'Arrivée',
'intake_type' => 'Mode d’entrée',
'intake_reason' => 'Motif d’entrée',
'intake_owner_care_home' => 'Entrée du propriétaire en établissement',
'depositor_contact_id' => 'Déposant',
'intake_circumstances' => 'Circonstances de l’entrée',
'rescue_location_name' => 'Commune d’origine',
'quarantine_until' => 'Fin quarantaine',
'fiv' => 'FIV',
'felv' => 'FELV',
'notes' => 'Notes',
default => $field,
};
// Evite d’exploser le log sur un gros champ notes
if (in_array($field, ['notes', 'website_description'], true)) {
$changes[] = "$label : (modifié)";
} else {
if (
in_array(
$field,
[
'compatibility_dogs',
'compatibility_cats',
'compatibility_children',
'house_trained',
],
true,
)
) {
$profileLabels = ['unknown' => 'Inconnu', 'yes' => 'Oui', 'no' => 'Non'];
$o = $profileLabels[$o] ?? $o;
$n = $profileLabels[$n] ?? $n;
}
$changes[] = "$label : " . ($o === '' ? '∅' : $o) . ' → ' . ($n === '' ? '∅' : $n);
}
}
}
if (!empty($changes)) {
$statusChanged = (string) ($old['status'] ?? '') !== (string) $status;
$type = $statusChanged ? 'status' : 'update';
$label = $statusChanged ? 'Changement de statut' : 'Fiche modifiée';
self::logHistory($id, $type, $label, implode("\n", $changes));
}
}
} else {
$stmt = $db->prepare("
INSERT INTO animals
(name, internal_code, status, refuge_room, species, sex, sterilized,sterilization_status,sterilization_date,sterilization_vet_contact_id,sterilization_clinic_contact_id, chip_id,identification_type,identification_date,identification_registration_status,adoption_availability,adoption_available_from,adoption_unavailability_reason,website_published,website_description, breed, color, hair_type,
compatibility_dogs,compatibility_cats,compatibility_children,house_trained,
free_cat_site_name,free_cat_city,free_cat_address,free_cat_caretaker_contact_id,free_cat_captured_at,free_cat_released_at,free_cat_tracking_status,
birth_date, birth_is_estimated, birth_estimated_months,
notes,intake_date,intake_type,intake_reason,intake_owner_care_home,depositor_contact_id,intake_circumstances,quarantine_until,fiv,felv,created_at,updated_at,rescue_address,rescue_location_name,rescue_location_confidence,rescue_location_evidence,rescue_lat,rescue_lng,current_address,current_lat,current_lng)
VALUES
(:name,:internal_code,:status,:refuge_room,:species,:sex,:sterilized,:sterilization_status,:sterilization_date,:sterilization_vet,:sterilization_clinic,:chip_id,:identification_type,:identification_date,:identification_registration_status,:adoption_availability,:adoption_available_from,:adoption_unavailability_reason,:website_published,:website_description,:breed,:color,:hair_type,
:compatibility_dogs,:compatibility_cats,:compatibility_children,:house_trained,
:free_cat_site_name,:free_cat_city,:free_cat_address,:free_cat_caretaker_contact_id,:free_cat_captured_at,:free_cat_released_at,:free_cat_tracking_status,
:birth_date,:birth_is_estimated,:birth_estimated_months,
:notes,:intake_date,:intake_type,:intake_reason,:intake_owner_care_home,:depositor_contact_id,:intake_circumstances,:quarantine_until,:fiv,:felv,datetime('now'),datetime('now'),
:rescue_address,:rescue_location_name,CASE WHEN TRIM(COALESCE(:rescue_location_name,''))<>'' OR TRIM(COALESCE(:rescue_address,''))<>'' THEN 'documented' ELSE NULL END,CASE WHEN TRIM(COALESCE(:rescue_location_name,''))<>'' OR TRIM(COALESCE(:rescue_address,''))<>'' THEN 'Lieu renseigné à la création de la fiche animale' ELSE NULL END,:rescue_lat,:rescue_lng,:current_address,:current_lat,:current_lng)
");
$stmt->execute([
':name' => $name,
':internal_code' => $internal_code,
':status' => $status,
':refuge_room' => $refugeRoom,
':species' => $species,
':sex' => $sex,
':sterilized' => $sterilized,
':sterilization_status' => $sterilizationStatus,
':sterilization_date' => $sterilizationDate ?: null,
':sterilization_vet' => $sterilizationVetId,
':sterilization_clinic' => $sterilizationClinicId,
':chip_id' => $chip_id,
':identification_type' => $identificationType,
':identification_date' => $identificationDate ?: null,
':identification_registration_status' => $identificationRegistration,
':adoption_availability' => $adoptionAvailability,
':adoption_available_from' => $adoptionAvailableFrom ?: null,
':adoption_unavailability_reason' => $adoptionUnavailabilityReason ?: null,
':website_published' => $websitePublished,
':website_description' => $websiteDescription ?: null,
':breed' => $breed,
':color' => $color !== '' ? $color : null,
':hair_type' => $hairType !== '' ? $hairType : null,
':compatibility_dogs' => $compatibilityDogs,
':compatibility_cats' => $compatibilityCats,
':compatibility_children' => $compatibilityChildren,
':house_trained' => $houseTrained,
':free_cat_site_name' => $freeCatSiteName ?: null,
':free_cat_city' => $freeCatCity ?: null,
':free_cat_address' => $freeCatAddress ?: null,
':free_cat_caretaker_contact_id' => $freeCatCaretakerId,
':free_cat_captured_at' => $freeCatCapturedAt ?: null,
':free_cat_released_at' => $freeCatReleasedAt ?: null,
':free_cat_tracking_status' => $freeCatTrackingStatus,
':birth_date' => $birth_date,
':birth_is_estimated' => $birth_is_estimated,
':birth_estimated_months' => $birth_estimated_months,
':notes' => $notes,
':intake_date' => $intake_date,
':intake_type' => $intakeType,
':intake_reason' => $intakeReason,
':intake_owner_care_home' => $intakeOwnerCareHome,
':depositor_contact_id' => $depositorId,
':intake_circumstances' => $intakeCircumstances ?: null,
':quarantine_until' => $quarantine_until,
':fiv' => $fiv,
':felv' => $felv,
':rescue_address' => $rescueAddress,
':rescue_location_name' => $rescueLocationName ?: null,
':rescue_lat' => $rescueLat,
':rescue_lng' => $rescueLng,
':current_address' => $currentAddress,
':current_lat' => $currentLat,
':current_lng' => $currentLng,
]);
$id = (int) $db->lastInsertId();
LocationHistoryService::record(
$db,
$id,
null,
[
'status' => $status,
'refuge_room' => $refugeRoom,
'care_box_key' => null,
'current_address' => $currentAddress,
],
'animal_creation',
'Emplacement lors de la création de la fiche',
);
self::logHistory($id, 'create', 'Animal ajouté', "Nom : $name\nStatut : $status");
}
self::syncIntakeMovement(
$db,
$id,
$intake_date,
$intakeType,
$submittedIntakeReason,
$depositorId,
$rescueLocationName,
$rescueAddress,
$intakeCircumstances,
);
if (!$isArchivedCorrection && in_array($status, ['fa', 'fa_permanente'], true) && $fosterContactId) {
$latest = $db->prepare(
"SELECT contact_id FROM animal_placements WHERE animal_id=:id AND event_type='foster' ORDER BY date(event_date) DESC,id DESC LIMIT 1",
);
$latest->execute([':id' => $id]);
$latestFosterId = (int) $latest->fetchColumn();
$enteredFoster = !$old || !in_array((string) ($old['status'] ?? ''), ['fa', 'fa_permanente'], true);
if ($enteredFoster || $latestFosterId !== $fosterContactId) {
$db->prepare(
"INSERT INTO animal_placements(animal_id,event_type,event_date,contact_id,reason,notes,created_by) VALUES(:animal,'foster',date('now'),:contact,:reason,:notes,:user)",
)->execute([
':animal' => $id,
':contact' => $fosterContactId,
':reason' => 'Changement d’emplacement depuis la fiche',
':notes' => $status === 'fa_permanente' ? 'Famille d’accueil permanente' : null,
':user' => Auth::id(),
]);
}
}
$initial_weight = trim((string) ($_POST['initial_weight'] ?? ''));
$initial_weight = $initial_weight === '' ? null : (float) $initial_weight;
if ($initial_weight !== null && $initial_weight > 0) {
$stmt = $db->prepare("
INSERT INTO measurements(animal_id, type, value, unit, measured_at)
VALUES(:aid,'weight',:v,'kg',datetime('now'))
");
$stmt->execute([
':aid' => $id,
':v' => $initial_weight,
]);
}
header('Location: /animal?id=' . $id);
exit();
}
}