Globinours/app/Controllers/AnimalRecordActions.php

1041 lines
49 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);
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' => '',
'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',
// adapte si tu as fiv/felv en colonnes
'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 dune 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 = strtoupper(trim($name));
$base = preg_replace('/[^A-Z0-9]+/u', '_', $base);
$base = trim($base, '_');
if ($base === '') {
$base = 'ANIMAL';
}
$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 (!in_array($identificationRegistration, ['unknown', 'pending', 'registered'], 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;
}
// FIV/FELV (selon ton stockage actuel)
$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 === '') {
http_response_code(400);
echo h(t('error.name_required'));
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;
// Code interne : auto si vide (et idéalement readonly en edit)
$internal_code = trim((string) ($_POST['internal_code'] ?? ''));
if ($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 {
// si pas en quarantaine, on vide la sortie (optionnel, mais logique)
$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;
// Pour linstant, tu bloques le lieu actuel
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);
$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) {
// Les nouvelles valeurs “connues” (ce que tu viens de sauver)
// Astuce: tu mets TOUT ce que tu passes à lUPDATE ici, mais sans maintenance lourde :
// si tu ajoutes un champ au formulaire + UPDATE, tu lajoutes déjà ici de toute façon.
$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,
// ajoute ici uniquement les champs que tu SAUVEGARDES dans UPDATE
// (si demain tu ajoutes 3 champs au form, tu les ajouteras déjà à lUPDATE -> copie/colle aussi ici)
];
// 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 didentification',
'identification_date' => 'Date didentification',
'identification_registration_status' => 'Enregistrement I-CAD',
'adoption_availability' => 'Disponibilité à ladoption',
'adoption_available_from' => 'Disponible à partir du',
'adoption_unavailability_reason' => 'Motif dindisponibilité',
'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 dentrée',
'intake_reason' => 'Motif dentrée',
'intake_owner_care_home' => 'Entrée du propriétaire en établissement',
'depositor_contact_id' => 'Déposant',
'intake_circumstances' => 'Circonstances de lentrée',
'rescue_location_name' => 'Commune dorigine',
'quarantine_until' => 'Fin quarantaine',
'fiv' => 'FIV',
'felv' => 'FELV',
'notes' => 'Notes',
default => $field,
};
// Evite dexploser 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 demplacement depuis la fiche',
':notes' => $status === 'fa_permanente' ? 'Famille daccueil 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();
}
}