Globinours/app/Controllers/AnimalRelationshipActions.php

576 lines
26 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
declare(strict_types=1);
trait AnimalRelationshipActions
{
public static function litterForm(): void
{
$db = DB::pdo();
$sourceAnimalId = (int) ($_GET['animal_id'] ?? 0);
$existingLitterId = (int) ($_GET['litter_id'] ?? 0);
$motherId = 0;
$fatherId = 0;
$birthDate = '';
if ($sourceAnimalId > 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;
}
}
if ($existingLitterId > 0) {
$stmt = $db->prepare('SELECT mother_id,father_id,birth_date FROM litters WHERE id=:id');
$stmt->execute([':id' => $existingLitterId]);
$existingLitter = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$existingLitter) {
http_response_code(404);
echo h(t('litter.not_found'));
return;
}
$motherId = (int) ($existingLitter['mother_id'] ?? 0);
$fatherId = (int) ($existingLitter['father_id'] ?? 0);
$birthDate = (string) ($existingLitter['birth_date'] ?? '');
}
$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,
'birthDate' => $birthDate,
'existingLitterId' => $existingLitterId,
'mothers' => $mothers,
'fathers' => $fathers,
'existingKittens' => $existingKittens,
]);
}
public static function saveLitter(): void
{
if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST') {
http_response_code(405);
return;
}
$db = DB::pdo();
$existingLitterId = (int) ($_POST['existing_litter_id'] ?? 0);
$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'];
$db->beginTransaction();
try {
if ($existingLitterId > 0) {
$stmt = $db->prepare('SELECT id,mother_id,father_id,birth_date FROM litters WHERE id=:id');
$stmt->execute([':id' => $existingLitterId]);
$existingLitter = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$existingLitter) {
throw new RuntimeException(t('litter.not_found'));
}
$litterId = (int) $existingLitter['id'];
$motherId = (int) ($existingLitter['mother_id'] ?? 0) ?: null;
$fatherId = (int) ($existingLitter['father_id'] ?? 0) ?: null;
$birthDate = (string) ($existingLitter['birth_date'] ?? '') ?: null;
} else {
$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)',
);
$positionBase = $existingLitterId > 0
? (int) $db->query('SELECT COALESCE(MAX(position),0) FROM litter_kittens WHERE litter_id=' . $litterId)->fetchColumn()
: 0;
foreach ($kittens as $index => $kitten) {
if (!is_array($kitten)) {
continue;
}
$position = $positionBase + $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 = $positionBase + 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 litterEditForm(): void
{
$db = DB::pdo();
$litterId = (int) ($_GET['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;
}
$parents = $db->query("SELECT id,name,sex FROM animals WHERE deleted_at IS NULL AND lower(species) IN ('chat','cat') ORDER BY name COLLATE NOCASE")->fetchAll(PDO::FETCH_ASSOC);
$stmt = $db->prepare('SELECT a.id,a.name,a.internal_code,a.sex,a.color,a.status,a.archived_at FROM litter_kittens lk JOIN animals a ON a.id=lk.animal_id WHERE lk.litter_id=:id ORDER BY lk.position,a.name COLLATE NOCASE');
$stmt->execute([':id' => $litterId]);
render('litter_edit.php', [
'title' => t('litter.edit'),
'pageDescription' => t('litter.edit_help'),
'litter' => $litter,
'mothers' => array_values(array_filter($parents, static fn(array $animal): bool => $animal['sex'] === 'F')),
'fathers' => array_values(array_filter($parents, static fn(array $animal): bool => $animal['sex'] === 'M')),
'kittens' => $stmt->fetchAll(PDO::FETCH_ASSOC),
'sourceAnimalId' => $sourceAnimalId,
]);
}
public static function updateLitter(): 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);
$motherId = (int) ($_POST['mother_id'] ?? 0) ?: null;
$fatherId = (int) ($_POST['father_id'] ?? 0) ?: null;
$birthDate = trim((string) ($_POST['birth_date'] ?? '')) ?: null;
if ($litterId <= 0 || ($motherId !== null && $motherId === $fatherId)) {
http_response_code(400);
echo h(t('litter.invalid_parent'));
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;
}
$ids = array_values(array_filter([$motherId, $fatherId]));
if ($ids) {
$placeholders = implode(',', array_fill(0, count($ids), '?'));
$stmt = $db->prepare("SELECT id,sex FROM animals WHERE id IN ($placeholders) AND deleted_at IS NULL AND lower(species) IN ('chat','cat')");
$stmt->execute($ids);
$valid = [];
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $animal) {
$valid[(int) $animal['id']] = $animal['sex'];
}
if (($motherId && ($valid[$motherId] ?? '') !== 'F') || ($fatherId && ($valid[$fatherId] ?? '') !== 'M')) {
http_response_code(400);
echo h(t('litter.invalid_parent'));
return;
}
$stmt = $db->prepare('SELECT COUNT(*) FROM litter_kittens WHERE litter_id=? AND animal_id IN (' . $placeholders . ')');
$stmt->execute([$litterId, ...$ids]);
if ((int) $stmt->fetchColumn() > 0) {
http_response_code(400);
echo h(t('litter.parent_is_kitten'));
return;
}
}
$exists = $db->prepare('SELECT 1 FROM litters WHERE id=:id');
$exists->execute([':id' => $litterId]);
if (!$exists->fetchColumn()) {
http_response_code(404);
return;
}
$db->prepare('UPDATE litters SET mother_id=:mother,father_id=:father,birth_date=:birth WHERE id=:id')->execute([':mother' => $motherId, ':father' => $fatherId, ':birth' => $birthDate, ':id' => $litterId]);
header('Location: /animal?id=' . ($sourceAnimalId ?: ($motherId ?: ($fatherId ?: 0))) . '#tab-litters');
exit();
}
public static function detachLitterKitten(): void
{
if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST') {
http_response_code(405);
return;
}
$litterId = (int) ($_POST['litter_id'] ?? 0);
$animalId = (int) ($_POST['animal_id'] ?? 0);
$sourceAnimalId = (int) ($_POST['source_animal_id'] ?? 0);
$stmt = DB::pdo()->prepare('DELETE FROM litter_kittens WHERE litter_id=:litter AND animal_id=:animal');
$stmt->execute([':litter' => $litterId, ':animal' => $animalId]);
if ($stmt->rowCount() > 0) {
self::logHistory($animalId, 'update', 'Retiré d’une portée', 'Ancienne portée n°' . $litterId);
}
$remaining = DB::pdo()->prepare('SELECT COUNT(*) FROM litter_kittens WHERE litter_id=:id');
$remaining->execute([':id' => $litterId]);
if ((int) $remaining->fetchColumn() === 0) {
DB::pdo()->prepare('DELETE FROM litters WHERE id=:id')->execute([':id' => $litterId]);
header('Location: /animal?id=' . ($sourceAnimalId ?: $animalId) . '#tab-litters');
exit();
}
header('Location: /litter/edit?id=' . $litterId . '&animal_id=' . $sourceAnimalId);
exit();
}
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();
}
}