Globinours/app/Controllers/AnimalRelationshipActions.php

443 lines
19 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 AnimalRelationshipActions
{
public static function litterForm(): void
{
$db = DB::pdo();
$sourceAnimalId = (int) ($_GET['animal_id'] ?? 0);
$motherId = 0;
$fatherId = 0;
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;
}
}
$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 dune 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();
}
}