Globinours/app/Views/animals_list.php

918 lines
48 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
/** @var array $animals */
/** @var string $q */
/** @var string $status */
/** @var array $statuses */
/** @var string $sort */
/** @var string $dir */
$quality = $quality ?? '';
$qualityLabel = $qualityLabel ?? '';
$species = $species ?? '';
$speciesOptions = $speciesOptions ?? [];
$viewMode = $viewMode ?? 'table';
$speciesByCode = [];
foreach ($speciesOptions as $speciesOption) {
$speciesByCode[SpeciesService::normalize((string)$speciesOption['code'])] = $speciesOption;
}
$shortContactName = static function (?string $name): string {
$name = trim((string)$name);
if ($name === '') {
return '';
}$parts = preg_split('/\s+/u', $name) ?: [];
if (count($parts) < 2) {
return $name;
}$first = (string)array_shift($parts);
$last = (string)array_pop($parts);
$initial = mb_strtoupper(mb_substr($last, 0, 1));
return $first . ($initial !== '' ? ' ' . $initial . '.' : '');
};
$tableQs = $_GET;
$tableQs['view'] = 'table';
$mosaicQs = $_GET;
$mosaicQs['view'] = 'mosaic';
?>
<div class="card">
<div class="card-body">
<form class="row g-2 align-items-center" method="get" action="/animals">
<?php if ($quality !== ''): ?><input type="hidden" name="quality" value="<?= h($quality) ?>"><?php endif; ?>
<input type="hidden" name="sort" value="<?= h($sort) ?>">
<input type="hidden" name="dir" value="<?= h(strtolower($dir)) ?>">
<div class="col-12">
<input class="form-control" name="q" value="<?= htmlspecialchars($q, ENT_QUOTES) ?>"
placeholder="<?= h(t('animals.search_placeholder')) ?>">
</div>
<div class="col-12 col-md-auto animal-filter animal-filter-status">
<select class="form-select" name="status" onchange="this.form.requestSubmit()">
<?php foreach ($statuses as $k => $label): ?>
<option value="<?= htmlspecialchars($k, ENT_QUOTES) ?>" <?= $status === $k ? 'selected' : '' ?>>
<?= htmlspecialchars($label, ENT_QUOTES) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="col-12 col-md-auto animal-filter animal-filter-species"><select class="form-select"
name="species" onchange="this.form.requestSubmit()">
<option value=""><?= h(t('animals.all_species')) ?></option>
<?php foreach ($speciesOptions as $option): ?><option value="<?= h($option['code']) ?>"
<?= $species === $option['code'] ? 'selected' : '' ?>>
<?= h($option['icon'] . ' ' . $option['name']) ?></option><?php endforeach; ?>
</select></div>
<div class="col-12 col-md-auto animal-filter animal-filter-availability"><select class="form-select"
name="availability" onchange="this.form.requestSubmit()">
<option value=""><?= h(t('animals.all_availability')) ?></option>
<option value="available" <?= (($availability ?? '') === 'available') ? 'selected' : '' ?>>
<?= h(t('animals.available')) ?></option>
<option value="not_available" <?= (($availability ?? '') === 'not_available') ? 'selected' : '' ?>>
<?= h(t('animals.unavailable')) ?></option>
<option value="unknown" <?= (($availability ?? '') === 'unknown') ? 'selected' : '' ?>>
<?= h(t('animals.to_define')) ?></option>
</select></div>
<div class="col-auto">
<a class="btn btn-outline-primary" href="/animal/new">🐈 <?= h(t('animals.add')) ?></a>
</div>
<div class="col-auto">
<a class="btn btn-outline-primary" href="/litter/new">🐾 <?= h(t('animals.add_litter')) ?></a>
</div>
<div class="col-auto ms-auto d-flex align-items-center gap-2">
<div class="btn-group" role="group" aria-label="<?= h(t('animals.table_view')) ?>"><a
class="btn px-3 <?= $viewMode === 'table' ? 'btn-dark' : 'btn-outline-secondary' ?>"
href="/animals?<?= h(http_build_query($tableQs)) ?>" title="<?= h(t('animals.table_view')) ?>"
aria-label="<?= h(t('animals.table_view')) ?>">☷</a><a
class="btn px-3 <?= $viewMode === 'mosaic' ? 'btn-dark' : 'btn-outline-secondary' ?>"
href="/animals?<?= h(http_build_query($mosaicQs)) ?>" title="<?= h(t('animals.mosaic_view')) ?>"
aria-label="<?= h(t('animals.mosaic_view')) ?>">▦</a></div>
<div class="dropdown flex-shrink-0">
<button class="btn btn-outline-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown"
aria-expanded="false" aria-label="<?= h(t('animals.removed_management')) ?>"
title="<?= h(t('animals.removed_management')) ?>">
⚙️
</button>
<div class="dropdown-menu dropdown-menu-end shadow">
<a class="dropdown-item" href="/animals/archived">📦 <?= h(t('animals.archived')) ?></a>
<a class="dropdown-item text-danger" href="/animals/trash">🗑 <?= h(t('animals.trash')) ?></a>
</div>
</div>
</form>
</div>
<?php if ($quality !== ''): ?><div
class="alert alert-warning mx-3 mt-3 mb-0 d-flex flex-wrap align-items-center gap-2"><span>🔎
<?= h(t('animals.quality_filter')) ?> : <strong><?= h($qualityLabel) ?></strong></span><span
class="badge bg-orange-lt"><?= h(t('animals.records', ['count' => count($animals)])) ?></span><a
class="btn btn-sm btn-outline-secondary ms-auto"
href="/animals?view=<?= h($viewMode) ?>"><?= h(t('animals.remove_filter')) ?></a></div><?php endif; ?>
<style>
@media(min-width:768px) {
.animal-filter-status {
width: 12.5rem
}
.animal-filter-species {
width: 10.5rem
}
.animal-filter-availability {
width: 12.5rem
}
}
.litter-kitten-row td:nth-child(2) {
padding-left: 1.5rem;
}
.litter-member-row td:first-child {
padding-left: 2.75rem;
}
.litter-member-row>td {
background-color: rgba(169, 127, 69, .045);
}
.bonded-member-row>td {
background-color: rgba(111, 66, 193, .045);
}
.litter-member-row.bonded-member-row>td {
background: linear-gradient(90deg, rgba(169, 127, 69, .045), rgba(111, 66, 193, .045));
}
.litter-parent-placeholder td {
background: var(--tblr-bg-surface-secondary, #f6f8fb);
border-top-width: 2px;
}
.bonded-group-placeholder td {
background: rgba(111, 66, 193, .06);
border-top: 2px solid rgba(111, 66, 193, .2);
}
.litter-parent-placeholder-icon {
display: inline-flex;
width: 44px;
height: 44px;
align-items: center;
justify-content: center;
border-radius: 10px;
background: rgba(98, 105, 118, .12);
color: var(--tblr-secondary);
font-size: 1.25rem;
}
.animal-alert-icon {
display: inline-flex;
width: 1.4rem;
height: 1.4rem;
flex: 0 0 1.4rem;
align-items: center;
justify-content: center;
border-radius: 50%;
}
.animal-alert-icon svg {
width: .9rem;
height: .9rem;
}
.animal-photo-trigger {
display: inline-flex;
border-radius: 10px;
outline: none;
cursor: zoom-in;
}
.animal-photo-trigger:focus-visible {
box-shadow: 0 0 0 3px rgba(32, 107, 196, .25);
}
.animal-photo-zoom {
position: fixed;
z-index: 1090;
display: none;
pointer-events: none;
padding: .35rem;
background: #fff;
border: 1px solid rgba(98, 105, 118, .25);
border-radius: 14px;
box-shadow: 0 1rem 3rem rgba(31, 41, 55, .28);
}
.animal-photo-zoom.is-visible {
display: block;
}
.animal-photo-zoom img {
display: block;
width: auto;
height: auto;
max-width: min(320px, 70vw);
max-height: 60vh;
border-radius: 10px;
object-fit: contain;
}
.animal-mosaic-grid {
display: grid;
grid-template-columns: repeat(7, minmax(0, 1fr));
gap: .85rem;
padding: 1rem;
}
.animal-mosaic-card {
min-width: 0;
overflow: hidden;
transition: transform .15s ease, box-shadow .15s ease;
}
.animal-mosaic-card:hover {
transform: translateY(-2px);
box-shadow: 0 .55rem 1.5rem rgba(31, 41, 55, .12);
}
.animal-mosaic-photo {
display: block;
width: 100%;
aspect-ratio: 1/1;
object-fit: cover;
border-radius: 0;
}
.animal-mosaic-card .card-body {
display: flex;
flex-direction: column;
}
.animal-mosaic-name {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.animal-mosaic-footer {
margin-top: auto;
}
.animal-mosaic-status {
text-align: center;
}
.animal-mosaic-icons {
min-height: 1.4rem;
justify-content: center;
text-align: center;
}
@media(max-width:1199.98px) {
.animal-mosaic-grid {
grid-template-columns: repeat(5, minmax(0, 1fr));
}
}
@media(max-width:767.98px) {
.animal-mosaic-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: .65rem;
padding: .75rem;
}
}
@media(max-width:479.98px) {
.animal-mosaic-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
.animal-sex-icon {
display: inline-flex;
width: 1.25rem;
height: 1.25rem;
flex: 0 0 1.25rem;
align-items: center;
justify-content: center;
border-radius: 50%;
font-size: .8rem;
font-weight: 700;
line-height: 1;
}
.animal-weight-column {
width: 7.5rem;
min-width: 7.5rem;
white-space: nowrap;
}
.animal-age-column {
width: 5.75rem;
min-width: 5.75rem;
white-space: nowrap;
}
.animal-species-column {
width: 7rem;
min-width: 7rem;
white-space: nowrap;
}
.life-profile-column {
width: 7.25rem;
min-width: 7.25rem;
text-align: center;
}
.life-profile-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
align-items: center;
gap: .15rem;
}
.life-profile-grid span {
display: inline-flex;
justify-content: center;
align-items: center;
min-width: 1.35rem;
line-height: 1.2;
font-weight: 700;
}
.life-profile-sort {
display: inline-flex;
justify-content: center;
color: inherit;
text-decoration: none;
}
.life-profile-sort:hover,
.life-profile-sort:focus {
color: inherit;
text-decoration: none;
background: transparent;
}
.life-profile-yes {
color: var(--tblr-success);
}
.life-profile-no {
color: var(--tblr-danger);
}
.life-profile-unknown {
color: var(--tblr-secondary);
font-weight: 400 !important;
}
/* Conservé pour une éventuelle réactivation de la colonne Prochain vaccin.
.animal-vaccine-column {
width: 6.5rem;
max-width: 6.5rem;
}
*/
</style>
<?php if ($viewMode === 'mosaic'): ?>
<div class="animal-mosaic-grid">
<?php foreach ($animals as $a):
$mosaicSrc = !empty($a['primary_photo']) ? '/media/animals/' . (int)$a['id'] . '/' . rawurlencode($a['primary_photo']) : '/assets/cat_placeholder.svg';
$mosaicSpeciesCode = SpeciesService::normalize((string)($a['species'] ?? ''));
$mosaicSpecies = $speciesByCode[$mosaicSpeciesCode] ?? ['name' => species_label($mosaicSpeciesCode),'icon' => '🐾','color' => '#6c757d'];
[$mosaicStatusClass,$mosaicStatusLabel] = status_badge((string)($a['status'] ?? ''));
[$mosaicSexSymbol,$mosaicSexClass,$mosaicSexLabel] = match((string)($a['sex'] ?? 'U')) {
'M' => ['♂','bg-blue-lt text-blue',t('sex.male')],'F' => ['♀','bg-purple-lt text-purple',t('sex.female')],default => ['?','bg-secondary-lt text-secondary',t('sex.unknown')]
};
$mosaicFoster = in_array((string)($a['status'] ?? ''), ['fa','fa_permanente'], true) ? $shortContactName($a['foster_contact_name'] ?? null) : '';
$mosaicFreeCat = (string)($a['status'] ?? '') === 'chat_libre' ? trim((string)($a['free_cat_site_name'] ?: $a['free_cat_city'])) : '';
?>
<article class="card animal-mosaic-card">
<a href="/animal?id=<?= (int)$a['id'] ?>"
class="animal-photo-trigger d-flex w-100 text-reset text-decoration-none"
data-zoom-src="<?= h($mosaicSrc) ?>"
aria-label="<?= h(t('animals.open_record', ['name' => $a['name']])) ?>"><img class="animal-mosaic-photo"
src="<?= h($mosaicSrc) ?>" alt="<?= h(t('animals.photo_of', ['name' => $a['name']])) ?>"></a>
<div class="card-body p-2">
<div class="d-flex align-items-center gap-1 mb-1"><a
class="fw-bold text-reset text-decoration-none animal-mosaic-name me-auto"
href="/animal?id=<?= (int)$a['id'] ?>"
title="<?= h($a['name']) ?>"><?= h($a['name']) ?></a><span
class="animal-sex-icon <?= h($mosaicSexClass) ?>"
title="<?= h($mosaicSexLabel) ?>"><?= h($mosaicSexSymbol) ?></span></div>
<div class="d-flex justify-content-center mb-1"><span class="badge"
style="background:<?= h($mosaicSpecies['color']) ?>18;color:<?= h($mosaicSpecies['color']) ?>"><?= h($mosaicSpecies['icon'] . ' ' . $mosaicSpecies['name']) ?></span>
</div>
<div class="animal-mosaic-footer">
<?php if ($mosaicFoster !== ''): ?><div class="small text-muted text-truncate text-center mb-1"
title="<?= h(t('animals.foster')) ?> : <?= h($a['foster_contact_name']) ?>">
<?= h($mosaicFoster) ?></div><?php endif; ?>
<?php if ($mosaicFreeCat !== ''): ?><div class="small text-muted text-truncate text-center mb-1"
title="<?= h(t('animals.free_cat_site')) ?> : <?= h($mosaicFreeCat) ?>">🌿
<?= h($mosaicFreeCat) ?></div><?php endif; ?>
<div class="animal-mosaic-status mb-1"><span class="badge <?= h($mosaicStatusClass) ?>"
title="<?= h($mosaicStatusLabel) ?>"><?= h($mosaicStatusLabel) ?></span></div>
<div class="animal-mosaic-icons d-flex align-items-center gap-2 small">
<?php foreach ([['compatibility_dogs','🐶',t('animals.dogs')],['compatibility_cats','🐱',t('animals.cats')],['compatibility_children','🧒',t('animals.children')],['house_trained','🧼',t('animals.cleanliness')]] as [$field,$icon,$label]): $value = (string)($a[$field] ?? 'unknown'); ?><span
title="<?= h($label . ' : ' . t($value === 'yes' ? 'common.yes' : ($value === 'no' ? 'common.no' : 'common.unknown'))) ?>"
class="<?= $value === 'yes' ? 'text-success' : ($value === 'no' ? 'text-danger' : 'text-muted') ?>"><?= $icon ?><?= $value === 'yes' ? '✓' : ($value === 'no' ? '✕' : '—') ?></span><?php endforeach; ?>
</div>
</div>
</div>
</article>
<?php endforeach; ?>
<?php if (!$animals): ?><div class="text-center text-muted py-5" style="grid-column:1/-1">
<?= h(t('animals.no_results')) ?></div><?php endif; ?>
</div>
<?php else: ?>
<div class="table-responsive mt-2">
<table class="table table-vcenter card-table">
<thead>
<tr>
<th><?= h(t('animals.photo')) ?></th>
<!--<th>Code</th>-->
<th>
<?= sort_th(t('animals.name'), 'name', $sort, $dir) ?>
<span class="text-muted mx-1">·</span>
<?= sort_th(t('animals.sex'), 'sex', $sort, $dir) ?>
</th>
<th class="animal-species-column text-center">
<?= sort_th(t('animals.species'), 'species', $sort, $dir) ?></th>
<th class="animal-age-column text-center"><?= sort_th(t('animals.age'), 'age', $sort, $dir) ?></th>
<th class="text-center"><?= sort_th(t('animals.status'), 'status', $sort, $dir) ?></th>
<th class="life-profile-column">
<div class="life-profile-grid" aria-label="<?= h(t('animals.life_profile')) ?>"><?php foreach (['compatibility_dogs' => ['🐶',t('animals.compatible_dogs')],'compatibility_cats' => ['🐱',t('animals.compatible_cats')],'compatibility_children' => ['🧒',t('animals.compatible_children')],'house_trained' => ['🧼',t('animals.cleanliness')]] as $profileSortKey => $profileHeader): $profileQs = $_GET;
$profileQs['sort'] = $profileSortKey;
$profileQs['dir'] = ($sort === $profileSortKey && strtoupper($dir) === 'ASC') ? 'desc' : 'asc';
$profileArrow = $sort === $profileSortKey ? (strtoupper($dir) === 'ASC' ? '↑' : '↓') : ''; ?><a
class="life-profile-sort text-reset"
href="/animals?<?= h(http_build_query($profileQs)) ?>"
title="<?= h(t('animals.sort_by', ['label' => mb_strtolower($profileHeader[1])])) ?>"
aria-label="<?= h(t('animals.sort_by', ['label' => mb_strtolower($profileHeader[1])])) ?>"><?= $profileHeader[0] ?><small><?= h($profileArrow) ?></small></a><?php endforeach; ?>
</div>
</th>
<th class="text-nowrap text-center" style="min-width:9rem"><?= h(t('animals.fiv_felv')) ?></th>
<!--<th class="animal-weight-column"><?= sort_th('Poids', 'weight', $sort, $dir) ?></th>-->
<!--<th><?= sort_th('Dernier médical', 'updated_at', $sort, $dir) ?></th>-->
<!--<th>Traitements</th>-->
<!--<th class="animal-vaccine-column"><?= h(t('animals.next_vaccine')) ?></th>-->
<th><?= sort_th(t('animals.location'), 'rescue_location_name', $sort, $dir) ?></th>
<th class="w-1 text-center"><?= h(t('animals.preview')) ?></th>
</tr>
</thead>
<tbody>
<?php foreach ($animals as $a): ?>
<?php
$src = !empty($a['primary_photo'])
? '/media/animals/' . (int)$a['id'] . '/' . rawurlencode($a['primary_photo'])
: '/assets/cat_placeholder.svg';
$animalSpecies = SpeciesService::normalize((string)($a['species'] ?? ''));
$speciesMeta = $speciesByCode[$animalSpecies] ?? ['name' => species_label($animalSpecies),'icon' => '🐾','color' => '#6c757d'];
$isCat = SpeciesService::isCat($animalSpecies);
$fiv = $a['fiv'] ?? null;
$felv = $a['felv'] ?? null;
$fivStatus = (string)(($a['fiv_status'] ?? '') ?: ((int)$fiv === 1 ? 'pos' : 'neg'));
$felvStatus = (string)(($a['felv_status'] ?? '') ?: ((int)$felv === 1 ? 'pos' : 'neg'));
$shortStatusLabels = ['unknown' => mb_strtolower(t('animals.test_unknown')), 'neg' => mb_strtolower(t('animals.test_negative')), 'pos' => mb_strtolower(t('animals.test_positive')), 'doubtful' => mb_strtolower(t('animals.test_doubtful'))];
$healthLabel = $isCat ? ('FIV ' . ($shortStatusLabels[$fivStatus] ?? mb_strtolower(t('animals.test_unknown')))
. ' · FeLV ' . ($shortStatusLabels[$felvStatus] ?? mb_strtolower(t('animals.test_unknown')))) : t('animals.tests_not_applicable');
$testBadgeMeta = [
'unknown' => ['?', 'bg-secondary-lt', t('animals.test_unknown')],
'neg' => ['', 'bg-green-lt', t('animals.test_negative')],
'pos' => ['+', 'bg-red-lt', t('animals.test_positive')],
'doubtful' => ['?', 'bg-yellow-lt', t('animals.test_doubtful')],
];
[$fivSymbol, $fivBadgeClass, $fivBadgeTitle] = $testBadgeMeta[$fivStatus] ?? $testBadgeMeta['unknown'];
[$felvSymbol, $felvBadgeClass, $felvBadgeTitle] = $testBadgeMeta[$felvStatus] ?? $testBadgeMeta['unknown'];
$ageLabel = '—';
$ageTitle = t('animals.age_unknown');
if (!empty($a['birth_date'])) {
try {
$birth = new DateTimeImmutable((string)$a['birth_date']);
$age = $birth->diff(new DateTimeImmutable('today'));
if ($age->y > 0) {
$ageLabel = $age->y . ' a';
if ($age->m > 0) {
$ageLabel .= ' · ' . $age->m . ' m';
}
$ageTitle = $age->y . ' an' . ($age->y > 1 ? 's' : '');
if ($age->m > 0) {
$ageTitle .= ' et ' . $age->m . ' mois';
}
} else {
$ageLabel = $age->m . ' m';
$ageTitle = $age->m . ' mois';
}
if (!empty($a['birth_is_estimated'])) {
$ageLabel = '~ ' . $ageLabel;
$ageTitle .= ' (' . t('animals.age_estimated') . ')';
}
} catch (Throwable) {
$ageLabel = '—';
}
}
$sexLabel = match ((string)($a['sex'] ?? 'U')) {
'M' => t('sex.male'),
'F' => t('sex.female'),
default => t('animals.sex_not_provided'),
};
$hairLabel = match ((string)($a['hair_type'] ?? '')) {
'court' => t('animals.short_hair'),
'mi-long' => t('animals.medium_hair'),
'long' => t('animals.long_hair'),
'frise' => t('animals.curly_hair'),
'sans-poil' => t('animals.hairless'),
default => t('animals.hair_unknown'),
};
$appearance = implode(' · ', [
(string)($a['breed'] ?: t('animals.breed_unknown')),
(string)($a['color'] ?: t('animals.coat_unknown')),
$hairLabel,
]);
$weightLabel = t('animals.weight_unknown');
if ($a['last_weight'] !== null) {
$trend = '→';
if ($a['prev_weight'] !== null) {
if ((float)$a['last_weight'] > (float)$a['prev_weight']) {
$trend = '↑';
} elseif ((float)$a['last_weight'] < (float)$a['prev_weight']) {
$trend = '↓';
}
}
$weightLabel = number_format((float)$a['last_weight'], 2, ',', ' ') . ' kg ' . $trend;
}
$vaccineLabel = !empty($a['next_vaccine_due'])
? (($a['next_vaccine_name'] ?: t('animal.vaccine_booster')) . ' · ' . $a['next_vaccine_due'])
: t('animals.no_vaccine');
$treatmentLabel = $a['ongoing_treatment_names'] ?: t('animals.no_current_treatment');
$medicalPoint = $a['latest_medical_point'] ?: t('animals.no_recent_medical');
$profileMark = static function (string $value, string $label): string {
[$symbol,$class,$state] = match($value) {
'yes' => ['✓','life-profile-yes',t('common.yes')],
'no' => ['✕','life-profile-no',t('common.no')],
default => ['—','life-profile-unknown',t('common.unknown')],
};
return '<span class="' . h($class) . '" title="' . h($label . ' : ' . $state) . '" aria-label="' . h($label . ' : ' . $state) . '">' . h($symbol) . '</span>';
};
?>
<?php if (!empty($a['_bonded_group'])): ?>
<tr class="bonded-group-placeholder">
<td><span class="litter-parent-placeholder-icon text-purple" aria-hidden="true">🔗</span></td>
<td>
<div class="fw-bold text-purple"><?= h($a['_bonded_group']['label']) ?></div>
<div class="small text-muted"><?= h($a['_bonded_group']['details']) ?></div>
<?php if ($a['_bonded_group']['notes'] !== ''): ?><div class="small text-muted">
<?= h($a['_bonded_group']['notes']) ?></div><?php endif; ?>
</td>
<td></td>
<td></td>
<td><span class="badge bg-purple-lt"><?= h(t('animals.bonded')) ?></span></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<?php endif; ?>
<?php if (!empty($a['_litter_group'])): ?>
<tr class="litter-parent-placeholder">
<td><span class="litter-parent-placeholder-icon" aria-hidden="true">🐾</span></td>
<td>
<div class="fw-bold text-muted"><?= h($a['_litter_group']['label']) ?></div>
<div class="small text-muted"><?= h($a['_litter_group']['details']) ?></div>
</td>
<td></td>
<td></td>
<td><span class="badge bg-secondary-lt"><?= h(t('animals.litter')) ?></span></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<?php endif; ?>
<tr
class="<?= (!empty($a['_litter_branch']) || !empty($a['_bonded_branch'])) ? 'litter-kitten-row ' : '' ?><?= !empty($a['_litter_branch']) ? 'litter-member-row ' : '' ?><?= !empty($a['_bonded_branch']) ? 'bonded-member-row' : '' ?>">
<td>
<span class="animal-photo-trigger" tabindex="0" data-zoom-src="<?= h($src) ?>"
aria-label="<?= h(t('animals.enlarge_photo', ['name' => $a['name']])) ?>">
<img src="<?= htmlspecialchars($src, ENT_QUOTES) ?>"
style="width:44px;height:44px;object-fit:cover;border-radius:10px" alt="">
</span>
</td>
<!--<td class="text-muted"><php echo //htmlspecialchars($a['internal_code'], ENT_QUOTES) ?></td>-->
<td>
<div class="d-flex align-items-center gap-2">
<?php $treeBranch = $a['_bonded_branch'] ?? $a['_litter_branch'] ?? ''; ?>
<?php if ($treeBranch !== ''): ?>
<span class="text-muted font-monospace" aria-hidden="true"><?= h($treeBranch) ?></span>
<?php endif; ?>
<span class="coat-swatch" style="<?= h(coat_swatch_style($a['color'] ?? '')) ?>"
title="<?= h($a['color'] ?: t('animals.coat_unknown')) ?>" aria-hidden="true"></span>
<a href="/animal?id=<?= (int)$a['id'] ?>" class="fw-bold">
<?= htmlspecialchars($a['name'], ENT_QUOTES) ?>
</a>
<?php
[$sexSymbol, $sexIconClass, $sexIconLabel] = match ((string)($a['sex'] ?? 'U')) {
'M' => ['♂', 'bg-blue-lt text-blue', t('sex.male')],
'F' => ['♀', 'bg-purple-lt text-purple', t('sex.female')],
default => ['?', 'bg-secondary-lt text-secondary', t('sex.unknown')],
};
?>
<span class="animal-sex-icon <?= h($sexIconClass) ?>" title="<?= h($sexIconLabel) ?>"
aria-label="<?= h($sexIconLabel) ?>"><?= h($sexSymbol) ?></span>
<?php if (!empty($a['_bonded_branch'])): ?><span
title="<?= h(t('animals.adopt_together')) ?>"
aria-label="<?= h(t('animals.adopt_together')) ?>">🔗</span><?php endif; ?>
<?php $sterilizationState = $a['sterilization_status'] ?? (!empty($a['sterilized']) ? 'yes' : 'unknown');
if ($sterilizationState !== 'yes'): ?>
<span
class="animal-alert-icon <?= $sterilizationState === 'no' ? 'bg-yellow-lt text-yellow' : 'bg-secondary-lt text-secondary' ?>"
title="<?= $sterilizationState === 'no' ? t('animals.not_sterilized') : t('animals.sterilization_unknown') ?>"
aria-label="<?= $sterilizationState === 'no' ? t('animals.not_sterilized') : t('animals.sterilization_unknown') ?>">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<circle cx="6" cy="7" r="3"></circle>
<circle cx="6" cy="17" r="3"></circle>
<path d="M8.7 8.3L20 14"></path>
<path d="M8.7 15.7L20 10"></path>
</svg>
</span>
<?php endif; ?>
<?php if (trim((string)($a['chip_id'] ?? '')) === ''): ?>
<span class="animal-alert-icon bg-orange-lt text-orange"
title="<?= h(t('animals.not_chipped')) ?>"
aria-label="<?= h(t('animals.not_chipped')) ?>">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<rect x="6" y="6" width="12" height="12" rx="2"></rect>
<path d="M9 1v3M15 1v3M9 20v3M15 20v3M20 9h3M20 14h3M1 9h3M1 14h3"></path>
<circle cx="12" cy="12" r="2"></circle>
</svg>
</span>
<?php endif; ?>
</div>
</td>
<td class="animal-species-column"><span class="badge"
style="background:<?= h($speciesMeta['color']) ?>18;color:<?= h($speciesMeta['color']) ?>"
title="<?= h(t('animals.species_label', ['name' => $speciesMeta['name']])) ?>"><?= h($speciesMeta['icon'] . ' ' . $speciesMeta['name']) ?></span>
</td>
<td class="animal-age-column" title="<?= h($ageTitle) ?>"><?= h($ageLabel) ?></td>
<td>
<?php [$stCls, $stLbl] = status_badge((string)($a['status'] ?? '')); ?>
<span class="badge <?= $stCls ?>"><?= htmlspecialchars($stLbl, ENT_QUOTES) ?></span>
<?php $availability = $a['adoption_availability'] ?? 'unknown';
if ($availability === 'available'): ?><span class="badge bg-green-lt ms-1"
title="<?= h(t('animals.available_title')) ?>"><?= h(t('animals.adoptable')) ?></span><?php elseif ($availability === 'not_available'): ?><span
class="badge bg-orange-lt ms-1"
title="<?= h($a['adoption_unavailability_reason'] ?: t('animals.unavailable_title')) ?>"><?= h(t('animals.not_adoptable')) ?></span><?php endif; ?>
<?php $fosterDisplay = in_array((string)($a['status'] ?? ''), ['fa','fa_permanente'], true) ? $shortContactName($a['foster_contact_name'] ?? null) : '';
if ($fosterDisplay !== ''): ?><div class="small text-muted mt-1"
title="<?= h(t('animals.foster')) ?> : <?= h($a['foster_contact_name']) ?>">
<?= h($fosterDisplay) ?></div><?php endif; ?>
<?php if ((string)($a['status'] ?? '') === 'chat_libre' && (!empty($a['free_cat_site_name']) || !empty($a['free_cat_city']))): ?>
<div class="small text-muted mt-1">🌿 <?= h($a['free_cat_site_name'] ?: $a['free_cat_city']) ?>
</div><?php endif; ?>
</td>
<td class="life-profile-column">
<div class="life-profile-grid">
<?= $profileMark((string)($a['compatibility_dogs'] ?? 'unknown'), t('animals.dogs')) ?><?= $profileMark((string)($a['compatibility_cats'] ?? 'unknown'), t('animals.cats')) ?><?= $profileMark((string)($a['compatibility_children'] ?? 'unknown'), t('animals.children')) ?><?= $profileMark((string)($a['house_trained'] ?? 'unknown'), t('animals.cleanliness')) ?>
</div>
</td>
<td class="text-nowrap">
<?php if ($isCat): ?>
<div class="d-flex flex-nowrap gap-1">
<span class="badge <?= $fivBadgeClass ?>" title="FIV : <?= h($fivBadgeTitle) ?>">FIV
<?= h($fivSymbol) ?></span>
<span class="badge <?= $felvBadgeClass ?>" title="FeLV : <?= h($felvBadgeTitle) ?>">FeLV
<?= h($felvSymbol) ?></span>
</div>
<?php else: ?><span class="text-muted">—</span><?php endif; ?>
</td>
<!--<td class="animal-weight-column">
<?php
$lw = $a['last_weight'];
$pw = $a['prev_weight'];
if ($lw === null) {
echo '—';
} else {
$arrow = '→';
if ($pw !== null) {
if ((float)$lw > (float)$pw) {
$arrow = '↑';
} elseif ((float)$lw < (float)$pw) {
$arrow = '↓';
}
}
echo htmlspecialchars(number_format((float)$lw, 2), ENT_QUOTES) . " kg <span class='text-muted'>$arrow</span>";
}
?>
</td>-->
<!--<td><?= $a['last_med_at'] ? htmlspecialchars($a['last_med_at'], ENT_QUOTES) : '—' ?></td>-->
<!--<td><?= (int)($a['ongoing_treatments'] ?? 0) ?></td>-->
<!--<td class="animal-vaccine-column"><?= $a['next_vaccine_due'] ? htmlspecialchars($a['next_vaccine_due'], ENT_QUOTES) : '—' ?></td>-->
<td>
<div class="fw-bold">
<?= h(($a['rescue_location_name'] ?? '') ?: ($a['rescue_address'] ?? '')) ?></div>
</td>
<td class="text-center">
<button class="btn btn-outline-primary btn-sm" data-bs-toggle="modal"
data-bs-target="#medPreview" data-id="<?= (int)$a['id'] ?>" data-name="<?= h($a['name']) ?>"
data-photo="<?= h($src) ?>" data-identity="<?= h($sexLabel . ' · ' . $ageLabel) ?>"
data-appearance="<?= h($appearance) ?>" data-coat="<?= h($a['color'] ?? '') ?>"
data-coat-style="<?= h(coat_swatch_style($a['color'] ?? '')) ?>"
data-health="<?= h($healthLabel) ?>" data-weight="<?= h($weightLabel) ?>"
data-vaccine="<?= h($vaccineLabel) ?>" data-treatment="<?= h($treatmentLabel) ?>"
data-deworming="<?= h($a['last_deworming_date'] ? (t('animals.latest') . ' : ' . (new DateTimeImmutable($a['last_deworming_date']))->format('d/m/Y') . ($a['next_deworming_due'] ? ' · ' . t('animals.next') . ' : ' . (new DateTimeImmutable($a['next_deworming_due']))->format('d/m/Y') : '')) : t('animals.never_provided')) ?>"
data-medical="<?= h($medicalPoint) ?>"
data-medical-date="<?= h($a['last_med_at'] ?: '') ?>">
<?= h(t('animals.medical_preview')) ?>
</button>
</td>
</tr>
<?php endforeach; ?>
<?php if (!$animals): ?>
<tr>
<td colspan="9" class="text-center text-muted py-4"><?= h(t('animals.no_results')) ?></td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
<div class="animal-photo-zoom" id="animalPhotoZoom" aria-hidden="true"><img src=""
alt="<?= h(t('animals.enlarged_preview')) ?>"></div>
<div class="modal modal-blur fade" id="medPreview" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><?= h(t('animals.medical_preview')) ?></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"
aria-label="<?= h(t('common.close')) ?>"></button>
</div>
<div class="modal-body">
<div class="d-flex align-items-center gap-3 mb-4">
<img id="medPreviewPhoto" src="/assets/cat_placeholder.svg" alt=""
style="width:76px;height:76px;object-fit:cover;border-radius:14px">
<div>
<div id="medPreviewName" class="h2 mb-1"></div>
<div id="medPreviewIdentity" class="text-muted"></div>
<div class="d-flex align-items-center gap-1 text-muted small mt-1">
<span id="medPreviewCoatSwatch" class="coat-swatch"></span>
<span id="medPreviewAppearance"></span>
</div>
</div>
</div>
<div class="row row-cards">
<div class="col-12 col-md-6">
<div class="card card-sm h-100">
<div class="card-body">
<div class="subheader mb-1"><?= h(t('animals.fiv_felv')) ?></div>
<div id="medPreviewHealth" class="fw-bold"></div>
</div>
</div>
</div>
<div class="col-12 col-md-6">
<div class="card card-sm h-100">
<div class="card-body">
<div class="subheader mb-1"><?= h(t('animals.last_weight')) ?></div>
<div id="medPreviewWeight" class="fw-bold"></div>
</div>
</div>
</div>
<div class="col-12 col-md-6">
<div class="card card-sm h-100">
<div class="card-body">
<div class="subheader mb-1"><?= h(t('animals.next_vaccine')) ?></div>
<div id="medPreviewVaccine" class="fw-bold"></div>
</div>
</div>
</div>
<div class="col-12 col-md-6">
<div class="card card-sm h-100">
<div class="card-body">
<div class="subheader mb-1"><?= h(t('animals.current_treatment')) ?></div>
<div id="medPreviewTreatment" class="fw-bold"></div>
</div>
</div>
</div>
<div class="col-12 col-md-6">
<div class="card card-sm h-100">
<div class="card-body">
<div class="subheader mb-1"><?= h(t('animals.dewormer')) ?></div>
<div id="medPreviewDeworming" class="fw-bold"></div>
</div>
</div>
</div>
<div class="col-12">
<div class="card card-sm">
<div class="card-body">
<div class="subheader mb-1"><?= h(t('animals.latest_diagnosis')) ?></div>
<div id="medPreviewMedical" class="fw-bold"></div>
<div id="medPreviewMedicalDate" class="text-muted small mt-1"></div>
</div>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-link link-secondary"
data-bs-dismiss="modal"><?= h(t('common.close')) ?></button>
<a id="medPreviewLink" class="btn btn-primary"
href="/animals"><?= h(t('animals.open_full_record')) ?></a>
</div>
</div>
</div>
</div>
<script>
const animalListTranslations = < ? = json_encode(['medicalPreview' => t('animals.medical_preview'),
'neverProvided' => t('animals.never_provided'), 'lastUpdate' => t('animals.last_update', ['date' =>
'__DATE__'
])
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ? > ;
(() => {
const zoom = document.getElementById('animalPhotoZoom');
const image = zoom?.querySelector('img');
if (!zoom || !image) return;
const position = (trigger) => {
const rect = trigger.getBoundingClientRect();
const width = Math.min(332, window.innerWidth * .7 + 12);
let left = rect.right + 12;
if (left + width > window.innerWidth - 12) left = Math.max(12, rect.left - width - 12);
let top = rect.top + rect.height / 2 - Math.min(320, window.innerHeight * .6) / 2;
top = Math.max(12, Math.min(top, window.innerHeight - Math.min(332, window.innerHeight * .6) - 12));
zoom.style.left = left + 'px';
zoom.style.top = top + 'px';
};
const show = (trigger) => {
image.src = trigger.dataset.zoomSrc || '';
position(trigger);
zoom.classList.add('is-visible');
zoom.setAttribute('aria-hidden', 'false');
};
const hide = () => {
zoom.classList.remove('is-visible');
zoom.setAttribute('aria-hidden', 'true');
image.removeAttribute('src');
};
document.querySelectorAll('.animal-photo-trigger').forEach((trigger) => {
trigger.addEventListener('mouseenter', () => show(trigger));
trigger.addEventListener('mouseleave', hide);
trigger.addEventListener('focus', () => show(trigger));
trigger.addEventListener('blur', hide);
});
window.addEventListener('scroll', hide, true);
window.addEventListener('resize', hide);
})();
document.getElementById('medPreview')?.addEventListener('show.bs.modal', (ev) => {
const btn = ev.relatedTarget;
const name = btn?.getAttribute('data-name') || '';
document.querySelector('#medPreview .modal-title').textContent = animalListTranslations.medicalPreview +
' — ' + name;
document.getElementById('medPreviewName').textContent = name;
document.getElementById('medPreviewIdentity').textContent = btn?.getAttribute('data-identity') || '—';
document.getElementById('medPreviewAppearance').textContent = btn?.getAttribute('data-appearance') ||
'—';
const coatSwatch = document.getElementById('medPreviewCoatSwatch');
const coat = btn?.getAttribute('data-coat') || '';
coatSwatch.style.cssText = btn?.getAttribute('data-coat-style') || 'background:#d9dde3';
coatSwatch.style.display = coat ? 'inline-block' : 'none';
document.getElementById('medPreviewHealth').textContent = btn?.getAttribute('data-health') || '—';
document.getElementById('medPreviewWeight').textContent = btn?.getAttribute('data-weight') || '—';
document.getElementById('medPreviewVaccine').textContent = btn?.getAttribute('data-vaccine') || '—';
document.getElementById('medPreviewTreatment').textContent = btn?.getAttribute('data-treatment') || '—';
document.getElementById('medPreviewDeworming').textContent = btn?.getAttribute('data-deworming') ||
animalListTranslations.neverProvided;
document.getElementById('medPreviewMedical').textContent = btn?.getAttribute('data-medical') || '—';
const medicalDate = btn?.getAttribute('data-medical-date') || '';
document.getElementById('medPreviewMedicalDate').textContent = medicalDate ? animalListTranslations
.lastUpdate.replace('__DATE__', medicalDate) : '';
document.getElementById('medPreviewPhoto').src = btn?.getAttribute('data-photo') ||
'/assets/cat_placeholder.svg';
document.getElementById('medPreviewLink').href = '/animal?id=' + encodeURIComponent(btn?.getAttribute(
'data-id') || '');
});
</script>