Publier Globinours 1.0.0-rc.3

This commit is contained in:
Alexandre NOEL 2026-09-03 12:39:15 +02:00
commit 9a2b4068da
325 changed files with 38230 additions and 20 deletions

View file

@ -0,0 +1,443 @@
<?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();
}
}