prepare('SELECT fiv, felv, fiv_status, felv_status FROM animals WHERE id = :id'); $animalStmt->execute([':id' => $animalId]); $old = $animalStmt->fetch(PDO::FETCH_ASSOC); if (!$old) { http_response_code(404); return; } $oldFivStatus = (string) ($old['fiv_status'] ?: ((int) $old['fiv'] === 1 ? 'pos' : 'neg')); $oldFelvStatus = (string) ($old['felv_status'] ?: ((int) $old['felv'] === 1 ? 'pos' : 'neg')); $db->beginTransaction(); try { $db->prepare( " UPDATE animals SET fiv_status = :fiv_status, felv_status = :felv_status, fiv = :fiv, felv = :felv, updated_at = datetime('now') WHERE id = :id ", )->execute([ ':fiv_status' => $fivStatus, ':felv_status' => $felvStatus, ':fiv' => $fivStatus === 'pos' ? 1 : 0, ':felv' => $felvStatus === 'pos' ? 1 : 0, ':id' => $animalId, ]); $noteStmt = $db->prepare(" INSERT INTO medical_notes (animal_id, noted_at, kind, reason, diagnosis, plan, created_at) VALUES (:animal_id, :noted_at, 'diagnostic', :reason, :diagnosis, :plan, datetime('now')) "); foreach ( [ ['label' => 'FIV', 'old' => $oldFivStatus, 'new' => $fivStatus], ['label' => 'FeLV', 'old' => $oldFelvStatus, 'new' => $felvStatus], ] as $test ) { if ($test['new'] === 'pos' && $test['old'] !== 'pos') { $diagnosis = $test['label'] . ' positif'; $noteStmt->execute([ ':animal_id' => $animalId, ':noted_at' => $testDate . ' 12:00:00', ':reason' => 'Résultat de test ' . $test['label'], ':diagnosis' => $diagnosis, ':plan' => $testNotes !== '' ? $testNotes : null, ]); self::logHistory( $animalId, 'medical', $diagnosis, 'Test positif le ' . $testDate . ($testNotes !== '' ? "\n" . $testNotes : ''), ); } } $conditionSelect = $db->prepare( 'SELECT status FROM animal_health_conditions WHERE id = :id AND animal_id = :animal_id', ); $conditionUpdate = $db->prepare(" UPDATE animal_health_conditions SET name = :name, status = :status, diagnosed_at = :diagnosed_at, resolved_at = :resolved_at, notes = :notes, updated_at = datetime('now') WHERE id = :id AND animal_id = :animal_id "); $conditionInsert = $db->prepare(' INSERT INTO animal_health_conditions (animal_id, name, status, diagnosed_at, resolved_at, notes) VALUES (:animal_id, :name, :status, :diagnosed_at, :resolved_at, :notes) '); foreach ($conditions as $key => $condition) { if (!is_array($condition)) { continue; } $name = trim((string) ($condition['name'] ?? '')); if ($name === '') { continue; } $status = ($condition['status'] ?? 'active') === 'resolved' ? 'resolved' : 'active'; $diagnosedAt = trim((string) ($condition['diagnosed_at'] ?? '')) ?: null; $notes = trim((string) ($condition['notes'] ?? '')) ?: null; $conditionId = ctype_digit((string) $key) ? (int) $key : 0; $previousStatus = null; if ($conditionId > 0) { $conditionSelect->execute([':id' => $conditionId, ':animal_id' => $animalId]); $previousStatus = $conditionSelect->fetchColumn(); if ($previousStatus === false) { continue; } $conditionUpdate->execute([ ':name' => $name, ':status' => $status, ':diagnosed_at' => $diagnosedAt, ':resolved_at' => $status === 'resolved' ? date('Y-m-d') : null, ':notes' => $notes, ':id' => $conditionId, ':animal_id' => $animalId, ]); } else { $conditionInsert->execute([ ':animal_id' => $animalId, ':name' => $name, ':status' => $status, ':diagnosed_at' => $diagnosedAt, ':resolved_at' => $status === 'resolved' ? date('Y-m-d') : null, ':notes' => $notes, ]); } if ($status === 'active' && $previousStatus !== 'active') { $noteStmt->execute([ ':animal_id' => $animalId, ':noted_at' => ($diagnosedAt ?: date('Y-m-d')) . ' 12:00:00', ':reason' => 'Statut médical global', ':diagnosis' => $name, ':plan' => $notes, ]); self::logHistory( $animalId, 'medical', 'Pathologie / vigilance ajoutée', $name . ($notes ? "\n" . $notes : ''), ); } } $changes = []; if ($oldFivStatus !== $fivStatus) { $changes[] = 'FIV : ' . $oldFivStatus . ' → ' . $fivStatus; } if ($oldFelvStatus !== $felvStatus) { $changes[] = 'FeLV : ' . $oldFelvStatus . ' → ' . $felvStatus; } if ($changes) { self::logHistory($animalId, 'medical', 'Statut médical mis à jour', implode("\n", $changes)); } $db->commit(); } catch (Throwable $e) { $db->rollBack(); throw $e; } header('Location: /animal?id=' . $animalId . '#tab-med'); exit(); } public static function addMedical(): void { if ($_SERVER['REQUEST_METHOD'] !== 'POST') { http_response_code(405); return; } $animalId = (int) ($_POST['animal_id'] ?? 0); if ($animalId <= 0) { http_response_code(400); return; } $kind = trim($_POST['kind'] ?? 'consult'); $reason = trim($_POST['reason'] ?? ''); $symptoms = trim($_POST['symptoms'] ?? ''); $exam = trim($_POST['exam'] ?? ''); $diagnosis = trim($_POST['diagnosis'] ?? ''); $plan = trim($_POST['plan'] ?? ''); $markSterilized = !empty($_POST['mark_sterilized']); if ($markSterilized) { $kind = 'acte'; if ($reason === '') { $reason = 'Stérilisation'; } } $temperature = $_POST['temperature_c'] ?? null; $weight = $_POST['weight_kg'] ?? null; $clinicId = (int) ($_POST['clinic_id'] ?? 0); $clinicId = $clinicId > 0 ? $clinicId : null; $db = DB::pdo(); $clinicContact = self::resolveDirectoryContact( $db, (int) ($_POST['clinic_contact_id'] ?? 0), (string) ($_POST['new_clinic_name'] ?? ''), 'organization', 'cabinet', ); $vetContact = self::resolveDirectoryContact( $db, (int) ($_POST['vet_contact_id'] ?? 0), (string) ($_POST['new_vet_name'] ?? ''), 'person', 'veterinaire', ); self::normalizeVeterinaryClinic($db, $vetContact, $clinicContact); if (!$vetContact || !$clinicContact) { http_response_code(400); echo h(t('error.vet_clinic_required')); return; } $vetName = $vetContact['name'] ?? null; // 1️⃣ Insertion note médicale $stmt = $db->prepare(' INSERT INTO medical_notes (animal_id, kind, reason, symptoms, exam, diagnosis, plan, temperature_c, weight_kg, clinic_id, vet_name, clinic_contact_id, vet_contact_id, created_by, created_at) VALUES (:aid, :kind, :reason, :symptoms, :exam, :diagnosis, :plan, :temp, :weight, :clinic, :vet, :clinic_contact, :vet_contact, :created_by, :created) '); $stmt->execute([ ':aid' => $animalId, ':kind' => $kind, ':reason' => $reason, ':symptoms' => $symptoms, ':exam' => $exam, ':diagnosis' => $diagnosis, ':plan' => $plan, ':temp' => $temperature !== '' ? (float) $temperature : null, ':weight' => $weight !== '' ? (float) $weight : null, ':clinic' => $clinicId, ':vet' => $vetName, ':clinic_contact' => $clinicContact['id'] ?? null, ':vet_contact' => $vetContact['id'] ?? null, ':created_by' => Auth::id(), ':created' => self::now(), ]); // 👉 RÉCUPÉRATION ID NOTE $medicalNoteId = $db->lastInsertId(); if ((int) ($_POST['tariff_id'] ?? 0) > 0) { PricingService::record( $db, $animalId, (int) $_POST['tariff_id'], 1, substr(self::now(), 0, 10), 'Ajoutée avec la note médicale', ); } // ========================== // 2️⃣ Upload photos médicales // ========================== if (!empty($_FILES['medical_photos']['name'][0])) { $uploadDir = PrivateMediaService::medicalDir($animalId); if (!is_dir($uploadDir)) { PrivateMediaService::ensure($uploadDir); } foreach ($_FILES['medical_photos']['tmp_name'] as $key => $tmpName) { if ($_FILES['medical_photos']['error'][$key] !== UPLOAD_ERR_OK) { continue; } if ($_FILES['medical_photos']['size'][$key] > 20 * 1024 * 1024) { continue; } $finfo = finfo_open(FILEINFO_MIME_TYPE); $mime = finfo_file($finfo, $tmpName); $allowedTypes = [ 'image/jpeg' => 'jpg', 'image/png' => 'png', 'image/webp' => 'webp', ]; if (!isset($allowedTypes[$mime])) { continue; } try { $stored = ImageService::storeUploaded($tmpName, $uploadDir, 'med', 'medical'); $safeName = $stored['filename']; @chmod($stored['path'], 0600); $stmtPhoto = $db->prepare(' INSERT INTO medical_photos (animal_id, medical_note_id, filename) VALUES (?, ?, ?) '); $stmtPhoto->execute([$animalId, $medicalNoteId, $safeName]); } catch (Throwable) { continue; } } } // 3️⃣ Poids → measurements (graph) if ($weight !== '' && is_numeric($weight)) { $db->prepare( " INSERT INTO measurements (animal_id, measured_at, type, value, unit, notes, created_by) VALUES (:aid, :at, 'weight', :v, 'kg', 'from medical note', :created_by) ", )->execute([ ':aid' => $animalId, ':at' => self::now(), ':v' => (float) $weight, ':created_by' => Auth::id(), ]); } // 4️⃣ Historique self::logHistory($animalId, 'medical', 'Note médicale ajoutée', $reason ?: null); // 5️⃣ Touch animal if ($markSterilized) { $db->prepare( "UPDATE animals SET sterilized=1,sterilization_status='yes',sterilization_date=COALESCE(sterilization_date,date(:date)),sterilization_vet_contact_id=COALESCE(:vet,sterilization_vet_contact_id),sterilization_clinic_contact_id=COALESCE(:clinic,sterilization_clinic_contact_id) WHERE id=:id", )->execute([ ':date' => self::now(), ':vet' => $vetContact['id'] ?? null, ':clinic' => $clinicContact['id'] ?? null, ':id' => $animalId, ]); } $db->prepare( ' UPDATE animals SET updated_at = :u WHERE id = :id ', )->execute([ ':u' => self::now(), ':id' => $animalId, ]); self::redirectToAnimal($animalId); } public static function addTreatment(): void { if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST') { http_response_code(405); echo h(t('error.method_not_allowed')); return; } $id = (int) ($_POST['animal_id'] ?? 0); $medId = (int) ($_POST['medication_id'] ?? 0); $createMedication = !empty($_POST['create_medication']); if ($id <= 0 || (!$createMedication && $medId <= 0)) { http_response_code(400); echo h(t('error.bad_request')); return; } $route = trim((string) ($_POST['route'] ?? '')); $dose = trim((string) ($_POST['dose_text'] ?? '')); $start = trim((string) ($_POST['start_date'] ?? date('Y-m-d'))); $end = trim((string) ($_POST['end_date'] ?? '')); $notes = trim((string) ($_POST['notes'] ?? '')); $giveMorning = !empty($_POST['give_morning']) ? 1 : 0; $giveEvening = !empty($_POST['give_evening']) ? 1 : 0; $giveAsNeeded = !empty($_POST['give_as_needed']) ? 1 : 0; $clinic_id = (int) ($_POST['clinic_id'] ?? 0); $clinic_id = $clinic_id > 0 ? $clinic_id : null; $vet_name = trim((string) ($_POST['vet_name'] ?? '')); $vet_name = $vet_name !== '' ? mb_strtoupper($vet_name, 'UTF-8') : null; if ($dose === '') { http_response_code(400); echo h(t('error.dose_required')); return; } $db = DB::pdo(); if ($createMedication) { $medicationName = trim((string) ($_POST['new_medication_name'] ?? '')); $medicationMolecule = trim((string) ($_POST['new_medication_molecule'] ?? '')); $medicationForm = trim((string) ($_POST['new_medication_form'] ?? '')); if ($medicationName === '') { http_response_code(400); echo h(t('error.medication_name_required')); return; } $existing = $db->prepare('SELECT id FROM ref_medications WHERE name = :name COLLATE NOCASE LIMIT 1'); $existing->execute([':name' => $medicationName]); $medId = (int) ($existing->fetchColumn() ?: 0); if ($medId <= 0) { $insertMedication = $db->prepare( 'INSERT INTO ref_medications(name, molecule, form) VALUES(:name,:molecule,:form)', ); $insertMedication->execute([ ':name' => mb_substr($medicationName, 0, 150), ':molecule' => $medicationMolecule !== '' ? mb_substr($medicationMolecule, 0, 150) : null, ':form' => $medicationForm !== '' ? mb_substr($medicationForm, 0, 100) : null, ]); $medId = (int) $db->lastInsertId(); } } $clinicContact = self::resolveDirectoryContact( $db, (int) ($_POST['clinic_contact_id'] ?? 0), (string) ($_POST['new_clinic_name'] ?? ''), 'organization', 'cabinet', ); $vetContact = self::resolveDirectoryContact( $db, (int) ($_POST['vet_contact_id'] ?? 0), (string) ($_POST['new_vet_name'] ?? ''), 'person', 'veterinaire', ); self::normalizeVeterinaryClinic($db, $vetContact, $clinicContact); if (!$vetContact || !$clinicContact) { http_response_code(400); echo h(t('error.vet_clinic_required')); return; } $vet_name = $vetContact['name'] ?? $vet_name; $stmt = $db->prepare(' INSERT INTO treatments(animal_id, medication_id, route, dose_text, start_date, end_date, ongoing, notes, clinic_id, vet_name, clinic_contact_id, vet_contact_id, give_morning, give_evening, give_as_needed, created_by) VALUES(:aid,:mid,:route,:dose,:start,:end,1,:notes,:clinic_id,:vet_name,:clinic_contact_id,:vet_contact_id,:give_morning,:give_evening,:give_as_needed,:created_by) '); $stmt->execute([ ':aid' => $id, ':mid' => $medId, ':route' => $route !== '' ? $route : null, ':dose' => $dose, ':start' => $start, ':end' => $end !== '' ? $end : null, ':notes' => $notes !== '' ? $notes : null, ':clinic_id' => $clinic_id, ':vet_name' => $vet_name, ':clinic_contact_id' => $clinicContact['id'] ?? null, ':vet_contact_id' => $vetContact['id'] ?? null, ':give_morning' => $giveMorning, ':give_evening' => $giveEvening, ':give_as_needed' => $giveAsNeeded, ':created_by' => Auth::id(), ]); if ((int) ($_POST['tariff_id'] ?? 0) > 0) { PricingService::record($db, $id, (int) $_POST['tariff_id'], 1, $start, 'Ajoutée avec le traitement'); } self::logHistory( $id, 'treatment', 'Traitement ajouté', "Médicament #$medId\nDose : $dose\nDébut : $start" . ($end !== '' ? "\nFin : $end" : '') . ($notes !== '' ? "\nNotes : $notes" : ''), ); $db->prepare("UPDATE animals SET updated_at = datetime('now') WHERE id = :id")->execute([':id' => $id]); self::redirectToAnimal($id); } public static function addExpense(): void { if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') { http_response_code(405); return; } $animal = (int) ($_POST['animal_id'] ?? 0); $tariff = (int) ($_POST['tariff_id'] ?? 0); $qty = (float) str_replace(',', '.', (string) ($_POST['quantity'] ?? 1)); $date = (string) ($_POST['occurred_on'] ?? date('Y-m-d')); if (!$animal || !$tariff || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) { http_response_code(400); echo h(t('error.invalid_expense')); return; } $db = DB::pdo(); try { $total = PricingService::record($db, $animal, $tariff, $qty, $date, trim((string) ($_POST['notes'] ?? ''))); self::logHistory( $animal, 'medical', 'Dépense vétérinaire ajoutée', number_format($total / 100, 2, ',', ' ') . ' €', ); AuditService::log( 'animal_expense_added', '/animal/add-expense', 'Dépense vétérinaire enregistrée', 'animal', $animal, ['total_cents' => $total], ); self::redirectToAnimal($animal); } catch (Throwable $e) { http_response_code(400); echo h($e->getMessage()); } } public static function addVaccine(): void { if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST') { http_response_code(405); echo h(t('error.method_not_allowed')); return; } $db = DB::pdo(); $id = (int) ($_POST['animal_id'] ?? 0); $vacId = (int) ($_POST['vaccine_id'] ?? 0); if ($id <= 0 || $vacId <= 0) { http_response_code(400); echo h(t('error.bad_request')); return; } $done = trim((string) ($_POST['done_date'] ?? date('Y-m-d'))); $due = trim((string) ($_POST['due_date'] ?? '')); // Si due_date est fourni, on respecte. // Sinon auto selon protocole. if ($due === '') { // Birthdate (pour l'âge) $stmt = $db->prepare('SELECT birth_date FROM animals WHERE id = :id'); $stmt->execute([':id' => $id]); $birth = (string) ($stmt->fetchColumn() ?: ''); $isUnderOne = false; if ($birth !== '') { $birthDt = new DateTime($birth); $doneDt = new DateTime($done); $ageDays = (int) $birthDt->diff($doneDt)->format('%r%a'); if ($ageDays >= 0) { $isUnderOne = $ageDays < 365; } } // Protocole vaccin (family TC / L) $stmt = $db->prepare('SELECT family, name FROM ref_vaccines WHERE id = :vid'); $stmt->execute([':vid' => $vacId]); $vac = $stmt->fetch() ?: []; $family = strtoupper((string) ($vac['family'] ?? '')); // Détermine si c'est une primo pour la famille L (Leucose) $isPrimoL = false; if ($family === 'L') { $stmt = $db->prepare(" SELECT COUNT(*) FROM vaccinations v JOIN ref_vaccines rv ON rv.id = v.vaccine_id WHERE v.animal_id = :aid AND upper(coalesce(rv.family,'')) = 'L' "); $stmt->execute([':aid' => $id]); $countL = (int) $stmt->fetchColumn(); $isPrimoL = $countL === 0; } // Règles if ($family === 'L') { $due = $isPrimoL ? date('Y-m-d', strtotime($done . ' +1 month')) : date('Y-m-d', strtotime($done . ' +1 year')); } else { // TC (ou inconnu => TC) $due = $isUnderOne ? date('Y-m-d', strtotime($done . ' +1 month')) : date('Y-m-d', strtotime($done . ' +1 year')); } } $lot = trim((string) ($_POST['lot'] ?? '')); $manufacturer = trim((string) ($_POST['manufacturer'] ?? '')); $batchExpiresOn = trim((string) ($_POST['batch_expires_on'] ?? '')); if ($batchExpiresOn !== '' && !preg_match('/^\d{4}-\d{2}-\d{2}$/', $batchExpiresOn)) { http_response_code(400); echo h(t('error.invalid_batch_expiry')); return; } $administratorUserId = (int) ($_POST['administered_by_user_id'] ?? 0) ?: null; $administratorName = trim((string) ($_POST['administered_by_name'] ?? '')); if ($administratorUserId) { $check = $db->prepare('SELECT 1 FROM users WHERE id=:id AND active=1'); $check->execute([':id' => $administratorUserId]); if (!$check->fetchColumn()) { $administratorUserId = null; } } $notes = trim((string) ($_POST['notes'] ?? '')); $clinic_id = (int) ($_POST['clinic_id'] ?? 0); $clinic_id = $clinic_id > 0 ? $clinic_id : null; $vet_name = trim((string) ($_POST['vet_name'] ?? '')); $vet_name = $vet_name !== '' ? mb_strtoupper($vet_name, 'UTF-8') : null; $clinicContact = self::resolveDirectoryContact( $db, (int) ($_POST['clinic_contact_id'] ?? 0), (string) ($_POST['new_clinic_name'] ?? ''), 'organization', 'cabinet', ); $vetContact = self::resolveDirectoryContact( $db, (int) ($_POST['vet_contact_id'] ?? 0), (string) ($_POST['new_vet_name'] ?? ''), 'person', 'veterinaire', ); self::normalizeVeterinaryClinic($db, $vetContact, $clinicContact); if (!$vetContact || !$clinicContact) { http_response_code(400); echo h(t('error.vet_clinic_required')); return; } $vet_name = $vetContact['name'] ?? $vet_name; $stmt = $db->prepare(' INSERT INTO vaccinations(animal_id, vaccine_id, done_date, due_date, lot,manufacturer,batch_expires_on,administered_by_user_id,administered_by_name, notes, clinic_id, vet_name, clinic_contact_id, vet_contact_id, created_by) VALUES(:aid,:vid,:done,:due,:lot,:manufacturer,:batch_expiry,:administrator_user,:administrator_name,:notes,:clinic_id,:vet_name,:clinic_contact_id,:vet_contact_id,:created_by) '); $stmt->execute([ ':aid' => $id, ':vid' => $vacId, ':done' => $done, ':due' => $due !== '' ? $due : null, ':lot' => $lot !== '' ? $lot : null, ':manufacturer' => $manufacturer ?: null, ':batch_expiry' => $batchExpiresOn ?: null, ':administrator_user' => $administratorUserId, ':administrator_name' => $administratorName ?: null, ':notes' => $notes !== '' ? $notes : null, ':clinic_id' => $clinic_id, ':vet_name' => $vet_name, ':clinic_contact_id' => $clinicContact['id'] ?? null, ':vet_contact_id' => $vetContact['id'] ?? null, ':created_by' => Auth::id(), ]); InventoryService::consumeBatch( (int) ($_POST['inventory_batch_id'] ?? 0), (float) str_replace(',', '.', (string) ($_POST['inventory_quantity'] ?? 0)), $id, 'Vaccination du ' . $done, ); if ((int) ($_POST['tariff_id'] ?? 0) > 0) { PricingService::record($db, $id, (int) $_POST['tariff_id'], 1, $done, 'Ajoutée avec la vaccination'); } self::logHistory( $id, 'vaccine', 'Vaccin ajouté', "Vaccin #$vacId\nFait le : $done" . ($due !== '' ? "\nRappel : $due" : '') . ($manufacturer !== '' ? "\nFabricant : $manufacturer" : '') . ($lot !== '' ? "\nLot : $lot" : '') . ($batchExpiresOn !== '' ? "\nExpiration du lot : $batchExpiresOn" : '') . ($administratorName !== '' || $administratorUserId ? "\nAdministré par : " . ($administratorName !== '' ? $administratorName : 'utilisateur #' . $administratorUserId) : ''), ); $db->prepare("UPDATE animals SET updated_at = datetime('now') WHERE id = :id")->execute([':id' => $id]); self::redirectToAnimal($id); } }