0) { $stmt = $db->prepare( 'SELECT id, sex FROM animals WHERE id = :id AND deleted_at IS NULL AND archived_at IS NULL', ); $stmt->execute([':id' => $sourceAnimalId]); $source = $stmt->fetch(PDO::FETCH_ASSOC); if (!$source) { http_response_code(404); echo h(t('error.animal_not_found')); return; } if (($source['sex'] ?? '') === 'F') { $motherId = $sourceAnimalId; } if (($source['sex'] ?? '') === 'M') { $fatherId = $sourceAnimalId; } } $eligibleSql = " SELECT id, name FROM animals WHERE deleted_at IS NULL AND archived_at IS NULL AND lower(species) IN ('chat', 'cat') AND sex = :sex AND lower(status) NOT IN ('adopte', 'adopté', 'decede', 'décédé') ORDER BY name COLLATE NOCASE "; $stmt = $db->prepare($eligibleSql); $stmt->execute([':sex' => 'F']); $mothers = $stmt->fetchAll(PDO::FETCH_ASSOC); $stmt->execute([':sex' => 'M']); $fathers = $stmt->fetchAll(PDO::FETCH_ASSOC); $existingKittens = $db ->query( "SELECT a.id,a.name,a.internal_code,a.color,a.sex,a.status,a.archived_at FROM animals a WHERE a.deleted_at IS NULL AND lower(a.species) IN ('chat','cat') AND NOT EXISTS (SELECT 1 FROM litter_kittens lk WHERE lk.animal_id=a.id) ORDER BY CASE WHEN a.archived_at IS NULL THEN 0 ELSE 1 END,a.name COLLATE NOCASE,a.id", ) ->fetchAll(PDO::FETCH_ASSOC); render('litter_form.php', [ 'title' => t('litter.create'), 'pageDescription' => 'Création groupée des fiches d’une portée de chatons.', 'sourceAnimalId' => $sourceAnimalId, 'motherId' => $motherId, 'fatherId' => $fatherId, 'mothers' => $mothers, 'fathers' => $fathers, 'existingKittens' => $existingKittens, ]); } public static function saveLitter(): void { if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST') { http_response_code(405); return; } $db = DB::pdo(); $motherId = (int) ($_POST['mother_id'] ?? 0) ?: null; $fatherId = (int) ($_POST['father_id'] ?? 0) ?: null; $birthDate = trim((string) ($_POST['birth_date'] ?? '')) ?: null; $kittenMode = in_array((string) ($_POST['kitten_mode'] ?? 'new'), ['new', 'existing'], true) ? (string) $_POST['kitten_mode'] : 'new'; if ($birthDate !== null && preg_match('/^(\d{2})\/(\d{2})\/(\d{4})$/', $birthDate, $dateParts)) { $birthDate = $dateParts[3] . '-' . $dateParts[2] . '-' . $dateParts[1]; } $kittens = is_array($_POST['kittens'] ?? null) ? array_values($_POST['kittens']) : []; $existingKittenIds = is_array($_POST['existing_kitten_ids'] ?? null) ? array_values(array_unique(array_filter(array_map('intval', $_POST['existing_kitten_ids'])))) : []; if ($kittenMode === 'new') { $existingKittenIds = []; } else { $kittens = []; } if (!$kittens && !$existingKittenIds) { http_response_code(400); echo h(t('litter.need_kitten')); return; } if ($motherId !== null && $fatherId !== null && $motherId === $fatherId) { http_response_code(400); echo h(t('litter.different_parents')); return; } if ($birthDate !== null && !preg_match('/^\d{4}-\d{2}-\d{2}$/', $birthDate)) { http_response_code(400); echo h(t('error.invalid_birth_date')); return; } $parentIds = array_values(array_filter([$motherId, $fatherId])); if ($parentIds) { $placeholders = implode(',', array_fill(0, count($parentIds), '?')); $stmt = $db->prepare( "SELECT id, name, sex FROM animals WHERE id IN ($placeholders) AND deleted_at IS NULL AND archived_at IS NULL AND lower(species) IN ('chat','cat') AND lower(status) NOT IN ('adopte','adopté','decede','décédé')", ); $stmt->execute($parentIds); $parents = []; foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $parent) { $parents[(int) $parent['id']] = $parent; } if ( ($motherId && ($parents[$motherId]['sex'] ?? '') !== 'F') || ($fatherId && ($parents[$fatherId]['sex'] ?? '') !== 'M') ) { http_response_code(400); echo h(t('litter.invalid_parent')); return; } } else { $parents = []; } if ($existingKittenIds) { $placeholders = implode(',', array_fill(0, count($existingKittenIds), '?')); $stmt = $db->prepare( "SELECT id FROM animals WHERE id IN ($placeholders) AND deleted_at IS NULL AND lower(species) IN ('chat','cat') AND id NOT IN (?,?) AND NOT EXISTS (SELECT 1 FROM litter_kittens lk WHERE lk.animal_id=animals.id)", ); $stmt->execute([...$existingKittenIds, (int) ($motherId ?? 0), (int) ($fatherId ?? 0)]); if (count($stmt->fetchAll(PDO::FETCH_ASSOC)) !== count($existingKittenIds)) { http_response_code(400); echo h(t('litter.invalid_selected')); return; } } $baseName = trim((string) ($parents[$motherId]['name'] ?? ($parents[$fatherId]['name'] ?? 'Portée'))); $allowedSexes = ['F', 'M', 'U']; try { PrivateMediaService::moveVisibility($animalId, (string) $photo['filename'], $isPublic === 1); } catch (Throwable $e) { http_response_code(500); echo h(t('error.upload_failed')); return; } $db->beginTransaction(); try { $stmt = $db->prepare( 'INSERT INTO litters(mother_id, father_id, birth_date) VALUES(:mother, :father, :birth)', ); $stmt->execute([':mother' => $motherId, ':father' => $fatherId, ':birth' => $birthDate]); $litterId = (int) $db->lastInsertId(); $insertAnimal = $db->prepare(" INSERT INTO animals(name, internal_code, status, species, sex, breed, color, birth_date, birth_is_estimated,intake_date,intake_type,intake_reason, created_at, updated_at) VALUES(:name, :code, 'refuge', 'chat', :sex, 'Chat Européen', :color, :birth, 0,:birth,'born_in_care','birth', datetime('now'), datetime('now')) "); $link = $db->prepare( 'INSERT INTO litter_kittens(litter_id, animal_id, position) VALUES(:litter, :animal, :position)', ); foreach ($kittens as $index => $kitten) { if (!is_array($kitten)) { continue; } $position = $index + 1; $name = trim((string) ($kitten['name'] ?? '')); if ($name === '') { $name = $baseName . '-Bébé-' . $position; } $sex = (string) ($kitten['sex'] ?? 'U'); if (!in_array($sex, $allowedSexes, true)) { $sex = 'U'; } $color = trim((string) ($kitten['color'] ?? '')) ?: null; $insertAnimal->execute([ ':name' => $name, ':code' => Ids::nextAnimalCode($name), ':sex' => $sex, ':color' => $color, ':birth' => $birthDate, ]); $animalId = (int) $db->lastInsertId(); $link->execute([':litter' => $litterId, ':animal' => $animalId, ':position' => $position]); self::syncIntakeMovement( $db, $animalId, $birthDate, 'born_in_care', 'birth', null, '', '', 'Naissance enregistrée avec la portée n°' . $litterId, ); LocationHistoryService::record( $db, $animalId, null, ['status' => 'refuge', 'refuge_room' => null, 'care_box_key' => null, 'current_address' => null], 'litter_creation', 'Emplacement lors de la création de la portée', $birthDate ? $birthDate . ' 12:00:00' : null, ); self::logHistory($animalId, 'create', 'Chaton ajouté avec sa portée', 'Portée n°' . $litterId); } $position = count($kittens); foreach ($existingKittenIds as $animalId) { $link->execute([':litter' => $litterId, ':animal' => $animalId, ':position' => ++$position]); self::logHistory($animalId, 'update', 'Rattaché à une nouvelle portée', 'Portée n°' . $litterId); } $db->commit(); $targetId = (int) ($_POST['source_animal_id'] ?? 0) ?: ($motherId ?: ($fatherId ?: 0)); header('Location: ' . ($targetId ? '/animal?id=' . $targetId . '#tab-litters' : '/animals')); } catch (Throwable $e) { if ($db->inTransaction()) { $db->rollBack(); } throw $e; } } public static function litterAddExistingForm(): void { $db = DB::pdo(); $litterId = (int) ($_GET['litter_id'] ?? 0); $sourceAnimalId = (int) ($_GET['animal_id'] ?? 0); $stmt = $db->prepare( 'SELECT l.*,m.name mother_name,f.name father_name FROM litters l LEFT JOIN animals m ON m.id=l.mother_id LEFT JOIN animals f ON f.id=l.father_id WHERE l.id=:id', ); $stmt->execute([':id' => $litterId]); $litter = $stmt->fetch(PDO::FETCH_ASSOC); if (!$litter) { http_response_code(404); echo h(t('litter.not_found')); return; } $stmt = $db->prepare(" SELECT a.id,a.name,a.internal_code,a.color,a.sex,a.status,a.archived_at FROM animals a WHERE a.deleted_at IS NULL AND lower(a.species) IN ('chat','cat') AND a.id NOT IN (COALESCE(:mother,0),COALESCE(:father,0)) AND NOT EXISTS (SELECT 1 FROM litter_kittens lk WHERE lk.animal_id=a.id) ORDER BY CASE WHEN a.archived_at IS NULL THEN 0 ELSE 1 END,a.name COLLATE NOCASE,a.id "); $stmt->execute([':mother' => $litter['mother_id'], ':father' => $litter['father_id']]); render('litter_add_existing.php', [ 'title' => t('litter.add_title'), 'pageDescription' => 'Rattacher des fiches animales existantes à une portée.', 'litter' => $litter, 'animals' => $stmt->fetchAll(PDO::FETCH_ASSOC), 'sourceAnimalId' => $sourceAnimalId, ]); } public static function saveExistingLitterKittens(): void { if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST') { http_response_code(405); return; } $db = DB::pdo(); $litterId = (int) ($_POST['litter_id'] ?? 0); $sourceAnimalId = (int) ($_POST['source_animal_id'] ?? 0); $ids = is_array($_POST['animal_ids'] ?? null) ? array_values(array_unique(array_filter(array_map('intval', $_POST['animal_ids'])))) : []; if ($litterId <= 0 || !$ids) { http_response_code(400); echo h(t('litter.select_one')); return; } $litterStmt = $db->prepare('SELECT id,mother_id,father_id FROM litters WHERE id=:id'); $litterStmt->execute([':id' => $litterId]); $litter = $litterStmt->fetch(PDO::FETCH_ASSOC); if (!$litter) { http_response_code(404); echo h(t('litter.not_found')); return; } $placeholders = implode(',', array_fill(0, count($ids), '?')); $stmt = $db->prepare( "SELECT id FROM animals WHERE id IN ($placeholders) AND deleted_at IS NULL AND lower(species) IN ('chat','cat') AND id NOT IN (?,?) AND NOT EXISTS (SELECT 1 FROM litter_kittens lk WHERE lk.animal_id=animals.id)", ); $stmt->execute([...$ids, (int) ($litter['mother_id'] ?? 0), (int) ($litter['father_id'] ?? 0)]); if (count($stmt->fetchAll(PDO::FETCH_ASSOC)) !== count($ids)) { http_response_code(400); echo h(t('litter.invalid_existing')); return; } $db->beginTransaction(); try { $position = (int) $db ->query('SELECT COALESCE(MAX(position),0) FROM litter_kittens WHERE litter_id=' . $litterId) ->fetchColumn(); $link = $db->prepare( 'INSERT INTO litter_kittens(litter_id,animal_id,position) VALUES(:litter,:animal,:position)', ); foreach ($ids as $animalId) { $link->execute([':litter' => $litterId, ':animal' => $animalId, ':position' => ++$position]); self::logHistory($animalId, 'update', 'Rattaché à une portée existante', 'Portée n°' . $litterId); } $db->commit(); header( 'Location: /animal?id=' . ($sourceAnimalId ?: ($litter['mother_id'] ?: ($litter['father_id'] ?: $ids[0]))) . '#tab-litters', ); exit(); } catch (Throwable $e) { if ($db->inTransaction()) { $db->rollBack(); } throw $e; } } public static function bondedForm(): void { $db = DB::pdo(); $sourceAnimalId = (int) ($_GET['animal_id'] ?? 0); if ($sourceAnimalId <= 0) { http_response_code(400); return; } $stmt = $db->prepare(" SELECT a.id, a.name, a.color, a.species, a.status FROM animals a WHERE a.deleted_at IS NULL AND a.archived_at IS NULL AND lower(a.species) IN ('chat', 'cat') AND lower(a.status) NOT IN ('adopte', 'adopté', 'decede', 'décédé') AND (a.id = :source OR NOT EXISTS ( SELECT 1 FROM bonded_group_members bgm JOIN bonded_groups bg ON bg.id = bgm.group_id AND bg.active = 1 WHERE bgm.animal_id = a.id )) ORDER BY a.name COLLATE NOCASE "); $stmt->execute([':source' => $sourceAnimalId]); $animals = $stmt->fetchAll(PDO::FETCH_ASSOC); if (!array_filter($animals, fn(array $animal): bool => (int) $animal['id'] === $sourceAnimalId)) { http_response_code(404); echo h(t('bonded.unavailable')); return; } render('bonded_form.php', [ 'title' => t('bonded.create'), 'pageDescription' => 'Associer plusieurs chats qui doivent rester ensemble.', 'sourceAnimalId' => $sourceAnimalId, 'animals' => $animals, ]); } public static function saveBondedGroup(): void { if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST') { http_response_code(405); return; } $ids = is_array($_POST['animal_ids'] ?? null) ? array_values(array_unique(array_filter(array_map('intval', $_POST['animal_ids'])))) : []; if (count($ids) < 2) { http_response_code(400); echo h(t('bonded.need_two')); return; } $db = DB::pdo(); $placeholders = implode(',', array_fill(0, count($ids), '?')); $stmt = $db->prepare(" SELECT a.id FROM animals a WHERE a.id IN ($placeholders) AND a.deleted_at IS NULL AND a.archived_at IS NULL AND lower(a.species) IN ('chat', 'cat') AND lower(a.status) NOT IN ('adopte', 'adopté', 'decede', 'décédé') AND NOT EXISTS ( SELECT 1 FROM bonded_group_members bgm JOIN bonded_groups bg ON bg.id=bgm.group_id WHERE bgm.animal_id=a.id AND bg.active=1 ) "); $stmt->execute($ids); if (count($stmt->fetchAll()) !== count($ids)) { http_response_code(400); echo h(t('bonded.invalid_member')); return; } $name = trim((string) ($_POST['name'] ?? '')) ?: null; $notes = trim((string) ($_POST['notes'] ?? '')) ?: null; $db->beginTransaction(); try { $stmt = $db->prepare('INSERT INTO bonded_groups(name, notes) VALUES(:name, :notes)'); $stmt->execute([':name' => $name, ':notes' => $notes]); $groupId = (int) $db->lastInsertId(); $link = $db->prepare( 'INSERT INTO bonded_group_members(group_id, animal_id, position) VALUES(:group_id, :animal_id, :position)', ); foreach ($ids as $index => $animalId) { $link->execute([':group_id' => $groupId, ':animal_id' => $animalId, ':position' => $index + 1]); self::logHistory( $animalId, 'update', 'Ajouté à un groupe inséparable', $name ?: 'Groupe n°' . $groupId, ); } $db->commit(); header('Location: /animal?id=' . $ids[0]); exit(); } catch (Throwable $e) { if ($db->inTransaction()) { $db->rollBack(); } throw $e; } } public static function dissolveBondedGroup(): void { if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST') { http_response_code(405); return; } $groupId = (int) ($_POST['group_id'] ?? 0); $animalId = (int) ($_POST['animal_id'] ?? 0); if ($groupId <= 0) { http_response_code(400); return; } $db = DB::pdo(); $members = $db->prepare('SELECT animal_id FROM bonded_group_members WHERE group_id=:id'); $members->execute([':id' => $groupId]); $memberIds = array_map('intval', array_column($members->fetchAll(PDO::FETCH_ASSOC), 'animal_id')); $stmt = $db->prepare( "UPDATE bonded_groups SET active=0, dissolved_at=datetime('now') WHERE id=:id AND active=1", ); $stmt->execute([':id' => $groupId]); foreach ($memberIds as $memberId) { self::logHistory($memberId, 'update', 'Groupe inséparable dissous'); } header('Location: ' . ($animalId > 0 ? '/animal?id=' . $animalId : '/animals')); exit(); } }