443 lines
19 KiB
PHP
443 lines
19 KiB
PHP
<?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 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();
|
||
}
|
||
}
|