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 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 = 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 l’instant, 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 à l’UPDATE ici, mais sans maintenance lourde : // si tu ajoutes un champ au formulaire + UPDATE, tu l’ajoutes 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à à l’UPDATE -> 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 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(); } }