Publier Globinours 1.0.0-rc.3
This commit is contained in:
parent
ea8c24d622
commit
9a2b4068da
325 changed files with 38230 additions and 20 deletions
82
app/Views/_animal_show_activity.php
Normal file
82
app/Views/_animal_show_activity.php
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
<?php
|
||||
// Sous-vue de la fiche animale ; les variables sont préparées par AnimalsController::show().
|
||||
?>
|
||||
<!-- ACTIVITÉ -->
|
||||
<div class="tab-pane" id="tab-activity">
|
||||
<?php /** @var array $activity */ ?>
|
||||
<?php if (empty($activity)): ?>
|
||||
<div class="text-muted"><?= h(t('animal.no_activity')) ?></div>
|
||||
<?php else: ?>
|
||||
|
||||
<?php
|
||||
$actMeta = [
|
||||
'history' => ['label' => t('animal.system'), 'iconBg' => 'bg-blue', 'icon' => 'history', 'badge' => 'bg-blue-lt'],
|
||||
'medical' => ['label' => t('animal.tab.medical'), 'iconBg' => 'bg-red', 'icon' => 'heart', 'badge' => 'bg-red-lt'],
|
||||
'vaccine' => ['label' => t('animal.vaccine'), 'iconBg' => 'bg-yellow', 'icon' => 'shield-check', 'badge' => 'bg-yellow-lt'],
|
||||
'deworming' => ['label' => t('animals.dewormer'), 'iconBg' => 'bg-lime', 'icon' => 'pill', 'badge' => 'bg-lime-lt'],
|
||||
'treatment' => ['label' => t('animal.treatment'), 'iconBg' => 'bg-orange', 'icon' => 'pill', 'badge' => 'bg-orange-lt'],
|
||||
'weight' => ['label' => t('common.weight'), 'iconBg' => 'bg-indigo', 'icon' => 'scale', 'badge' => 'bg-indigo-lt'],
|
||||
'care_round' => ['label' => t('animal.care_round'), 'iconBg' => 'bg-red', 'icon' => 'alert-triangle','badge' => 'bg-red-lt'],
|
||||
'death' => ['label' => t('status.decede'), 'iconBg' => 'bg-dark', 'icon' => 'rainbow', 'badge' => 'bg-dark-lt'],
|
||||
];
|
||||
?>
|
||||
|
||||
<ul class="list list-timeline" style="list-style-type:none;">
|
||||
<?php foreach ($activity as $a):
|
||||
$kind = (string)($a['kind'] ?? 'history');
|
||||
if (!$canViewMedical && in_array($kind, ['medical','vaccine','treatment','deworming','weight','care_round','death'], true)) {
|
||||
continue;
|
||||
}
|
||||
$meta = $actMeta[$kind] ?? ['label' => ucfirst($kind), 'iconBg' => 'bg-muted', 'icon' => 'circle', 'badge' => 'bg-muted-lt'];
|
||||
|
||||
$lbl = (string)($a['label'] ?? '');
|
||||
$details = (string)($a['details'] ?? '');
|
||||
$at = (string)($a['at'] ?? '');
|
||||
?>
|
||||
<li>
|
||||
<!-- icône timeline (gauche) -->
|
||||
<div class="list-timeline-icon <?= h($meta['iconBg']) ?>">
|
||||
<i class="ti ti-<?= h($meta['icon']) ?>"></i>
|
||||
</div>
|
||||
|
||||
<div class="list-timeline-content">
|
||||
<div class="d-flex align-items-start justify-content-between">
|
||||
|
||||
<div class="d-flex align-items-center gap-3 flex-grow-1">
|
||||
|
||||
<!-- BADGE FIXE aligné -->
|
||||
<div style="min-width:110px">
|
||||
<span class="badge <?= h($meta['badge']) ?>">
|
||||
<?= strtoupper(h($meta['label'])) ?>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- TITRE -->
|
||||
<div class="fw-bold">
|
||||
<?= h($lbl) ?>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- DATE -->
|
||||
<div class="text-muted small text-end" style="min-width:140px">
|
||||
<?= h($at) ?>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<?php //if ($details !== ''):?>
|
||||
<!-- <div class="mt-2 ms-3"> -->
|
||||
<?php //echo nl2br(h($details));?>
|
||||
<!-- </div>-->
|
||||
<?php //endif;?>
|
||||
|
||||
|
||||
</div>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
48
app/Views/_animal_show_documents.php
Normal file
48
app/Views/_animal_show_documents.php
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
<?php
|
||||
// Sous-vue de la fiche animale ; les variables sont préparées par AnimalsController::show().
|
||||
?>
|
||||
<!-- DOCUMENTS -->
|
||||
<?php if ($canViewMedical): ?>
|
||||
<div class="tab-pane" id="tab-documents">
|
||||
<div class="card card-sm">
|
||||
<div class="card-header">
|
||||
<div>
|
||||
<h3 class="card-title mb-1"><?= h(t('animal.adoption_contract')) ?></h3>
|
||||
<div class="text-muted small"><?= h(t('animal.generated_copy_help')) ?></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3 align-items-center">
|
||||
<div class="col-md">
|
||||
<div class="fw-bold"><?= h(t('animal.prefilled_information')) ?></div>
|
||||
<div class="text-muted small"><?= h(t('animal.adoption_prefill')) ?></div>
|
||||
<?php if (!empty($adoption)): ?><div class="text-success small mt-1">✓
|
||||
<?= h(t('animal.linked_adopter', ['name' => $adoption['adopter_name'] ?? ''])) ?></div>
|
||||
<?php else: ?><div class="text-warning small mt-1">⚠ <?= h(t('animal.no_linked_adopter')) ?></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="col-md-auto d-flex flex-wrap gap-2"><a class="btn btn-outline-primary"
|
||||
href="/animal/documents/adoption?animal_id=<?= (int)$animal['id'] ?>&format=odt"><?= h(t('animal.download_odt')) ?></a><a
|
||||
class="btn btn-danger"
|
||||
href="/animal/documents/adoption?animal_id=<?= (int)$animal['id'] ?>&format=pdf"
|
||||
target="_blank"><?= h(t('animal.view_print_pdf')) ?></a></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card card-sm mt-3">
|
||||
<div class="card-header">
|
||||
<div>
|
||||
<h3 class="card-title mb-1"><?= h(t('animal.abandon_found')) ?></h3>
|
||||
<div class="text-muted small"><?= h(t('animal.known_info_added')) ?></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body d-flex flex-wrap align-items-center gap-2">
|
||||
<div class="me-auto text-muted small"><?= h(t('animal.identity_summary')) ?></div><a
|
||||
class="btn btn-outline-primary"
|
||||
href="/animal/documents/abandon?animal_id=<?= (int)$animal['id'] ?>&format=odt"><?= h(t('animal.download_odt')) ?></a><a
|
||||
class="btn btn-danger" href="/animal/documents/abandon?animal_id=<?= (int)$animal['id'] ?>&format=pdf"
|
||||
target="_blank"><?= h(t('animal.view_print_pdf')) ?></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
31
app/Views/_animal_show_history.php
Normal file
31
app/Views/_animal_show_history.php
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
// Sous-vue de la fiche animale ; les variables sont préparées par AnimalsController::show().
|
||||
?>
|
||||
<!-- HISTORIQUE -->
|
||||
<div class="tab-pane" id="tab-hist">
|
||||
<?php $visibleHistory = $canViewMedical ? $history : array_values(array_filter($history, static fn (array $row): bool => !in_array((string)($row['type'] ?? ''), ['medical','treatment','vaccine','deworming','death'], true))); ?>
|
||||
<?php if (empty($visibleHistory)): ?>
|
||||
<div class="text-muted"><?= h(t('animal.no_history_event')) ?></div>
|
||||
<?php else: ?>
|
||||
<ul class="list list-timeline">
|
||||
<?php foreach ($visibleHistory as $h):
|
||||
$style = $historyStyles[$h['type']] ?? ['bg' => 'bg-muted','icon' => 'circle'];
|
||||
?>
|
||||
<li>
|
||||
<div class="list-timeline-icon <?= $style['bg'] ?>">
|
||||
<i class="ti ti-<?= $style['icon'] ?>"></i>
|
||||
</div>
|
||||
<div class="list-timeline-content">
|
||||
<div class="fw-bold"><?= h($h['label']) ?></div>
|
||||
<div class="text-muted small"><?= h($h['created_at']) ?></div>
|
||||
<?php if (!empty($h['actor_name'])): ?><div class="text-muted small">
|
||||
<?= h(t('common.by', ['name' => $h['actor_name']])) ?></div><?php endif; ?>
|
||||
<?php if (!empty($h['details'])): ?>
|
||||
<div class="mt-1"><?= nl2br(h($h['details'])) ?></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
459
app/Views/_animal_show_identity.php
Normal file
459
app/Views/_animal_show_identity.php
Normal file
|
|
@ -0,0 +1,459 @@
|
|||
<?php
|
||||
// Sous-vue de la fiche animale ; les variables sont préparées par AnimalsController::show().
|
||||
?>
|
||||
<!-- IDENTITÉ -->
|
||||
<div class="tab-pane active show" id="tab-ident">
|
||||
|
||||
<?php if ($canViewMedical && !empty($death)): ?>
|
||||
<?php
|
||||
$deathCauseLabels = array_combine(array_keys(DeathController::CAUSES), array_map(static fn ($key) => DeathController::causeLabel($key), array_keys(DeathController::CAUSES)));
|
||||
$deathPlaceLabels = array_combine(array_keys(DeathController::PLACES), array_map(static fn ($key) => DeathController::placeLabel($key), array_keys(DeathController::PLACES)));
|
||||
$deathDispositionLabels = array_combine(array_keys(DeathController::DISPOSITIONS), array_map(static fn ($key) => DeathController::dispositionLabel($key), array_keys(DeathController::DISPOSITIONS)));
|
||||
$deathDate = DateTimeImmutable::createFromFormat('Y-m-d', (string)$death['deceased_date']);
|
||||
$deathDateLabel = !empty($death['death_year']) ? t('animal.year', ['year' => (int)$death['death_year']]) : ($deathDate ? $deathDate->format('d/m/Y') : t('animal.date_unknown'));
|
||||
?>
|
||||
<div class="card border-dark mb-3 overflow-hidden">
|
||||
<div class="card-status-start bg-dark"></div>
|
||||
<div class="card-header d-flex align-items-center justify-content-between">
|
||||
<div>
|
||||
<h3 class="card-title mb-1">🌈 <?= h(t('animal.death_recorded')) ?></h3>
|
||||
<div class="text-muted small"><?= h($deathDateLabel) ?> ·
|
||||
<?= h($deathCauseLabels[$death['cause_code']] ?? $death['cause_code']) ?></div>
|
||||
</div><a class="btn btn-outline-dark btn-sm"
|
||||
href="/animal/death?id=<?= (int)$animal['id'] ?>"><?= h(t('common.edit')) ?></a>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-3">
|
||||
<div class="subheader"><?= h(t('animal.statistics')) ?></div><span
|
||||
class="badge <?= !empty($death['occurred_in_care']) ? 'bg-dark' : 'bg-secondary-lt' ?>"><?= h(t(!empty($death['occurred_in_care']) ? 'animal.in_shelter_figures' : 'animal.outside_care')) ?></span>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="subheader"><?= h(t('death.place')) ?></div>
|
||||
<strong><?= h($deathPlaceLabels[$death['place_type']] ?? $death['place_type']) ?></strong><?php if ($death['place_details']): ?>
|
||||
<div class="small text-muted"><?= h($death['place_details']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="subheader"><?= h(t('death.care_body')) ?></div>
|
||||
<strong><?= h($deathDispositionLabels[$death['body_disposition']] ?? $death['body_disposition']) ?></strong><?php if ($death['crematorium_name']): ?>
|
||||
<div class="small text-muted"><?= h($death['crematorium_name']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="subheader"><?= h(t('animal.context')) ?></div>
|
||||
<?php if (!empty($death['euthanized'])): ?><span
|
||||
class="badge bg-purple-lt"><?= h(t('animal.euthanasia')) ?></span><?php else: ?><span
|
||||
class="text-muted"><?= h(t('animal.natural_death_short')) ?></span><?php endif; ?><?php if ($death['veterinarian_name']): ?>
|
||||
<div class="small text-muted"><?= h($death['veterinarian_name']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
<?php if ($death['cause_details'] || $death['notes']): ?><div class="col-12 small">
|
||||
<?php if ($death['cause_details']): ?><strong><?= h(t('animal.cause')) ?> :</strong>
|
||||
<?= nl2br(h($death['cause_details'])) ?><?php endif; ?><?php if ($death['notes']): ?><div
|
||||
class="mt-1"><strong><?= h(t('common.notes')) ?> :</strong> <?= nl2br(h($death['notes'])) ?>
|
||||
</div><?php endif; ?></div><?php endif; ?>
|
||||
<?php if (!empty($death['death_age_value'])): ?><div class="col-12 small">
|
||||
<strong><?= h(t('animal.death_age')) ?></strong> <?= (int)$death['death_age_value'] ?> <?= h(match($death['death_age_unit']) {
|
||||
'day' => t('animal.age_days'),'week' => t('animal.age_weeks'),'month' => t('animal.age_months'),'year' => t('animal.age_years'),default => ''
|
||||
}) ?></div><?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($bondedGroup)): ?>
|
||||
<div class="card border-purple mb-3">
|
||||
<div class="card-status-start bg-purple"></div>
|
||||
<div class="card-header d-flex align-items-center justify-content-between">
|
||||
<div>
|
||||
<h3 class="card-title mb-1">🔗 <?= h($bondedGroup['name'] ?: t('animal.bonded_cats')) ?></h3>
|
||||
<div class="text-muted small"><?= h(t('animal.bonded_help')) ?></div>
|
||||
</div>
|
||||
<form method="post" action="/bonded/dissolve"
|
||||
onsubmit="return confirm(<?= h(json_encode(t('animal.bonded_dissolve_confirm'), JSON_HEX_APOS | JSON_HEX_QUOT)) ?>);">
|
||||
<input type="hidden" name="group_id" value="<?= (int)$bondedGroup['id'] ?>">
|
||||
<input type="hidden" name="animal_id" value="<?= (int)$animal['id'] ?>">
|
||||
<button class="btn btn-outline-secondary btn-sm"
|
||||
type="submit"><?= h(t('animal.dissolve_group')) ?></button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<?php if (!empty($bondedGroup['notes'])): ?><p class="text-muted mb-3">
|
||||
<?= nl2br(h($bondedGroup['notes'])) ?></p><?php endif; ?>
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<?php foreach ($bondedMembers as $member): ?>
|
||||
<a class="btn <?= (int)$member['id'] === (int)$animal['id'] ? 'btn-purple' : 'btn-outline-purple' ?>"
|
||||
href="/animal?id=<?= (int)$member['id'] ?>">
|
||||
<span class="coat-swatch me-1"
|
||||
style="<?= h(coat_swatch_style($member['color'] ?? '')) ?>"></span><?= h($member['name']) ?>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php
|
||||
$lifeProfileMeta = [
|
||||
'yes' => [t('common.yes'),'bg-green-lt text-green','✓'],
|
||||
'no' => [t('common.no'),'bg-red-lt text-red','✕'],
|
||||
'unknown' => [t('common.unknown'),'bg-secondary-lt text-secondary','—'],
|
||||
];
|
||||
?>
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">
|
||||
<div>
|
||||
<h3 class="card-title mb-1"><?= h(t('animal.form.intake')) ?></h3>
|
||||
<div class="text-muted small"><?= h(t('animal.intake_origin_help')) ?></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-6 col-lg-3">
|
||||
<div class="subheader"><?= h(t('common.date')) ?></div>
|
||||
<strong><?= h(!empty($animal['intake_date']) ? date('d/m/Y', strtotime((string)$animal['intake_date'])) : '—') ?></strong>
|
||||
</div>
|
||||
<div class="col-6 col-lg-3">
|
||||
<div class="subheader"><?= h(t('animal.intake_mode')) ?></div>
|
||||
<strong><?= h($intakeTypes[$animal['intake_type'] ?? 'unknown'] ?? t('common.not_provided')) ?></strong>
|
||||
</div>
|
||||
<div class="col-6 col-lg-3">
|
||||
<div class="subheader"><?= h(t('animal.reason')) ?></div>
|
||||
<strong><?= h($intakeReasons[$animal['intake_reason'] ?? 'unknown'] ?? t('common.not_provided')) ?></strong>
|
||||
</div>
|
||||
<div class="col-6 col-lg-3">
|
||||
<div class="subheader"><?= h(t('animal.origin_city')) ?></div>
|
||||
<strong><?= h(($animal['rescue_location_name'] ?? '') ?: '—') ?></strong>
|
||||
</div>
|
||||
<div class="col-12 col-lg-6">
|
||||
<div class="subheader"><?= h(t('animal.form.depositor')) ?></div><?php if ($depositor): ?><a
|
||||
class="fw-bold"
|
||||
href="/directory/edit?id=<?= (int)$depositor['id'] ?>"><?= h($depositor['name']) ?></a><?php if (!empty($depositor['city'])): ?><span
|
||||
class="text-muted"> · <?= h($depositor['city']) ?></span><?php endif; ?><?php else: ?><span
|
||||
class="text-muted"><?= h(t('animal.form.no_depositor')) ?></span><?php endif; ?>
|
||||
</div>
|
||||
<?php if (!empty($animal['intake_circumstances'])): ?><div class="col-12">
|
||||
<div class="subheader"><?= h(t('animal.form.circumstances')) ?></div>
|
||||
<div><?= nl2br(h($animal['intake_circumstances'])) ?></div>
|
||||
</div><?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">
|
||||
<div>
|
||||
<h3 class="card-title mb-1"><?= h(t('animal.form.life_profile')) ?></h3>
|
||||
<div class="text-muted small"><?= h(t('animal.life_profile_help')) ?></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<?php foreach ([
|
||||
'compatibility_dogs' => ['🐶',t('animals.dogs')],
|
||||
'compatibility_cats' => ['🐱',t('animals.cats')],
|
||||
'compatibility_children' => ['🧒',t('animals.children')],
|
||||
'house_trained' => ['🧼',t('animals.cleanliness')],
|
||||
] as $field => $profile): [$profileLabel,$profileClass,$profileSymbol] = $lifeProfileMeta[$animal[$field] ?? 'unknown'] ?? $lifeProfileMeta['unknown']; ?>
|
||||
<div class="col-6 col-lg-3">
|
||||
<div class="border rounded p-3 h-100 d-flex align-items-center gap-3"><span class="fs-2"
|
||||
aria-hidden="true"><?= $profile[0] ?></span>
|
||||
<div>
|
||||
<div class="text-muted small"><?= h($profile[1]) ?></div><span
|
||||
class="badge <?= h($profileClass) ?>"><?= h($profileSymbol . ' ' . $profileLabel) ?></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php require __DIR__ . '/_expense_summary.php'; ?>
|
||||
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title m-0"><?= h(t('common.notes')) ?></h3>
|
||||
</div>
|
||||
<div class="card-body"><?= $animal['notes'] ? nl2br(h($animal['notes'])) : '—' ?></div>
|
||||
</div>
|
||||
|
||||
|
||||
<?php
|
||||
$rescueAddr = trim((string)($animal['rescue_address'] ?? ''));
|
||||
$rescueLat = isset($animal['rescue_lat']) && $animal['rescue_lat'] !== '' ? (float)$animal['rescue_lat'] : null;
|
||||
$rescueLng = isset($animal['rescue_lng']) && $animal['rescue_lng'] !== '' ? (float)$animal['rescue_lng'] : null;
|
||||
|
||||
$currentAddr = trim((string)($animal['current_address'] ?? '5 RUE NOTRE DAME 29260 LESNEVEN'));
|
||||
$currentLat = isset($animal['current_lat']) && $animal['current_lat'] !== '' ? (float)$animal['current_lat'] : null;
|
||||
$currentLng = isset($animal['current_lng']) && $animal['current_lng'] !== '' ? (float)$animal['current_lng'] : null;
|
||||
|
||||
$hasRescue = ($rescueLat !== null && $rescueLng !== null);
|
||||
$hasCurrent = ($currentLat !== null && $currentLng !== null);
|
||||
?>
|
||||
|
||||
|
||||
<div class="card mt-3">
|
||||
<div class="card-header d-flex align-items-center justify-content-between">
|
||||
<h3 class="card-title m-0"><?= h(t('animal.location')) ?></h3>
|
||||
<div class="d-flex gap-2">
|
||||
<span class="badge bg-azure-lt"><?= h(t('animal.rescue')) ?></span>
|
||||
<span class="badge bg-lime-lt"><?= h(t('animal.current')) ?></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<div class="text-muted mb-1"><?= h(t('animal.rescue_place')) ?></div>
|
||||
<div class="fw-bold"><?= $rescueAddr !== '' ? h($rescueAddr) : '—' ?></div>
|
||||
<?php if (!$hasRescue): ?>
|
||||
<div class="text-muted small mt-1"><?= h(t('animal.coordinates_missing')) ?></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="text-muted mb-1"><?= h(t('animal.form.current_location')) ?></div>
|
||||
<div class="fw-bold"><?= h($currentAddr) ?></div>
|
||||
|
||||
<?php if (!$hasCurrent): ?>
|
||||
<div class="text-muted small mt-1"><?= h(t('animal.coordinates_missing')) ?></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<?php if ($animal['status'] === 'adopte'): ?>
|
||||
|
||||
<div class="alert alert-success mt-3">
|
||||
<strong><?= h(t('animal.adopted_by')) ?> :</strong><br>
|
||||
<?= htmlspecialchars($adoption['adopter_name'] . ' - ' . $adoption['adopter_address'] . ' ' . $adoption['adopter_postal_code'] . ' ' . $adoption['adopter_city'] . ' - (' . $adoption['adopter_phone'] . ' - ' . $adoption['adopter_email'] . ')') ?>
|
||||
</div>
|
||||
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="mt-3" id="mapJourney"
|
||||
style="height:320px;border-radius:10px;overflow:hidden; border:1px solid #ccc"></div>
|
||||
|
||||
<?php
|
||||
function haversine_km($lat1, $lon1, $lat2, $lon2)
|
||||
{
|
||||
$earthRadius = 6371; // km
|
||||
|
||||
$dLat = deg2rad($lat2 - $lat1);
|
||||
$dLon = deg2rad($lon2 - $lon1);
|
||||
|
||||
$a = sin($dLat / 2) * sin($dLat / 2) +
|
||||
cos(deg2rad($lat1)) * cos(deg2rad($lat2)) *
|
||||
sin($dLon / 2) * sin($dLon / 2);
|
||||
|
||||
$c = 2 * atan2(sqrt($a), sqrt(1 - $a));
|
||||
return $earthRadius * $c;
|
||||
}
|
||||
|
||||
$distanceKm = null;
|
||||
|
||||
if ($hasRescue && $hasCurrent) {
|
||||
$distanceKm = haversine_km(
|
||||
$rescueLat,
|
||||
$rescueLng,
|
||||
$currentLat,
|
||||
$currentLng,
|
||||
);
|
||||
}
|
||||
?>
|
||||
|
||||
<?php if ($distanceKm !== null): ?>
|
||||
<div class="mt-3 text-muted small">
|
||||
<?= h(t('animal.distance_story', ['name' => $animal['name'],'distance' => number_format($distanceKm, 2)])) ?><br>
|
||||
<em><?= h(t('animal.distance_caption')) ?></em>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
<?php
|
||||
/** @var array|null $icad Structure = ['cached'=>..., 'stale'=>bool] comme ton endpoint */
|
||||
$cache = $icad['cached'] ?? null;
|
||||
$data = $cache['data'] ?? null;
|
||||
$status = $cache['status'] ?? 'empty';
|
||||
$fetched = $cache['fetched_at'] ?? '';
|
||||
$icadUrl = $data['icad_url'] ?? null;
|
||||
|
||||
$badge = match ($status) {
|
||||
'ok' => 'bg-success-lt',
|
||||
'error' => 'bg-danger-lt',
|
||||
default => 'bg-muted-lt',
|
||||
};
|
||||
|
||||
$label = match ($status) {
|
||||
'ok' => 'i-CAD : OK',
|
||||
'error' => 'i-CAD : Erreur',
|
||||
default => t('animal.icad_not_checked'),
|
||||
};
|
||||
|
||||
$err = (string)($cache['error'] ?? '');
|
||||
?>
|
||||
|
||||
<div class="card mt-3">
|
||||
<div class="card-header d-flex align-items-center justify-content-between">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<span class="badge <?= $badge ?>"><?= h($label) ?></span>
|
||||
<?php if ($fetched): ?>
|
||||
<span class="text-muted small"><?= h(t('animal.last_sync', ['date' => $fetched])) ?></span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<?php if ($icadUrl): ?>
|
||||
<a class="btn btn-sm btn-outline-primary" href="<?= h($icadUrl) ?>" target="_blank" rel="noopener">
|
||||
<?= h(t('animal.open_icad')) ?>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
|
||||
<button class="btn btn-sm btn-outline-secondary" type="button" id="btn-icad-refresh"
|
||||
data-animal-id="<?= (int)$animal['id'] ?>">
|
||||
<?= h(t('common.refresh')) ?>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body" id="icad-card-body">
|
||||
<?php if ($status === 'error'): ?>
|
||||
<div class="alert alert-danger">
|
||||
<strong><?= h(t('animal.icad_error')) ?></strong><br>
|
||||
<?= h($err) ?>
|
||||
</div>
|
||||
<?php elseif ($status !== 'ok' || !$data): ?>
|
||||
<div class="text-muted"><?= h(t('animal.no_icad')) ?></div>
|
||||
<?php else: ?>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<div class="datagrid">
|
||||
<div class="datagrid-item">
|
||||
<div class="datagrid-title"><?= h(t('animal.icad_name')) ?></div>
|
||||
<div class="datagrid-content"><?= h($data['name'] ?? '') ?></div>
|
||||
</div>
|
||||
<div class="datagrid-item">
|
||||
<div class="datagrid-title">Puce</div>
|
||||
<div class="datagrid-content"><?= h($data['insert'] ?? '') ?></div>
|
||||
</div>
|
||||
<div class="datagrid-item">
|
||||
<div class="datagrid-title"><?= h(t('animal.sex')) ?></div>
|
||||
<div class="datagrid-content"><?= h($data['sex'] ?? '') ?></div>
|
||||
</div>
|
||||
<div class="datagrid-item">
|
||||
<div class="datagrid-title"><?= h(t('animal.birth')) ?></div>
|
||||
<div class="datagrid-content"><?= h($data['birth_date'] ?? '') ?></div>
|
||||
</div>
|
||||
<div class="datagrid-item">
|
||||
<div class="datagrid-title"><?= h(t('animal.appearance')) ?></div>
|
||||
<div class="datagrid-content"><?= h($data['appearance'] ?? '') ?></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
$holder =
|
||||
$data['holder'] ??
|
||||
$data['owner'] ??
|
||||
$data['detenteur'] ??
|
||||
null;
|
||||
|
||||
// Si ton parse aplatit au lieu d'imbriquer :
|
||||
if (!$holder && (
|
||||
isset($data['holder_name']) || isset($data['holder_phone']) || isset($data['holder_email'])
|
||||
)) {
|
||||
$holder = [
|
||||
'name' => $data['holder_name'] ?? '',
|
||||
'phone' => $data['holder_phone'] ?? '',
|
||||
'email' => $data['holder_email'] ?? '',
|
||||
'type' => $data['holder_type'] ?? '',
|
||||
'address' => $data['holder_address'] ?? '',
|
||||
];
|
||||
}
|
||||
?>
|
||||
|
||||
<?php if (is_array($holder) && array_filter($holder, fn ($v) => trim((string)$v) !== '')): ?>
|
||||
<hr class="my-3">
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-md-12">
|
||||
<div class="d-flex align-items-center justify-content-between mb-2">
|
||||
<div class="fw-bold"><?= h(t('animal.holder')) ?></div>
|
||||
<span class="text-muted small"><span
|
||||
class="badge bg-danger-lt"><?= h(t('animal.do_not_share')) ?></span></span>
|
||||
</div>
|
||||
|
||||
<div class="datagrid">
|
||||
<?php if (!empty($holder['type'])): ?>
|
||||
<div class="datagrid-item">
|
||||
<div class="datagrid-title">Type</div>
|
||||
<div class="datagrid-content"><?= h($holder['type']) ?></div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($holder['name'])): ?>
|
||||
<div class="datagrid-item">
|
||||
<div class="datagrid-title"><?= h(t('common.name')) ?></div>
|
||||
<div class="datagrid-content"><?= h($holder['name']) ?></div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($holder['phone'])): ?>
|
||||
<div class="datagrid-item">
|
||||
<div class="datagrid-title"><?= h(t('common.phone')) ?></div>
|
||||
<div class="datagrid-content"><?= h($holder['phone']) ?></div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($holder['email'])): ?>
|
||||
<div class="datagrid-item">
|
||||
<div class="datagrid-title"><?= h(t('common.email')) ?></div>
|
||||
<div class="datagrid-content"><?= h($holder['email']) ?></div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($holder['address'])): ?>
|
||||
<div class="datagrid-item" style="grid-column: 1 / -1;">
|
||||
<div class="datagrid-title"><?= h(t('common.address')) ?></div>
|
||||
<div class="datagrid-content"><?= nl2br(h($holder['address'])) ?></div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="text-muted mb-2"><?= h(t('animal.icad_history')) ?></div>
|
||||
<?php $events = $data['events'] ?? []; ?>
|
||||
<?php if (empty($events)): ?>
|
||||
<div class="text-muted"><?= h(t('animal.no_event')) ?></div>
|
||||
<?php else: ?>
|
||||
<ul class="list list-timeline">
|
||||
<?php foreach ($events as $e): ?>
|
||||
<li>
|
||||
<div class="list-timeline-icon bg-blue-lt">
|
||||
<i class="ti ti-history"></i>
|
||||
</div>
|
||||
<div class="list-timeline-content">
|
||||
<div class="fw-bold"><?= h($e['label'] ?? '') ?></div>
|
||||
<div class="text-muted small">
|
||||
<?= h(t('common.date')) ?> : <?= h($e['date'] ?? '') ?> —
|
||||
<?= h(t('animal.recorded')) ?> : <?= h($e['recorded_at'] ?? '') ?>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
68
app/Views/_animal_show_litters.php
Normal file
68
app/Views/_animal_show_litters.php
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
<?php
|
||||
// Sous-vue de la fiche animale ; les variables sont préparées par AnimalsController::show().
|
||||
?>
|
||||
<!-- PORTÉES -->
|
||||
<?php if ($isCat): ?><div class="tab-pane" id="tab-litters">
|
||||
<div class="d-flex align-items-center justify-content-between mb-3">
|
||||
<div>
|
||||
<h3 class="mb-0"><?= h(t('animal.litters_parentage')) ?></h3>
|
||||
<div class="text-muted"><?= h(t('animal.litters_help')) ?></div>
|
||||
</div>
|
||||
<a class="btn btn-primary" href="/litter/new?animal_id=<?= (int)$animal['id'] ?>">+
|
||||
<?= h(t('animal.add_litter')) ?></a>
|
||||
</div>
|
||||
<?php if (empty($litters)): ?>
|
||||
<div class="empty">
|
||||
<div class="empty-description"><?= h(t('animal.no_litter')) ?></div>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="row row-cards">
|
||||
<?php foreach ($litters as $litter): ?>
|
||||
<?php
|
||||
$litterDateLabel = t('animal.date_unknown');
|
||||
if (!empty($litter['birth_date'])) {
|
||||
$litterDate = DateTimeImmutable::createFromFormat('Y-m-d', (string)$litter['birth_date']);
|
||||
if ($litterDate) {
|
||||
$litterDateLabel = $litterDate->format('d/m/Y');
|
||||
}
|
||||
}
|
||||
?>
|
||||
<div class="col-12">
|
||||
<div class="card card-sm">
|
||||
<div class="card-header">
|
||||
<div>
|
||||
<h4 class="card-title mb-1"><?= h(t('animal.litter_on', ['date' => $litterDateLabel])) ?></h4>
|
||||
<div class="text-muted small">
|
||||
<?= h(t('animal.mother')) ?> :
|
||||
<?= $litter['mother_id'] ? '<a href="/animal?id=' . (int)$litter['mother_id'] . '">' . h($litter['mother_name']) . '</a>' : h(t('animal.unknown_female')) ?>
|
||||
· <?= h(t('animal.father')) ?> :
|
||||
<?= $litter['father_id'] ? '<a href="/animal?id=' . (int)$litter['father_id'] . '">' . h($litter['father_name']) . '</a>' : h(t('animal.unknown_male')) ?>
|
||||
</div>
|
||||
</div>
|
||||
<a class="btn btn-outline-primary btn-sm ms-auto"
|
||||
href="/litter/add-existing?litter_id=<?= (int)$litter['id'] ?>&animal_id=<?= (int)$animal['id'] ?>">+
|
||||
<?= h(t('animal.add_existing_cat')) ?></a>
|
||||
</div>
|
||||
<div class="list-group list-group-flush">
|
||||
<?php foreach ($litter['kittens'] as $index => $kitten): ?>
|
||||
<a class="list-group-item list-group-item-action d-flex align-items-center gap-2"
|
||||
href="/animal?id=<?= (int)$kitten['id'] ?>">
|
||||
<span
|
||||
class="text-muted font-monospace"><?= $index === count($litter['kittens']) - 1 ? '└' : '├' ?></span>
|
||||
<span class="coat-swatch" style="<?= h(coat_swatch_style($kitten['color'] ?? '')) ?>"></span>
|
||||
<strong><?= h($kitten['name']) ?></strong>
|
||||
<?php if (!empty($kitten['archived_at'])): ?><span
|
||||
class="badge bg-secondary-lt"><?= h(t('animal.archived')) ?></span><?php endif; ?>
|
||||
<span
|
||||
class="text-muted"><?= h(($kitten['sex'] ?? 'U') === 'F' ? t('sex.female') : (($kitten['sex'] ?? 'U') === 'M' ? t('sex.male') : t('animal.sex_define'))) ?></span>
|
||||
<?php [$kittenStatusClass, $kittenStatusLabel] = status_badge((string)$kitten['status']); ?>
|
||||
<span class="badge <?= h($kittenStatusClass) ?> ms-auto"><?= h($kittenStatusLabel) ?></span>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div><?php endif; ?>
|
||||
71
app/Views/_animal_show_locations.php
Normal file
71
app/Views/_animal_show_locations.php
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
<?php
|
||||
// Sous-vue de la fiche animale ; les variables sont préparées par AnimalsController::show().
|
||||
?>
|
||||
<!-- EMPLACEMENTS -->
|
||||
<div class="tab-pane" id="tab-placements">
|
||||
<div class="d-flex justify-content-between align-items-start gap-2 mb-3">
|
||||
<div>
|
||||
<h3 class="mb-1"><?= h(t('animal.placement_journey')) ?></h3>
|
||||
<div class="text-muted"><?= h(t('animal.placement_journey_help')) ?></div>
|
||||
</div><button class="btn btn-primary btn-sm" data-bs-toggle="modal"
|
||||
data-bs-target="#modalPlacement"><?= h(t('animal.add_event')) ?></button>
|
||||
</div>
|
||||
<?php $placementLabels = ['reservation' => t('animal.reservation'),'foster' => t('animal.form.foster'),'adoption' => t('animal.final_adoption'),'return' => t('animal.return_shelter'),'cancellation' => t('animal.cancellation')];
|
||||
$placementColors = ['reservation' => 'purple','foster' => 'cyan','adoption' => 'green','return' => 'orange','cancellation' => 'secondary']; ?>
|
||||
<?php if (empty($placements)): ?><div class="text-muted"><?= h(t('animal.no_placement')) ?></div><?php else: ?><ul
|
||||
class="list list-timeline"><?php foreach ($placements as $placement): ?><li>
|
||||
<div class="list-timeline-icon bg-<?= h($placementColors[$placement['event_type']] ?? 'secondary') ?>"><i
|
||||
class="ti ti-home"></i></div>
|
||||
<div class="list-timeline-content">
|
||||
<div class="d-flex justify-content-between gap-2">
|
||||
<div><span
|
||||
class="badge bg-<?= h($placementColors[$placement['event_type']] ?? 'secondary') ?>-lt"><?= h($placementLabels[$placement['event_type']] ?? $placement['event_type']) ?></span><?php if (!empty($placement['contact_name'])): ?>
|
||||
<strong class="ms-2"><?= h($placement['contact_name']) ?></strong><?php endif; ?></div>
|
||||
<div class="text-muted small"><?= h(date('d/m/Y', strtotime((string)$placement['event_date']))) ?>
|
||||
</div>
|
||||
</div><?php if (!empty($placement['reason'])): ?><div class="mt-1"><strong><?= h(t('animal.reason')) ?>
|
||||
:</strong> <?= h($placement['reason']) ?></div>
|
||||
<?php endif; ?><?php if (!empty($placement['notes'])): ?><div class="text-muted">
|
||||
<?= nl2br(h($placement['notes'])) ?></div>
|
||||
<?php endif; ?><?php if (!empty($placement['actor_name'])): ?><div class="text-muted small">
|
||||
<?= h(t('common.by', ['name' => $placement['actor_name']])) ?></div><?php endif; ?>
|
||||
</div>
|
||||
</li><?php endforeach; ?></ul><?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="tab-pane" id="tab-locations">
|
||||
<div class="d-flex flex-wrap justify-content-between align-items-start gap-2 mb-3">
|
||||
<div>
|
||||
<h3 class="mb-1"><?= h(t('animal.location_history')) ?></h3>
|
||||
<div class="text-muted"><?= h(t('animal.location_history_help')) ?></div>
|
||||
</div>
|
||||
<span class="badge bg-blue-lt"><?= h(tn('animal.step', 'animal.steps', count($locationHistory ?? []))) ?></span>
|
||||
</div>
|
||||
<?php if (empty($locationHistory)): ?>
|
||||
<div class="text-muted"><?= h(t('animal.no_location')) ?></div>
|
||||
<?php else: ?>
|
||||
<ul class="list list-timeline">
|
||||
<?php foreach ($locationHistory as $location):
|
||||
$fromLabel = LocationHistoryService::label($location, 'from');
|
||||
$toLabel = LocationHistoryService::label($location, 'to');
|
||||
$isInitial = empty($location['from_status']) && empty($location['from_refuge_room']) && empty($location['from_care_box_key']) && empty($location['from_address']);
|
||||
?>
|
||||
<li>
|
||||
<div class="list-timeline-icon bg-blue"><i class="ti ti-map-pin"></i></div>
|
||||
<div class="list-timeline-content">
|
||||
<div class="d-flex flex-wrap justify-content-between gap-2">
|
||||
<div class="fw-bold">
|
||||
<?= $isInitial ? h($toLabel) : h($fromLabel) . ' <span class="text-muted mx-1">→</span> ' . h($toLabel) ?>
|
||||
</div>
|
||||
<div class="text-muted small"><?= h($location['moved_at']) ?></div>
|
||||
</div>
|
||||
<?php if (!empty($location['reason'])): ?><div class="text-muted mt-1"><?= h($location['reason']) ?>
|
||||
</div><?php endif; ?>
|
||||
<?php if (!empty($location['actor_name'])): ?><div class="text-muted small mt-1">
|
||||
<?= h(t('common.by', ['name' => $location['actor_name']])) ?></div><?php endif; ?>
|
||||
</div>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
436
app/Views/_animal_show_medical.php
Normal file
436
app/Views/_animal_show_medical.php
Normal file
|
|
@ -0,0 +1,436 @@
|
|||
<?php
|
||||
// Sous-vue de la fiche animale ; les variables sont préparées par AnimalsController::show().
|
||||
?>
|
||||
<!-- MÉDICAL -->
|
||||
<?php if ($canViewMedical): ?>
|
||||
<div class="tab-pane" id="tab-med">
|
||||
<div class="card mt-3">
|
||||
<div class="card-header d-flex align-items-center justify-content-between">
|
||||
<div>
|
||||
<h3 class="card-title m-0"><?= h(t('animal.medical_status_global')) ?></h3>
|
||||
<div class="text-muted small"><?= h(t('animal.current_health_help')) ?></div>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-outline-primary" type="button" data-bs-toggle="modal"
|
||||
data-bs-target="#modalMedicalStatus"><?= h(t('common.edit')) ?></button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<?php if ($isCat): ?><div class="col-12 col-md-3">
|
||||
<div class="subheader mb-1">FIV</div>
|
||||
<span class="badge <?= $fivClass ?>"><?= h($fivLabel) ?></span>
|
||||
</div>
|
||||
<div class="col-12 col-md-3">
|
||||
<div class="subheader mb-1">FeLV</div>
|
||||
<span class="badge <?= $felvClass ?>"><?= h($felvLabel) ?></span>
|
||||
</div><?php endif; ?>
|
||||
<div class="col-12 <?= $isCat ? 'col-md-6' : '' ?>">
|
||||
<div class="subheader mb-1"><?= h(t('animal.conditions_short')) ?></div>
|
||||
<?php $activeConditions = array_values(array_filter($healthConditions ?? [], fn ($condition) => ($condition['status'] ?? '') === 'active')); ?>
|
||||
<?php if (!$activeConditions): ?>
|
||||
<span class="text-muted"><?= h(t('animal.no_active_condition')) ?></span>
|
||||
<?php else: ?>
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<?php foreach ($activeConditions as $condition): ?>
|
||||
<span class="badge bg-red-lt"><?= h($condition['name']) ?></span>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-3 mt-0">
|
||||
<div class="col-12 col-xl-5">
|
||||
<div class="card h-100">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h3 class="card-title mb-0">📄 <?= h(t('medical_document.prescriptions')) ?></h3>
|
||||
<div class="text-muted small"><?= h(t('medical_document.private_documents')) ?></div>
|
||||
</div><button class="btn btn-sm btn-outline-primary" data-bs-toggle="modal"
|
||||
data-bs-target="#modalPrescription"><?= h(t('common.add')) ?></button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<?php if (empty($prescriptions)): ?><div class="text-muted">
|
||||
<?= h(t('medical_document.no_prescription')) ?></div><?php else: ?><div class="divide-y">
|
||||
<?php foreach ($prescriptions as $p): $statusKey = 'medical_document.status_' . $p['status']; ?>
|
||||
<div class="py-2">
|
||||
<div class="d-flex justify-content-between gap-2">
|
||||
<div>
|
||||
<strong><?= h((new DateTimeImmutable($p['prescribed_on']))->format('d/m/Y')) ?></strong>
|
||||
<span
|
||||
class="badge <?= $p['status'] === 'active' ? 'bg-green-lt' : 'bg-secondary-lt' ?>"><?= h(t($statusKey)) ?></span>
|
||||
</div><a class="btn btn-sm btn-ghost-primary" target="_blank"
|
||||
href="/animal/medical-document?kind=prescription&id=<?= (int)$p['id'] ?>"><i
|
||||
class="ti ti-file-search"></i> <?= h(t('common.open')) ?></a>
|
||||
</div><?php if (!empty($p['clinic_name']) || !empty($p['veterinarian_name'])): ?><div
|
||||
class="text-muted small">
|
||||
<?= h(implode(' · ', array_filter([$p['clinic_name'],$p['veterinarian_name']]))) ?>
|
||||
</div><?php endif; ?><?php if (!empty($prescriptionTreatments[$p['id']])): ?><div
|
||||
class="mt-1 d-flex flex-wrap gap-1">
|
||||
<?php foreach ($prescriptionTreatments[$p['id']] as $linked): ?><span
|
||||
class="badge bg-orange-lt">💊
|
||||
<?= h($linked['medication_name'] ?: t('animal.treatment')) ?></span><?php endforeach; ?>
|
||||
</div><?php endif; ?><?php if (!empty($p['notes'])): ?><div class="small mt-1">
|
||||
<?= nl2br(h($p['notes'])) ?></div><?php endif; ?>
|
||||
</div><?php endforeach; ?></div><?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-xl-7">
|
||||
<div class="card h-100">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h3 class="card-title mb-0">🧪 <?= h(t('medical_document.lab_results')) ?></h3>
|
||||
<div class="text-muted small"><?= h(t('medical_document.lab_help')) ?></div>
|
||||
</div><button class="btn btn-sm btn-outline-primary" data-bs-toggle="modal"
|
||||
data-bs-target="#modalLabReport"><?= h(t('common.add')) ?></button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<?php if (empty($labReports)): ?><div class="text-muted"><?= h(t('medical_document.no_analysis')) ?>
|
||||
</div><?php else: ?><div class="accordion" id="labReportsAccordion">
|
||||
<?php foreach ($labReports as $i => $report): $results = $labResultsByReport[$report['id']] ?? []; ?>
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header"><button class="accordion-button <?= $i ? 'collapsed' : '' ?>"
|
||||
type="button" data-bs-toggle="collapse"
|
||||
data-bs-target="#lab-report-<?= (int)$report['id'] ?>"><span
|
||||
class="me-2"><?= h((new DateTimeImmutable($report['sampled_on']))->format('d/m/Y')) ?></span><span
|
||||
class="text-muted"><?= h($report['report_type'] ?: t('medical_document.analysis')) ?></span></button>
|
||||
</h2>
|
||||
<div id="lab-report-<?= (int)$report['id'] ?>"
|
||||
class="accordion-collapse collapse <?= !$i ? 'show' : '' ?>"
|
||||
data-bs-parent="#labReportsAccordion">
|
||||
<div class="accordion-body"><?php if ($report['stored_name']): ?><a
|
||||
class="btn btn-sm btn-outline-primary mb-2" target="_blank"
|
||||
href="/animal/medical-document?kind=analysis&id=<?= (int)$report['id'] ?>"><i
|
||||
class="ti ti-paperclip"></i>
|
||||
<?= h(t('medical_document.open_report')) ?></a><?php endif; ?><?php if ($results): ?>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-vcenter mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><?= h(t('medical_document.parameter')) ?></th>
|
||||
<th><?= h(t('medical_document.value')) ?></th>
|
||||
<th><?= h(t('medical_document.reference')) ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody><?php foreach ($results as $result): $hasReference = $result['reference_min'] !== null || $result['reference_max'] !== null;
|
||||
$low = $result['reference_min'] !== null && (float)$result['value'] < (float)$result['reference_min'];
|
||||
$high = $result['reference_max'] !== null && (float)$result['value'] > (float)$result['reference_max']; ?><tr>
|
||||
<td><?= h($result['parameter_name']) ?></td>
|
||||
<td><strong
|
||||
class="<?= !$hasReference ? '' : ($low || $high ? 'text-danger' : 'text-success') ?>"><?= h((string)$result['value'] . ' ' . ($result['unit'] ?? '')) ?></strong>
|
||||
</td>
|
||||
<td><?= $hasReference ? h(($result['reference_min'] ?? '…') . ' – ' . ($result['reference_max'] ?? '…') . ' ' . ($result['unit'] ?? '')) : '—' ?>
|
||||
</td>
|
||||
</tr><?php endforeach; ?></tbody>
|
||||
</table>
|
||||
</div><?php endif; ?><div class="text-muted small mt-2">
|
||||
<?= h(implode(' · ', array_filter([$report['laboratory_name'],$report['clinic_name'],$report['veterinarian_name']]))) ?>
|
||||
</div><?php if ($report['notes']): ?><div class="small mt-1">
|
||||
<?= nl2br(h($report['notes'])) ?></div><?php endif; ?></div>
|
||||
</div>
|
||||
</div><?php endforeach; ?></div><?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($labSeries)): ?><div class="card mt-3">
|
||||
<div class="card-header">
|
||||
<div>
|
||||
<h3 class="card-title mb-0"><?= h(t('medical_document.evolution')) ?></h3>
|
||||
<div class="text-muted small"><?= h(t('medical_document.graph_help')) ?></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3"><?php foreach (array_values($labSeries) as $i => $series): ?><div
|
||||
class="col-12 col-xl-6">
|
||||
<div class="border rounded p-3">
|
||||
<h4 class="mb-2">
|
||||
<?= h($series['name']) ?><?= $series['unit'] ? ' <span class="text-muted fw-normal">(' . h($series['unit']) . ')</span>' : '' ?>
|
||||
</h4>
|
||||
<div style="height:230px"><canvas class="lab-chart"
|
||||
data-series="<?= h(json_encode($series, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)) ?>"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div><?php endforeach; ?></div>
|
||||
</div>
|
||||
</div><?php endif; ?>
|
||||
|
||||
<div class="card mt-3">
|
||||
<div class="card-header d-flex align-items-center justify-content-between">
|
||||
<div>
|
||||
<h3 class="card-title m-0">🏥 Chirurgies</h3>
|
||||
<div class="text-muted small">Interventions planifiées et réalisées</div>
|
||||
</div><a class="btn btn-sm btn-outline-primary"
|
||||
href="/animal/surgery/new?animal_id=<?= (int)$animal['id'] ?>"><?= h(t('common.add')) ?></a>
|
||||
</div>
|
||||
<div class="card-body"><?php if (empty($surgeries)):?><span class="text-muted">Aucune chirurgie
|
||||
enregistrée.</span><?php else:?><div class="table-responsive">
|
||||
<table class="table table-sm table-vcenter mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Intervention</th>
|
||||
<th>Structure / vétérinaire</th>
|
||||
<th>Statut</th>
|
||||
<th class="text-end">Coût</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody><?php foreach ($surgeries as $s):?><tr>
|
||||
<td><?= h(date('d/m/Y', strtotime($s['surgery_date']))) ?></td>
|
||||
<td><strong><?= h($s['procedure_name']) ?></strong><?php if ($s['notes']):?><div
|
||||
class="small text-muted"><?= h($s['notes']) ?></div><?php endif?></td>
|
||||
<td><?= h(implode(' · ', array_filter([$s['clinic_name'],$s['veterinarian_name']])) ?: '—') ?>
|
||||
</td>
|
||||
<td><span
|
||||
class="badge <?= $s['status'] === 'completed' ? 'bg-green-lt' : ($s['status'] === 'planned' ? 'bg-blue-lt' : 'bg-secondary-lt') ?>"><?= h(['completed' => 'Réalisée','planned' => 'Planifiée','cancelled' => 'Annulée'][$s['status']] ?? $s['status']) ?></span>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<?= $s['amount_cents'] === null ? '—' : number_format($s['amount_cents'] / 100, 2, ',', ' ') . ' €' ?>
|
||||
</td>
|
||||
</tr><?php endforeach?></tbody>
|
||||
</table>
|
||||
</div><?php endif?></div>
|
||||
</div>
|
||||
|
||||
<div class="card mt-3">
|
||||
<div class="card-header d-flex align-items-center justify-content-between">
|
||||
<div>
|
||||
<h3 class="card-title m-0"><?= h(t('animals.dewormer')) ?></h3>
|
||||
<div class="text-muted small"><?= h(t('animal.deworming_schedule')) ?></div>
|
||||
</div><button class="btn btn-sm btn-outline-primary" data-bs-toggle="modal"
|
||||
data-bs-target="#modalDeworming"><?= h(t('common.add')) ?></button>
|
||||
</div>
|
||||
<div class="card-body"><?php if (!$dewormings): ?><span
|
||||
class="text-muted"><?= h(t('animal.no_deworming')) ?></span><?php else: ?><div class="mb-3">
|
||||
<strong><?= h(t('animal.latest_deworming', ['date' => (new DateTimeImmutable($lastDeworming['administered_on']))->format('d/m/Y')])) ?></strong><?php if ($lastDeworming['next_due_date']): $due = new DateTimeImmutable($lastDeworming['next_due_date']);
|
||||
$late = $due < new DateTimeImmutable('today'); ?><span
|
||||
class="badge ms-2 <?= $late ? 'bg-red-lt' : 'bg-blue-lt' ?>"><?= h(t('animal.next_date', ['date' => $due->format('d/m/Y')])) ?></span><?php endif; ?>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-vcenter mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><?= h(t('common.date')) ?></th>
|
||||
<th><?= h(t('animal.product')) ?></th>
|
||||
<th><?= h(t('animal.dose')) ?></th>
|
||||
<th><?= h(t('common.weight')) ?></th>
|
||||
<th><?= h(t('animal.recorded_by')) ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody><?php foreach ($dewormings as $d): ?><tr>
|
||||
<td><?= h((new DateTimeImmutable($d['administered_on']))->format('d/m/Y')) ?></td>
|
||||
<td><?= h($d['medication_name'] ?: t('common.not_provided')) ?></td>
|
||||
<td><?= h($d['dose_text'] ?: '—') ?></td>
|
||||
<td><?= $d['weight_kg'] !== null ? h(number_format((float)$d['weight_kg'], 2, ',', ' ')) . ' kg' : '—' ?>
|
||||
</td>
|
||||
<td><?= h($d['actor_name'] ?: '—') ?></td>
|
||||
</tr><?php endforeach; ?></tbody>
|
||||
</table>
|
||||
</div><?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title m-0"><?= h(t('animal.weight_evolution')) ?></h3>
|
||||
</div>
|
||||
|
||||
<?php if (!$weights): ?>
|
||||
<div class="card-body text-muted"><?= h(t('animal.no_weight')) ?></div>
|
||||
<?php else: ?>
|
||||
<div style="height:260px">
|
||||
<canvas id="weightChart"></canvas>
|
||||
</div>
|
||||
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<hr />
|
||||
<?php if (!$medical_notes): ?>
|
||||
<div class="text-muted"><?= h(t('animal.no_medical_note')) ?></div>
|
||||
<?php else: ?>
|
||||
<div class="divide-y">
|
||||
<?php foreach ($medical_notes as $n): ?>
|
||||
<div class="py-3">
|
||||
<div class="d-flex justify-content-between">
|
||||
<div>
|
||||
<?php
|
||||
$kind = (string)$n['kind'];
|
||||
$badge = match ($kind) {
|
||||
'consult' => 'bg-blue-lt',
|
||||
'controle' => 'bg-green-lt',
|
||||
'symptome' => 'bg-yellow-lt',
|
||||
'diagnostic' => 'bg-purple-lt',
|
||||
'acte' => 'bg-orange-lt',
|
||||
'hosp' => 'bg-red-lt',
|
||||
default => 'bg-secondary-lt',
|
||||
};
|
||||
|
||||
$stmtPhotos = DB::pdo()->prepare('
|
||||
SELECT *
|
||||
FROM medical_photos
|
||||
WHERE medical_note_id = ?
|
||||
ORDER BY created_at ASC
|
||||
');
|
||||
$stmtPhotos->execute([(int)$n['id']]);
|
||||
$notePhotos = $stmtPhotos->fetchAll(PDO::FETCH_ASSOC);
|
||||
?>
|
||||
<span class="badge <?= $badge ?>"><?= ucfirst(h($kind)) ?></span>
|
||||
|
||||
<?php if (!empty($n['reason'])): ?>
|
||||
<strong class="ms-2"><?= h($n['reason']) ?></strong>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="text-muted"><?= h($n['noted_at']) ?></div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="mt-2">
|
||||
<?php if (!empty($n['weight_kg']) || !empty($n['temperature_c'])): ?>
|
||||
<div class="text-muted">
|
||||
<?= !empty($n['weight_kg']) ? (h(t('pdf.weight')) . ': <strong>' . h($n['weight_kg']) . ' kg</strong>') : '' ?>
|
||||
<?= (!empty($n['weight_kg']) && !empty($n['temperature_c'])) ? ' • ' : '' ?>
|
||||
<?= !empty($n['temperature_c']) ? ('Temp: <strong>' . h($n['temperature_c']) . ' °C</strong>') : '' ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($n['clinic_name']) || !empty($n['vet_name'])): ?>
|
||||
<div class="text-muted">
|
||||
<?= !empty($n['clinic_name']) ? h($n['clinic_name']) : '' ?>
|
||||
<?= (!empty($n['clinic_name']) && !empty($n['vet_name'])) ? ' • ' : '' ?>
|
||||
<?= !empty($n['vet_name']) ? h($n['vet_name']) : '' ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
|
||||
<?php if (!empty($n['symptoms'])): ?>
|
||||
<div><span class="text-muted"><?= h(t('animal.symptoms')) ?> :</span> <?= nl2br(h($n['symptoms'])) ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($n['exam'])): ?>
|
||||
<div><span class="text-muted"><?= h(t('animal.exam')) ?> :</span> <?= nl2br(h($n['exam'])) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($n['diagnosis'])): ?>
|
||||
<div><span class="text-muted"><?= h(t('animal.diagnosis')) ?> :</span> <?= nl2br(h($n['diagnosis'])) ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($n['plan'])): ?>
|
||||
<div><span class="text-muted"><?= h(t('animal.plan')) ?> :</span> <?= nl2br(h($n['plan'])) ?></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($notePhotos)): ?>
|
||||
<div class="mt-3">
|
||||
<div class="text-muted mb-1"><?= h(t('animal.photos')) ?> :</div>
|
||||
|
||||
<div class="row row-cards">
|
||||
<?php foreach ($notePhotos as $p): ?>
|
||||
<?php
|
||||
$src = '/private-media?kind=medical&id=' . (int)$p['id'];
|
||||
?>
|
||||
<div class="col-6 col-sm-4 col-md-3 col-lg-2">
|
||||
<a href="<?= $src ?>" target="_blank" class="d-block">
|
||||
<img src="<?= $src ?>" class="img-fluid rounded border"
|
||||
style="height:140px;object-fit:cover;">
|
||||
</a>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<!-- TRAITEMENTS -->
|
||||
<div class="tab-pane" id="tab-trt">
|
||||
<?php if (!$treatments): ?>
|
||||
<div class="text-muted"><?= h(t('animal.no_treatment')) ?></div>
|
||||
<?php else: ?>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-vcenter">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><?= h(t('animal.status')) ?></th>
|
||||
<th><?= h(t('animal.medication')) ?></th>
|
||||
<th><?= h(t('animal.route')) ?></th>
|
||||
<th><?= h(t('animal.dosage')) ?></th>
|
||||
<th><?= h(t('animal.start')) ?></th>
|
||||
<th><?= h(t('dashboard.end')) ?></th>
|
||||
<th><?= h(t('common.notes')) ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($treatments as $t): ?>
|
||||
<?php
|
||||
$isFinished = !empty($t['end_date'])
|
||||
&& $t['end_date'] < date('Y-m-d');
|
||||
?>
|
||||
<tr class="<?= $isFinished ? 'table-secondary text-muted' : '' ?>">
|
||||
<td>
|
||||
<span class="badge <?= $isFinished ? 'bg-grey-lt' : 'bg-green-lt' ?>">
|
||||
<?= h(t($isFinished ? 'animal.finished' : 'animal.ongoing')) ?>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<strong><?= h($t['medication_name']) ?></strong>
|
||||
<?php if (!empty($t['molecule'])): ?>
|
||||
<div class="text-muted">
|
||||
<?= h($t['molecule']) ?><?= !empty($t['form']) ? (' • ' . h($t['form'])) : '' ?></div>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td><?= $t['route'] ? h($t['route']) : '—' ?></td>
|
||||
<td><?= h($t['dose_text']) ?></td>
|
||||
<td><?= h($t['start_date']) ?></td>
|
||||
<td><?= $t['end_date'] ? h($t['end_date']) : '—' ?></td>
|
||||
<td><?= $t['notes'] ? h($t['notes']) : '—' ?></td>
|
||||
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<!-- VACCINS -->
|
||||
<div class="tab-pane" id="tab-vac">
|
||||
<?php if (!$vaccinations): ?>
|
||||
<div class="text-muted"><?= h(t('animal.no_vaccine_recorded')) ?></div>
|
||||
<?php else: ?>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-vcenter">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><?= h(t('animal.vaccine')) ?></th>
|
||||
<th><?= h(t('animal.done_on')) ?></th>
|
||||
<th><?= h(t('animal.reminder_due')) ?></th>
|
||||
<th><?= h(t('animal.batch_traceability')) ?></th>
|
||||
<th><?= h(t('animal.administered_by')) ?></th>
|
||||
<th><?= h(t('common.notes')) ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($vaccinations as $v): ?>
|
||||
<tr>
|
||||
<td><strong><?= h($v['vaccine_name']) ?></strong></td>
|
||||
<td><?= h($v['done_date']) ?></td>
|
||||
<td><?= $v['due_date'] ? h($v['due_date']) : '—' ?></td>
|
||||
<td><?php if (!empty($v['manufacturer'])): ?><strong><?= h($v['manufacturer']) ?></strong><br><?php endif; ?><?= $v['lot'] ? h(t('animal.batch')) . ' ' . h($v['lot']) : '—' ?><?php if (!empty($v['batch_expires_on'])): ?>
|
||||
<div class="text-muted small">
|
||||
<?= h(t('animal.expires_on', ['date' => date('d/m/Y', strtotime((string)$v['batch_expires_on']))])) ?>
|
||||
</div><?php endif; ?></td>
|
||||
<td><?= !empty($v['administrator_name']) ? h($v['administrator_name']) : '—' ?></td>
|
||||
<td><?= $v['notes'] ? h($v['notes']) : '—' ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
1105
app/Views/_animal_show_modals.php
Normal file
1105
app/Views/_animal_show_modals.php
Normal file
File diff suppressed because it is too large
Load diff
205
app/Views/_animal_show_photos.php
Normal file
205
app/Views/_animal_show_photos.php
Normal file
|
|
@ -0,0 +1,205 @@
|
|||
<?php
|
||||
// Sous-vue de la fiche animale ; les variables sont préparées par AnimalsController::show().
|
||||
?>
|
||||
<!-- PHOTOS -->
|
||||
<div class="tab-pane" id="tab-photos">
|
||||
<?php /** @var array $photos */ ?>
|
||||
|
||||
<?php
|
||||
$publicPhotos = array_values(array_filter($photos ?? [], fn ($p) => (int)($p['is_public'] ?? 1) === 1));
|
||||
$privatePhotos = array_values(array_filter($photos ?? [], fn ($p) => (int)($p['is_public'] ?? 1) === 0));
|
||||
?>
|
||||
|
||||
<div class="row row-cards">
|
||||
|
||||
<!-- PUBLIC -->
|
||||
<div class="col-12">
|
||||
<div class="card border-success">
|
||||
<div class="card-header d-flex align-items-center justify-content-between">
|
||||
<div class="d-flex align-items-baseline gap-2">
|
||||
<h3 class="card-title m-0"><?= h(t('animal.public_photos')) ?></h3>
|
||||
<div class="text-muted small">(<?= h(t('animal.visible_online')) ?>)</div>
|
||||
</div>
|
||||
|
||||
<form method="post" action="/animal/photos/upload" enctype="multipart/form-data"
|
||||
class="d-flex gap-2">
|
||||
<input type="hidden" name="animal_id" value="<?= (int)$animal['id'] ?>">
|
||||
<input type="hidden" name="is_public" value="1">
|
||||
<input type="hidden" name="redirect_hash" value="#tab-photos">
|
||||
<input class="form-control form-control-sm" type="file" name="photos[]" multiple
|
||||
accept="image/*" required>
|
||||
<button class="btn btn-sm btn-success"
|
||||
type="submit"><?= h(t('animal.upload_public')) ?></button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<?php if (empty($publicPhotos)): ?>
|
||||
<div class="text-muted"><?= h(t('animal.no_public_photo')) ?></div>
|
||||
<?php else: ?>
|
||||
<div class="row row-cards">
|
||||
<?php foreach ($publicPhotos as $p): ?>
|
||||
<?php
|
||||
$src = '/media/animals/' . (int)$animal['id'] . '/' . h($p['filename']);
|
||||
$isPrimary = (int)($p['is_primary'] ?? 0) === 1;
|
||||
?>
|
||||
<div class="col-6 col-sm-4 col-md-3 col-lg-2">
|
||||
<div class="card card-sm">
|
||||
<a href="<?= $src ?>" target="_blank" class="d-block">
|
||||
<img src="<?= $src ?>" class="card-img-top" style="height:140px;object-fit:cover"
|
||||
alt="">
|
||||
</a>
|
||||
|
||||
<div class="card-body p-2">
|
||||
<?= $isPrimary
|
||||
? '<span class="badge bg-primary-lt">' . h(t('animal.primary')) . '</span>'
|
||||
: '<span class="badge bg-success-lt">' . h(t('animal.public')) . '</span>' ?>
|
||||
|
||||
<?php if ($isPrimary): ?>
|
||||
<button type="submit" class="btn btn-sm btn-icon btn-light position-absolute"
|
||||
title="<?= h(t('animal.set_primary')) ?>"
|
||||
style="border-radius:999px; top:10px;right:10px;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="18" height="18"
|
||||
viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"
|
||||
fill="currentColor" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path fill="#edd180"
|
||||
d="M12 17.75l-5.878 3.09l1.122-6.545l-4.756-4.635l6.57-.955l2.942-5.955l2.942 5.955l6.57 .955l-4.756 4.635l1.122 6.545z" />
|
||||
</svg>
|
||||
</button>
|
||||
<?php else: ?>
|
||||
<form method="post" action="/animal/photos/primary" class="position-absolute"
|
||||
style="top:8px;right:8px;">
|
||||
<input type="hidden" name="animal_id" value="<?= (int)$animal['id'] ?>">
|
||||
<input type="hidden" name="photo_id" value="<?= (int)$p['id'] ?>">
|
||||
<input type="hidden" name="redirect_hash" value="#tab-photos">
|
||||
<button type="submit" class="btn btn-sm btn-icon btn-light"
|
||||
title="<?= h(t('animal.set_primary')) ?>"
|
||||
style="border-radius:999px;top:-2px; right:2px;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="18" height="18"
|
||||
viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"
|
||||
fill="currentColor" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path
|
||||
d="M12 17.75l-5.878 3.09l1.122-6.545l-4.756-4.635l6.57-.955l2.942-5.955l2.942 5.955l6.57 .955l-4.756 4.635l1.122 6.545z" />
|
||||
</svg>
|
||||
</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="d-flex gap-1 mt-2">
|
||||
|
||||
<form method="post" action="/animal/photos/toggle" class="flex-fill">
|
||||
<input type="hidden" name="animal_id" value="<?= (int)$animal['id'] ?>">
|
||||
<input type="hidden" name="photo_id" value="<?= (int)$p['id'] ?>">
|
||||
<input type="hidden" name="is_public" value="0">
|
||||
<input type="hidden" name="redirect_hash" value="#tab-photos">
|
||||
<button class="btn btn-sm btn-outline-warning w-100"
|
||||
type="submit"><?= h(t('animal.private')) ?></button>
|
||||
</form>
|
||||
|
||||
<form method="post" action="/animal/photos/delete"
|
||||
onsubmit="return confirm(<?= h(json_encode(t('animal.delete_photo_confirm'), JSON_HEX_APOS | JSON_HEX_QUOT)) ?>);"
|
||||
class="flex-fill">
|
||||
<input type="hidden" name="animal_id" value="<?= (int)$animal['id'] ?>">
|
||||
<input type="hidden" name="photo_id" value="<?= (int)$p['id'] ?>">
|
||||
<input type="hidden" name="redirect_hash" value="#tab-photos">
|
||||
<button class="btn btn-sm btn-outline-danger w-100"
|
||||
type="submit"><?= h(t('common.delete_short')) ?></button>
|
||||
</form>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- PRIVE -->
|
||||
<div class="col-12">
|
||||
<div class="card border-danger">
|
||||
<div class="card-header d-flex align-items-center justify-content-between">
|
||||
<div class="d-flex align-items-baseline gap-2">
|
||||
<h3 class="card-title m-0"><?= h(t('animal.private_photos')) ?></h3>
|
||||
<div class="text-muted small">(<?= h(t('animal.not_visible_online')) ?>)</div>
|
||||
</div>
|
||||
|
||||
<form method="post" action="/animal/photos/upload" enctype="multipart/form-data"
|
||||
class="d-flex gap-2">
|
||||
<input type="hidden" name="animal_id" value="<?= (int)$animal['id'] ?>">
|
||||
<input type="hidden" name="is_public" value="0">
|
||||
<input type="hidden" name="redirect_hash" value="#tab-photos">
|
||||
<input class="form-control form-control-sm" type="file" name="photos[]" multiple
|
||||
accept="image/*" required>
|
||||
<button class="btn btn-sm btn-danger"
|
||||
type="submit"><?= h(t('animal.upload_private')) ?></button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<?php if (empty($privatePhotos)): ?>
|
||||
<div class="text-muted"><?= h(t('animal.no_private_photo')) ?></div>
|
||||
<?php else: ?>
|
||||
<div class="row row-cards">
|
||||
<?php foreach ($privatePhotos as $p): ?>
|
||||
<?php $src = '/private-media?kind=photo&id=' . (int)$p['id']; ?>
|
||||
<div class="col-6 col-sm-4 col-md-3 col-lg-2">
|
||||
<div class="card card-sm bg-muted-lt">
|
||||
<a href="<?= $src ?>" target="_blank" class="d-block">
|
||||
<img src="<?= $src ?>" class="card-img-top"
|
||||
style="height:140px;object-fit:cover;filter:grayscale(20%);" alt="">
|
||||
</a>
|
||||
|
||||
<div class="card-body p-2">
|
||||
<span class="badge bg-danger-lt"><?= h(t('animal.private')) ?></span>
|
||||
<?php if (!empty($p['caption'])): ?><div class="small mt-2">
|
||||
<?= nl2br(h($p['caption'])) ?></div><?php endif; ?>
|
||||
<?php if (!empty($p['care_round_id'])): ?><div class="text-muted small mt-1">📋
|
||||
<?= h(t('animal.round_photo')) ?></div><?php endif; ?>
|
||||
|
||||
<div class="d-flex gap-1 mt-2">
|
||||
<form method="post" action="/animal/photos/toggle" class="flex-fill">
|
||||
<input type="hidden" name="animal_id" value="<?= (int)$animal['id'] ?>">
|
||||
<input type="hidden" name="photo_id" value="<?= (int)$p['id'] ?>">
|
||||
<input type="hidden" name="is_public" value="1">
|
||||
<input type="hidden" name="redirect_hash" value="#tab-photos">
|
||||
<button class="btn btn-sm btn-outline-success w-100"
|
||||
type="submit"><?= h(t('animal.public')) ?></button>
|
||||
</form>
|
||||
|
||||
<form method="post" action="/animal/photos/delete"
|
||||
onsubmit="return confirm(<?= h(json_encode(t('animal.delete_photo_confirm'), JSON_HEX_APOS | JSON_HEX_QUOT)) ?>);"
|
||||
class="flex-fill">
|
||||
<input type="hidden" name="animal_id" value="<?= (int)$animal['id'] ?>">
|
||||
<input type="hidden" name="photo_id" value="<?= (int)$p['id'] ?>">
|
||||
<input type="hidden" name="redirect_hash" value="#tab-photos">
|
||||
<button class="btn btn-sm btn-outline-danger w-100"
|
||||
type="submit"><?= h(t('common.delete_short')) ?></button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
67
app/Views/_deworming_modal.php
Normal file
67
app/Views/_deworming_modal.php
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
<div class="modal modal-blur fade" id="modalDewormingCatalog" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<form class="modal-content" method="post" action="/animal/add-deworming">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title"><?= h(t('deworm.catalog_record')) ?></h5><button type="button" class="btn-close"
|
||||
data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body"><input type="hidden" name="animal_id" value="<?= (int)$animal['id'] ?>">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6"><label class="form-label"><?= h(t('common.date')) ?></label><input
|
||||
class="form-control" type="date" name="administered_on" value="<?= date('Y-m-d') ?>"
|
||||
required></div>
|
||||
<div class="col-md-6"><label class="form-label"><?= h(t('deworm.product')) ?></label><select
|
||||
class="form-select" name="dewormer_id">
|
||||
<option value=""><?= h(t('common.not_provided')) ?></option>
|
||||
<?php foreach (($dewormers ?? []) as $d): ?><option value="<?= (int)$d['id'] ?>">
|
||||
<?= h($d['name']) ?></option><?php endforeach; ?>
|
||||
</select></div>
|
||||
<div class="col-12"><label class="form-check"><input class="form-check-input" type="checkbox"
|
||||
name="create_dewormer" value="1" id="dewormerNewToggle"><span
|
||||
class="form-check-label"><?= h(t('deworm.add_catalog')) ?></span></label></div>
|
||||
<div class="col-12 d-none" id="dewormerNewFields">
|
||||
<div class="row g-2">
|
||||
<div class="col-md-5"><input class="form-control" name="new_dewormer_name"
|
||||
placeholder="<?= h(t('deworm.name')) ?>"></div>
|
||||
<div class="col-md-4"><input class="form-control" name="new_dewormer_ingredient"
|
||||
placeholder="<?= h(t('deworm.ingredient')) ?>"></div>
|
||||
<div class="col-md-3"><input class="form-control" name="new_dewormer_form"
|
||||
placeholder="<?= h(t('deworm.form')) ?>"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6"><label class="form-label"><?= h(t('animal.dose')) ?></label><input
|
||||
class="form-control" name="dose_text" placeholder="<?= h(t('deworm.dose_example')) ?>">
|
||||
</div>
|
||||
<div class="col-md-6"><label class="form-label"><?= h(t('common.weight')) ?></label>
|
||||
<div class="input-group"><input class="form-control" type="number" min="0" step="0.01"
|
||||
name="weight_kg"><span class="input-group-text">kg</span></div>
|
||||
</div>
|
||||
<div class="col-12"><label class="form-label"><?= h(t('deworm.next_due')) ?> <span
|
||||
class="text-muted"><?= h(t('common.optional_parentheses')) ?></span></label><input
|
||||
class="form-control" type="date" name="next_due_date">
|
||||
<div class="form-hint"><?= h(t('deworm.auto_age')) ?></div>
|
||||
</div>
|
||||
<?php if (!empty($litters)): ?><div class="col-12"><label class="form-check"><input
|
||||
class="form-check-input" type="checkbox" name="apply_litter" value="1"><span
|
||||
class="form-check-label"><?= h(t('deworm.apply_litter')) ?></span></label></div>
|
||||
<?php endif; ?>
|
||||
<div class="col-12"><label class="form-label"><?= h(t('animal.comment')) ?></label><textarea
|
||||
class="form-control" name="notes" rows="2"></textarea></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer"><button class="btn me-auto" type="button"
|
||||
data-bs-dismiss="modal"><?= h(t('common.cancel')) ?></button><button
|
||||
class="btn btn-primary"><?= h(t('common.save')) ?></button></div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
document.querySelectorAll('[data-bs-target="#modalDeworming"]').forEach(b => b.setAttribute(
|
||||
'data-bs-target', '#modalDewormingCatalog'));
|
||||
const t = document.getElementById('dewormerNewToggle'),
|
||||
p = document.getElementById('dewormerNewFields');
|
||||
t?.addEventListener('change', () => p?.classList.toggle('d-none', !t.checked));
|
||||
});
|
||||
|
||||
</script>
|
||||
29
app/Views/_directory_medical_fields.php
Normal file
29
app/Views/_directory_medical_fields.php
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
<?php $directoryFieldId = $directoryFieldId ?? uniqid('medical_', false); ?>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label"><?= h(t('animal.veterinary_clinic')) ?></label>
|
||||
<select class="form-select directory-contact-select medical-clinic-select" name="clinic_contact_id"
|
||||
data-pair="<?= h($directoryFieldId) ?>" data-new-target="<?= h($directoryFieldId) ?>ClinicNew" required>
|
||||
<option value="">— <?= h(t('common.choose')) ?> —</option>
|
||||
<?php foreach ($directoryClinics as $contact): ?><option value="<?= (int)$contact['id'] ?>">
|
||||
<?= h($contact['name']) ?></option><?php endforeach; ?>
|
||||
<option value="new">+ <?= h(t('animal.create_clinic')) ?></option>
|
||||
</select>
|
||||
<input class="form-control mt-2 d-none" id="<?= h($directoryFieldId) ?>ClinicNew" name="new_clinic_name"
|
||||
placeholder="<?= h(t('animal.new_clinic_name')) ?>">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label"><?= h(t('animal.form.veterinarian')) ?></label>
|
||||
<select class="form-select directory-contact-select medical-vet-select" name="vet_contact_id"
|
||||
data-pair="<?= h($directoryFieldId) ?>" data-new-target="<?= h($directoryFieldId) ?>VetNew" required>
|
||||
<option value="">— <?= h(t('common.choose')) ?> —</option>
|
||||
<?php foreach ($veterinarians as $contact): ?><option value="<?= (int)$contact['id'] ?>"
|
||||
data-organization="<?= (int)($contact['organization_id'] ?? 0) ?>"><?= h($contact['name']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
<option value="new">+ <?= h(t('animal.create_vet')) ?></option>
|
||||
</select>
|
||||
<input class="form-control mt-2 d-none" id="<?= h($directoryFieldId) ?>VetNew" name="new_vet_name"
|
||||
placeholder="<?= h(t('animal.new_vet_name')) ?>">
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="form-hint"><?= h(t('animal.medical_pair_hint')) ?></div>
|
||||
</div>
|
||||
63
app/Views/_expense_modal.php
Normal file
63
app/Views/_expense_modal.php
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
<?php if ($canViewMedical): ?>
|
||||
<div class="modal modal-blur fade" id="modalExpense" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg modal-dialog-centered">
|
||||
<form class="modal-content" method="post" action="/animal/add-expense">
|
||||
<div class="modal-header">
|
||||
<div>
|
||||
<h5 class="modal-title"><?= h(t('expense.add')) ?></h5>
|
||||
<div class="text-muted small"><?= h(t('expense.history_help')) ?></div>
|
||||
</div><button class="btn-close" type="button" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body"><input type="hidden" name="animal_id" value="<?= (int)$animal['id'] ?>">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-8"><label
|
||||
class="form-label required"><?= h(t('expense.act_product')) ?></label><select
|
||||
class="form-select" name="tariff_id" id="expenseTariff" required>
|
||||
<option value="">— <?= h(t('expense.choose')) ?> —</option><?php $lastClinic = '';
|
||||
foreach ($expenseTariffs as $t):if ($lastClinic !== $t['clinic_name']):if ($lastClinic !== ''):?></optgroup><?php endif;
|
||||
$lastClinic = $t['clinic_name'];?><optgroup label="<?= h($lastClinic) ?>"><?php endif;?><option
|
||||
value="<?= (int)$t['id'] ?>" data-price="<?= (int)$t['effective_cents'] ?>"
|
||||
data-discount="<?= h((string)$t['discount_percent']) ?>"><?= h($t['label']) ?> ·
|
||||
<?= number_format($t['effective_cents'] / 100, 2, ',', ' ') ?> €</option><?php endforeach;
|
||||
if ($lastClinic !== ''):?></optgroup><?php endif;?>
|
||||
</select>
|
||||
<div class="form-hint" id="expenseHint"><?= h(t('expense.price_after_discount')) ?></div>
|
||||
</div>
|
||||
<div class="col-md-4"><label class="form-label"><?= h(t('common.date')) ?></label><input
|
||||
class="form-control" type="date" name="occurred_on" value="<?= date('Y-m-d') ?>" required>
|
||||
</div>
|
||||
<div class="col-md-4"><label class="form-label"><?= h(t('expense.quantity')) ?></label><input
|
||||
class="form-control" type="number" min="0.01" step="0.01" name="quantity" value="1"
|
||||
required></div>
|
||||
<div class="col-md-8"><label class="form-label"><?= h(t('expense.comment')) ?></label><input
|
||||
class="form-control" name="notes" placeholder="<?= h(t('expense.comment_placeholder')) ?>">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer"><button class="btn me-auto" type="button"
|
||||
data-bs-dismiss="modal"><?= h(t('common.cancel')) ?></button><button
|
||||
class="btn btn-primary"><?= h(t('expense.save')) ?></button></div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const s = document.getElementById('expenseTariff'),
|
||||
h = document.getElementById('expenseHint'),
|
||||
template = < ? = json_encode(t('expense.unit_estimate'), JSON_UNESCAPED_UNICODE |
|
||||
JSON_UNESCAPED_SLASHES) ? > ,
|
||||
empty = < ? = json_encode(t('expense.price_after_discount'), JSON_UNESCAPED_UNICODE |
|
||||
JSON_UNESCAPED_SLASHES) ? > ;
|
||||
s?.addEventListener('change', () => {
|
||||
const o = s.selectedOptions[0],
|
||||
price = (Number(o?.dataset.price || 0) / 100).toLocaleString( < ? = json_encode(I18n::
|
||||
locale() === 'fr' ? 'fr-FR' : 'en-GB') ? > , {
|
||||
minimumFractionDigits: 2
|
||||
});
|
||||
h.textContent = o?.value ? template.replace(':price', price).replace(':discount', o.dataset
|
||||
.discount) : empty;
|
||||
});
|
||||
});
|
||||
|
||||
</script>
|
||||
<?php endif; ?>
|
||||
37
app/Views/_expense_summary.php
Normal file
37
app/Views/_expense_summary.php
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
<?php if ($canViewMedical): ?>
|
||||
<div class="card mt-3">
|
||||
<div class="card-body d-flex flex-wrap align-items-center gap-3">
|
||||
<?php $expenseCount = count($expenses ?? []); ?><div class="me-auto">
|
||||
<div class="subheader"><?= h(t('expense.estimated_vet')) ?></div>
|
||||
<div class="text-muted small"><?= h(t('expense.recorded', ['count' => $expenseCount])) ?></div>
|
||||
</div>
|
||||
<div class="h2 mb-0"><?= number_format(($expenseTotal ?? 0) / 100, 2, ',', ' ') ?> €</div>
|
||||
<?php if (!empty($expenses)): ?><button class="btn btn-sm btn-outline-secondary" type="button"
|
||||
data-bs-toggle="collapse" data-bs-target="#animalExpenseDetails"
|
||||
aria-expanded="false"><?= h(t('expense.details')) ?></button><?php endif; ?>
|
||||
</div>
|
||||
<?php if (!empty($expenses)): ?><div class="collapse" id="animalExpenseDetails">
|
||||
<div class="table-responsive border-top">
|
||||
<table class="table table-sm table-vcenter mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><?= h(t('common.date')) ?></th>
|
||||
<th><?= h(t('expense.expense')) ?></th>
|
||||
<th><?= h(t('pricing.structure')) ?></th>
|
||||
<th><?= h(t('pricing.discount')) ?></th>
|
||||
<th class="text-end"><?= h(t('expense.total')) ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody><?php foreach ($expenses as $e): ?><tr>
|
||||
<td><?= h(date('d/m/Y', strtotime($e['occurred_on']))) ?></td>
|
||||
<td><?= h($e['label']) ?><?php if ((float)$e['quantity'] !== 1.0): ?><span
|
||||
class="text-muted small"> × <?= h((string)$e['quantity']) ?></span><?php endif; ?></td>
|
||||
<td><?= h($e['clinic_name'] ?: '—') ?></td>
|
||||
<td><?= number_format((float)$e['discount_percent'], 2, ',', ' ') ?> %</td>
|
||||
<td class="text-end fw-bold"><?= number_format($e['total_cents'] / 100, 2, ',', ' ') ?> €</td>
|
||||
</tr><?php endforeach; ?></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div><?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
15
app/Views/_expense_tariff_field.php
Normal file
15
app/Views/_expense_tariff_field.php
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<div class="col-12"><label class="form-label"><?= h(t('animal.tariff_record')) ?> <span
|
||||
class="text-muted">(<?= h(mb_strtolower(t('common.optional'))) ?>)</span></label><select class="form-select"
|
||||
name="tariff_id">
|
||||
<option value=""><?= h(t('animal.no_expense')) ?></option><?php $expenseClinic = '';
|
||||
foreach (($expenseTariffs ?? []) as $t):if ($expenseClinic !== $t['clinic_name']):if ($expenseClinic !== ''):?>
|
||||
</optgroup><?php endif;
|
||||
$expenseClinic = $t['clinic_name'];?><optgroup label="<?= h($expenseClinic) ?>"><?php endif;?><option
|
||||
value="<?= (int)$t['id'] ?>"><?= h($t['label']) ?> ·
|
||||
<?= number_format($t['effective_cents'] / 100, 2, ',', ' ') ?> €
|
||||
<?= h(t('animal.net')) ?><?= (float)$t['discount_percent'] > 0 ? ' (−' . number_format((float)$t['discount_percent'], 0) . ' %)' : '' ?>
|
||||
</option><?php endforeach;
|
||||
if ($expenseClinic !== ''):?></optgroup><?php endif;?>
|
||||
</select>
|
||||
<div class="form-hint"><?= h(t('animal.expense_hint')) ?></div>
|
||||
</div>
|
||||
99
app/Views/_helpers.php
Normal file
99
app/Views/_helpers.php
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
<?php
|
||||
|
||||
// app/Views/_helpers.php
|
||||
|
||||
if (!function_exists('h')) {
|
||||
function h($s): string
|
||||
{
|
||||
return htmlspecialchars((string)$s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
||||
}
|
||||
}
|
||||
|
||||
function app_version(): string
|
||||
{
|
||||
static $version = null;
|
||||
if ($version !== null) {
|
||||
return $version;
|
||||
}
|
||||
$path = dirname(__DIR__, 2) . '/VERSION';
|
||||
$value = is_file($path) ? trim((string) file_get_contents($path)) : '';
|
||||
return $version = ($value !== '' ? $value : 'développement');
|
||||
}
|
||||
|
||||
function status_badge(string $st): array
|
||||
{
|
||||
$st = strtolower(trim($st));
|
||||
return match ($st) {
|
||||
'refuge' => ['bg-blue-lt',t('status.refuge')],
|
||||
'fa' => ['bg-cyan-lt',t('status.fa')],
|
||||
'fa_permanente' => ['bg-indigo-lt',t('status.fa_permanente')],
|
||||
'chat_libre' => ['bg-green-lt text-green','🌿 ' . t('status.chat_libre')],
|
||||
'quarantaine' => ['bg-yellow-lt',t('status.quarantaine')],
|
||||
'soin' => ['bg-red-lt',t('status.soin')],
|
||||
'isolation' => ['bg-orange-lt',t('status.isolation')],
|
||||
'hospitalise','hospitalisé' => ['bg-red-lt',t('status.hospitalise')],
|
||||
'reserve','réservé' => ['bg-purple-lt',t('status.reserve')],
|
||||
'adopte','adopté' => ['bg-green-lt',t('status.adopte')],
|
||||
'decede','décédé' => ['bg-dark-lt',t('status.decede')],
|
||||
'enfui' => ['bg-secondary-lt',t('status.enfui')],
|
||||
default => ['bg-secondary-lt', $st !== '' ? $st : '—'],
|
||||
};
|
||||
}
|
||||
|
||||
function species_label(?string $species): string
|
||||
{
|
||||
if (class_exists('SpeciesService')) {
|
||||
return SpeciesService::label($species);
|
||||
}
|
||||
return match (strtolower(trim((string)$species))) {
|
||||
'cat', 'chat' => t('species.cat'),
|
||||
'dog', 'chien' => t('species.dog'),
|
||||
'nac' => 'NAC',
|
||||
'' => '—',
|
||||
default => ucfirst(trim((string)$species)),
|
||||
};
|
||||
}
|
||||
|
||||
function coat_swatch_style(?string $coat): string
|
||||
{
|
||||
return match (mb_strtolower(trim((string)$coat), 'UTF-8')) {
|
||||
'bicolore noir / blanc' => 'background:linear-gradient(135deg,#252525 0 50%,#fff 50% 100%)',
|
||||
'bicolore gris / blanc' => 'background:linear-gradient(135deg,#8b95a1 0 50%,#fff 50% 100%)',
|
||||
'bicolore roux / blanc' => 'background:linear-gradient(135deg,#d9822b 0 50%,#fff 50% 100%)',
|
||||
'bicolore tigré et blanc' => 'background:linear-gradient(90deg,transparent 0 55%,#fff 55% 100%),repeating-linear-gradient(135deg,#8a6848 0 5px,#3f3328 5px 8px)',
|
||||
'gris' => 'background:#8b95a1',
|
||||
'roux' => 'background:#d9822b',
|
||||
'noir' => 'background:#252525',
|
||||
'chocolat' => 'background:#6f4e37',
|
||||
'blanc' => 'background:#fff',
|
||||
'tricolore / calico' => 'background:conic-gradient(#252525 0 33%,#fff 33% 66%,#d9822b 66% 100%)',
|
||||
'écaille de tortue' => 'background:conic-gradient(#2d241f 0 28%,#b86326 28% 48%,#171513 48% 72%,#d18a45 72% 100%)',
|
||||
'colourpoint / siamois' => 'background:radial-gradient(circle at center,#f2dfbb 0 48%,#654b3c 52% 100%)',
|
||||
'tigré / tabby' => 'background:repeating-linear-gradient(135deg,#8a6848 0 5px,#3f3328 5px 8px)',
|
||||
default => 'background:#d9dde3',
|
||||
};
|
||||
}
|
||||
|
||||
function sort_th(string $label, string $key, string $sort, string $dir, string $path = '/animals'): string
|
||||
{
|
||||
$nextDir = ($sort === $key && strtoupper($dir) === 'ASC') ? 'desc' : 'asc';
|
||||
$arrow = ($sort === $key) ? (strtoupper($dir) === 'ASC' ? ' ↑' : ' ↓') : '';
|
||||
$qs = $_GET;
|
||||
$qs['sort'] = $key;
|
||||
$qs['dir'] = $nextDir;
|
||||
$href = $path . '?' . http_build_query($qs);
|
||||
return '<a class="text-reset text-decoration-none" href="' . htmlspecialchars($href, ENT_QUOTES) . '">' . $label . $arrow . '</a>';
|
||||
|
||||
}
|
||||
|
||||
function sortLink($column, $label, $sort, $dir, $year, $q, $species)
|
||||
{
|
||||
$newDir = ($sort === $column && $dir === 'ASC') ? 'desc' : 'asc';
|
||||
|
||||
$arrow = '';
|
||||
if ($sort === $column) {
|
||||
$arrow = $dir === 'ASC' ? ' ↑' : ' ↓';
|
||||
}
|
||||
|
||||
return '<a href="?year=' . $year . '&q=' . urlencode($q) . '&species=' . urlencode($species) . '&sort=' . $column . '&dir=' . $newDir . '">' . $label . $arrow . '</a>';
|
||||
}
|
||||
35
app/Views/_layout_footer.php
Normal file
35
app/Views/_layout_footer.php
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
/** Pied de page commun. */
|
||||
?>
|
||||
<footer class="globinours-footer d-print-none">
|
||||
<div class="container-xl">
|
||||
<div class="footer-surface d-flex flex-wrap align-items-center justify-content-between gap-3">
|
||||
<div>
|
||||
<div class="fw-bold">Globinours</div>
|
||||
<div class="text-muted small"><?= h(t('footer.description')) ?></div>
|
||||
</div>
|
||||
<div class="footer-meta d-flex flex-wrap align-items-center gap-3"><span
|
||||
class="d-inline-flex align-items-center gap-1"><span role="img"
|
||||
aria-label="<?= h(t('footer.brewed_label')) ?>">🧪 <?= h(t('footer.brewed')) ?></span>
|
||||
<?= t('footer.by_brittany', ['developer' => '<a href="https://oversu.fr" target="_blank" rel="noopener noreferrer" class="text-reset fw-semibold">OverSu</a>']) ?>
|
||||
<span class="breton-flag" role="img" aria-label="<?= h(t('footer.breton_flag')) ?>"
|
||||
title="Gwen ha Du"></span></span><span class="badge footer-version">Version
|
||||
<?= h(app_version()) ?></span><a class="footer-source" href="/evolutions"
|
||||
title="<?= h(t('footer.evolutions_title')) ?>">🧭
|
||||
<span><?= h(t('footer.evolutions')) ?></span></a><a class="footer-source"
|
||||
href="https://nnsprod.com/git/oversu/Globinours" target="_blank" rel="noopener noreferrer"
|
||||
title="<?= h(t('footer.source_title')) ?>" aria-label="<?= h(t('footer.source_title')) ?>"><svg
|
||||
xmlns="http://www.w3.org/2000/svg" class="icon" 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="6" r="2" />
|
||||
<circle cx="18" cy="6" r="2" />
|
||||
<circle cx="6" cy="18" r="2" />
|
||||
<path d="M6 8v8" />
|
||||
<path d="M8 6h8" />
|
||||
<path d="M18 8a6 6 0 0 1 -6 6h-4" />
|
||||
</svg><span><?= h(t('footer.source')) ?></span></a></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="paw-footer" aria-hidden="true"></div>
|
||||
</footer>
|
||||
152
app/Views/_layout_topbar.php
Normal file
152
app/Views/_layout_topbar.php
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
<?php
|
||||
/** Barre de navigation et recherche globale. */
|
||||
?>
|
||||
<header class="navbar navbar-expand-md d-print-none">
|
||||
<div class="container-xl">
|
||||
<div class="topbar-surface d-flex align-items-center justify-content-between w-100">
|
||||
<a class="navbar-brand d-flex flex-row align-items-center py-0"
|
||||
href="<?= h(PermissionService::can('dashboard') ? '/dashboard' : PermissionService::firstAllowedPath()) ?>">
|
||||
<img src="/assets/logo-globinours-64.png" width="52" height="54" alt="" class="flex-shrink-0"
|
||||
style="margin-right:10px">
|
||||
<span class="d-flex flex-column align-items-start">
|
||||
<span class="fw-bold">Globinours</span>
|
||||
<span class="text-muted fw-normal" style="font-size:.72rem; line-height:1.2; letter-spacing:0;">
|
||||
<?= h(t('footer.tagline')) ?>
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<div class="navbar-nav flex-row order-md-last">
|
||||
<span class="search-trigger-wrap"><button class="search-trigger" type="button" data-bs-toggle="modal"
|
||||
data-bs-target="#globalSearchModal"
|
||||
title="<?= h(t('search.open')) ?> — <?= h(t('search.shortcut')) ?>"
|
||||
aria-label="<?= h(t('search.open')) ?> (<?= h(t('search.shortcut')) ?>)"><svg
|
||||
xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24" viewBox="0 0 24 24"
|
||||
stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round"
|
||||
stroke-linejoin="round" aria-hidden="true">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<circle cx="10" cy="10" r="7" />
|
||||
<path d="M21 21l-6 -6" />
|
||||
</svg></button></span>
|
||||
<!-- Dashboard -->
|
||||
<?php if (PermissionService::can('dashboard')): ?>
|
||||
<a class="btn <?= $dashboardActive ? 'btn-teal' : 'btn-outline-teal' ?> btn-sm me-2" href="/dashboard">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon me-1" width="18" height="18" viewBox="0 0 24 24"
|
||||
stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round"
|
||||
stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M4 4h6v8h-6z" />
|
||||
<path d="M4 16h6v4h-6z" />
|
||||
<path d="M14 12h6v8h-6z" />
|
||||
<path d="M14 4h6v6h-6z" />
|
||||
</svg>
|
||||
<?= h(t('nav.dashboard')) ?>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Animaux -->
|
||||
<?php if (PermissionService::can('animals')): ?>
|
||||
<a class="btn <?= $animalsActive ? 'btn-primary' : 'btn-outline-primary' ?> btn-sm me-2"
|
||||
href="/animals">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon me-1" width="18" height="18" viewBox="0 0 24 24"
|
||||
stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round"
|
||||
stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M4.5 9.5a2.5 2.5 0 1 0 0 -5a2.5 2.5 0 0 0 0 5z" />
|
||||
<path d="M9 5.5a2.5 2.5 0 1 0 5 0a2.5 2.5 0 0 0 -5 0z" />
|
||||
<path d="M17 9.5a2.5 2.5 0 1 0 0 -5a2.5 2.5 0 0 0 0 5z" />
|
||||
<path
|
||||
d="M7 15.5c0 -2.5 2.2 -4.5 5 -4.5s5 2 5 4.5c0 2 -1.3 4 -3.5 4c-.8 0 -1.1 -.5 -1.5 -.5s-.7 .5 -1.5 .5c-2.2 0 -3.5 -2 -3.5 -4z" />
|
||||
</svg>
|
||||
<?= h(t('nav.animals')) ?>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Administratif -->
|
||||
<?php if (PermissionService::can('directory')): ?>
|
||||
<a class="btn <?= $directoryActive ? 'btn-purple' : 'btn-outline-purple' ?> btn-sm me-2"
|
||||
href="/directory">👥 <?= h(t('nav.directory')) ?></a>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Tournée de soins -->
|
||||
<?php if (PermissionService::can('care')): ?>
|
||||
<a class="btn <?= $careRoundActive ? 'btn-orange' : 'btn-outline-orange' ?> btn-sm me-2"
|
||||
href="/care-round">🩺 <?= h(t('nav.care_round')) ?></a>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Agenda -->
|
||||
<?php if (PermissionService::can('agenda')): ?><a
|
||||
class="btn <?= $agendaActive ? 'btn-pink' : 'btn-outline-pink' ?> btn-sm me-2" href="/agenda">📅
|
||||
<?= h(t('nav.agenda')) ?></a><?php endif; ?>
|
||||
|
||||
<!-- Statistiques -->
|
||||
<?php if (PermissionService::can('statistics')): ?>
|
||||
<a class="btn <?= $statisticsActive ? 'btn-cyan' : 'btn-outline-cyan' ?> btn-sm me-2" href="/statistics"
|
||||
title="<?= h(t('nav.statistics')) ?>">📊 <?= h(t('nav.statistics')) ?></a>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (PermissionService::can('accounting')): ?><a
|
||||
class="btn <?= $accountingActive ? 'btn-teal' : 'btn-outline-teal' ?> btn-sm me-2"
|
||||
href="/accounting?year=0">🧾 Comptabilité</a><?php endif; ?>
|
||||
|
||||
<div class="dropdown settings-dropdown"><button class="btn btn-dark btn-sm me-2 settings-trigger"
|
||||
data-bs-toggle="dropdown" aria-expanded="false" title="<?= h(t('account.settings')) ?>"
|
||||
aria-label="<?= h(t('account.settings')) ?>"><svg xmlns="http://www.w3.org/2000/svg"
|
||||
class="icon m-0" width="22" height="22" viewBox="0 0 24 24" stroke-width="2"
|
||||
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M12 15.5a3.5 3.5 0 1 0 0 -7a3.5 3.5 0 0 0 0 7z" />
|
||||
<path
|
||||
d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06 .06a2 2 0 1 1 -2.83 2.83l-.06 -.06a1.65 1.65 0 0 0 -1.82 -.33a1.65 1.65 0 0 0 -1 1.51v.17a2 2 0 1 1 -4 0v-.09a1.65 1.65 0 0 0 -1.08 -1.51a1.65 1.65 0 0 0 -1.82 .33l-.06 .06a2 2 0 1 1 -2.83 -2.83l.06 -.06a1.65 1.65 0 0 0 .33 -1.82a1.65 1.65 0 0 0 -1.51 -1h-.17a2 2 0 1 1 0 -4h.09a1.65 1.65 0 0 0 1.51 -1.08a1.65 1.65 0 0 0 -.33 -1.82l-.06 -.06a2 2 0 1 1 2.83 -2.83l.06 .06a1.65 1.65 0 0 0 1.82 .33h.08a1.65 1.65 0 0 0 1 -1.51v-.17a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51a1.65 1.65 0 0 0 1.82 -.33l.06 -.06a2 2 0 1 1 2.83 2.83l-.06 .06a1.65 1.65 0 0 0 -.33 1.82v.08a1.65 1.65 0 0 0 1.51 1h.17a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0 -1.51 1z" />
|
||||
</svg></button>
|
||||
<div class="dropdown-menu dropdown-menu-end">
|
||||
<div class="dropdown-item-text">
|
||||
<div class="fw-bold">👤 <?= h($currentUser['display_name'] ?: $currentUser['username']) ?>
|
||||
</div>
|
||||
<div class="small text-muted"><?= h(ucfirst((string)$currentUser['role'])) ?></div>
|
||||
</div>
|
||||
<div class="dropdown-divider"></div><?php if (PermissionService::can('administrative')):?><a
|
||||
class="dropdown-item" href="/admin/register">📁
|
||||
<?= h(t('nav.administrative')) ?></a><?php endif;?><?php if (PermissionService::can('grants')):?><a
|
||||
class="dropdown-item" href="/admin/grants">📊
|
||||
<?= h(t('nav.grants')) ?></a><?php endif;?><?php if (PermissionService::can('administrative') || PermissionService::can('grants')):?>
|
||||
<div class="dropdown-divider"></div>
|
||||
<?php endif;?><?php if (Auth::is('admin')): ?><?php $lastSettingsGroup = null;
|
||||
foreach ($settingsMenuLinks as [$settingsHref,$settingsIcon,$settingsLabel,$settingsGroup]): if ($settingsGroup !== $lastSettingsGroup): if ($lastSettingsGroup !== null): ?>
|
||||
<div class="dropdown-divider"></div><?php endif; ?><div class="dropdown-header">
|
||||
<?= h(settingsGroupLabel($settingsGroup)) ?></div>
|
||||
<?php $lastSettingsGroup = $settingsGroup;endif; ?><a class="dropdown-item"
|
||||
href="<?= h($settingsHref) ?>"><?= $settingsIcon ?>
|
||||
<?= h($settingsLabel) ?></a><?php endforeach; ?><div class="dropdown-divider"></div>
|
||||
<?php endif; ?><form method="post" action="/logout"><input type="hidden" name="csrf"
|
||||
value="<?= h(Auth::csrf()) ?>"><button class="dropdown-item text-danger" type="submit">↪
|
||||
<?= h(t('account.logout')) ?></button></form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<div class="modal modal-blur fade" id="globalSearchModal" tabindex="-1" aria-hidden="true"
|
||||
data-hint="<?= h(t('search.hint')) ?>" data-loading="<?= h(t('search.loading')) ?>"
|
||||
data-none="<?= h(t('search.none')) ?>" data-error="<?= h(t('search.error')) ?>"
|
||||
data-all-results="<?= h(t('search.all_results')) ?>"
|
||||
data-groups="<?= h(json_encode(['animals' => t('search.group.animals'),'directory' => t('search.group.directory'),'agenda' => t('search.group.agenda'),'accounting' => t('search.group.accounting')], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)) ?>">
|
||||
<div class="modal-dialog modal-lg modal-dialog-centered modal-dialog-scrollable">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title"><?= h(t('search.title')) ?></h2><button type="button" class="btn-close"
|
||||
data-bs-dismiss="modal" aria-label="<?= h(t('common.close')) ?>"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="input-group input-group-lg mb-3"><span class="input-group-text">⌕</span><input
|
||||
class="form-control" id="globalSearchInput" maxlength="120" autocomplete="off"
|
||||
placeholder="<?= h(t('search.placeholder')) ?>"></div>
|
||||
<div id="globalSearchResults" class="list-group">
|
||||
<div class="text-muted text-center py-4"><?= h(t('search.hint')) ?></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
14
app/Views/_public_animal_cards.php
Normal file
14
app/Views/_public_animal_cards.php
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<?php if (!$animals): ?><div class="empty"><?= h(t('public_site.no_animals')) ?></div><?php else: ?><div class="grid">
|
||||
<?php foreach ($animals as $a): ?><a class="animal-card"
|
||||
href="<?= h($siteUrl('animal', ['id' => (int)$a['id']])) ?>"><?php if ($a['primary_photo']): ?><img
|
||||
loading="lazy" src="/media/animals/<?= (int)$a['id'] ?>/<?= h(rawurlencode($a['primary_photo'])) ?>"
|
||||
alt="<?= h($a['name']) ?>"><?php else: ?><div class="photo-placeholder"><?= h($a['species_icon'] ?: '🐾') ?>
|
||||
</div><?php endif; ?><div class="animal-card-body">
|
||||
<h3><?= h($a['name']) ?></h3>
|
||||
<div class="meta">
|
||||
<?= h(($a['species_name'] ?: SpeciesService::label($a['species'])) . ' · ' . public_age($a)) ?></div>
|
||||
<div class="chips"><?php if ($a['sex'] === 'F'): ?><span class="chip">♀
|
||||
<?= h(t('sex.female')) ?></span><?php elseif ($a['sex'] === 'M'): ?><span class="chip">♂
|
||||
<?= h(t('sex.male')) ?></span><?php endif; ?><?php if ($a['sterilization_status'] === 'yes'): ?><span
|
||||
class="chip">✓ <?= h(t('public_site.sterilized')) ?></span><?php endif; ?></div>
|
||||
</div></a><?php endforeach; ?></div><?php endif; ?>
|
||||
31
app/Views/_settings_links.php
Normal file
31
app/Views/_settings_links.php
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
|
||||
if (!function_exists('settingsNavigationLinks')) {
|
||||
function settingsGroupLabel(string$group): string
|
||||
{
|
||||
return match($group) {
|
||||
'general' => t('settings.group_general'),'business' => t('settings.group_business'),'access' => t('settings.group_access'),'maintenance' => t('settings.group_maintenance'),default => $group
|
||||
};
|
||||
}
|
||||
function settingsNavigationLinks(): array
|
||||
{
|
||||
return [
|
||||
'general' => ['/settings/general','⚙️',t('settings.general'),'general'],
|
||||
'public-site' => ['/settings/public-site','🌐',t('settings.public_site'),'general'],
|
||||
'species' => ['/settings/species','🐾',t('settings.species'),'business'],
|
||||
'rooms' => ['/settings/rooms','🚪',t('settings.rooms'),'business'],
|
||||
'pricing' => ['/settings/pricing','💶',t('settings.pricing'),'business'],
|
||||
'inventory' => ['/settings/inventory','💊',t('settings.inventory'),'business'],
|
||||
'lab-references' => ['/settings/lab-references','🩸',t('settings.lab_references'),'business'],
|
||||
'users' => ['/settings/users','👥',t('settings.users'),'access'],
|
||||
'permissions' => ['/settings/permissions','🔐',t('settings.permissions'),'access'],
|
||||
'privacy' => ['/settings/privacy','🔏',t('settings.privacy'),'access'],
|
||||
'backups' => ['/settings/backups','💾',t('settings.backups'),'maintenance'],
|
||||
'health' => ['/settings/health','🩺',t('settings.health'),'maintenance'],
|
||||
'notifications' => ['/settings/notifications','🔔',t('settings.notifications'),'maintenance'],
|
||||
'data' => ['/settings/data','🧪',t('settings.data'),'maintenance'],
|
||||
'asm3' => ['/settings/asm3','🚚',t('settings.asm3'),'maintenance'],
|
||||
'audit' => ['/settings/audit','📜',t('settings.audit'),'maintenance'],
|
||||
];
|
||||
}
|
||||
}
|
||||
21
app/Views/access_denied.php
Normal file
21
app/Views/access_denied.php
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<?php $editing = ($deniedAction ?? 'view') === 'edit';
|
||||
$module = $deniedModule ?? t('access.requested'); ?>
|
||||
<div class="row justify-content-center py-5">
|
||||
<div class="col-md-8 col-lg-6">
|
||||
<div class="card text-center overflow-hidden">
|
||||
<div class="card-status-top bg-orange"></div>
|
||||
<div class="card-body p-5">
|
||||
<div class="display-3 mb-3">🔒</div>
|
||||
<div class="text-uppercase text-muted fw-bold small mb-2"><?= h(t('access.denied')) ?></div>
|
||||
<h2 class="mb-3"><?= h(t($editing ? 'access.edit_denied' : 'access.page_denied')) ?></h2>
|
||||
<p class="text-muted mb-4">
|
||||
<?= h(t($editing ? 'access.explanation_edit' : 'access.explanation_view', ['module' => $module])) ?>
|
||||
</p>
|
||||
<div class="d-flex flex-wrap justify-content-center gap-2"><a class="btn btn-primary"
|
||||
href="<?= h(PermissionService::firstAllowedPath()) ?>"><?= h(t('access.back_space')) ?></a><button
|
||||
class="btn btn-outline-secondary" type="button"
|
||||
onclick="history.back()"><?= h(t('access.previous')) ?></button></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
35
app/Views/account_devices.php
Normal file
35
app/Views/account_devices.php
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
<div class="d-flex flex-wrap align-items-center gap-2 mb-3">
|
||||
<div class="me-auto">
|
||||
<h2 class="mb-1">📱 <?= h(t('devices.title')) ?></h2>
|
||||
<div class="text-muted"><?= h(t('devices.intro')) ?></div>
|
||||
</div>
|
||||
<form method="post" action="/account/devices/revoke-all"
|
||||
onsubmit="return confirm(<?= h(json_encode(t('devices.revoke_all_confirm'), JSON_HEX_APOS | JSON_HEX_QUOT)) ?>)">
|
||||
<button class="btn btn-outline-danger"><?= h(t('devices.revoke_all')) ?></button></form>
|
||||
</div>
|
||||
<?php if ($saved): ?><div class="alert alert-success">✅ <?= h(t('devices.revoked')) ?></div><?php endif; ?>
|
||||
<div class="row g-3">
|
||||
<?php foreach ($devices as $device): $active = !$device['revoked_at'] && strtotime($device['expires_at']) > time(); ?>
|
||||
<div class="col-12 col-lg-6">
|
||||
<div class="card h-100">
|
||||
<div class="card-body d-flex gap-3">
|
||||
<div class="fs-1">📱</div>
|
||||
<div class="flex-fill">
|
||||
<div class="d-flex flex-wrap gap-2 align-items-center">
|
||||
<strong><?= h($device['device_name']) ?></strong><?php if ($device['selector'] === $currentSelector): ?><span
|
||||
class="badge bg-green-lt"><?= h(t('devices.this_device')) ?></span><?php endif; ?><span
|
||||
class="badge <?= $active ? 'bg-blue-lt' : 'bg-secondary-lt' ?>"><?= h(t($active ? 'devices.active' : 'devices.inactive')) ?></span>
|
||||
</div>
|
||||
<div class="small text-muted mt-2">
|
||||
<?= h(t('devices.created', ['date' => date('d/m/Y H:i', strtotime($device['created_at']))])) ?><br><?= h(t('devices.last_used', ['date' => date('d/m/Y H:i', strtotime($device['last_used_at']))])) ?><br><?= h(t('devices.expires', ['date' => date('d/m/Y', strtotime($device['expires_at']))])) ?>
|
||||
</div>
|
||||
</div><?php if ($active): ?><form method="post" action="/account/devices/revoke"><input type="hidden"
|
||||
name="id" value="<?= (int)$device['id'] ?>"><button class="btn btn-sm btn-outline-danger"
|
||||
title="<?= h(t('devices.revoke')) ?>">×</button></form><?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div><?php endforeach; ?><?php if (!$devices): ?><div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-body text-center text-muted py-5"><?= h(t('devices.none')) ?></div>
|
||||
</div>
|
||||
</div><?php endif; ?></div>
|
||||
315
app/Views/accounting.php
Normal file
315
app/Views/accounting.php
Normal file
|
|
@ -0,0 +1,315 @@
|
|||
<style>
|
||||
.accounting-dashboard-structures>.card>.table-responsive {
|
||||
padding: .75rem 1rem 1rem
|
||||
}
|
||||
|
||||
</style>
|
||||
<?php $sum = (int)($accountingTotals['billed'] ?? array_sum(array_column($invoices, 'total_ttc_cents')));
|
||||
$due = (int)($accountingTotals['due'] ?? array_sum(array_column($invoices, 'amount_due_cents')));
|
||||
$paid = $sum - $due;
|
||||
$money = static fn (int$c) => number_format($c / 100, 2, ',', ' ') . ' €';
|
||||
$statusLabels = ['paid' => 'Payée','to_pay' => 'À payer','partial' => 'Partiellement payée','disputed' => 'Contestée','cancelled' => 'Annulée','credit' => 'Avoir'];
|
||||
$methodLabels = ['transfer' => 'Virement','direct_debit' => 'Prélèvement','card' => 'Carte','check' => 'Chèque','cash' => 'Espèces','other' => 'Autre','unknown' => 'Non renseigné'];?>
|
||||
<?php $accountingFilters = $accountingFilters ?? ['q' => '','vendor_id' => 0,'status' => '','type' => '','payment_method' => ''];
|
||||
$filterQuery = array_merge(['year' => (int)$year], $accountingFilters);?>
|
||||
<div class="d-flex flex-wrap justify-content-between align-items-center gap-2 mb-3 d-print-none">
|
||||
<div class="text-muted">Pilotage des factures, paiements et partenaires</div>
|
||||
<div class="btn-list"><a class="btn btn-outline-primary"
|
||||
href="/accounting/export.csv?<?= h(http_build_query($filterQuery)) ?>">⬇ Export CSV</a><button
|
||||
class="btn btn-outline-secondary" type="button" onclick="window.print()">🖨 Imprimer / PDF</button></div>
|
||||
</div>
|
||||
<form method="get" action="/accounting" class="card mb-3 d-print-none">
|
||||
<div class="card-body">
|
||||
<div class="row g-2 align-items-end">
|
||||
<div class="col-md-4"><label class="form-label">Rechercher</label><input class="form-control" name="q"
|
||||
value="<?= h($accountingFilters['q']) ?>" placeholder="Référence, structure ou notes…"></div>
|
||||
<div class="col-md-2"><label class="form-label">Année</label><select class="form-select" name="year">
|
||||
<option value="0">Toutes les années</option><?php foreach ($years as $y):?><option
|
||||
value="<?= h($y) ?>" <?= (int)$y === $year ? 'selected' : ''?>><?= h($y) ?></option>
|
||||
<?php endforeach?>
|
||||
</select></div>
|
||||
<div class="col-md-3"><label class="form-label">Structure</label><select class="form-select"
|
||||
name="vendor_id">
|
||||
<option value="0">Toutes les structures</option><?php foreach ($vendors as $v):?><option
|
||||
value="<?= (int)$v['id'] ?>"
|
||||
<?= (int)$v['id'] === (int)$accountingFilters['vendor_id'] ? 'selected' : ''?>>
|
||||
<?= h($v['name']) ?></option><?php endforeach?>
|
||||
</select></div>
|
||||
<div class="col-md-3"><label class="form-label">Statut</label><select class="form-select" name="status">
|
||||
<option value="">Tous les statuts</option><?php foreach ($statusLabels as $k => $label):?><option
|
||||
value="<?= h($k) ?>" <?= $accountingFilters['status'] === $k ? 'selected' : ''?>>
|
||||
<?= h($label) ?></option><?php endforeach?>
|
||||
</select></div>
|
||||
<div class="col-md-3"><label class="form-label">Type de pièce</label><select class="form-select"
|
||||
name="type">
|
||||
<option value="">Tous les types</option>
|
||||
<?php foreach (['invoice' => 'Facture','credit' => 'Avoir','adjustment' => 'Régularisation'] as $k => $label):?>
|
||||
<option value="<?= h($k) ?>" <?= $accountingFilters['type'] === $k ? 'selected' : ''?>>
|
||||
<?= h($label) ?></option><?php endforeach?>
|
||||
</select></div>
|
||||
<div class="col-md-3"><label class="form-label">Règlement</label><select class="form-select"
|
||||
name="payment_method">
|
||||
<option value="">Tous les règlements</option><?php foreach ($methodLabels as $k => $label):?><option
|
||||
value="<?= h($k) ?>" <?= $accountingFilters['payment_method'] === $k ? 'selected' : ''?>>
|
||||
<?= h($label) ?></option><?php endforeach?>
|
||||
</select></div>
|
||||
<div class="col-md-6">
|
||||
<div class="btn-list justify-content-end"><a class="btn btn-outline-secondary"
|
||||
href="/accounting?year=0">Réinitialiser</a><button class="btn btn-primary">Filtrer</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-sm-6 col-xl-3">
|
||||
<div class="card card-sm h-100">
|
||||
<div class="card-body">
|
||||
<div class="text-muted">Payé net</div>
|
||||
<div class="h2 mb-0 text-green"><?= $money($paid) ?></div>
|
||||
<div class="small text-muted">
|
||||
<?= $sum ? number_format(max(0, $paid / $sum * 100), 1, ',', ' ') . ' % du facturé' : '—' ?></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-xl-3">
|
||||
<div class="card card-sm h-100">
|
||||
<div class="card-body">
|
||||
<div class="text-muted">À régler</div>
|
||||
<div class="h2 mb-0 text-orange"><?= $money($due) ?></div>
|
||||
<div class="small text-muted"><?= (int)($accountingTotals['to_pay'] ?? 0) ?> pièce(s)</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-xl-3">
|
||||
<div class="card card-sm h-100">
|
||||
<div class="card-body">
|
||||
<div class="text-muted">Échéances dépassées</div>
|
||||
<div class="h2 mb-0 <?= !empty($accountingTotals['overdue']) ? 'text-red' : '' ?>">
|
||||
<?= (int)($accountingTotals['overdue'] ?? 0) ?></div>
|
||||
<div class="small text-muted">Avec une date d’échéance connue</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-xl-3">
|
||||
<div class="card card-sm h-100">
|
||||
<div class="card-body">
|
||||
<div class="text-muted">Taux restant</div>
|
||||
<div class="h2 mb-0"><?= $sum ? number_format(max(0, $due / $sum * 100), 1, ',', ' ') . ' %' : '—' ?>
|
||||
</div>
|
||||
<div class="small text-muted">Sur la période choisie</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-xl-7">
|
||||
<div class="card h-100">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Coût et paiements par structure</h3>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-vcenter mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Structure</th>
|
||||
<th class="text-end">Facturé</th>
|
||||
<th class="text-end">Payé</th>
|
||||
<th class="text-end">Reste dû</th>
|
||||
<th class="text-end">Animaux</th>
|
||||
<th class="text-end">Coût/animal</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody><?php foreach (($vendorStats ?? []) as $v):$vendorPaid = $v['billed'] - $v['due'];?><tr>
|
||||
<td><?php if ($v['id']):?><a class="fw-semibold"
|
||||
href="/directory/view?id=<?= (int)$v['id'] ?>&year=<?= $year ?: 'all' ?>"><?= h($v['label']) ?></a><?php else:?><?= h($v['label']) ?><?php endif?>
|
||||
<div class="small text-muted">
|
||||
<?= (int)$v['paid_documents'] ?>/<?= (int)$v['documents'] ?> pièce(s) soldée(s)
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-end"><?= $money((int)$v['billed']) ?></td>
|
||||
<td class="text-end text-green"><?= $money((int)$vendorPaid) ?></td>
|
||||
<td class="text-end <?= $v['due'] ? 'text-orange fw-bold' : '' ?>">
|
||||
<?= $money((int)$v['due']) ?></td>
|
||||
<td class="text-end"><?= (int)$v['animals'] ?: '—' ?></td>
|
||||
<td class="text-end">
|
||||
<?= $v['animals'] ? $money((int)round($v['animal_cost'] / $v['animals'])) : '—' ?></td>
|
||||
</tr><?php endforeach?></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="card-footer small text-muted">Le coût par animal utilise les actes déjà rapprochés des fiches
|
||||
animales ; il peut différer du total des factures.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xl-5">
|
||||
<div class="card h-100">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Évolution annuelle des paiements</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div style="height:250px"><canvas id="accountingAnnualChart"
|
||||
data-series="<?= h(json_encode($annualStats ?? [], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)) ?>"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-md-6">
|
||||
<div class="card h-100">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">État des pièces</h3>
|
||||
</div>
|
||||
<div class="card-body d-flex flex-wrap gap-2"><?php foreach (($statusStats ?? []) as $key => $count):?><span
|
||||
class="badge <?= $key === 'paid' ? 'bg-green-lt' : ($key === 'to_pay' ? 'bg-orange-lt' : 'bg-secondary-lt') ?> p-2"><?= h($statusLabels[$key] ?? $key) ?>
|
||||
· <?= (int)$count ?></span><?php endforeach?></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="card h-100">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Modes de règlement</h3>
|
||||
</div>
|
||||
<div class="card-body d-flex flex-wrap gap-2"><?php foreach (($methodStats ?? []) as $key => $count):?><span
|
||||
class="badge bg-blue-lt p-2"><?= h($methodLabels[$key] ?? $key) ?> ·
|
||||
<?= (int)$count ?></span><?php endforeach?></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php $credits = array_sum(array_map(static fn ($r) => $r['document_type'] === 'credit' ? (int)$r['total_ttc_cents'] : 0, $invoices));?>
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-md-4">
|
||||
<div class="card card-sm">
|
||||
<div class="card-body">
|
||||
<div class="text-muted">Facturé TTC net</div>
|
||||
<div class="h2 mb-0"><?= $money($sum) ?></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card card-sm">
|
||||
<div class="card-body">
|
||||
<div class="text-muted">Avoirs inclus</div>
|
||||
<div class="h2 mb-0 text-green"><?= $money($credits) ?></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card card-sm">
|
||||
<div class="card-body">
|
||||
<div class="text-muted">Pièces comptables</div>
|
||||
<div class="h2 mb-0"><?= count($invoices) ?></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">🧾 Nouvelle facture ou avoir</h3>
|
||||
</div>
|
||||
<form method="post" action="/accounting/save" enctype="multipart/form-data"><input type="hidden" name="csrf"
|
||||
value="<?= h(Auth::csrf()) ?>">
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-3"><label class="form-label">Référence *</label><input class="form-control"
|
||||
name="reference" required></div>
|
||||
<div class="col-md-3"><label class="form-label">Fournisseur / structure</label><select
|
||||
class="form-select" name="vendor_contact_id">
|
||||
<option value="">—</option><?php foreach ($vendors as $v):?><option
|
||||
value="<?= (int)$v['id'] ?>"><?= h($v['name']) ?></option><?php endforeach?>
|
||||
</select></div>
|
||||
<div class="col-md-3"><label class="form-label">Date *</label><input class="form-control" type="date"
|
||||
name="invoice_date" value="<?= h(date('Y-m-d')) ?>" required></div>
|
||||
<div class="col-md-3"><label class="form-label">Échéance</label><input class="form-control" type="date"
|
||||
name="due_date"></div>
|
||||
<div class="col-md-2"><label class="form-label">Type</label><select class="form-select"
|
||||
name="document_type">
|
||||
<option value="invoice">Facture</option>
|
||||
<option value="credit">Avoir</option>
|
||||
<option value="adjustment">Régularisation</option>
|
||||
</select></div>
|
||||
<div class="col-md-2"><label class="form-label">Montant HT</label><input class="form-control"
|
||||
name="total_ht" inputmode="decimal"></div>
|
||||
<div class="col-md-2"><label class="form-label">Montant TTC *</label><input class="form-control"
|
||||
name="total_ttc" inputmode="decimal" required></div>
|
||||
<div class="col-md-2"><label class="form-label">Statut</label><select class="form-select" name="status">
|
||||
<option value="to_pay">À payer</option>
|
||||
<option value="paid">Payée</option>
|
||||
<option value="partial">Partiellement payée</option>
|
||||
<option value="disputed">Contestée</option>
|
||||
<option value="credit">Avoir</option>
|
||||
<option value="cancelled">Annulée</option>
|
||||
</select></div>
|
||||
<div class="col-md-2"><label class="form-label">Règlement</label><select class="form-select"
|
||||
name="payment_method">
|
||||
<option value="">—</option>
|
||||
<option value="transfer">Virement</option>
|
||||
<option value="direct_debit">Prélèvement</option>
|
||||
<option value="card">Carte</option>
|
||||
<option value="check">Chèque</option>
|
||||
<option value="cash">Espèces</option>
|
||||
<option value="other">Autre</option>
|
||||
</select></div>
|
||||
<div class="col-md-2"><label class="form-label">Reste dû</label><input class="form-control"
|
||||
name="amount_due" inputmode="decimal"></div>
|
||||
<div class="col-md-6"><label class="form-label">Facture (PDF ou image, 20 Mo max.)</label><input
|
||||
class="form-control" type="file" name="document"
|
||||
accept="application/pdf,image/jpeg,image/png,image/webp"></div>
|
||||
<div class="col-md-6"><label class="form-label">Notes</label><input class="form-control" name="notes">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer text-end"><button class="btn btn-primary">Ajouter</button></div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header d-flex align-items-center">
|
||||
<div>
|
||||
<h3 class="card-title mb-0">Journal des factures</h3>
|
||||
<div class="text-muted small">
|
||||
<?= $year ? 'Période : ' . (int)$year : 'Toutes les années · historique complet' ?></div>
|
||||
</div>
|
||||
<form class="ms-auto"><select class="form-select" name="year" onchange="this.form.submit()">
|
||||
<option value="0">Toutes les années</option><?php foreach ($years as $y):?><option value="<?= h($y) ?>"
|
||||
<?= (int)$y === $year ? 'selected' : ''?>><?= h($y) ?></option><?php endforeach?>
|
||||
</select></form>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-vcenter">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Référence</th>
|
||||
<th>Type</th>
|
||||
<th>Structure</th>
|
||||
<th>HT</th>
|
||||
<th>TTC</th>
|
||||
<th>Statut</th>
|
||||
<th>Règlement</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody><?php foreach ($invoices as $r):?><tr>
|
||||
<td><?= h(date('d/m/Y', strtotime($r['invoice_date']))) ?></td>
|
||||
<td><?= h($r['reference']) ?></td>
|
||||
<td><span
|
||||
class="badge <?= $r['document_type'] === 'credit' ? 'bg-green-lt' : 'bg-blue-lt' ?>"><?= h(['invoice' => 'Facture','credit' => 'Avoir','adjustment' => 'Régularisation'][$r['document_type']] ?? $r['document_type']) ?></span>
|
||||
</td>
|
||||
<td><?= h($r['vendor_name'] ?? '—') ?></td>
|
||||
<td><?= $r['total_ht_cents'] === null ? '—' : number_format($r['total_ht_cents'] / 100, 2, ',', ' ') . ' €' ?>
|
||||
</td>
|
||||
<td class="fw-bold <?= $r['total_ttc_cents'] < 0 ? 'text-green' : '' ?>">
|
||||
<?= number_format($r['total_ttc_cents'] / 100, 2, ',', ' ') ?> €</td>
|
||||
<td><span
|
||||
class="badge <?= $r['status'] === 'paid' ? 'bg-green-lt' : ($r['status'] === 'to_pay' ? 'bg-orange-lt' : 'bg-secondary-lt') ?>"><?= h(['paid' => 'Payée','to_pay' => 'À payer','partial' => 'Partielle','disputed' => 'Contestée','cancelled' => 'Annulée','credit' => 'Avoir'][$r['status']] ?? $r['status']) ?></span>
|
||||
</td>
|
||||
<td><?= h(['transfer' => 'Virement','direct_debit' => 'Prélèvement','card' => 'Carte','check' => 'Chèque','cash' => 'Espèces','other' => 'Autre'][$r['payment_method']] ?? '—') ?>
|
||||
</td>
|
||||
<td><?php if ($r['stored_filename']):?><a class="btn btn-sm btn-outline-primary" target="_blank"
|
||||
href="/accounting/document?id=<?= (int)$r['id'] ?>">Voir</a><?php endif?></td>
|
||||
</tr><?php endforeach?><?php if (!$invoices):?><tr>
|
||||
<td colspan="9" class="text-center text-muted py-4">Aucune facture pour cette période.</td>
|
||||
</tr><?php endif?></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
71
app/Views/accounting_edit.php
Normal file
71
app/Views/accounting_edit.php
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
<div class="row justify-content-center">
|
||||
<div class="col-xl-9">
|
||||
<div class="card">
|
||||
<form method="post" action="/accounting/update" enctype="multipart/form-data"><input type="hidden"
|
||||
name="csrf" value="<?= h(Auth::csrf()) ?>"><input type="hidden" name="id"
|
||||
value="<?= (int)$invoice['id'] ?>">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Modifier <?= h($invoice['reference']) ?></h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4"><label class="form-label">Référence *</label><input class="form-control"
|
||||
name="reference" value="<?= h($invoice['reference']) ?>" required></div>
|
||||
<div class="col-md-4"><label class="form-label">Structure</label><select class="form-select"
|
||||
name="vendor_contact_id">
|
||||
<option value="">—</option><?php foreach ($vendors as $v):?><option
|
||||
value="<?= (int)$v['id'] ?>"
|
||||
<?= (int)$v['id'] === (int)$invoice['vendor_contact_id'] ? 'selected' : ''?>>
|
||||
<?= h($v['name']) ?></option><?php endforeach?>
|
||||
</select></div>
|
||||
<div class="col-md-2"><label class="form-label">Date *</label><input class="form-control"
|
||||
type="date" name="invoice_date" value="<?= h($invoice['invoice_date']) ?>" required>
|
||||
</div>
|
||||
<div class="col-md-2"><label class="form-label">Échéance</label><input class="form-control"
|
||||
type="date" name="due_date" value="<?= h($invoice['due_date'] ?? '') ?>"></div>
|
||||
<div class="col-md-3"><label class="form-label">Type</label><select class="form-select"
|
||||
name="document_type"><?php foreach (['invoice' => 'Facture','credit' => 'Avoir','adjustment' => 'Régularisation'] as $k => $v):?>
|
||||
<option value="<?= $k ?>" <?= $invoice['document_type'] === $k ? 'selected' : ''?>>
|
||||
<?= $v ?></option><?php endforeach?></select></div>
|
||||
<div class="col-md-3"><label class="form-label">Montant HT</label><input class="form-control"
|
||||
name="total_ht"
|
||||
value="<?= $invoice['total_ht_cents'] === null ? '' : h(number_format($invoice['total_ht_cents'] / 100, 2, ',', '')) ?>">
|
||||
</div>
|
||||
<div class="col-md-3"><label class="form-label">Montant TTC *</label><input class="form-control"
|
||||
name="total_ttc"
|
||||
value="<?= h(number_format($invoice['total_ttc_cents'] / 100, 2, ',', '')) ?>" required>
|
||||
</div>
|
||||
<div class="col-md-3"><label class="form-label">Reste dû</label><input class="form-control"
|
||||
name="amount_due"
|
||||
value="<?= $invoice['amount_due_cents'] === null ? '' : h(number_format($invoice['amount_due_cents'] / 100, 2, ',', '')) ?>">
|
||||
</div>
|
||||
<div class="col-md-4"><label class="form-label">Statut</label><select class="form-select"
|
||||
name="status"><?php foreach (['to_pay' => 'À payer','paid' => 'Payée','partial' => 'Partiellement payée','disputed' => 'Contestée','cancelled' => 'Annulée','credit' => 'Avoir'] as $k => $v):?>
|
||||
<option value="<?= $k ?>" <?= $invoice['status'] === $k ? 'selected' : ''?>><?= $v ?>
|
||||
</option><?php endforeach?></select></div>
|
||||
<div class="col-md-4"><label class="form-label">Règlement</label><select class="form-select"
|
||||
name="payment_method">
|
||||
<option value="">—</option>
|
||||
<?php foreach (['transfer' => 'Virement','direct_debit' => 'Prélèvement','card' => 'Carte','check' => 'Chèque','cash' => 'Espèces','other' => 'Autre'] as $k => $v):?>
|
||||
<option value="<?= $k ?>" <?= $invoice['payment_method'] === $k ? 'selected' : ''?>>
|
||||
<?= $v ?></option><?php endforeach?>
|
||||
</select></div>
|
||||
<div class="col-md-4"><label class="form-label">Remplacer le justificatif</label><input
|
||||
class="form-control" type="file" name="document"
|
||||
accept="application/pdf,image/jpeg,image/png,image/webp"></div>
|
||||
<div class="col-12"><label class="form-label">Notes</label><textarea class="form-control"
|
||||
name="notes" rows="3"><?= h($invoice['notes'] ?? '') ?></textarea></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer d-flex justify-content-between"><a class="btn btn-link"
|
||||
href="/accounting?year=0">Annuler</a><button class="btn btn-primary">Enregistrer les
|
||||
modifications</button></div>
|
||||
</form>
|
||||
</div>
|
||||
<form method="post" action="/accounting/delete" class="mt-3 text-end"
|
||||
onsubmit="return confirm('Mettre cette facture à la corbeille ? Elle ne sera plus comptée dans les totaux.');">
|
||||
<input type="hidden" name="csrf" value="<?= h(Auth::csrf()) ?>"><input type="hidden" name="id"
|
||||
value="<?= (int)$invoice['id'] ?>"><button class="btn btn-outline-danger">Mettre à la corbeille</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
37
app/Views/admin_exports.php
Normal file
37
app/Views/admin_exports.php
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
<div class="d-flex flex-wrap align-items-center justify-content-between gap-2 mb-3">
|
||||
<div>
|
||||
<h2 class="mb-1"><?= h(t('admin.export_data')) ?></h2>
|
||||
<div class="text-muted"><?= h(t('admin.exports_help')) ?></div>
|
||||
</div><a class="btn btn-outline-secondary" href="/admin/register">← <?= h(t('admin.back')) ?></a>
|
||||
</div>
|
||||
<div class="alert alert-warning"><strong><?= h(t('admin.sensitive')) ?></strong> <?= h(t('admin.sensitive_help')) ?>
|
||||
</div>
|
||||
<div class="card mb-4 overflow-hidden">
|
||||
<div class="card-status-top bg-primary"></div>
|
||||
<div class="card-body d-flex flex-wrap align-items-center gap-3">
|
||||
<div class="me-auto">
|
||||
<h3 class="mb-1"><?= h(t('admin.portability')) ?></h3>
|
||||
<div class="text-muted"><?= h(t('admin.portability_help')) ?></div>
|
||||
</div><a class="btn btn-primary" href="/admin/exports/download?type=all">⬇ <?= h(t('admin.download_all')) ?></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row row-cards"><?php foreach ($datasets as $key => $dataset): ?><div class="col-sm-6 col-lg-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-start gap-3"><span class="avatar bg-blue-lt">CSV</span>
|
||||
<div>
|
||||
<h3 class="card-title mb-1"><?= h($dataset['label']) ?></h3>
|
||||
<div class="text-muted small"><?= h($dataset['filename']) ?></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer"><a class="btn btn-outline-primary w-100"
|
||||
href="/admin/exports/download?type=<?= h($key) ?>"><?= h(t('common.download')) ?></a></div>
|
||||
</div>
|
||||
</div><?php endforeach; ?></div>
|
||||
<div class="card mt-4">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title"><?= h(t('admin.compatibility')) ?></h3>
|
||||
<p class="text-muted mb-0"><?= h(t('admin.compatibility_help')) ?></p>
|
||||
</div>
|
||||
</div>
|
||||
314
app/Views/admin_grants.php
Normal file
314
app/Views/admin_grants.php
Normal file
|
|
@ -0,0 +1,314 @@
|
|||
<?php $money = static fn ($v) => number_format((float)$v, 2, ',', ' ') . ' €';
|
||||
$date = static fn ($v) => $v ? (new DateTimeImmutable($v))->format('d/m/Y') : '—'; ?>
|
||||
<?php if ($saved): ?><div class="alert alert-success"><?= h(t('grant.saved')) ?></div><?php endif; ?>
|
||||
<?php if ($error): ?><div class="alert alert-danger"><?= h(t('grant.save_error')) ?></div><?php endif; ?>
|
||||
<div class="card mb-4">
|
||||
<div class="card-body d-flex flex-wrap align-items-end gap-3">
|
||||
<form method="get" action="/admin/grants" class="d-flex align-items-end gap-2">
|
||||
<div><label class="form-label"><?= h(t('grant.file_year')) ?></label><select class="form-select"
|
||||
name="year"><?php for ($y = (int)date('Y') + 1;$y >= (int)date('Y') - 10;$y--): ?><option
|
||||
value="<?= $y ?>" <?= $y === $year ? 'selected' : '' ?>><?= $y ?></option>
|
||||
<?php endfor; ?></select></div><button class="btn btn-primary"><?= h(t('common.show')) ?></button>
|
||||
</form>
|
||||
<div class="ms-auto d-flex gap-2"><a class="btn btn-outline-secondary"
|
||||
href="/admin/register?year=<?= $year ?>"><?= h(t('grant.register', ['year' => $year])) ?></a><a
|
||||
class="btn btn-dark" target="_blank" href="/admin/grants/print?year=<?= $year ?>">🖨
|
||||
<?= h(t('grant.print_report')) ?></a></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row row-cards mb-4">
|
||||
<?php foreach ([[t('grant.entries'),$stats['entries'],'green'],[t('grant.exits'),$stats['exits'],'red'],[t('grant.adoptions'),$stats['adoptions'],'blue'],[t('grant.deaths'),$stats['deaths'],'dark'],[t('grant.births'),$stats['births'],'purple'],[t('grant.vaccinations'),$stats['vaccinations'],'cyan'],[t('grant.treatments'),$stats['treatments'],'orange'],[t('grant.current'),$stats['current'],'azure']] as [$label,$value,$color]): ?>
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="card h-100">
|
||||
<div class="card-status-top bg-<?= $color ?>"></div>
|
||||
<div class="card-body">
|
||||
<div class="text-muted small text-uppercase"><?= h($label) ?></div>
|
||||
<div class="display-6 fw-bold"><?= (int)$value ?></div>
|
||||
</div>
|
||||
</div>
|
||||
</div><?php endforeach; ?>
|
||||
</div>
|
||||
<div class="alert alert-info"><?= h(t('grant.indicators_help', ['year' => $year])) ?> <?= h(t('grant.avg_adoption')) ?>
|
||||
: <strong><?= $stats['avg'] > 0 ? h(t('common.days', ['count' => round($stats['avg'])])) : '—' ?></strong>.</div>
|
||||
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<div>
|
||||
<h3 class="card-title"><?= h(t('grant.comparison', ['year' => $year - 1])) ?></h3>
|
||||
<div class="text-muted small"><?= h(t('grant.comparison_help')) ?></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-vcenter mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><?= h(t('grant.indicator')) ?></th>
|
||||
<th class="text-end"><?= $year - 1 ?></th>
|
||||
<th class="text-end"><?= $year ?></th>
|
||||
<th class="text-end"><?= h(t('grant.evolution')) ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach (['entries' => t('grant.entries'),'exits' => t('grant.exits'),'adoptions' => t('grant.adoptions'),'deaths' => t('grant.deaths'),'births' => t('grant.births'),'vaccinations' => t('grant.vaccinations'),'treatments' => t('grant.treatments'),'avg' => t('grant.avg_adoption')] as $key => $label): $c = $comparison[$key]; ?>
|
||||
<tr>
|
||||
<td><?= h($label) ?></td>
|
||||
<td class="text-end"><?= (int)round($c['previous']) ?></td>
|
||||
<td class="text-end fw-bold"><?= (int)round($c['current']) ?></td>
|
||||
<td class="text-end"><span
|
||||
class="badge <?= $c['difference'] > 0 ? 'bg-blue-lt' : ($c['difference'] < 0 ? 'bg-orange-lt' : 'bg-secondary-lt') ?>"><?= $c['difference'] > 0 ? '+' : '' ?><?= (int)round($c['difference']) ?><?= $c['percent'] !== null ? ' · ' . ($c['percent'] > 0 ? '+' : '') . number_format($c['percent'], 1, ',', ' ') . ' %' : '' ?></span>
|
||||
</td>
|
||||
</tr><?php endforeach; ?></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
$detailGroups = [
|
||||
[t('grant.sex_entries'),$details['sex'],[]],
|
||||
[t('grant.age_entry'),$details['ages'],[]],
|
||||
[t('grant.origins'),$details['origins'],[]],
|
||||
[t('grant.death_causes'),$details['deathCauses'],array_combine(array_keys(DeathController::CAUSES), array_map(static fn ($key) => DeathController::causeLabel($key), array_keys(DeathController::CAUSES)))],
|
||||
[t('grant.adoption_stay'),$details['stays'],[]],
|
||||
];
|
||||
?>
|
||||
<div class="row row-cards mb-4">
|
||||
<?php foreach ($detailGroups as [$groupTitle,$items,$labels]): $maximum = $items ? max($items) : 0; ?><div
|
||||
class="col-md-6">
|
||||
<div class="card h-100">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title"><?= h($groupTitle) ?></h3>
|
||||
</div>
|
||||
<div class="card-body"><?php if (!$items): ?><div class="text-muted">
|
||||
<?= h(t('grant.no_data_year', ['year' => $year])) ?></div><?php else: ?><div class="vstack gap-3"><?php foreach ($items as $label => $total): $display = $labels[$label] ?? $label;
|
||||
$width = $maximum > 0 ? max(5, round($total / $maximum * 100)) : 0; ?><div>
|
||||
<div class="d-flex justify-content-between gap-3 mb-1">
|
||||
<span><?= h($display) ?></span><strong><?= (int)$total ?></strong></div>
|
||||
<div class="progress progress-sm">
|
||||
<div class="progress-bar bg-primary" style="width:<?= $width ?>%" role="progressbar"
|
||||
aria-label="<?= h($display) ?>"></div>
|
||||
</div>
|
||||
</div><?php endforeach; ?></div><?php endif; ?></div>
|
||||
</div>
|
||||
</div><?php endforeach; ?></div>
|
||||
|
||||
<form method="post" action="/admin/grants/save" class="card mb-4"><input type="hidden" name="csrf"
|
||||
value="<?= h(Auth::csrf()) ?>"><input type="hidden" name="year" value="<?= $year ?>">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title"><?= h(t('grant.annual_report', ['year' => $year])) ?></h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-sm-6 col-lg-3"><label class="form-label"><?= h(t('grant.members')) ?></label><input
|
||||
class="form-control" type="number" min="0" name="members_count"
|
||||
value="<?= (int)$annual['members_count'] ?>"></div>
|
||||
<div class="col-sm-6 col-lg-3"><label class="form-label"><?= h(t('grant.volunteers')) ?></label><input
|
||||
class="form-control" type="number" min="0" name="volunteers_count"
|
||||
value="<?= (int)$annual['volunteers_count'] ?>"></div>
|
||||
<div class="col-sm-6 col-lg-3"><label class="form-label"><?= h(t('grant.employees')) ?></label><input
|
||||
class="form-control" type="number" min="0" name="employees_count"
|
||||
value="<?= (int)$annual['employees_count'] ?>"></div>
|
||||
<div class="col-sm-6 col-lg-3"><label class="form-label"><?= h(t('grant.employee_fte')) ?></label><input
|
||||
class="form-control" type="number" min="0" step="0.01" name="employees_fte"
|
||||
value="<?= h($annual['employees_fte']) ?>"></div>
|
||||
<div class="col-md-4"><label class="form-label"><?= h(t('grant.last_ag')) ?></label><input
|
||||
class="form-control" type="date" lang="fr" name="last_ag_date"
|
||||
value="<?= h($annual['last_ag_date']) ?>"></div>
|
||||
<div class="col-md-8"><label class="form-label"><?= h(t('grant.beneficiaries')) ?></label><input
|
||||
class="form-control" name="beneficiaries" value="<?= h($annual['beneficiaries']) ?>"
|
||||
placeholder="<?= h(t('grant.beneficiaries_placeholder')) ?>"></div>
|
||||
<?php foreach (['annual_report' => t('grant.activity_summary'),'completed_actions' => t('grant.completed_actions'),'planned_actions' => t('grant.planned_actions'),'officers_notes' => t('grant.officers'),'in_kind_support' => t('grant.in_kind')] as $field => $label): ?>
|
||||
<div class="col-md-6"><label class="form-label"><?= h($label) ?></label><textarea class="form-control"
|
||||
rows="4" name="<?= $field ?>"><?= h($annual[$field]) ?></textarea></div><?php endforeach; ?>
|
||||
</div>
|
||||
<hr>
|
||||
<h3 class="mb-3"><?= h(t('grant.financial_summary')) ?></h3>
|
||||
<div class="row g-3">
|
||||
<?php foreach (['actual_expenses' => t('grant.actual_expenses'),'actual_income' => t('grant.actual_income'),'forecast_expenses' => t('grant.forecast_expenses'),'forecast_income' => t('grant.forecast_income'),'treasury_amount' => t('grant.treasury'),'voluntary_contributions' => t('grant.voluntary')] as $field => $label): ?>
|
||||
<div class="col-sm-6 col-lg-4"><label class="form-label"><?= h($label) ?></label>
|
||||
<div class="input-group"><input class="form-control" inputmode="decimal" name="<?= $field ?>"
|
||||
value="<?= h((string)$annual[$field]) ?>"><span class="input-group-text">€</span></div>
|
||||
</div><?php endforeach; ?></div>
|
||||
</div>
|
||||
<div class="card-footer text-end"><button class="btn btn-primary"><?= h(t('grant.save_report')) ?></button></div>
|
||||
</form>
|
||||
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title"><?= h(t('grant.applications')) ?></h3>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-vcenter">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><?= h(t('grant.organization')) ?></th>
|
||||
<th><?= h(t('grant.deadline')) ?></th>
|
||||
<th><?= h(t('grant.requested')) ?></th>
|
||||
<th><?= h(t('grant.previous_aid')) ?></th>
|
||||
<th><?= h(t('grant.decision')) ?></th>
|
||||
<th>Notes</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody><?php if (!$applications): ?><tr>
|
||||
<td colspan="6" class="text-muted text-center py-4">
|
||||
<?= h(t('grant.no_application', ['year' => $year])) ?></td>
|
||||
</tr><?php endif; ?><?php foreach ($applications as $a): ?><tr>
|
||||
<td class="fw-bold"><?= h($a['organization_name']) ?></td>
|
||||
<td><?= $date($a['deadline']) ?></td>
|
||||
<td><?= $money($a['requested_amount']) ?></td>
|
||||
<td><?= $money($a['previous_amount']) ?></td>
|
||||
<td>
|
||||
<form method="post" action="/admin/grants/application" class="d-flex flex-wrap gap-1"><input
|
||||
type="hidden" name="csrf" value="<?= h(Auth::csrf()) ?>"><input type="hidden"
|
||||
name="year" value="<?= $year ?>"><input type="hidden" name="application_id"
|
||||
value="<?= (int)$a['id'] ?>"><select class="form-select form-select-sm" name="status"
|
||||
style="width:auto"><?php foreach (GrantService::STATUSES as $key => $label): ?><option
|
||||
value="<?= h($key) ?>" <?= $a['status'] === $key ? 'selected' : '' ?>>
|
||||
<?= h(GrantService::statusLabel($key)) ?></option><?php endforeach; ?></select>
|
||||
<div class="input-group input-group-sm" style="width:130px"><input class="form-control"
|
||||
inputmode="decimal" name="awarded_amount"
|
||||
value="<?= h((string)($a['awarded_amount'] ?? '')) ?>"
|
||||
placeholder="<?= h(t('grant.awarded')) ?>"><span class="input-group-text">€</span>
|
||||
</div><button class="btn btn-outline-primary btn-sm">OK</button>
|
||||
</form>
|
||||
</td>
|
||||
<td class="text-muted"><?= nl2br(h($a['notes'])) ?></td>
|
||||
</tr><?php endforeach; ?></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="card-body border-top">
|
||||
<form method="post" action="/admin/grants/application" class="row g-3"><input type="hidden" name="csrf"
|
||||
value="<?= h(Auth::csrf()) ?>"><input type="hidden" name="year" value="<?= $year ?>">
|
||||
<div class="col-md-4"><label class="form-label"><?= h(t('grant.organization_required')) ?></label><input
|
||||
required class="form-control" name="organization_name"></div>
|
||||
<div class="col-md-2"><label class="form-label"><?= h(t('grant.deadline')) ?></label><input
|
||||
class="form-control" type="date" lang="fr" name="deadline"></div>
|
||||
<div class="col-md-2"><label class="form-label"><?= h(t('grant.requested_amount')) ?></label><input
|
||||
class="form-control" inputmode="decimal" name="requested_amount"></div>
|
||||
<div class="col-md-2"><label class="form-label"><?= h(t('grant.previous_aid')) ?></label><input
|
||||
class="form-control" inputmode="decimal" name="previous_amount"></div>
|
||||
<div class="col-md-2"><label class="form-label"><?= h(t('grant.state')) ?></label><select
|
||||
class="form-select" name="status"><?php foreach (GrantService::STATUSES as $key => $label): ?>
|
||||
<option value="<?= h($key) ?>"><?= h($label) ?></option><?php endforeach; ?></select></div>
|
||||
<div class="col-md-10"><label class="form-label"><?= h(t('grant.notes')) ?></label><input
|
||||
class="form-control" name="notes"></div>
|
||||
<div class="col-md-2 d-flex align-items-end"><button
|
||||
class="btn btn-primary w-100"><?= h(t('common.add')) ?></button></div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<div>
|
||||
<h3 class="card-title"><?= h(t('grant.checklist')) ?></h3>
|
||||
<div class="text-muted small"><?= h(t('grant.checklist_help')) ?></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row row-cards"><?php if (!$applications): ?><div class="text-muted"><?= h(t('grant.add_first')) ?>
|
||||
</div>
|
||||
<?php endif; ?><?php foreach ($applications as $application): $check = $checklist[(int)$application['id']] ?? ['items' => [],'valid' => 0,'total' => count(GrantService::REQUIRED),'complete' => false]; ?>
|
||||
<div class="col-lg-6">
|
||||
<div class="card h-100 <?= $check['complete'] ? 'border-success' : 'border-warning' ?>">
|
||||
<div class="card-status-top <?= $check['complete'] ? 'bg-green' : 'bg-orange' ?>"></div>
|
||||
<div class="card-header">
|
||||
<div class="me-auto">
|
||||
<h3 class="card-title mb-1"><?= h($application['organization_name']) ?></h3>
|
||||
<div class="text-muted small">
|
||||
<?= h(t('grant.valid_parts', ['valid' => (int)$check['valid'],'total' => (int)$check['total']])) ?>
|
||||
</div>
|
||||
</div><span
|
||||
class="badge <?= $check['complete'] ? 'bg-green-lt' : 'bg-orange-lt' ?>"><?= $check['complete'] ? t('grant.complete') : t('grant.incomplete') ?></span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-2"><?php foreach ($check['items'] as $category => $item): $state = $item['state'];
|
||||
$meta = match($state) {
|
||||
'valid' => ['✓','bg-green-lt',t('grant.valid')],'expiring' => ['⌛','bg-yellow-lt',t('grant.expiring')],'expired' => ['!','bg-red-lt',t('grant.expired')],default => ['×','bg-secondary-lt',t('grant.missing')]
|
||||
}; ?><div class="col-sm-6">
|
||||
<div class="d-flex align-items-center gap-2 py-1"><span class="badge <?= $meta[1] ?>"
|
||||
title="<?= h($meta[2]) ?>"><?= $meta[0] ?></span><span
|
||||
class="small"><?= h(GrantService::categoryLabel($category)) ?></span></div>
|
||||
</div><?php endforeach; ?></div>
|
||||
</div>
|
||||
</div>
|
||||
</div><?php endforeach; ?></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div>
|
||||
<h3 class="card-title"><?= h(t('grant.vault')) ?></h3>
|
||||
<div class="text-muted small"><?= h(t('grant.vault_help')) ?></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3 mb-4"><?php foreach ($documents as $d): ?><div class="col-md-6 col-lg-4"><a
|
||||
class="card card-link h-100" target="_blank" href="/admin/grants/document?id=<?= (int)$d['id'] ?>">
|
||||
<div class="card-body">
|
||||
<div class="d-flex gap-3"><span
|
||||
class="avatar bg-purple-lt"><?= strtoupper(pathinfo($d['original_name'], PATHINFO_EXTENSION)) ?></span>
|
||||
<div>
|
||||
<div class="fw-bold"><?= h($d['title']) ?></div>
|
||||
<div class="text-muted small"><?= h(GrantService::categoryLabel($d['category'])) ?> ·
|
||||
<?= $d['year'] ?: t('grant.permanent') ?></div><?php if ($d['application_name']): ?>
|
||||
<div class="small text-blue">
|
||||
<?= h(t('grant.folder', ['name' => $d['application_name']])) ?></div><?php endif; ?><?php if ($d['valid_until']): $expired = $d['valid_until'] < date('Y-m-d');
|
||||
$expiring = !$expired && $d['valid_until'] <= date('Y-m-d', strtotime('+30 days')); ?><div
|
||||
class="small mt-1 <?= $expired ? 'text-danger fw-bold' : ($expiring ? 'text-warning' : '') ?>">
|
||||
<?= $expired ? t('grant.expired_on') : t('grant.valid_until') ?>
|
||||
<?= $date($d['valid_until']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a></div><?php endforeach; ?><?php if (!$documents): ?><div class="text-muted">
|
||||
<?= h(t('grant.no_document')) ?></div><?php endif; ?></div>
|
||||
<form method="post" action="/admin/grants/document" enctype="multipart/form-data"
|
||||
class="row g-3 border-top pt-3"><input type="hidden" name="csrf" value="<?= h(Auth::csrf()) ?>"><input
|
||||
type="hidden" name="year" value="<?= $year ?>">
|
||||
<div class="col-md-3"><label class="form-label"><?= h(t('grant.category')) ?></label><select
|
||||
class="form-select" name="category"
|
||||
id="grantDocumentCategory"><?php foreach (GrantService::CATEGORIES as $key => $label): ?><option
|
||||
value="<?= h($key) ?>"><?= h(GrantService::categoryLabel($key)) ?></option>
|
||||
<?php endforeach; ?></select></div>
|
||||
<div class="col-md-3"><label class="form-label"><?= h(t('grant.related_folder')) ?></label><select
|
||||
class="form-select" name="application_id" id="grantDocumentApplication">
|
||||
<option value=""><?= h(t('grant.common_part')) ?></option>
|
||||
<?php foreach ($applications as $application): ?><option value="<?= (int)$application['id'] ?>">
|
||||
<?= h($application['organization_name']) ?></option><?php endforeach; ?>
|
||||
</select></div>
|
||||
<div class="col-md-3"><label class="form-label"><?= h(t('grant.document_title')) ?></label><input
|
||||
class="form-control" name="title"></div>
|
||||
<div class="col-md-3"><label class="form-label"><?= h(t('grant.pdf_odt')) ?></label><input required
|
||||
class="form-control" type="file" name="document"
|
||||
accept=".pdf,.odt,application/pdf,application/vnd.oasis.opendocument.text"></div>
|
||||
<div class="col-md-3"><label class="form-label"><?= h(t('grant.valid_until')) ?></label><input
|
||||
class="form-control" type="date" lang="fr" name="valid_until"></div>
|
||||
<div class="col-md-6 d-flex align-items-end"><label class="form-check mb-2"><input class="form-check-input"
|
||||
type="checkbox" name="global_document" value="1" id="grantDocumentGlobal"><span
|
||||
class="form-check-label"><?= h(t('grant.permanent_document')) ?></span></label></div>
|
||||
<div class="col-md-3 d-flex align-items-end"><button
|
||||
class="btn btn-primary w-100"><?= h(t('grant.add_document')) ?></button></div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const category = document.getElementById('grantDocumentCategory'),
|
||||
application = document.getElementById('grantDocumentApplication'),
|
||||
global = document.getElementById('grantDocumentGlobal');
|
||||
const sync = () => {
|
||||
const official = category?.value === 'official_form';
|
||||
if (application) application.required = official;
|
||||
if (global) {
|
||||
global.disabled = official || Boolean(application?.value);
|
||||
if (global.disabled) global.checked = false;
|
||||
}
|
||||
};
|
||||
category?.addEventListener('change', sync);
|
||||
application?.addEventListener('change', sync);
|
||||
sync();
|
||||
});
|
||||
|
||||
</script>
|
||||
197
app/Views/admin_grants_print.php
Normal file
197
app/Views/admin_grants_print.php
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
<?php $e = static fn ($v) => htmlspecialchars((string)$v, ENT_QUOTES, 'UTF-8');
|
||||
$m = static fn ($v) => number_format((float)$v, 2, ',', ' ') . ' €'; ?>
|
||||
<!doctype html>
|
||||
<html lang="<?= h(I18n::locale()) ?>">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title><?= h(t('grant.annual_report', ['year' => $year])) ?></title>
|
||||
<style>
|
||||
@page {
|
||||
size: A4;
|
||||
margin: 15mm
|
||||
}
|
||||
|
||||
body {
|
||||
font: 11pt Arial, sans-serif;
|
||||
color: #222
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #76552d;
|
||||
margin-bottom: 4px
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin-top: 24px;
|
||||
border-bottom: 2px solid #ddd;
|
||||
padding-bottom: 5px
|
||||
}
|
||||
|
||||
.meta {
|
||||
color: #666
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 8px
|
||||
}
|
||||
|
||||
.stat {
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 7px;
|
||||
padding: 10px
|
||||
}
|
||||
|
||||
.stat strong {
|
||||
font-size: 20px;
|
||||
display: block
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
border: 1px solid #ddd;
|
||||
padding: 6px;
|
||||
text-align: left;
|
||||
vertical-align: top
|
||||
}
|
||||
|
||||
th {
|
||||
background: #f3f0eb
|
||||
}
|
||||
|
||||
.text {
|
||||
white-space: pre-wrap
|
||||
}
|
||||
|
||||
.cert {
|
||||
margin-bottom: 20px;
|
||||
padding: 8px;
|
||||
background: #f3f0eb;
|
||||
font-size: 9pt
|
||||
}
|
||||
|
||||
.cols {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 14px
|
||||
}
|
||||
|
||||
.avoid {
|
||||
break-inside: avoid
|
||||
}
|
||||
|
||||
@media print {
|
||||
.no-print {
|
||||
display: none
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body><button class="no-print" onclick="window.print()"><?= $e(t('grant.print_report')) ?></button>
|
||||
<div class="cert">
|
||||
<?= $e(t('grant.generated', ['association' => AppSettings::get('association_name'),'date' => $generatedAt->format('d/m/Y'),'time' => $generatedAt->format("H\hi")])) ?>
|
||||
</div>
|
||||
<h1><?= $e(t('grant.annual_report', ['year' => $year])) ?></h1>
|
||||
<div class="meta"><?= $e(AppSettings::get('association_name')) ?> · SIRET
|
||||
<?= $e(AppSettings::get('association_siret')) ?> · RNA <?= $e(AppSettings::get('association_rna')) ?></div>
|
||||
<h2><?= $e(t('grant.activity_summary')) ?></h2>
|
||||
<div class="grid">
|
||||
<?php foreach (['entries' => t('grant.entries'),'exits' => t('grant.exits'),'adoptions' => t('grant.adoptions'),'deaths' => t('grant.deaths'),'births' => t('grant.births'),'vaccinations' => t('grant.vaccinations'),'treatments' => t('export.treatments'),'current' => t('grant.current')] as $key => $label): ?>
|
||||
<div class="stat"><strong><?= (int)$stats[$key] ?></strong><?= $e($label) ?></div><?php endforeach; ?></div>
|
||||
<p><?= $e(t('grant.avg_adoption')) ?> :
|
||||
<strong><?= $stats['avg'] > 0 ? $e(t('common.days', ['count' => round($stats['avg'])])) : $e(t('grant.not_calculable')) ?></strong>.
|
||||
</p>
|
||||
<h2><?= $e(t('grant.comparison', ['year' => $year - 1])) ?></h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th><?= $e(t('grant.indicator')) ?></th>
|
||||
<th><?= $year - 1 ?></th>
|
||||
<th><?= $year ?></th>
|
||||
<th><?= $e(t('grant.evolution')) ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach (['entries' => t('grant.entries'),'exits' => t('grant.exits'),'adoptions' => t('grant.adoptions'),'deaths' => t('grant.deaths'),'births' => t('grant.births'),'vaccinations' => t('grant.vaccinations'),'treatments' => t('export.treatments')] as $key => $label): $c = $comparison[$key]; ?>
|
||||
<tr>
|
||||
<td><?= $e($label) ?></td>
|
||||
<td><?= (int)$c['previous'] ?></td>
|
||||
<td><?= (int)$c['current'] ?></td>
|
||||
<td><?= $c['difference'] > 0 ? '+' : '' ?><?= (int)$c['difference'] ?><?= $c['percent'] !== null ? ' (' . number_format($c['percent'], 1, ',', ' ') . ' %)' : '' ?>
|
||||
</td>
|
||||
</tr><?php endforeach; ?></tbody>
|
||||
</table>
|
||||
<h2><?= $e(t('statistics.details', 'Detailed breakdown')) ?></h2>
|
||||
<div class="cols">
|
||||
<?php foreach ([[t('grant.sex_entries'),$details['sex'],[]],[t('grant.age_entry'),$details['ages'],[]],[t('grant.origins'),$details['origins'],[]],[t('grant.death_causes'),$details['deathCauses'],array_combine(array_keys(DeathController::CAUSES), array_map(static fn ($key) => DeathController::causeLabel($key), array_keys(DeathController::CAUSES)))],[t('grant.adoption_stay'),$details['stays'],[]]] as [$title,$items,$labels]): ?>
|
||||
<div class="avoid">
|
||||
<h3><?= $e($title) ?></h3><?php if ($items): ?><table><?php foreach ($items as $label => $total): ?><tr>
|
||||
<td><?= $e($labels[$label] ?? $label) ?></td>
|
||||
<td><?= (int)$total ?></td>
|
||||
</tr><?php endforeach; ?></table><?php else: ?><p class="meta">
|
||||
<?= $e(t('common.no_data', 'No data.')) ?></p><?php endif; ?>
|
||||
</div><?php endforeach; ?></div>
|
||||
<h2><?= $e(t('grant.association_life', 'Association life')) ?></h2>
|
||||
<table>
|
||||
<tr>
|
||||
<th><?= $e(t('grant.members')) ?></th>
|
||||
<td><?= (int)$annual['members_count'] ?></td>
|
||||
<th><?= $e(t('grant.volunteers')) ?></th>
|
||||
<td><?= (int)$annual['volunteers_count'] ?></td>
|
||||
<th><?= $e(t('grant.employees')) ?> / <?= $e(t('grant.employee_fte')) ?></th>
|
||||
<td><?= (int)$annual['employees_count'] ?> / <?= $e($annual['employees_fte']) ?></td>
|
||||
</tr>
|
||||
</table>
|
||||
<?php foreach (['annual_report' => t('grant.report_activity'),'completed_actions' => t('grant.completed_actions'),'planned_actions' => t('grant.actions_planned'),'beneficiaries' => t('grant.beneficiaries_short'),'in_kind_support' => t('grant.in_kind_short')] as $key => $label): ?><?php if (trim((string)$annual[$key]) !== ''): ?>
|
||||
<h3><?= $e($label) ?></h3>
|
||||
<div class="text"><?= $e($annual[$key]) ?></div><?php endif; ?><?php endforeach; ?><h2>
|
||||
<?= $e(t('grant.financial_summary')) ?></h2>
|
||||
<table>
|
||||
<tr>
|
||||
<th><?= (t('grant.actual_expenses')) ?></th>
|
||||
<td><?= $m($annual['actual_expenses']) ?></td>
|
||||
<th><?= (t('grant.actual_income')) ?></th>
|
||||
<td><?= $m($annual['actual_income']) ?></td>
|
||||
<th><?= (t('grant.treasury')) ?></th>
|
||||
<td><?= $m($annual['treasury_amount']) ?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><?= (t('grant.forecast_expenses')) ?></th>
|
||||
<td><?= $m($annual['forecast_expenses']) ?></td>
|
||||
<th><?= (t('grant.forecast_income')) ?></th>
|
||||
<td><?= $m($annual['forecast_income']) ?></td>
|
||||
<th><?= (t('grant.voluntary_contributions')) ?></th>
|
||||
<td><?= $m($annual['voluntary_contributions']) ?></td>
|
||||
</tr>
|
||||
</table>
|
||||
<h2><?= $e(t('grant.applications')) ?></h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th><?= $e(t('grant.organization')) ?></th>
|
||||
<th><?= $e(t('grant.deadline')) ?></th>
|
||||
<th><?= $e(t('grant.requested')) ?></th>
|
||||
<th><?= $e(t('grant.awarded')) ?></th>
|
||||
<th><?= $e(t('grant.state')) ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody><?php foreach ($applications as $a): ?><tr>
|
||||
<td><?= $e($a['organization_name']) ?></td>
|
||||
<td><?= $e($a['deadline'] ?: '—') ?></td>
|
||||
<td><?= $m($a['requested_amount']) ?></td>
|
||||
<td><?= $a['awarded_amount'] === null ? '—' : $m($a['awarded_amount']) ?></td>
|
||||
<td><?= $e(GrantService::statusLabel($a['status'])) ?></td>
|
||||
</tr><?php endforeach; ?></tbody>
|
||||
</table>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
236
app/Views/admin_register.php
Normal file
236
app/Views/admin_register.php
Normal file
|
|
@ -0,0 +1,236 @@
|
|||
<?php
|
||||
/** @var array $rows */
|
||||
/** @var int $year */
|
||||
/** @var string $q */
|
||||
/** @var string $species */
|
||||
?>
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h3><?= h(t('admin.register')) ?></h3><br />
|
||||
<?php if (PermissionService::can('grants') || PermissionService::can('administrative')): ?><div
|
||||
class="d-flex justify-content-end gap-2 mb-3"><?php if (PermissionService::can('grants')): ?><a
|
||||
class="btn btn-outline-primary" href="/admin/grants">📊
|
||||
<?= h(t('admin.grants')) ?></a><?php endif; ?><?php if (PermissionService::can('administrative')): ?><a
|
||||
class="btn btn-outline-primary" href="/admin/exports">⬇
|
||||
<?= h(t('admin.export_data')) ?></a><?php endif; ?></div><?php endif; ?>
|
||||
<div class="card mb-3">
|
||||
<button class="card-header w-100 border-0 text-start bg-transparent d-flex align-items-center" type="button"
|
||||
data-bs-toggle="collapse" data-bs-target="#administrativeDocuments" aria-expanded="false"
|
||||
aria-controls="administrativeDocuments">
|
||||
<div>
|
||||
<h3 class="card-title mb-1">📄 <?= h(t('admin.documents')) ?> <span
|
||||
class="badge bg-blue-lt ms-1"><?= h(t('admin.models', ['count' => 5])) ?></span></h3>
|
||||
<div class="text-muted small"><?= h(t('admin.models_help')) ?></div>
|
||||
</div><span class="ms-auto text-muted fs-2">⌄</span>
|
||||
</button>
|
||||
<div class="collapse" id="administrativeDocuments">
|
||||
<div class="card-body border-top">
|
||||
<div class="vstack gap-3">
|
||||
<?php foreach ([
|
||||
'adoption' => [t('admin.adoption_document'),'Version 1.5'],
|
||||
'abandon' => [t('admin.abandon_document'),'Version 1.0'],
|
||||
'benevole' => [t('admin.volunteer_document'),'Version 2.0'],
|
||||
'fa' => [t('admin.foster_document'),'Version 1.1'],
|
||||
'pre_adoption' => [t('admin.pre_adoption_document'),'Version 1.1'],
|
||||
] as $documentType => $document): ?>
|
||||
<div class="d-flex flex-wrap align-items-center gap-2">
|
||||
<div class="me-auto">
|
||||
<div class="fw-bold"><?= h($document[0]) ?></div>
|
||||
<div class="text-muted small"><?= h($document[1]) ?> — <?= h(t('admin.blank_model')) ?>
|
||||
</div>
|
||||
</div><a class="btn btn-outline-primary"
|
||||
href="/documents/blank?type=<?= h($documentType) ?>&format=odt"><?= h(t('animal.download_odt')) ?></a><a
|
||||
class="btn btn-danger" href="/documents/blank?type=<?= h($documentType) ?>&format=pdf"
|
||||
target="_blank"><?= h(t('animal.view_print_pdf')) ?></a>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form class="row g-2 align-items-end" method="get" action="/admin/register">
|
||||
<div class="col-md-2">
|
||||
<label class="form-label"><?= h(t('admin.year')) ?></label>
|
||||
<select class="form-select" name="year">
|
||||
<?php
|
||||
$currentYear = (int)date('Y');
|
||||
|
||||
for ($y = $currentYear; $y >= $currentYear - 10; $y--):
|
||||
?>
|
||||
<option value="<?= $y ?>" <?= $year == $y ? 'selected' : '' ?>>
|
||||
<?= $y ?>
|
||||
</option>
|
||||
<?php endfor; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3">
|
||||
<label class="form-label"><?= h(t('admin.search')) ?></label>
|
||||
<input class="form-control" name="q" value="<?= h($q) ?>"
|
||||
placeholder="<?= h(t('admin.search_placeholder')) ?>">
|
||||
</div>
|
||||
|
||||
<div class="col-md-3">
|
||||
<label class="form-label"><?= h(t('animal.species')) ?></label>
|
||||
<select class="form-select" name="species">
|
||||
<option value="">— <?= h(t('admin.all_species')) ?> —</option>
|
||||
<option value="chat" <?= strtolower($species) === 'chat' ? 'selected' : '' ?>>
|
||||
<?= h(t('admin.cat')) ?></option>
|
||||
<option value="chien" <?= strtolower($species) === 'chien' ? 'selected' : '' ?>>
|
||||
<?= h(t('admin.dog')) ?></option>
|
||||
<option value="nac" <?= strtolower($species) === 'nac' ? 'selected' : '' ?>>NAC</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4 d-flex gap-2">
|
||||
<button class="btn btn-primary" type="submit"><?= h(t('common.filter')) ?></button>
|
||||
<a class="btn btn-outline-secondary" href="/admin/register"><?= h(t('common.reset')) ?></a>
|
||||
<a class="btn btn-danger"
|
||||
href="/admin/register/pdf?year=<?= $year ?>&q=<?= urlencode($q) ?>&species=<?= urlencode($species) ?>&sort=<?= $sort ?>&dir=<?= strtolower($dir) ?>">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-file-text" width="18"
|
||||
height="18" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none"
|
||||
stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M14 3v4a1 1 0 0 0 1 1h4" />
|
||||
<path d="M17 21h-10a2 2 0 0 1 -2 -2v-14a2 2 0 0 1 2 -2h7l5 5v11a2 2 0 0 1 -2 2z" />
|
||||
<path d="M9 9l1 0" />
|
||||
<path d="M9 13l6 0" />
|
||||
<path d="M9 17l6 0" />
|
||||
</svg>
|
||||
<?= h(t('admin.print_register')) ?>
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-vcenter table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th><?= sortLink('type', t('animal.form.type'), $sort, $dir, $year, $q, $species) ?></th>
|
||||
<th><?= sortLink('date', t('common.date'), $sort, $dir, $year, $q, $species) ?></th>
|
||||
<th><?= sortLink('name', t('common.name'), $sort, $dir, $year, $q, $species) ?></th>
|
||||
<th><?= sortLink('sex', t('animal.sex'), $sort, $dir, $year, $q, $species) ?></th>
|
||||
<th><?= sortLink('species', t('animal.species'), $sort, $dir, $year, $q, $species) ?></th>
|
||||
<th><?= h(t('admin.birth')) ?></th>
|
||||
<th><?= h(t('admin.identification')) ?></th>
|
||||
<th><?= h(t('admin.reason_contact')) ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php $i = 0;
|
||||
foreach ($rows as $r): $i++; ?>
|
||||
<?php
|
||||
$rowClass = '';
|
||||
|
||||
switch ($r['kind']) {
|
||||
case 'entry':
|
||||
$type_txt = t('admin.entry');
|
||||
$type_color = 'green';
|
||||
break;
|
||||
|
||||
case 'exit':
|
||||
$type_txt = t('admin.exit');
|
||||
$type_color = 'red';
|
||||
break;
|
||||
|
||||
default:
|
||||
$type_txt = ucfirst($r['kind']);
|
||||
$type_color = 'secondary';
|
||||
}
|
||||
|
||||
switch ($r['lieu']) {
|
||||
case 'Adoption':
|
||||
$type2_txt = t('admin.adoption');
|
||||
$type2_color = 'azure';
|
||||
break;
|
||||
|
||||
case 'Décès du propriétaire':
|
||||
$type2_txt = t('admin.owner_death');
|
||||
$type2_color = 'red';
|
||||
break;
|
||||
|
||||
case 'Décès':
|
||||
case 'Décès de l\'animal':
|
||||
$type2_txt = t('admin.animal_death');
|
||||
$type2_color = 'red';
|
||||
break;
|
||||
|
||||
case 'Sauvetage':
|
||||
$type2_txt = t('admin.rescue');
|
||||
$type2_color = 'green';
|
||||
break;
|
||||
|
||||
default:
|
||||
$type2_txt = h($r['lieu']);
|
||||
$type2_color = 'secondary';
|
||||
}
|
||||
?>
|
||||
<tr>
|
||||
<td class="text-muted"><?= $i ?></td>
|
||||
|
||||
<td>
|
||||
<span class="badge bg-<?= $type_color; ?>-lt"><?= $type_txt; ?></span>
|
||||
</td>
|
||||
|
||||
<td><?= $r['created_at'] ? h(substr($r['created_at'], 0, 10)) : '—' ?></td>
|
||||
|
||||
<td>
|
||||
<a href="/animal?id=<?= (int)$r['animal_id'] ?>" class="fw-bold"><?= h($r['name'] ?? '') ?></a>
|
||||
<?php if (!empty($r['internal_code'])): ?>
|
||||
<div class="text-muted small"><?= h($r['internal_code']) ?></div>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
|
||||
<td class="text-center"><?= $r['sex'] ? h($r['sex']) : '—' ?></td>
|
||||
<td class="text-center"><?= h(species_label($r['species'] ?? '')) ?></td>
|
||||
<td class="text-center"><?= $r['birth_date'] ? h($r['birth_date']) : '—' ?></td>
|
||||
<td><?= $r['chip_id'] ? h($r['chip_id']) : '—' ?></td>
|
||||
<td>
|
||||
<?php if (!empty($r['lieu'])): ?>
|
||||
<span class="badge bg-<?= $type2_color; ?>-lt"><?= $type2_txt; ?></span><br />
|
||||
|
||||
<?= $r['contact_name'] && empty($r['death_cause_code']) ? h($r['contact_name']) . '<br />' : '' ?>
|
||||
<?= $r['place'] ? h($r['place']) . '<br />' : '' ?>
|
||||
<?= $r['contact_phone'] ? h($r['contact_phone']) . '<br />' : '' ?>
|
||||
<?= $r['contact_email'] ? h($r['contact_email']) . '<br />' : '' ?>
|
||||
|
||||
<?php if (!empty($r['death_cause_code'])): ?>
|
||||
<div class="mt-1 small"><strong><?= h(t('animal.cause')) ?> :</strong>
|
||||
<?= h(DeathController::causeLabel((string)$r['death_cause_code'])) ?><?= !empty($r['death_cause_details']) ? ' — ' . h($r['death_cause_details']) : '' ?>
|
||||
</div>
|
||||
<?php if ((int)$r['death_euthanized'] === 1): ?><div class="small">
|
||||
<strong><?= h(t('animal.euthanasia')) ?> :</strong> <?= h(t('common.yes')) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($r['death_veterinarian'])): ?><div class="small">
|
||||
<strong><?= h(t('animal.form.veterinarian')) ?> :</strong>
|
||||
<?= h($r['death_veterinarian']) ?></div><?php endif; ?>
|
||||
<?php if (!empty($r['death_crematorium'])): ?><div class="small">
|
||||
<strong><?= h(t('admin.crematorium')) ?> :</strong> <?= h($r['death_crematorium']) ?></div>
|
||||
<?php endif; ?>
|
||||
<div class="small"><strong><?= h(t('animal.care')) ?> :</strong>
|
||||
<?= h(DeathController::dispositionLabel((string)$r['death_disposition'])) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
|
||||
<?php else: ?>
|
||||
—
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
|
||||
<?php if (!$rows): ?>
|
||||
<tr>
|
||||
<td colspan="12" class="text-center text-muted py-4"><?= h(t('admin.no_data')) ?></td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
180
app/Views/admin_register_print.php
Normal file
180
app/Views/admin_register_print.php
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
<?php
|
||||
$escape = static fn ($value): string => htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8');
|
||||
$speciesLabel = static fn ($value): string => match (strtolower((string)$value)) {
|
||||
'cat', 'chat' => t('species.cat'), 'dog', 'chien' => t('species.dog'), 'nac' => t('species.nac'), default => ucfirst((string)$value),
|
||||
};
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="<?= $escape(I18n::locale()) ?>">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title><?= $escape(t('pdf.register', ['year' => (int)$year])) ?></title>
|
||||
<style>
|
||||
@page {
|
||||
size: A4 portrait;
|
||||
margin: 14mm 10mm 18mm
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
color: #111;
|
||||
font: 10px Arial, Helvetica, sans-serif
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0 0 14px;
|
||||
text-align: center;
|
||||
font-size: 18px
|
||||
}
|
||||
|
||||
.meta {
|
||||
margin: -8px 0 12px;
|
||||
text-align: center;
|
||||
color: #626976
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse
|
||||
}
|
||||
|
||||
thead {
|
||||
display: table-header-group
|
||||
}
|
||||
|
||||
tr {
|
||||
break-inside: avoid
|
||||
}
|
||||
|
||||
th {
|
||||
background: #f0f0f0;
|
||||
border: 1px solid #bbb;
|
||||
padding: 6px;
|
||||
text-align: left
|
||||
}
|
||||
|
||||
td {
|
||||
border: 1px solid #ddd;
|
||||
padding: 5px;
|
||||
vertical-align: top
|
||||
}
|
||||
|
||||
tbody tr:nth-child(even) {
|
||||
background: #fafafa
|
||||
}
|
||||
|
||||
.entry {
|
||||
color: #1b7a1b;
|
||||
font-weight: 700
|
||||
}
|
||||
|
||||
.exit {
|
||||
color: #b71c1c;
|
||||
font-weight: 700
|
||||
}
|
||||
|
||||
.small {
|
||||
color: #666;
|
||||
font-size: 8px
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
margin: 0 0 16px;
|
||||
padding: 10px;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #ddd
|
||||
}
|
||||
|
||||
.toolbar a,
|
||||
.toolbar button {
|
||||
border: 1px solid #1d273b;
|
||||
border-radius: 6px;
|
||||
padding: 8px 13px;
|
||||
background: #fff;
|
||||
color: #1d273b;
|
||||
font: 600 14px Arial, sans-serif;
|
||||
text-decoration: none;
|
||||
cursor: pointer
|
||||
}
|
||||
|
||||
.toolbar button {
|
||||
background: #1d273b;
|
||||
color: #fff
|
||||
}
|
||||
|
||||
.document-certification {
|
||||
margin: 0 0 12px;
|
||||
padding: 7px 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
background: #f7f7f7;
|
||||
color: #626976;
|
||||
font-size: 9px
|
||||
}
|
||||
|
||||
@media print {
|
||||
.toolbar {
|
||||
display: none !important
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="toolbar"><a href="/admin/register?year=<?= (int)$year ?>">←
|
||||
<?= $escape(t('pdf.back_register')) ?></a><button type="button" onclick="window.print()">🖨
|
||||
<?= $escape(t('pdf.print')) ?></button></div>
|
||||
<div class="document-certification">
|
||||
<?= $escape(t('pdf.generated_by', ['association' => AppSettings::get('association_name'),'date' => $generatedAt->format('d/m/Y'),'time' => $generatedAt->format('H\\hi')])) ?>
|
||||
</div>
|
||||
<h1><?= $escape(t('pdf.register', ['year' => (int)$year])) ?></h1>
|
||||
<div class="meta"><?= $escape(t('pdf.movements', ['count' => count($rows)])) ?></div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th><?= $escape(t('common.date')) ?></th>
|
||||
<th><?= $escape(t('animal.form.type')) ?></th>
|
||||
<th><?= $escape(t('common.name')) ?></th>
|
||||
<th><?= $escape(t('animal.sex')) ?></th>
|
||||
<th><?= $escape(t('animal.species')) ?></th>
|
||||
<th><?= $escape(t('animal.birth')) ?></th>
|
||||
<th><?= $escape(t('animal.form.identification')) ?></th>
|
||||
<th><?= $escape(t('pdf.reason_contact')) ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($rows as $row): $isEntry = ($row['kind'] ?? '') === 'entry'; ?><tr>
|
||||
<td><?= $escape(substr((string)$row['created_at'], 0, 10)) ?></td>
|
||||
<td class="<?= $isEntry ? 'entry' : 'exit' ?>"><?= $escape($isEntry ? t('pdf.entry') : t('pdf.exit')) ?>
|
||||
</td>
|
||||
<td><strong><?= $escape($row['name']) ?></strong><br><span
|
||||
class="small"><?= $escape($row['internal_code']) ?></span></td>
|
||||
<td><?= $escape($row['sex']) ?></td>
|
||||
<td><?= $escape($speciesLabel($row['species'])) ?></td>
|
||||
<td><?= $escape($row['birth_date']) ?></td>
|
||||
<td><?= $escape($row['chip_id']) ?></td>
|
||||
<td><?= $escape($row['lieu'] ?? '') ?><br><?= $escape($row['contact_name'] ?? '') ?><br><?= $escape($row['place'] ?? '') ?><br><?= $escape($row['contact_phone'] ?? '') ?><br><?= $escape($row['contact_email'] ?? '') ?>
|
||||
</td>
|
||||
</tr><?php endforeach; ?>
|
||||
<?php if (!$rows): ?><tr>
|
||||
<td colspan="8" style="padding:20px;text-align:center;color:#666"><?= $escape(t('pdf.no_movement')) ?>
|
||||
</td>
|
||||
</tr><?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
561
app/Views/agenda.php
Normal file
561
app/Views/agenda.php
Normal file
|
|
@ -0,0 +1,561 @@
|
|||
<?php
|
||||
$first = $cursor->modify('first day of this month');
|
||||
$offset = (int)$first->format('N') - 1;
|
||||
$days = (int)$cursor->format('t');
|
||||
$trailing = (7 - (($offset + $days) % 7)) % 7;
|
||||
$weekStart = $weekCursor->modify('monday this week');
|
||||
$weekEnd = $weekStart->modify('+6 days');
|
||||
$byDay = [];
|
||||
foreach ($items as $item) {
|
||||
$byDay[substr($item['instance_starts_at'], 0, 10)][] = $item;
|
||||
}$edit = $editItem ?? [];
|
||||
$editStart = !empty($edit['starts_at']) ? new DateTimeImmutable($edit['starts_at']) : null;
|
||||
$colors = ['appointment' => 'blue','veterinary' => 'red','transport' => 'orange','pre_adoption' => 'purple','task' => 'green','other' => 'secondary','vaccine' => 'cyan','deworming' => 'yellow','treatment_start' => 'teal','treatment_end' => 'pink'];
|
||||
$sourceTabs = ['vaccine' => 'tab-vac','deworming' => 'tab-med','treatment_start' => 'tab-trt','treatment_end' => 'tab-trt'];
|
||||
$legend = ['organization' => ['appointment' => '#206bc4','veterinary' => '#d63939','transport' => '#f76707','pre_adoption' => '#ae3ec9','task' => '#2fb344'],'medical' => ['vaccine' => '#17a2b8','deworming' => '#f59f00','treatment_start' => '#0ca678','treatment_end' => '#d6336c']];
|
||||
?>
|
||||
<style>
|
||||
.agenda-calendar {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, minmax(120px, 1fr));
|
||||
min-width: 735px;
|
||||
border-radius: 12px;
|
||||
overflow: hidden
|
||||
}
|
||||
|
||||
.agenda-weekday {
|
||||
padding: .65rem;
|
||||
background: var(--tblr-bg-surface-secondary);
|
||||
font-weight: 700;
|
||||
text-align: center
|
||||
}
|
||||
|
||||
.agenda-day {
|
||||
min-height: 128px;
|
||||
padding: .5rem;
|
||||
border-top: 1px solid var(--tblr-border-color);
|
||||
border-right: 1px solid var(--tblr-border-color);
|
||||
background: var(--tblr-bg-surface)
|
||||
}
|
||||
|
||||
.agenda-day:nth-child(7n) {
|
||||
border-right: 0
|
||||
}
|
||||
|
||||
.agenda-day.muted {
|
||||
background: rgba(98, 105, 118, .04)
|
||||
}
|
||||
|
||||
.agenda-number {
|
||||
font-weight: 700;
|
||||
font-size: .8rem
|
||||
}
|
||||
|
||||
.agenda-event {
|
||||
display: block;
|
||||
margin-top: .3rem;
|
||||
padding: .3rem .4rem;
|
||||
border-radius: 6px;
|
||||
background: rgba(32, 107, 196, .09);
|
||||
border-left: 3px solid var(--tblr-primary);
|
||||
font-size: .68rem;
|
||||
color: inherit;
|
||||
text-decoration: none
|
||||
}
|
||||
|
||||
.agenda-event strong {
|
||||
display: block;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis
|
||||
}
|
||||
|
||||
.agenda-event.red {
|
||||
border-color: #d63939;
|
||||
background: rgba(214, 57, 57, .09)
|
||||
}
|
||||
|
||||
.agenda-event.orange {
|
||||
border-color: #f76707;
|
||||
background: rgba(247, 103, 7, .09)
|
||||
}
|
||||
|
||||
.agenda-event.purple {
|
||||
border-color: #ae3ec9;
|
||||
background: rgba(174, 62, 201, .09)
|
||||
}
|
||||
|
||||
.agenda-event.green {
|
||||
border-color: #2fb344;
|
||||
background: rgba(47, 179, 68, .09)
|
||||
}
|
||||
|
||||
.agenda-event.cyan {
|
||||
border-color: #17a2b8;
|
||||
background: rgba(23, 162, 184, .1)
|
||||
}
|
||||
|
||||
.agenda-event.yellow {
|
||||
border-color: #f59f00;
|
||||
background: rgba(245, 159, 0, .11)
|
||||
}
|
||||
|
||||
.agenda-event.teal {
|
||||
border-color: #0ca678;
|
||||
background: rgba(12, 166, 120, .1)
|
||||
}
|
||||
|
||||
.agenda-event.pink {
|
||||
border-color: #d6336c;
|
||||
background: rgba(214, 51, 108, .1)
|
||||
}
|
||||
|
||||
.agenda-legend {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: .65rem;
|
||||
padding: .7rem;
|
||||
background: rgba(98, 105, 118, .045);
|
||||
border-radius: 10px
|
||||
}
|
||||
|
||||
.agenda-legend-group {
|
||||
display: flex;
|
||||
flex: 1 1 360px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: .45rem
|
||||
}
|
||||
|
||||
.agenda-legend-title {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: .35rem;
|
||||
margin-right: .15rem;
|
||||
font-size: .72rem;
|
||||
font-weight: 700;
|
||||
color: var(--tblr-secondary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .025em
|
||||
}
|
||||
|
||||
.agenda-legend-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: .38rem;
|
||||
padding: .28rem .55rem;
|
||||
border-radius: 999px;
|
||||
background: var(--tblr-bg-surface);
|
||||
box-shadow: 0 1px 2px rgba(24, 36, 51, .08);
|
||||
font-size: .72rem;
|
||||
color: var(--tblr-body-color);
|
||||
white-space: nowrap
|
||||
}
|
||||
|
||||
.agenda-legend-dot {
|
||||
width: .55rem;
|
||||
height: .55rem;
|
||||
border-radius: 50%;
|
||||
flex: 0 0 auto;
|
||||
box-shadow: 0 0 0 2px rgba(255, 255, 255, .8)
|
||||
}
|
||||
|
||||
@media(max-width:900px) {
|
||||
.agenda-calendar {
|
||||
grid-template-columns: repeat(7, minmax(105px, 1fr))
|
||||
}
|
||||
|
||||
.agenda-legend-group {
|
||||
flex-basis: 100%
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
<style>
|
||||
.agenda-calendar-week .agenda-day {
|
||||
min-height: 360px
|
||||
}
|
||||
|
||||
.agenda-day.agenda-day-clickable {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
transition: background-color .15s ease
|
||||
}
|
||||
|
||||
.agenda-day.agenda-day-clickable:hover {
|
||||
background-color: rgba(32, 107, 196, .055)
|
||||
}
|
||||
|
||||
.agenda-day.agenda-day-clickable::after {
|
||||
content: '+';
|
||||
position: absolute;
|
||||
right: .45rem;
|
||||
top: .35rem;
|
||||
width: 1.35rem;
|
||||
height: 1.35rem;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border-radius: 50%;
|
||||
color: var(--tblr-primary);
|
||||
background: rgba(32, 107, 196, .1);
|
||||
font-weight: 700;
|
||||
opacity: 0;
|
||||
transform: scale(.85);
|
||||
transition: .15s ease
|
||||
}
|
||||
|
||||
.agenda-day.agenda-day-clickable:hover::after,
|
||||
.agenda-day.agenda-day-clickable:focus-visible::after {
|
||||
opacity: 1;
|
||||
transform: scale(1)
|
||||
}
|
||||
|
||||
.agenda-day.agenda-day-clickable:focus-visible {
|
||||
outline: 2px solid var(--tblr-primary);
|
||||
outline-offset: -2px
|
||||
}
|
||||
|
||||
</style>
|
||||
<div class="d-flex flex-wrap justify-content-between align-items-center gap-2 mb-3">
|
||||
<div class="text-muted"><?= h(t('agenda.intro')) ?></div>
|
||||
<?php if (PermissionService::can('agenda', 'edit')): ?><button class="btn btn-primary" id="agendaAddButton"
|
||||
data-bs-toggle="modal" data-bs-target="#agendaModal">+ <?= h(t('agenda.add')) ?></button><?php endif; ?>
|
||||
</div>
|
||||
<div class="d-flex flex-wrap align-items-center justify-content-between gap-2 mb-3">
|
||||
<div class="btn-group" role="group" aria-label="<?= h(t('agenda.view')) ?>"><a
|
||||
class="btn btn-sm <?= $viewMode === 'month' ? 'btn-primary' : 'btn-outline-secondary' ?>"
|
||||
href="/agenda?view=month&month=<?= h(date('Y-m')) ?>">▦ <?= h(t('agenda.month_view')) ?></a><a
|
||||
class="btn btn-sm <?= $viewMode === 'week' ? 'btn-primary' : 'btn-outline-secondary' ?>"
|
||||
href="/agenda?view=week&date=<?= h(date('Y-m-d')) ?>">▥ <?= h(t('agenda.week_view')) ?></a></div>
|
||||
<div class="d-flex flex-wrap gap-2"><a class="btn btn-sm btn-outline-secondary"
|
||||
href="/agenda?view=<?= h($viewMode) ?><?= $viewMode === 'week' ? '&date=' . h(date('Y-m-d')) : '&month=' . h(date('Y-m')) ?>"><?= h(t('agenda.today')) ?></a><a
|
||||
class="btn btn-sm btn-outline-primary"
|
||||
href="/agenda/export.ics?view=<?= h($viewMode) ?><?= $viewMode === 'week' ? '&date=' . h($weekCursor->format('Y-m-d')) : '&month=' . h($cursor->format('Y-m')) ?>">⇩
|
||||
<?= h(t('agenda.export_ics')) ?></a></div>
|
||||
</div>
|
||||
<form class="card card-body mb-3 py-2" method="get" action="/agenda" id="agendaFilters"><input type="hidden" name="view"
|
||||
value="<?= h($viewMode) ?>"><?php if ($viewMode === 'week'): ?><input type="hidden" name="date"
|
||||
value="<?= h($weekCursor->format('Y-m-d')) ?>"><?php else: ?><input type="hidden" name="month"
|
||||
value="<?= h($cursor->format('Y-m')) ?>"><?php endif; ?><div class="row g-2 align-items-end">
|
||||
<div class="col-sm-6 col-lg-2"><label class="form-label small mb-1"><?= h(t('agenda.scope')) ?></label><select
|
||||
class="form-select form-select-sm" name="scope" onchange="this.form.submit()">
|
||||
<option value="all"><?= h(t('agenda.scope_all')) ?></option>
|
||||
<option value="mine" <?= $filters['scope'] === 'mine' ? 'selected' : '' ?>>
|
||||
<?= h(t('agenda.scope_mine')) ?></option>
|
||||
</select></div>
|
||||
<div class="col-sm-6 col-lg-2"><label
|
||||
class="form-label small mb-1"><?= h(t('agenda.assigned')) ?></label><select
|
||||
class="form-select form-select-sm" name="assigned_user_id" onchange="this.form.submit()">
|
||||
<option value="">—</option><?php foreach ($users as $u): ?><option value="<?= (int)$u['id'] ?>"
|
||||
<?= $filters['assigned_user_id'] == (int)$u['id'] ? 'selected' : '' ?>><?= h($u['name']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select></div>
|
||||
<div class="col-sm-6 col-lg-2"><label
|
||||
class="form-label small mb-1"><?= h(t('agenda.volunteer')) ?></label><select
|
||||
class="form-select form-select-sm" name="volunteer_contact_id" onchange="this.form.submit()">
|
||||
<option value="">—</option><?php foreach ($volunteers as $v): ?><option value="<?= (int)$v['id'] ?>"
|
||||
<?= $filters['volunteer_contact_id'] == (int)$v['id'] ? 'selected' : '' ?>><?= h($v['name']) ?>
|
||||
</option><?php endforeach; ?>
|
||||
</select></div>
|
||||
<div class="col-sm-6 col-lg-2"><label class="form-label small mb-1"><?= h(t('agenda.type')) ?></label><select
|
||||
class="form-select form-select-sm" name="category" onchange="this.form.submit()">
|
||||
<option value=""><?= h(t('common.all')) ?></option><?php foreach (array_keys($colors) as $type): ?>
|
||||
<option value="<?= h($type) ?>" <?= $filters['category'] === $type ? 'selected' : '' ?>>
|
||||
<?= h(t('agenda.type_' . $type)) ?></option><?php endforeach; ?>
|
||||
</select></div>
|
||||
<div class="col-sm-8 col-lg-3"><label class="form-label small mb-1"><?= h(t('common.search')) ?></label><input
|
||||
class="form-control form-control-sm" name="q" value="<?= h($filters['q']) ?>"
|
||||
placeholder="<?= h(t('agenda.search_placeholder')) ?>"></div>
|
||||
<div class="col-sm-4 col-lg-1 d-flex gap-1"><button class="btn btn-sm btn-primary flex-fill"
|
||||
title="<?= h(t('common.search')) ?>">⌕</button><a class="btn btn-sm btn-outline-secondary"
|
||||
href="/agenda?view=<?= h($viewMode) ?>" title="<?= h(t('common.reset')) ?>">×</a></div>
|
||||
<div class="col-12"><input type="hidden" name="show_medical" value="0"><label
|
||||
class="form-check form-switch mb-0"><input class="form-check-input" type="checkbox" name="show_medical"
|
||||
value="1" <?= $filters['show_medical'] ? 'checked' : '' ?> onchange="this.form.submit()"><span
|
||||
class="form-check-label small"><?= h(t('agenda.show_medical')) ?></span></label></div>
|
||||
</div>
|
||||
</form>
|
||||
<div class="agenda-legend mb-3" aria-label="<?= h(t('agenda.legend')) ?>">
|
||||
<?php foreach ($legend as $group => $entries): ?><div class="agenda-legend-group"><span
|
||||
class="agenda-legend-title"><?= $group === 'medical' ? '❤' : '📅' ?>
|
||||
<?= h(t('agenda.legend_' . $group)) ?></span><?php foreach ($entries as $type => $color): ?><span
|
||||
class="agenda-legend-item"><span class="agenda-legend-dot"
|
||||
style="background:<?= h($color) ?>"></span><?= h(t('agenda.type_' . $type)) ?></span><?php endforeach; ?>
|
||||
</div><?php endforeach; ?></div>
|
||||
<details class="card mb-3">
|
||||
<summary class="card-header cursor-pointer"><span class="card-title">🔄
|
||||
<?= h(t('agenda.subscription')) ?></span><span
|
||||
class="ms-auto badge <?= $feedEnabled ? 'bg-green-lt' : 'bg-secondary-lt' ?>"><?= h(t($feedEnabled ? 'agenda.feed_active' : 'agenda.feed_inactive')) ?></span>
|
||||
</summary>
|
||||
<div class="card-body">
|
||||
<p class="text-muted"><?= h(t('agenda.subscription_help')) ?></p><?php if ($feedUrl): ?><div
|
||||
class="alert alert-success"><strong><?= h(t('agenda.copy_now')) ?></strong>
|
||||
<div class="input-group mt-2"><input class="form-control font-monospace" id="agendaFeedUrl" readonly
|
||||
value="<?= h($feedUrl) ?>"><button class="btn btn-outline-success" type="button"
|
||||
onclick="navigator.clipboard.writeText(document.getElementById('agendaFeedUrl').value)"><?= h(t('common.copy')) ?></button>
|
||||
</div>
|
||||
</div><?php elseif ($feedEnabled): ?><div class="alert alert-info"><?= h(t('agenda.feed_hidden')) ?></div>
|
||||
<?php endif; ?><div class="d-flex flex-wrap gap-2">
|
||||
<form method="post" action="/agenda/feed/regenerate"><input type="hidden" name="csrf"
|
||||
value="<?= h(Auth::csrf()) ?>"><button
|
||||
class="btn btn-outline-primary"><?= h($feedEnabled ? t('agenda.regenerate_feed') : t('agenda.create_feed')) ?></button>
|
||||
</form><?php if ($feedEnabled): ?><form method="post" action="/agenda/feed/revoke"
|
||||
onsubmit="return confirm(<?= h(json_encode(t('agenda.revoke_confirm'), JSON_HEX_APOS | JSON_HEX_QUOT)) ?>)">
|
||||
<input type="hidden" name="csrf" value="<?= h(Auth::csrf()) ?>"><button
|
||||
class="btn btn-outline-danger"><?= h(t('agenda.revoke_feed')) ?></button></form><?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
<template id="agendaVolunteerField">
|
||||
<div class="col-md-4 agenda-volunteer-field"><label
|
||||
class="form-label"><?= h(t('agenda.volunteers')) ?></label><select class="form-select"
|
||||
name="volunteer_contact_ids[]" multiple size="4"><?php $selectedVolunteers = array_map('intval', array_filter(explode(',', (string)($edit['volunteer_ids'] ?? ''))));
|
||||
foreach ($volunteers as $v): ?><option value="<?= (int)$v['id'] ?>"
|
||||
<?= in_array((int)$v['id'], $selectedVolunteers, true) ? 'selected' : '' ?>><?= h($v['name']) ?>
|
||||
</option><?php endforeach; ?></select>
|
||||
<div class="form-hint"><?= h(t('agenda.volunteers_help')) ?></div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const assigned = document.querySelector('#agendaModal select[name="assigned_user_id"]');
|
||||
const template = document.getElementById('agendaVolunteerField');
|
||||
if (assigned && template && !document.querySelector('#agendaModal .agenda-volunteer-field')) assigned
|
||||
.closest('[class*="col-md-"]')?.after(template.content.cloneNode(true));
|
||||
const form = document.getElementById('agendaFilters');
|
||||
if (form) {
|
||||
const params = new URLSearchParams(new FormData(form));
|
||||
['view', 'date', 'month'].forEach(key => params.delete(key));
|
||||
document.querySelectorAll('a[href^="/agenda?"] , a[href^="/agenda/export.ics?"]').forEach(link => {
|
||||
if (link.title === < ? = json_encode(t('common.reset')) ? > ) return;
|
||||
const url = new URL(link.href, location.origin);
|
||||
params.forEach((value, key) => url.searchParams.set(key, value));
|
||||
link.href = url.pathname + '?' + url.searchParams.toString();
|
||||
});
|
||||
}
|
||||
const modal = document.getElementById('agendaModal');
|
||||
const addButton = document.getElementById('agendaAddButton');
|
||||
if (modal && addButton && < ? = (!$edit && PermissionService::can('agenda', 'edit')) ? 'true' :
|
||||
'false' ? > ) {
|
||||
const month = < ? = json_encode($cursor - > format('Y-m')) ? > ;
|
||||
const weekDates = < ? = json_encode(array_map(static fn($i) => $weekStart - > modify('+'.$i.
|
||||
' days') - > format('Y-m-d'), range(0, 6))) ? > ;
|
||||
document.querySelectorAll('.agenda-calendar .agenda-day:not(.muted)').forEach((cell, index) => {
|
||||
const number = cell.querySelector('.agenda-number')?.textContent.trim();
|
||||
const date = < ? = $viewMode === 'week' ? 'weekDates[index]' :
|
||||
'month+\'-\'+String(parseInt(number,10)).padStart(2,\'0\')' ? > ;
|
||||
if (!date) return;
|
||||
cell.classList.add('agenda-day-clickable');
|
||||
cell.tabIndex = 0;
|
||||
cell.setAttribute('role', 'button');
|
||||
cell.setAttribute('aria-label', < ? = json_encode(t('agenda.add_on_date')) ? > +' ' +
|
||||
date.split('-').reverse().join('/'));
|
||||
const open = event => {
|
||||
if (event.target.closest('.agenda-event,button,a,input,select,textarea,label'))
|
||||
return;
|
||||
modal.querySelector('input[name="date"]').value = date;
|
||||
addButton.click();
|
||||
};
|
||||
cell.addEventListener('click', open);
|
||||
cell.addEventListener('keydown', event => {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault();
|
||||
open(event);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
<?php if ($status): ?><div class="alert <?= $status === 'conflict' ? 'alert-warning' : 'alert-success' ?>">
|
||||
<?= $status === 'conflict' ? '⚠️' : '✅' ?>
|
||||
<?= h(t($status === 'conflict' ? 'agenda.conflict_saved' : 'agenda.saved')) ?></div>
|
||||
<?php endif; ?><?php if ($error): ?><div class="alert alert-danger"><?= h($error) ?></div><?php endif; ?>
|
||||
<?php if ($viewMode === 'month'): ?><div class="card mb-3">
|
||||
<div class="card-header d-flex justify-content-between"><a class="btn btn-sm btn-outline-secondary"
|
||||
href="/agenda?view=month&month=<?= h($cursor->modify('-1 month')->format('Y-m')) ?>">←</a>
|
||||
<h3 class="card-title text-capitalize">
|
||||
<?= h(I18n::locale() === 'fr' ? ['janvier','février','mars','avril','mai','juin','juillet','août','septembre','octobre','novembre','décembre'][(int)$cursor->format('n') - 1] . ' ' . $cursor->format('Y') : $cursor->format('F Y')) ?>
|
||||
</h3><a class="btn btn-sm btn-outline-secondary"
|
||||
href="/agenda?view=month&month=<?= h($cursor->modify('+1 month')->format('Y-m')) ?>">→</a>
|
||||
</div>
|
||||
<div class="card-body p-0 overflow-auto">
|
||||
<div class="agenda-calendar">
|
||||
<?php foreach ([t('agenda.mon'),t('agenda.tue'),t('agenda.wed'),t('agenda.thu'),t('agenda.fri'),t('agenda.sat'),t('agenda.sun')] as $label): ?>
|
||||
<div class="agenda-weekday"><?= h($label) ?></div>
|
||||
<?php endforeach; ?><?php for ($i = 0;$i < $offset;$i++): ?><div class="agenda-day muted"></div>
|
||||
<?php endfor; ?><?php for ($day = 1;$day <= $days;$day++): $date = $cursor->format('Y-m-') . str_pad((string)$day, 2, '0', STR_PAD_LEFT); ?>
|
||||
<div class="agenda-day <?= $date === date('Y-m-d') ? 'bg-yellow-lt' : '' ?>">
|
||||
<div class="agenda-number"><?= $day ?></div>
|
||||
<?php foreach ($byDay[$date] ?? [] as $item): $source = $item['source_type'] ?? 'agenda';
|
||||
$isLinkedReminder = $source !== 'agenda';
|
||||
$eventHref = $isLinkedReminder ? '/animal?id=' . (int)$item['animal_id'] . '#' . ($sourceTabs[$source] ?? 'tab-med') : '/agenda?view=month&month=' . h($cursor->format('Y-m')) . '&edit=' . (int)$item['id']; ?><a
|
||||
class="agenda-event <?= h($colors[$item['item_type']] ?? 'secondary') ?>" href="<?= $eventHref ?>"
|
||||
title="<?= h($item['title']) ?>"><strong><?= $item['all_day'] ? '' : h(substr($item['instance_starts_at'], 11, 5) . ' ') ?><?= h($item['title']) ?></strong><?php if ($item['animal_name']): ?>🐾
|
||||
<?= h($item['animal_name']) ?><?php endif; ?></a><?php endforeach; ?>
|
||||
</div><?php endfor; ?><?php for ($i = 0;$i < $trailing;$i++): ?><div class="agenda-day muted"></div>
|
||||
<?php endfor; ?></div>
|
||||
</div>
|
||||
</div><?php else: ?><div class="card mb-3">
|
||||
<div class="card-header d-flex justify-content-between align-items-center"><a
|
||||
class="btn btn-sm btn-outline-secondary"
|
||||
href="/agenda?view=week&date=<?= h($weekStart->modify('-7 days')->format('Y-m-d')) ?>">←</a>
|
||||
<h3 class="card-title">
|
||||
<?= h(t('agenda.week_range', ['start' => $weekStart->format('d/m/Y'),'end' => $weekEnd->format('d/m/Y')])) ?>
|
||||
</h3><a class="btn btn-sm btn-outline-secondary"
|
||||
href="/agenda?view=week&date=<?= h($weekStart->modify('+7 days')->format('Y-m-d')) ?>">→</a>
|
||||
</div>
|
||||
<div class="card-body p-0 overflow-auto">
|
||||
<div class="agenda-calendar agenda-calendar-week"><?php $weekLabels = [t('agenda.mon'),t('agenda.tue'),t('agenda.wed'),t('agenda.thu'),t('agenda.fri'),t('agenda.sat'),t('agenda.sun')];
|
||||
foreach ($weekLabels as $i => $label):$weekDate = $weekStart->modify('+' . $i . ' days'); ?><div
|
||||
class="agenda-weekday"><span class="d-block"><?= h($label) ?></span><span
|
||||
class="small fw-normal"><?= h($weekDate->format('d/m')) ?></span></div><?php endforeach; ?><?php for ($i = 0;$i < 7;$i++):$weekDate = $weekStart->modify('+' . $i . ' days');
|
||||
$date = $weekDate->format('Y-m-d'); ?><div
|
||||
class="agenda-day <?= $date === date('Y-m-d') ? 'bg-yellow-lt' : '' ?>">
|
||||
<div class="agenda-number"><?= h($weekDate->format('d/m')) ?></div>
|
||||
<?php foreach ($byDay[$date] ?? [] as $item):$source = $item['source_type'] ?? 'agenda';
|
||||
$isLinkedReminder = $source !== 'agenda';
|
||||
$eventHref = $isLinkedReminder ? '/animal?id=' . (int)$item['animal_id'] . '#' . ($sourceTabs[$source] ?? 'tab-med') : '/agenda?view=week&date=' . h($weekStart->format('Y-m-d')) . '&edit=' . (int)$item['id']; ?><a
|
||||
class="agenda-event <?= h($colors[$item['item_type']] ?? 'secondary') ?>" href="<?= $eventHref ?>"
|
||||
title="<?= h($item['title']) ?>"><strong><?= $item['all_day'] ? '' : h(substr($item['instance_starts_at'], 11, 5) . ' ') ?><?= h($item['title']) ?></strong><?php if ($item['animal_name']): ?>🐾
|
||||
<?= h($item['animal_name']) ?><?php endif; ?></a><?php endforeach; ?>
|
||||
</div><?php endfor; ?></div>
|
||||
</div>
|
||||
</div><?php endif; ?>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title"><?= h(t('agenda.upcoming')) ?></h3>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-vcenter card-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><?= h(t('common.date')) ?></th>
|
||||
<th><?= h(t('agenda.event')) ?></th>
|
||||
<th><?= h(t('animal.animal')) ?></th>
|
||||
<th><?= h(t('agenda.assigned')) ?></th>
|
||||
<th><?= h(t('agenda.status')) ?></th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody><?php foreach ($upcoming as $item): $source = $item['source_type'] ?? 'agenda';
|
||||
$isLinkedReminder = $source !== 'agenda';
|
||||
$animalHref = '/animal?id=' . (int)$item['animal_id'] . '#' . ($sourceTabs[$source] ?? 'tab-med'); ?><tr>
|
||||
<td><?= h(date('d/m/Y', strtotime($item['instance_starts_at']))) ?><div class="small text-muted">
|
||||
<?= $item['all_day'] ? h(t('agenda.all_day')) : h(substr($item['instance_starts_at'], 11, 5)) ?>
|
||||
</div>
|
||||
</td>
|
||||
<td><strong><?= h($item['title']) ?></strong>
|
||||
<div class="small text-muted">
|
||||
<?= h(t('agenda.type_' . $item['item_type'])) ?><?= $item['location'] ? ' · ' . h($item['location']) : '' ?>
|
||||
</div>
|
||||
</td>
|
||||
<td><?= $item['animal_name'] ? '<a href="' . h($animalHref) . '">' . h($item['animal_name']) . '</a>' : '—' ?>
|
||||
</td>
|
||||
<td><?= h($item['assigned_name'] ?: '—') ?></td>
|
||||
<td><span
|
||||
class="badge bg-<?= h($item['status'] === 'completed' ? 'green' : 'blue') ?>-lt"><?= h(t('agenda.status_' . $item['status'])) ?></span>
|
||||
</td>
|
||||
<td class="text-end"><?php if ($isLinkedReminder): ?><a class="btn btn-sm btn-outline-secondary"
|
||||
href="<?= h($animalHref) ?>"><?= h(t('agenda.open_animal')) ?></a><?php elseif (PermissionService::can('agenda', 'edit')): ?>
|
||||
<div class="d-flex gap-1 justify-content-end"><a class="btn btn-sm btn-outline-secondary"
|
||||
href="/agenda?edit=<?= (int)$item['id'] ?>"><?= h(t('common.edit')) ?></a><?php if ($item['status'] === 'planned'): ?>
|
||||
<form method="post" action="/agenda/status"><input type="hidden" name="id"
|
||||
value="<?= (int)$item['id'] ?>"><input type="hidden" name="status"
|
||||
value="completed"><button class="btn btn-sm btn-outline-success">✓</button></form>
|
||||
<?php endif; ?></div><?php endif; ?></td>
|
||||
</tr><?php endforeach; ?><?php if (!$upcoming): ?><tr>
|
||||
<td colspan="6" class="text-center text-muted py-4"><?= h(t('agenda.none')) ?></td>
|
||||
</tr><?php endif; ?></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<?php if (PermissionService::can('agenda', 'edit')): ?><div class="modal modal-blur fade <?= $edit ? 'show' : '' ?>"
|
||||
id="agendaModal" tabindex="-1"
|
||||
<?= $edit ? 'style="display:block" aria-modal="true" role="dialog"' : 'aria-hidden="true"' ?>>
|
||||
<div class="modal-dialog modal-lg modal-dialog-centered">
|
||||
<form class="modal-content" method="post" action="/agenda/save"><input type="hidden" name="id"
|
||||
value="<?= (int)($edit['id'] ?? 0) ?>">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title"><?= h($edit ? t('agenda.edit') : t('agenda.add')) ?></h5><a class="btn-close"
|
||||
href="/agenda?month=<?= h($cursor->format('Y-m')) ?>" aria-label="<?= h(t('common.close')) ?>"></a>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4"><label class="form-label"><?= h(t('agenda.type')) ?></label><select
|
||||
class="form-select" name="item_type"><?php foreach (AgendaService::TYPES as $type): ?>
|
||||
<option value="<?= h($type) ?>"
|
||||
<?= ($edit['item_type'] ?? 'task') === $type ? 'selected' : '' ?>>
|
||||
<?= h(t('agenda.type_' . $type)) ?></option><?php endforeach; ?></select></div>
|
||||
<div class="col-md-8"><label class="form-label"><?= h(t('common.title')) ?></label><input
|
||||
class="form-control" name="title" required maxlength="180"
|
||||
value="<?= h($edit['title'] ?? '') ?>"></div>
|
||||
<div class="col-md-3"><label class="form-label"><?= h(t('common.date')) ?></label><input
|
||||
class="form-control" type="date" name="date" required
|
||||
value="<?= h($editStart ? $editStart->format('Y-m-d') : date('Y-m-d')) ?>"></div>
|
||||
<div class="col-md-3"><label class="form-label"><?= h(t('agenda.time')) ?></label><input
|
||||
class="form-control" type="time" name="time"
|
||||
value="<?= h($editStart ? $editStart->format('H:i') : '09:00') ?>"></div>
|
||||
<div class="col-md-3"><label class="form-label"><?= h(t('agenda.end_date')) ?></label><input
|
||||
class="form-control" type="date" name="end_date"
|
||||
value="<?= h(!empty($edit['ends_at']) ? substr($edit['ends_at'], 0, 10) : '') ?>"></div>
|
||||
<div class="col-md-3"><label class="form-label"><?= h(t('agenda.end_time')) ?></label><input
|
||||
class="form-control" type="time" name="end_time"
|
||||
value="<?= h(!empty($edit['ends_at']) ? substr($edit['ends_at'], 11, 5) : '') ?>"></div>
|
||||
<div class="col-12"><label class="form-check"><input class="form-check-input" type="checkbox"
|
||||
name="all_day" <?= !empty($edit['all_day']) ? 'checked' : '' ?>><span
|
||||
class="form-check-label"><?= h(t('agenda.all_day')) ?></span></label></div>
|
||||
<div class="col-md-4"><label class="form-label"><?= h(t('agenda.priority')) ?></label><select
|
||||
class="form-select"
|
||||
name="priority"><?php foreach (['low','normal','high','urgent'] as $p): ?><option
|
||||
value="<?= $p ?>" <?= ($edit['priority'] ?? 'normal') === $p ? 'selected' : '' ?>>
|
||||
<?= h(t('agenda.priority_' . $p)) ?></option><?php endforeach; ?></select></div>
|
||||
<div class="col-md-4"><label class="form-label"><?= h(t('agenda.assigned')) ?></label><select
|
||||
class="form-select" name="assigned_user_id">
|
||||
<option value="">—</option><?php foreach ($users as $u): ?><option
|
||||
value="<?= (int)$u['id'] ?>"
|
||||
<?= (int)($edit['assigned_user_id'] ?? 0) === (int)$u['id'] ? 'selected' : '' ?>>
|
||||
<?= h($u['name']) ?></option><?php endforeach; ?>
|
||||
</select></div>
|
||||
<div class="col-md-4"><label class="form-label"><?= h(t('agenda.reminder')) ?></label><select
|
||||
class="form-select" name="reminder_minutes">
|
||||
<option value="">—</option><?php foreach ([0,30,60,1440,2880,10080] as $m): ?><option
|
||||
value="<?= $m ?>"
|
||||
<?= ($edit['reminder_minutes'] ?? '') !== '' && (int)$edit['reminder_minutes'] === $m ? 'selected' : '' ?>>
|
||||
<?= h(t('agenda.reminder_' . $m)) ?></option><?php endforeach; ?>
|
||||
</select></div>
|
||||
<div class="col-md-6"><label class="form-label"><?= h(t('animal.animal')) ?></label><select
|
||||
class="form-select" name="animal_id">
|
||||
<option value="">—</option><?php foreach ($animals as $a): ?><option
|
||||
value="<?= (int)$a['id'] ?>"
|
||||
<?= (int)($edit['animal_id'] ?? 0) === (int)$a['id'] ? 'selected' : '' ?>>
|
||||
<?= h($a['name']) ?></option><?php endforeach; ?>
|
||||
</select></div>
|
||||
<div class="col-md-6"><label class="form-label"><?= h(t('agenda.contact')) ?></label><select
|
||||
class="form-select" name="contact_id">
|
||||
<option value="">—</option><?php foreach ($contacts as $c): ?><option
|
||||
value="<?= (int)$c['id'] ?>"
|
||||
<?= (int)($edit['contact_id'] ?? 0) === (int)$c['id'] ? 'selected' : '' ?>>
|
||||
<?= h($c['name']) ?></option><?php endforeach; ?>
|
||||
</select></div>
|
||||
<div class="col-md-6"><label class="form-label"><?= h(t('agenda.location')) ?></label><input
|
||||
class="form-control" name="location" value="<?= h($edit['location'] ?? '') ?>"></div>
|
||||
<div class="col-md-3"><label class="form-label"><?= h(t('agenda.repeat')) ?></label><select
|
||||
class="form-select"
|
||||
name="repeat_rule"><?php foreach (['none','daily','weekly','monthly'] as $r): ?><option
|
||||
value="<?= $r ?>" <?= ($edit['repeat_rule'] ?? 'none') === $r ? 'selected' : '' ?>>
|
||||
<?= h(t('agenda.repeat_' . $r)) ?></option><?php endforeach; ?></select></div>
|
||||
<div class="col-md-3"><label class="form-label"><?= h(t('agenda.repeat_until')) ?></label><input
|
||||
class="form-control" type="date" name="repeat_until"
|
||||
value="<?= h($edit['repeat_until'] ?? '') ?>"></div>
|
||||
<div class="col-12"><label class="form-label"><?= h(t('common.description')) ?></label><textarea
|
||||
class="form-control" name="description"
|
||||
rows="3"><?= h($edit['description'] ?? '') ?></textarea></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer"><a class="btn me-auto" href="/agenda"><?= h(t('common.cancel')) ?></a><button
|
||||
class="btn btn-primary"><?= h(t('common.save')) ?></button></div>
|
||||
</form>
|
||||
</div>
|
||||
</div><?php if ($edit): ?><div class="modal-backdrop fade show"></div><?php endif; ?><?php endif; ?>
|
||||
93
app/Views/animal_adoption.php
Normal file
93
app/Views/animal_adoption.php
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
<div class="modal fade" id="modalAdoption" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<form method="post" action="/animal/adopt" class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title"><?= h(t('adoption.modal_title')) ?></h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<input type="hidden" name="animal_id" value="<?= (int)$animal['id'] ?>">
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><?= h(t('adoption.directory_adopter')) ?></label>
|
||||
<select class="form-select" name="adopter_contact_id" id="adopterContactSelect">
|
||||
<option value="new">+ <?= h(t('adoption.new_adopter')) ?></option>
|
||||
<?php foreach (($adopters ?? []) as $contact): ?>
|
||||
<option value="<?= (int)$contact['id'] ?>" data-name="<?= h($contact['name']) ?>"
|
||||
data-phone="<?= h($contact['phone']) ?>" data-email="<?= h($contact['email']) ?>"
|
||||
data-address="<?= h($contact['address']) ?>" data-postal="<?= h($contact['postal_code']) ?>"
|
||||
data-city="<?= h($contact['city']) ?>">
|
||||
<?= h($contact['name']) ?><?= $contact['city'] ? ' · ' . h($contact['city']) : '' ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<div class="form-hint"><?= h(t('adoption.contact_help')) ?></div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><?= h(t('adoption.adopter_name')) ?></label>
|
||||
<input type="text" name="adopter_name" id="adopterName" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><?= h(t('common.address')) ?></label>
|
||||
<input type="text" name="adopter_address" id="adopterAddress" class="form-control">
|
||||
</div>
|
||||
|
||||
<div class="mb-3 row">
|
||||
<div class="col">
|
||||
<label class="form-label"><?= h(t('directory.postal_code')) ?></label>
|
||||
<input type="text" name="adopter_postal_code" id="adopterPostal" class="form-control">
|
||||
</div>
|
||||
<div class="col">
|
||||
<label class="form-label"><?= h(t('directory.city')) ?></label>
|
||||
<input type="text" name="adopter_city" id="adopterCity" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><?= h(t('common.phone')) ?></label>
|
||||
<input type="text" name="adopter_phone" id="adopterPhone" class="form-control">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><?= h(t('common.email')) ?></label>
|
||||
<input type="email" name="adopter_email" id="adopterEmail" class="form-control">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><?= h(t('adoption.date')) ?></label>
|
||||
<input type="date" name="adoption_date" class="form-control" value="<?= date('Y-m-d') ?>" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><?= h(t('common.notes')) ?></label>
|
||||
<textarea name="notes" class="form-control" rows="3"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const select = document.getElementById('adopterContactSelect');
|
||||
select?.addEventListener('change', () => {
|
||||
const option = select.options[select.selectedIndex];
|
||||
const values = select.value === 'new' ? {} : option.dataset;
|
||||
document.getElementById('adopterName').value = values.name || '';
|
||||
document.getElementById('adopterPhone').value = values.phone || '';
|
||||
document.getElementById('adopterEmail').value = values.email || '';
|
||||
document.getElementById('adopterAddress').value = values.address || '';
|
||||
document.getElementById('adopterPostal').value = values.postal || '';
|
||||
document.getElementById('adopterCity').value = values.city || '';
|
||||
});
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary"
|
||||
data-bs-dismiss="modal"><?= h(t('common.cancel')) ?></button>
|
||||
<button type="submit" class="btn btn-success"><?= h(t('adoption.validate')) ?></button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
99
app/Views/animal_death_form.php
Normal file
99
app/Views/animal_death_form.php
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
<div class="row justify-content-center">
|
||||
<div class="col-xl-9">
|
||||
<div class="d-flex align-items-center justify-content-between mb-3">
|
||||
<div>
|
||||
<h2 class="mb-1"><?= h(t('death.record', ['name' => $animal['name']])) ?></h2>
|
||||
<div class="text-muted"><?= h(t('death.intro')) ?></div>
|
||||
</div><a class="btn btn-outline-secondary" href="/animal?id=<?= (int)$animal['id'] ?>">←
|
||||
<?= h(t('animal.back_record')) ?></a>
|
||||
</div>
|
||||
|
||||
<form method="post" action="/animal/death/save" class="card border-dark overflow-hidden">
|
||||
<input type="hidden" name="animal_id" value="<?= (int)$animal['id'] ?>">
|
||||
<div class="card-status-top bg-dark"></div>
|
||||
<div class="card-body">
|
||||
<div class="alert alert-info"><strong><?= h(t('death.important')) ?></strong>
|
||||
<?= h(t('death.important_help')) ?></div>
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4"><label class="form-label required"><?= h(t('death.date')) ?></label><input
|
||||
class="form-control" type="date" name="deceased_date"
|
||||
value="<?= h($death['deceased_date']) ?>" required></div>
|
||||
<div class="col-md-4"><label
|
||||
class="form-label required"><?= h(t('death.main_cause')) ?></label><select
|
||||
class="form-select" name="cause_code" required><?php foreach ($causes as $key => $label): ?>
|
||||
<option value="<?= h($key) ?>" <?= $death['cause_code'] === $key ? 'selected' : '' ?>>
|
||||
<?= h(DeathController::causeLabel($key)) ?></option><?php endforeach; ?></select></div>
|
||||
<div class="col-md-4 d-flex align-items-end"><label class="form-check mb-2"><input
|
||||
class="form-check-input" type="checkbox" name="euthanized"
|
||||
<?= !empty($death['euthanized']) ? 'checked' : '' ?>><span
|
||||
class="form-check-label"><?= h(t('death.euthanized')) ?></span></label></div>
|
||||
<div class="col-12"><label class="form-label"><?= h(t('death.cause_details')) ?></label><textarea
|
||||
class="form-control" name="cause_details" rows="2"
|
||||
placeholder="<?= h(t('death.cause_placeholder')) ?>"><?= h($death['cause_details']) ?></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="my-4">
|
||||
<h3 class="card-title mb-3"><?= h(t('death.context_place')) ?></h3>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-5"><label class="form-label required"><?= h(t('death.place')) ?></label><select
|
||||
class="form-select" name="place_type" required><?php foreach ($places as $key => $label): ?>
|
||||
<option value="<?= h($key) ?>" <?= $death['place_type'] === $key ? 'selected' : '' ?>>
|
||||
<?= h(DeathController::placeLabel($key)) ?></option><?php endforeach; ?></select></div>
|
||||
<div class="col-md-7"><label class="form-label"><?= h(t('death.place_details')) ?></label><input
|
||||
class="form-control" name="place_details" value="<?= h($death['place_details']) ?>"
|
||||
placeholder="<?= h(t('death.place_placeholder')) ?>"></div>
|
||||
<div class="col-12"><label class="form-check form-switch"><input class="form-check-input"
|
||||
type="checkbox" name="occurred_in_care"
|
||||
<?= !empty($death['occurred_in_care']) ? 'checked' : '' ?>><span
|
||||
class="form-check-label"><strong><?= h(t('death.in_care')) ?></strong><br><span
|
||||
class="text-muted small"><?= h(t('death.in_care_help')) ?></span></span></label>
|
||||
</div>
|
||||
<div class="col-md-6"><label class="form-label"><?= h(t('death.veterinarian')) ?></label><select
|
||||
class="form-select" name="veterinarian_contact_id">
|
||||
<option value="">— <?= h(t('common.not_provided_option')) ?> —</option>
|
||||
<?php foreach ($veterinarians as $contact): ?><option value="<?= (int)$contact['id'] ?>"
|
||||
<?= (int)$death['veterinarian_contact_id'] === (int)$contact['id'] ? 'selected' : '' ?>>
|
||||
<?= h($contact['name']) ?><?= $contact['city'] ? ' — ' . h($contact['city']) : '' ?>
|
||||
</option><?php endforeach; ?>
|
||||
</select></div>
|
||||
</div>
|
||||
|
||||
<hr class="my-4">
|
||||
<h3 class="card-title mb-3"><?= h(t('death.care_body')) ?></h3>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-5"><label
|
||||
class="form-label required"><?= h(t('death.body_disposition')) ?></label><select
|
||||
class="form-select" name="body_disposition"
|
||||
required><?php foreach ($dispositions as $key => $label): ?><option value="<?= h($key) ?>"
|
||||
<?= $death['body_disposition'] === $key ? 'selected' : '' ?>>
|
||||
<?= h(DeathController::dispositionLabel($key)) ?></option><?php endforeach; ?></select>
|
||||
</div>
|
||||
<div class="col-md-5"><label class="form-label"><?= h(t('death.crematorium')) ?></label><select
|
||||
class="form-select" name="crematorium_contact_id">
|
||||
<option value="">— <?= h(t('common.not_provided_option')) ?> —</option>
|
||||
<?php foreach ($crematoriums as $contact): ?><option value="<?= (int)$contact['id'] ?>"
|
||||
<?= (int)$death['crematorium_contact_id'] === (int)$contact['id'] ? 'selected' : '' ?>>
|
||||
<?= h($contact['name']) ?><?= $contact['city'] ? ' — ' . h($contact['city']) : '' ?>
|
||||
</option><?php endforeach; ?>
|
||||
</select>
|
||||
<div class="form-hint"><?= h(t('death.crematorium_help')) ?></div>
|
||||
</div>
|
||||
<div class="col-md-2"><label class="form-label"><?= h(t('death.cremation_date')) ?></label><input
|
||||
class="form-control" type="date" name="cremation_date"
|
||||
value="<?= h($death['cremation_date']) ?>"></div>
|
||||
<div class="col-12"><label class="form-label"><?= h(t('death.internal_notes')) ?></label><textarea
|
||||
class="form-control" name="notes" rows="3"
|
||||
placeholder="<?= h(t('death.notes_placeholder')) ?>"><?= h($death['notes']) ?></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer d-flex"><a class="btn btn-link"
|
||||
href="/animal?id=<?= (int)$animal['id'] ?>"><?= h(t('common.cancel')) ?></a><button
|
||||
class="btn btn-dark ms-auto"
|
||||
onclick="return confirm(<?= h(json_encode(t('death.confirm'), JSON_HEX_APOS | JSON_HEX_QUOT)) ?>);"><?= h(t('death.save')) ?></button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
717
app/Views/animal_form.php
Normal file
717
app/Views/animal_form.php
Normal file
|
|
@ -0,0 +1,717 @@
|
|||
<?php
|
||||
/** @var array $animal */
|
||||
/** @var array $statuses */
|
||||
/** @var array $speciesOptions */
|
||||
|
||||
// Liste races (tu pourras l’externaliser plus tard en table ref)
|
||||
$breeds = [
|
||||
'Chat Européen',
|
||||
'Abyssin','Bengal','British Shorthair','Chartreux','Maine Coon','Norvégien',
|
||||
'Persan','Ragdoll','Siamois','Sphynx','Sacré de Birmanie','Scottish Fold',
|
||||
'Autre / Inconnu',
|
||||
];
|
||||
|
||||
$statuses = ['refuge' => t('status.refuge'),'fa' => t('status.fa'),'fa_permanente' => t('status.fa_permanente'),'chat_libre' => t('status.chat_libre'),'quarantaine' => t('status.quarantaine'),'soin' => t('status.soin'),'isolation' => t('status.isolation'),'hospitalise' => t('status.hospitalise'),'reserve' => t('status.reserve'),'adopte' => t('status.adopte'),'enfui' => t('status.enfui')];
|
||||
if (in_array((string)($animal['status'] ?? ''), ['decede','décédé'], true)) {
|
||||
$statuses['decede'] = t('status.decede');
|
||||
}
|
||||
|
||||
$sexLabels = [
|
||||
'M' => t('sex.male'),
|
||||
'F' => t('sex.female'),
|
||||
'U' => t('animals.to_define'),
|
||||
];
|
||||
|
||||
$hairTypes = [
|
||||
'' => t('animals.to_define'),
|
||||
'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'),
|
||||
];
|
||||
|
||||
$lifeProfileOptions = [
|
||||
'unknown' => t('animals.test_unknown'),
|
||||
'yes' => 'Oui',
|
||||
'no' => 'Non',
|
||||
];
|
||||
|
||||
$coatColors = [
|
||||
'Bicolore Noir / Blanc',
|
||||
'Bicolore Gris / Blanc',
|
||||
'Bicolore Roux / Blanc',
|
||||
'Bicolore Tigré et Blanc',
|
||||
'Gris',
|
||||
'Roux',
|
||||
'Noir',
|
||||
'Chocolat',
|
||||
'Blanc',
|
||||
'Tricolore / Calico',
|
||||
'Écaille de tortue',
|
||||
'Colourpoint / Siamois',
|
||||
'Tigré / Tabby',
|
||||
];
|
||||
|
||||
$breed = (string)($animal['breed'] ?? '');
|
||||
|
||||
|
||||
$id = (int)($animal['id'] ?? 0);
|
||||
$isEdit = $id > 0;
|
||||
$isArchivedEdit = $isEdit && !empty($animal['archived_at']);
|
||||
|
||||
$status = (string)($animal['status'] ?? 'refuge');
|
||||
$sex = (string)($animal['sex'] ?? 'U');
|
||||
|
||||
$birth_is_estimated = (int)($animal['birth_is_estimated'] ?? 0);
|
||||
$birth_estimated_months = (int)($animal['birth_estimated_months'] ?? 0);
|
||||
|
||||
$estYears = $birth_estimated_months > 0 ? intdiv($birth_estimated_months, 12) : '';
|
||||
$estMonths = $birth_estimated_months > 0 ? ($birth_estimated_months % 12) : '';
|
||||
|
||||
?>
|
||||
<div class="row row-cards">
|
||||
<div class="col-12">
|
||||
<form method="post" action="/animal/save" class="card">
|
||||
|
||||
<div class="card-body">
|
||||
<input type="hidden" name="id" value="<?= $id ?>">
|
||||
<?php if ($isArchivedEdit): ?><input type="hidden" name="status" value="<?= h($status) ?>">
|
||||
<div class="alert alert-info">📦 <?= h(t('animal.archive_edit_help')) ?></div><?php endif; ?>
|
||||
|
||||
<div class="row g-3">
|
||||
|
||||
<!-- IDENTITÉ -->
|
||||
<div class="col-12">
|
||||
<div class="card card-sm">
|
||||
<div class="card-status-top bg-primary"></div>
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<span class="status status-blue me-2"></span>
|
||||
<h4 class="m-0"><?= h(t('animal.tab.identity')) ?></h4>
|
||||
</div>
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-12 col-md-8">
|
||||
<label class="form-label"><?= h(t('common.name')) ?></label>
|
||||
<input class="form-control" name="name" value="<?= h($animal['name'] ?? '') ?>"
|
||||
required>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-4">
|
||||
<label class="form-label required"><?= h(t('animal.species')) ?></label>
|
||||
<select class="form-select" name="species" id="animalSpecies" required>
|
||||
<?php foreach (($speciesOptions ?? []) as $option): ?>
|
||||
<option value="<?= h($option['code']) ?>"
|
||||
data-category="<?= h($option['category']) ?>"
|
||||
<?= SpeciesService::normalize((string)($animal['species'] ?? 'chat')) === $option['code'] ? 'selected' : '' ?>>
|
||||
<?= h($option['icon'] . ' ' . $option['name']) ?><?= empty($option['active']) ? ' (' . h(t('common.disabled')) . ')' : '' ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<div class="form-hint"><?= h(t('animal.form.species_hint')) ?></div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-4">
|
||||
<label class="form-label"><?= h(t('common.weight')) ?></label>
|
||||
<input class="form-control" name="initial_weight" type="number" step="0.01"
|
||||
min="0" value="<?= h($animal['initial_weight'] ?? '') ?>"
|
||||
placeholder="ex: 4.25">
|
||||
<div class="form-hint"><?= h(t('animal.form.weight_hint')) ?></div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-4">
|
||||
<label class="form-label"><?= h(t('animal.status')) ?></label>
|
||||
<select class="form-select" name="status_display" id="status"
|
||||
<?= $isArchivedEdit ? 'disabled' : '' ?>>
|
||||
<?php foreach ($statuses as $k => $label): ?>
|
||||
<option value="<?= h($k) ?>" <?= ($status === $k) ? 'selected' : '' ?>>
|
||||
<?= h($label) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<div class="form-hint"><?= h(t('animal.form.status_hint')) ?></div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-4" id="refugeRoomField">
|
||||
<label class="form-label"><?= h(t('animal.form.shelter_room')) ?></label>
|
||||
<select class="form-select" name="refuge_room"
|
||||
<?= $isArchivedEdit ? 'disabled' : '' ?>>
|
||||
<option value=""><?= h(t('animal.assign')) ?></option>
|
||||
<?php foreach (($shelterRooms ?? []) as $room): ?><option
|
||||
value="<?= h($room['code']) ?>"
|
||||
data-status="<?= h($room['status_code']) ?>"
|
||||
<?= (($animal['refuge_room'] ?? '') === $room['code']) ? 'selected' : '' ?>>
|
||||
<?= h($room['name']) ?></option><?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-4" id="fosterContactField">
|
||||
<label class="form-label"><?= h(t('animal.form.foster')) ?></label>
|
||||
<select class="form-select" name="foster_contact_id">
|
||||
<option value=""><?= h(t('animal.form.provide_later')) ?></option>
|
||||
<?php foreach (($fosterContacts ?? []) as $contact): ?>
|
||||
<option value="<?= (int)$contact['id'] ?>"
|
||||
<?= (int)($currentFosterId ?? 0) === (int)$contact['id'] ? 'selected' : '' ?>>
|
||||
<?= h($contact['name'] . (!empty($contact['city']) ? ' · ' . $contact['city'] : '')) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<div class="form-hint"><?= h(t('animal.form.foster_hint')) ?></div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 d-none" id="freeCatFields">
|
||||
<div class="card bg-green-lt border-green-lt">
|
||||
<div class="card-body py-3">
|
||||
<div class="d-flex align-items-center gap-2 mb-3"><span
|
||||
aria-hidden="true">🌿</span>
|
||||
<div><strong><?= h(t('freecat.journey')) ?></strong>
|
||||
<div class="text-muted small">
|
||||
<?= h(t('freecat.journey_help')) ?></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6"><label
|
||||
class="form-label"><?= h(t('freecat.site')) ?></label><input
|
||||
class="form-control" name="free_cat_site_name"
|
||||
value="<?= h($animal['free_cat_site_name'] ?? '') ?>"
|
||||
maxlength="180"
|
||||
placeholder="<?= h(t('freecat.site_example')) ?>"></div>
|
||||
<div class="col-md-3"><label
|
||||
class="form-label"><?= h(t('common.city')) ?></label><input
|
||||
class="form-control" name="free_cat_city"
|
||||
value="<?= h($animal['free_cat_city'] ?? '') ?>"
|
||||
maxlength="120"></div>
|
||||
<div class="col-md-3"><label
|
||||
class="form-label"><?= h(t('freecat.tracking_state')) ?></label><select
|
||||
class="form-select"
|
||||
name="free_cat_tracking_status"><?php foreach (['present' => t('freecat.present'),'missing' => t('freecat.missing'),'moved' => t('freecat.moved')] as $key => $label): ?>
|
||||
<option value="<?= h($key) ?>"
|
||||
<?= ($animal['free_cat_tracking_status'] ?? 'present') === $key ? 'selected' : '' ?>>
|
||||
<?= h($label) ?></option><?php endforeach; ?></select>
|
||||
<div class="form-hint"><?= h(t('freecat.death_help')) ?></div>
|
||||
</div>
|
||||
<div class="col-md-6"><label
|
||||
class="form-label"><?= h(t('freecat.precise_location')) ?>
|
||||
<span
|
||||
class="text-muted">(<?= h(t('common.internal')) ?>)</span></label><input
|
||||
class="form-control" name="free_cat_address"
|
||||
value="<?= h($animal['free_cat_address'] ?? '') ?>"
|
||||
maxlength="255"
|
||||
placeholder="<?= h(t('freecat.address_placeholder')) ?>">
|
||||
</div>
|
||||
<div class="col-md-6"><label
|
||||
class="form-label"><?= h(t('freecat.caretaker')) ?></label><select
|
||||
class="form-select" name="free_cat_caretaker_contact_id">
|
||||
<option value=""><?= h(t('common.not_provided')) ?></option>
|
||||
<?php foreach (($freeCatCaretakers ?? []) as $contact): ?>
|
||||
<option value="<?= (int)$contact['id'] ?>"
|
||||
<?= (int)($animal['free_cat_caretaker_contact_id'] ?? 0) === (int)$contact['id'] ? 'selected' : '' ?>>
|
||||
<?= h($contact['name'] . (!empty($contact['city']) ? ' · ' . $contact['city'] : '')) ?>
|
||||
</option><?php endforeach; ?>
|
||||
</select>
|
||||
<div class="form-hint"><?= h(t('freecat.directory_person')) ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6"><label
|
||||
class="form-label"><?= h(t('freecat.capture_date')) ?></label><input
|
||||
class="form-control" type="date" name="free_cat_captured_at"
|
||||
value="<?= h($animal['free_cat_captured_at'] ?? '') ?>">
|
||||
</div>
|
||||
<div class="col-md-6"><label
|
||||
class="form-label"><?= h(t('freecat.release_date')) ?></label><input
|
||||
class="form-control" type="date" name="free_cat_released_at"
|
||||
value="<?= h($animal['free_cat_released_at'] ?? '') ?>">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($animal['id'])): ?>
|
||||
<div class="col-12 col-md-4">
|
||||
<label class="form-label"><?= h(t('animal.form.location_reason')) ?></label>
|
||||
<input class="form-control" name="location_change_reason" maxlength="255"
|
||||
placeholder="Facultatif : changement de salle, retour de FA…"
|
||||
<?= $isArchivedEdit ? 'disabled' : '' ?>>
|
||||
<div class="form-hint"><?= h(t('animal.form.location_reason_hint')) ?></div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="col-12 col-md-4">
|
||||
<label class="form-label"><?= h(t('animal.breed')) ?></label>
|
||||
<input class="form-control" name="breed" list="breedSuggestions"
|
||||
value="<?= h($breed) ?>" placeholder="Race ou type, facultatif">
|
||||
<datalist id="breedSuggestions"><?php foreach ($breeds as $b): ?><option
|
||||
value="<?= h($b) ?>"><?php endforeach; ?></datalist>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-6">
|
||||
<label class="form-label"><?= h(t('animal.coat')) ?></label>
|
||||
<?php $currentColor = trim((string)($animal['color'] ?? '')); ?>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><span id="coatColorSwatch"
|
||||
class="coat-swatch"
|
||||
style="<?= h(coat_swatch_style($currentColor)) ?>"></span></span>
|
||||
<select class="form-select" name="color" id="coatColorSelect">
|
||||
<option value="" data-swatch="<?= h(coat_swatch_style('')) ?>"
|
||||
<?= $currentColor === '' ? 'selected' : '' ?>>
|
||||
<?= h(t('animals.to_define')) ?></option>
|
||||
<?php if ($currentColor !== '' && !in_array($currentColor, $coatColors, true)): ?>
|
||||
<option value="<?= h($currentColor) ?>"
|
||||
data-swatch="<?= h(coat_swatch_style($currentColor)) ?>" selected>
|
||||
<?= h($currentColor) ?> (valeur actuelle)</option>
|
||||
<?php endif; ?>
|
||||
<?php foreach ($coatColors as $coatColor): ?>
|
||||
<option value="<?= h($coatColor) ?>"
|
||||
data-swatch="<?= h(coat_swatch_style($coatColor)) ?>"
|
||||
<?= $currentColor === $coatColor ? 'selected' : '' ?>>
|
||||
<?= h($coatColor) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-6">
|
||||
<label class="form-label"><?= h(t('animal.form.hair_type')) ?></label>
|
||||
<select class="form-select" name="hair_type">
|
||||
<?php foreach ($hairTypes as $value => $label): ?>
|
||||
<option value="<?= h($value) ?>"
|
||||
<?= ((string)($animal['hair_type'] ?? '') === $value) ? 'selected' : '' ?>>
|
||||
<?= h($label) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-3">
|
||||
<label class="form-label"><?= h(t('animal.sex')) ?></label>
|
||||
<select class="form-select" name="sex">
|
||||
<option value="U" <?= $sex === 'U' ? 'selected' : '' ?>>
|
||||
<?= h(t('animals.to_define')) ?></option>
|
||||
<option value="M" <?= $sex === 'M' ? 'selected' : '' ?>>
|
||||
<?= h(t('sex.male')) ?></option>
|
||||
<option value="F" <?= $sex === 'F' ? 'selected' : '' ?>>
|
||||
<?= h(t('sex.female')) ?></option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-3">
|
||||
<label class="form-label"><?= h(t('animal.form.sterilization')) ?></label>
|
||||
<select class="form-select" name="sterilization_status"
|
||||
id="sterilizationStatus">
|
||||
<option value="unknown"
|
||||
<?= (($animal['sterilization_status'] ?? 'unknown') === 'unknown') ? 'selected' : '' ?>>
|
||||
<?= h(t('common.unknown')) ?></option>
|
||||
<option value="no"
|
||||
<?= (($animal['sterilization_status'] ?? 'unknown') === 'no') ? 'selected' : '' ?>>
|
||||
<?= h(t('animals.not_sterilized')) ?></option>
|
||||
<option value="yes"
|
||||
<?= (($animal['sterilization_status'] ?? 'unknown') === 'yes') ? 'selected' : '' ?>>
|
||||
<?= h(t('animal.sterilized')) ?></option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-12 row g-3 m-0 p-0 <?= (($animal['sterilization_status'] ?? 'unknown') === 'yes') ? '' : 'd-none' ?>"
|
||||
id="sterilizationDetails">
|
||||
<div class="col-md-4"><label
|
||||
class="form-label"><?= h(t('animal.form.sterilization_date')) ?></label><input
|
||||
class="form-control" type="date" name="sterilization_date"
|
||||
value="<?= h($animal['sterilization_date'] ?? '') ?>"></div>
|
||||
<div class="col-md-4"><label
|
||||
class="form-label"><?= h(t('animal.form.veterinarian')) ?></label><select
|
||||
class="form-select" name="sterilization_vet_contact_id">
|
||||
<option value=""><?= h(t('common.not_provided')) ?></option>
|
||||
<?php foreach (($sterilizationVets ?? []) as $contact): ?><option
|
||||
value="<?= (int)$contact['id'] ?>"
|
||||
<?= ((int)($animal['sterilization_vet_contact_id'] ?? 0) === (int)$contact['id']) ? 'selected' : '' ?>>
|
||||
<?= h($contact['name']) ?></option><?php endforeach; ?>
|
||||
</select></div>
|
||||
<div class="col-md-4"><label
|
||||
class="form-label"><?= h(t('animal.form.clinic')) ?></label><select
|
||||
class="form-select" name="sterilization_clinic_contact_id">
|
||||
<option value=""><?= h(t('common.not_provided')) ?></option>
|
||||
<?php foreach (($sterilizationClinics ?? []) as $contact): ?><option
|
||||
value="<?= (int)$contact['id'] ?>"
|
||||
<?= ((int)($animal['sterilization_clinic_contact_id'] ?? 0) === (int)$contact['id']) ? 'selected' : '' ?>>
|
||||
<?= h($contact['name']) ?></option><?php endforeach; ?>
|
||||
</select></div>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<hr class="my-1">
|
||||
<h5 class="mb-0"><?= h(t('animal.form.identification')) ?></h5>
|
||||
</div>
|
||||
<div class="col-12 col-md-3"><label
|
||||
class="form-label"><?= h(t('animal.form.type')) ?></label><select
|
||||
class="form-select" name="identification_type">
|
||||
<option value="unknown"
|
||||
<?= (($animal['identification_type'] ?? 'unknown') === 'unknown') ? 'selected' : '' ?>>
|
||||
<?= h(t('animal.form.none_identification')) ?></option>
|
||||
<option value="microchip"
|
||||
<?= (($animal['identification_type'] ?? 'unknown') === 'microchip') ? 'selected' : '' ?>>
|
||||
<?= h(t('animal.microchip')) ?></option>
|
||||
<option value="tattoo"
|
||||
<?= (($animal['identification_type'] ?? 'unknown') === 'tattoo') ? 'selected' : '' ?>>
|
||||
<?= h(t('animal.tattoo')) ?></option>
|
||||
</select></div>
|
||||
<div class="col-12 col-md-3"><label
|
||||
class="form-label"><?= h(t('animal.form.number')) ?></label><input
|
||||
class="form-control" name="chip_id"
|
||||
value="<?= h($animal['chip_id'] ?? '') ?>"
|
||||
placeholder="<?= h(t('animal.form.chip_number')) ?>"></div>
|
||||
<div class="col-12 col-md-3"><label
|
||||
class="form-label"><?= h(t('animal.form.implant_date')) ?></label><input
|
||||
class="form-control" type="date" lang="<?= h(I18n::locale()) ?>"
|
||||
name="identification_date"
|
||||
value="<?= h($animal['identification_date'] ?? '') ?>"></div>
|
||||
<div class="col-12 col-md-3"><label
|
||||
class="form-label"><?= h(t('animal.form.registration')) ?></label><select
|
||||
class="form-select" name="identification_registration_status">
|
||||
<option value="unknown"
|
||||
<?= (($animal['identification_registration_status'] ?? 'unknown') === 'unknown') ? 'selected' : '' ?>>
|
||||
<?= h(t('common.unknown')) ?></option>
|
||||
<option value="pending"
|
||||
<?= (($animal['identification_registration_status'] ?? 'unknown') === 'pending') ? 'selected' : '' ?>>
|
||||
<?= h(t('animal.form.pending')) ?></option>
|
||||
<option value="registered"
|
||||
<?= (($animal['identification_registration_status'] ?? 'unknown') === 'registered') ? 'selected' : '' ?>>
|
||||
<?= h(t('animal.form.registered')) ?></option>
|
||||
</select></div>
|
||||
|
||||
<div class="col-12">
|
||||
<hr class="my-1">
|
||||
<h5 class="mb-0"><?= h(t('animal.form.adoption')) ?></h5>
|
||||
</div>
|
||||
<div class="col-12 col-md-4"><label
|
||||
class="form-label"><?= h(t('animal.form.availability')) ?></label><select
|
||||
class="form-select" name="adoption_availability" id="adoptionAvailability">
|
||||
<option value="unknown"
|
||||
<?= (($animal['adoption_availability'] ?? 'unknown') === 'unknown') ? 'selected' : '' ?>>
|
||||
<?= h(t('animals.to_define')) ?></option>
|
||||
<option value="not_available"
|
||||
<?= (($animal['adoption_availability'] ?? 'unknown') === 'not_available') ? 'selected' : '' ?>>
|
||||
<?= h(t('animal.form.not_available')) ?></option>
|
||||
<option value="available"
|
||||
<?= (($animal['adoption_availability'] ?? 'unknown') === 'available') ? 'selected' : '' ?>>
|
||||
<?= h(t('animals.available_title')) ?></option>
|
||||
</select></div>
|
||||
<div class="col-12 col-md-4" id="adoptionAvailableFrom"><label
|
||||
class="form-label"><?= h(t('animal.form.available_from')) ?></label><input
|
||||
class="form-control" type="date" lang="<?= h(I18n::locale()) ?>"
|
||||
name="adoption_available_from"
|
||||
value="<?= h($animal['adoption_available_from'] ?? '') ?>"></div>
|
||||
<div class="col-12 col-md-4" id="adoptionUnavailableReason"><label
|
||||
class="form-label"><?= h(t('animal.form.unavailable_reason')) ?></label><input
|
||||
class="form-control" name="adoption_unavailability_reason" maxlength="255"
|
||||
value="<?= h($animal['adoption_unavailability_reason'] ?? '') ?>"
|
||||
placeholder="Soins, sociabilisation…"></div>
|
||||
<div class="col-12">
|
||||
<div class="card card-sm bg-green-lt">
|
||||
<div class="card-body"><label class="form-check"><input
|
||||
class="form-check-input" type="checkbox"
|
||||
name="website_published" value="1"
|
||||
<?= !empty($animal['website_published']) ? 'checked' : '' ?>><span
|
||||
class="form-check-label fw-bold"><?= h(t('public_site.publish_animal')) ?></span></label>
|
||||
<div class="form-hint mb-2">
|
||||
<?= h(t('public_site.publish_animal_help')) ?></div><label
|
||||
class="form-label"><?= h(t('public_site.public_description')) ?></label><textarea
|
||||
class="form-control" name="website_description" rows="4"
|
||||
maxlength="5000"
|
||||
placeholder="<?= h(t('public_site.public_description_example')) ?>"><?= h($animal['website_description'] ?? '') ?></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<div class="text-muted"><?= h(t('animal.rescue_location')) ?></div>
|
||||
<input class="form-control" type="text" name="rescue_address"
|
||||
value="<?= h($animal['rescue_address'] ?? '') ?>"
|
||||
placeholder="<?= h(t('animal.rescue_location_placeholder')) ?>">
|
||||
<div class="text-muted small mt-1"><?= h(t('animal.geocode_help')) ?></div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<div class="text-muted"><?= h(t('animal.form.current_location')) ?></div>
|
||||
<input class="form-control" type="text" name="current_address"
|
||||
value="<?= h($animal['current_address'] ?? '5 RUE NOTRE DAME 29260 LESNEVEN') ?>"
|
||||
<?= $isArchivedEdit ? 'readonly' : '' ?> disabled>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- PROFIL DE VIE -->
|
||||
<div class="col-12">
|
||||
<div class="card card-sm">
|
||||
<div class="card-status-top bg-cyan"></div>
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<span class="status status-cyan me-2"></span>
|
||||
<div>
|
||||
<h4 class="m-0"><?= h(t('animal.form.life_profile')) ?></h4>
|
||||
<div class="text-muted small"><?= h(t('animal.form.life_profile_hint')) ?></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
<?php foreach ([
|
||||
'compatibility_dogs' => ['🐶',t('animal.compatible_dogs')],
|
||||
'compatibility_cats' => ['🐱',t('animal.compatible_cats')],
|
||||
'compatibility_children' => ['🧒',t('animal.compatible_children')],
|
||||
'house_trained' => ['🧼',t('animal.house_trained')],
|
||||
] as $field => $profile): ?>
|
||||
<div class="col-12 col-sm-6 col-lg-3">
|
||||
<label class="form-label"><span aria-hidden="true"><?= $profile[0] ?></span>
|
||||
<?= h($profile[1]) ?></label>
|
||||
<select class="form-select" name="<?= h($field) ?>">
|
||||
<?php foreach ($lifeProfileOptions as $value => $label): ?><option
|
||||
value="<?= h($value) ?>"
|
||||
<?= (($animal[$field] ?? 'unknown') === $value) ? 'selected' : '' ?>>
|
||||
<?= h($label) ?></option><?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ÂGE -->
|
||||
<div class="col-12">
|
||||
<div class="card card-sm">
|
||||
<div class="card-status-top bg-red"></div>
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<span class="status status-red me-2"></span>
|
||||
<h4 class="m-0"><?= h(t('animal.form.age')) ?></h4>
|
||||
</div>
|
||||
|
||||
<div class="row g-3 align-items-start">
|
||||
<div class="col-12 col-md-5">
|
||||
<label class="form-label"><?= h(t('animal.form.birth_date')) ?></label>
|
||||
<input class="form-control" type="date" name="birth_date"
|
||||
value="<?= h($animal['birth_date'] ?? '') ?>">
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-2">
|
||||
<label class="form-label"><?= h(t('animal.form.estimated_birth')) ?></label>
|
||||
<div class="pt-2">
|
||||
<label class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox"
|
||||
name="birth_is_estimated" value="1" id="birth_is_estimated"
|
||||
<?= $birth_is_estimated ? 'checked' : '' ?>>
|
||||
<span class="form-check-label">Oui</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-6 col-md-2">
|
||||
<label class="form-label"><?= h(t('animal.form.estimated_years')) ?></label>
|
||||
<input class="form-control" type="number" min="0" max="40"
|
||||
name="birth_est_years" value="<?= h($estYears) ?>" placeholder="ex: 7">
|
||||
</div>
|
||||
|
||||
<div class="col-6 col-md-3">
|
||||
<label class="form-label"><?= h(t('animal.form.estimated_months')) ?></label>
|
||||
<input class="form-control" type="number" min="0" max="11"
|
||||
name="birth_est_months" value="<?= h($estMonths) ?>" placeholder="ex: 4">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- LOCALISATION -->
|
||||
<div class="col-12">
|
||||
<div class="card card-sm">
|
||||
<div class="card-status-top bg-orange"></div>
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<span class="status status-orange me-2"></span>
|
||||
<h4 class="m-0"><?= h(t('animal.form.location')) ?></h4>
|
||||
</div>
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label"><?= h(t('animal.form.origin_city')) ?></label>
|
||||
<input type="text" name="rescue_location_name" class="form-control"
|
||||
value="<?= h($animal['rescue_location_name'] ?? '') ?>"
|
||||
placeholder="<?= h(t('common.city_example')) ?>">
|
||||
<div class="form-hint"><?= h(t('animal.form.origin_hint')) ?></div>
|
||||
|
||||
<input type="hidden" name="rescue_lat"
|
||||
value="<?= h($animal['rescue_lat'] ?? '') ?>">
|
||||
<input type="hidden" name="rescue_lng"
|
||||
value="<?= h($animal['rescue_lng'] ?? '') ?>">
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label"><?= h(t('animal.form.current_location')) ?></label>
|
||||
<input type="text" name="location_name" class="form-control"
|
||||
value="<?= h($animal['location_name'] ?? '5 Rue Notre Dame 29260 LESNEVEN') ?>"
|
||||
readonly>
|
||||
<div class="form-hint"><?= h(t('animal.form.current_location_hint')) ?></div>
|
||||
|
||||
<input type="hidden" name="location_lat"
|
||||
value="<?= h($animal['location_lat'] ?? '') ?>">
|
||||
<input type="hidden" name="location_lng"
|
||||
value="<?= h($animal['location_lng'] ?? '') ?>">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ENTRÉE AU REFUGE -->
|
||||
<div class="col-12">
|
||||
<div class="card card-sm">
|
||||
<div class="card-status-top bg-green"></div>
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<span class="status status-green me-2"></span>
|
||||
<div>
|
||||
<h4 class="m-0"><?= h(t('animal.form.intake')) ?></h4>
|
||||
<div class="text-muted small"><?= h(t('animal.form.intake_hint')) ?></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-12 col-md-4" id="intakeWrap">
|
||||
<label class="form-label"><?= h(t('animal.form.intake_date')) ?></label>
|
||||
<input class="form-control" type="date" name="intake_date"
|
||||
value="<?= h($animal['intake_date'] ?? '') ?>">
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-4">
|
||||
<label class="form-label"><?= h(t('animal.form.intake_type')) ?></label>
|
||||
<select class="form-select"
|
||||
name="intake_type"><?php foreach (($intakeTypes ?? []) as $value => $label): ?>
|
||||
<option value="<?= h($value) ?>"
|
||||
<?= (($animal['intake_type'] ?? 'unknown') === $value) ? 'selected' : '' ?>>
|
||||
<?= h($label) ?></option><?php endforeach; ?></select>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-4">
|
||||
<label class="form-label"><?= h(t('animal.form.intake_reason')) ?></label>
|
||||
<select class="form-select"
|
||||
name="intake_reason"><?php foreach (($intakeReasons ?? []) as $value => $label): ?>
|
||||
<option value="<?= h($value) ?>"
|
||||
<?= (($animal['intake_reason'] ?? 'unknown') === $value) ? 'selected' : '' ?>>
|
||||
<?= h($label) ?></option><?php endforeach; ?></select>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-6">
|
||||
<label class="form-label"><?= h(t('animal.form.depositor')) ?></label>
|
||||
<select class="form-select" name="depositor_contact_id" id="depositorContact">
|
||||
<option value=""><?= h(t('animal.form.no_depositor')) ?></option>
|
||||
<?php foreach (($depositors ?? []) as $contact): ?><option
|
||||
value="<?= (int)$contact['id'] ?>"
|
||||
<?= ((int)($animal['depositor_contact_id'] ?? 0) === (int)$contact['id']) ? 'selected' : '' ?>>
|
||||
<?= h($contact['name'] . (!empty($contact['city']) ? ' · ' . $contact['city'] : '')) ?>
|
||||
</option><?php endforeach; ?><option value="new">+
|
||||
<?= h(t('animal.form.new_depositor')) ?></option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-6 d-none" id="newDepositorWrap">
|
||||
<label class="form-label"><?= h(t('animal.form.new_depositor_name')) ?></label>
|
||||
<input class="form-control" name="new_depositor_name" maxlength="160"
|
||||
placeholder="Personne ou structure">
|
||||
<div class="form-hint"><?= h(t('animal.form.depositor_hint')) ?></div>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<label class="form-label"><?= h(t('animal.form.circumstances')) ?></label>
|
||||
<textarea class="form-control" name="intake_circumstances" rows="3"
|
||||
placeholder="<?= h(t('animal.intake_circumstances_placeholder')) ?>"><?= h($animal['intake_circumstances'] ?? '') ?></textarea>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-6" id="quUntilWrap">
|
||||
<label class="form-label"><?= h(t('animal.form.quarantine_exit')) ?></label>
|
||||
<input class="form-control" value="<?= h($animal['quarantine_until'] ?? '') ?>"
|
||||
readonly>
|
||||
<div class="form-hint">
|
||||
<?= h(t('animal.form.quarantine_hint', ['count' => AppSettings::int('quarantine_days', 1, 90)])) ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<label class="form-label"><?= h(t('common.notes')) ?></label>
|
||||
<textarea class="form-control" name="notes"
|
||||
rows="5"><?= h($animal['notes'] ?? '') ?></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-footer d-flex justify-content-between">
|
||||
<a class="btn" href="/animals">Annuler</a>
|
||||
<button class="btn btn-primary" type="submit"><?= h(t('common.save')) ?></button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
const status = document.getElementById('status');
|
||||
const intakeWrap = document.getElementById('intakeWrap');
|
||||
const quUntilWrap = document.getElementById('quUntilWrap');
|
||||
const refugeRoomField = document.getElementById('refugeRoomField');
|
||||
const fosterContactField = document.getElementById('fosterContactField');
|
||||
const freeCatFields = document.getElementById('freeCatFields');
|
||||
const coatSelect = document.getElementById('coatColorSelect');
|
||||
const coatSwatch = document.getElementById('coatColorSwatch');
|
||||
|
||||
function sync() {
|
||||
const v = (status.value || '').toLowerCase();
|
||||
const show = (v === 'quarantaine');
|
||||
quUntilWrap.style.display = show ? '' : 'none';
|
||||
const roomSelect = refugeRoomField.querySelector('select');
|
||||
const hasRooms = Array.from(roomSelect.options).some(option => option.dataset.status === v);
|
||||
refugeRoomField.style.display = hasRooms ? '' : 'none';
|
||||
roomSelect.querySelectorAll('option[data-status]').forEach(option => option.hidden = option.dataset
|
||||
.status !== v);
|
||||
if (roomSelect.selectedOptions[0]?.hidden) roomSelect.value = '';
|
||||
fosterContactField.style.display = (v === 'fa' || v === 'fa_permanente') ? '' : 'none';
|
||||
freeCatFields?.classList.toggle('d-none', v !== 'chat_libre');
|
||||
}
|
||||
status.addEventListener('change', sync);
|
||||
sync();
|
||||
|
||||
function syncCoatSwatch() {
|
||||
const option = coatSelect?.options[coatSelect.selectedIndex];
|
||||
if (coatSwatch && option) coatSwatch.style.cssText = option.dataset.swatch || 'background:#d9dde3';
|
||||
}
|
||||
coatSelect?.addEventListener('change', syncCoatSwatch);
|
||||
syncCoatSwatch();
|
||||
const sterilizationStatus = document.getElementById('sterilizationStatus');
|
||||
const sterilizationDetails = document.getElementById('sterilizationDetails');
|
||||
const syncSterilization = () => sterilizationDetails?.classList.toggle('d-none', sterilizationStatus
|
||||
?.value !== 'yes');
|
||||
sterilizationStatus?.addEventListener('change', syncSterilization);
|
||||
syncSterilization();
|
||||
const depositorContact = document.getElementById('depositorContact');
|
||||
const newDepositorWrap = document.getElementById('newDepositorWrap');
|
||||
const syncDepositor = () => newDepositorWrap?.classList.toggle('d-none', depositorContact?.value !== 'new');
|
||||
depositorContact?.addEventListener('change', syncDepositor);
|
||||
syncDepositor();
|
||||
const adoptionAvailability = document.getElementById('adoptionAvailability');
|
||||
const adoptionAvailableFrom = document.getElementById('adoptionAvailableFrom');
|
||||
const adoptionUnavailableReason = document.getElementById('adoptionUnavailableReason');
|
||||
const syncAdoption = () => {
|
||||
const value = adoptionAvailability?.value;
|
||||
adoptionAvailableFrom?.classList.toggle('d-none', value !== 'available');
|
||||
adoptionUnavailableReason?.classList.toggle('d-none', value !== 'not_available');
|
||||
};
|
||||
adoptionAvailability?.addEventListener('change', syncAdoption);
|
||||
syncAdoption();
|
||||
})();
|
||||
|
||||
</script>
|
||||
36
app/Views/animal_history.php
Normal file
36
app/Views/animal_history.php
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title"><?= h(t('history.title')) ?></h3>
|
||||
|
||||
<?php if (empty($history)): ?>
|
||||
<div class="text-muted"><?= h(t('history.none')) ?></div>
|
||||
<?php else: ?>
|
||||
<table class="table table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><?= h(t('common.date')) ?></th>
|
||||
<th><?= h(t('history.event')) ?></th>
|
||||
<th><?= h(t('history.detail')) ?></th>
|
||||
<th><?= h(t('history.user')) ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($history as $h): ?>
|
||||
<tr>
|
||||
<td class="text-muted"><?= htmlspecialchars($h['created_at'], ENT_QUOTES) ?></td>
|
||||
<td>
|
||||
<span class="badge bg-secondary-lt">
|
||||
<?= htmlspecialchars($h['label'], ENT_QUOTES) ?>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<?= !empty($h['details']) ? nl2br(htmlspecialchars($h['details'], ENT_QUOTES)) : '—' ?>
|
||||
</td>
|
||||
<td><?= htmlspecialchars($h['actor_name'] ?? '—', ENT_QUOTES) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
276
app/Views/animal_pdf.php
Normal file
276
app/Views/animal_pdf.php
Normal file
|
|
@ -0,0 +1,276 @@
|
|||
<?php
|
||||
// Variables disponibles : $animal, $medical_notes, $treatments, $vaccinations, $photos, $title
|
||||
|
||||
if (!function_exists('h')) {
|
||||
function h($s)
|
||||
{
|
||||
return htmlspecialchars((string)$s, ENT_QUOTES);
|
||||
}
|
||||
}
|
||||
|
||||
$baseDir = realpath(__DIR__ . '/../../public');
|
||||
$photoDir = $baseDir . '/media/animals/' . (int)$animal['id'];
|
||||
|
||||
function fileToDataUri(string $path): ?string
|
||||
{
|
||||
if (!is_file($path)) {
|
||||
return null;
|
||||
}
|
||||
$data = file_get_contents($path);
|
||||
if ($data === false) {
|
||||
return null;
|
||||
}
|
||||
$ext = strtolower(pathinfo($path, PATHINFO_EXTENSION));
|
||||
$mime = match($ext) {
|
||||
'jpg','jpeg' => 'image/jpeg',
|
||||
'png' => 'image/png',
|
||||
'webp' => 'image/webp',
|
||||
'gif' => 'image/gif',
|
||||
default => 'application/octet-stream'
|
||||
};
|
||||
return "data:$mime;base64," . base64_encode($data);
|
||||
}
|
||||
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="<?= h(I18n::locale()) ?>">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title><?= h($title) ?></title>
|
||||
<style>
|
||||
body {
|
||||
font-family: DejaVu Sans, sans-serif;
|
||||
font-size: 12px;
|
||||
color: #111;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 18px;
|
||||
margin: 0 0 6px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 14px;
|
||||
margin: 18px 0 8px;
|
||||
border-bottom: 1px solid #ddd;
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
|
||||
.muted {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.grid {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.grid td,
|
||||
.grid th {
|
||||
border: 1px solid #ddd;
|
||||
padding: 6px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.grid th {
|
||||
background: #f3f3f3;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 2px 6px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #ccc;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.photos {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.photo {
|
||||
display: inline-block;
|
||||
width: 32%;
|
||||
margin: 0 1% 10px 0;
|
||||
}
|
||||
|
||||
.photo img {
|
||||
width: 100%;
|
||||
height: 140px;
|
||||
object-fit: cover;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.small {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<h1><?= h(t('pdf.medical_record', ['name' => $animal['name'] ?? ''])) ?></h1>
|
||||
<div class="muted small">
|
||||
<?= h(t('pdf.generated_on', ['date' => date('d/m/Y H:i')])) ?>
|
||||
</div>
|
||||
|
||||
<h2><?= h(t('pdf.identity')) ?></h2>
|
||||
<table class="grid">
|
||||
<tr>
|
||||
<th><?= h(t('pdf.internal_code')) ?></th>
|
||||
<td><?= h($animal['code'] ?? '') ?></td>
|
||||
<th><?= h(t('animal.species')) ?></th>
|
||||
<td><?= h(match (strtolower((string)($animal['species'] ?? ''))) {
|
||||
'cat', 'chat' => t('species.cat'), 'dog', 'chien' => t('species.dog'), 'nac' => t('species.nac'), default => (string)($animal['species'] ?? '')
|
||||
}) ?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><?= h(t('animal.sex')) ?></th>
|
||||
<td><?= h($animal['sex'] ?? '') ?></td>
|
||||
<th><?= h(t('animal.form.birth_date')) ?></th>
|
||||
<td><?= h($animal['birth_date'] ?? '') ?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><?= h(t('animal.status')) ?></th>
|
||||
<td><?= h($animal['status'] ?? '') ?></td>
|
||||
<th><?= h(t('common.notes')) ?></th>
|
||||
<td><?= h($animal['notes'] ?? '') ?></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<?php if (!empty($photos)): ?>
|
||||
<h2><?= h(t('animal.photos')) ?></h2>
|
||||
<div class="photos">
|
||||
<?php foreach ($photos as $p): ?>
|
||||
<?php
|
||||
$path = $photoDir . '/' . (string)$p['filename'];
|
||||
$uri = fileToDataUri($path);
|
||||
if (!$uri) {
|
||||
continue;
|
||||
}
|
||||
?>
|
||||
<div class="photo">
|
||||
<img src="<?= h($uri) ?>" alt="">
|
||||
<?php if ((int)$p['is_primary'] === 1): ?><div class="small muted"><?= h(t('pdf.main_photo')) ?></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<h2><?= h(t('pdf.medical_notes')) ?></h2>
|
||||
<?php if (empty($medical_notes)): ?>
|
||||
<div class="muted"><?= h(t('pdf.no_medical_notes')) ?></div>
|
||||
<?php else: ?>
|
||||
<table class="grid">
|
||||
<tr>
|
||||
<th style="width:90px;"><?= h(t('common.date')) ?></th>
|
||||
<th style="width:70px;"><?= h(t('animal.form.type')) ?></th>
|
||||
<th><?= h(t('pdf.content')) ?></th>
|
||||
<th style="width:160px;"><?= h(t('pdf.clinic_vet')) ?></th>
|
||||
</tr>
|
||||
<?php foreach ($medical_notes as $n): ?>
|
||||
<tr>
|
||||
<td><?= h(substr((string)$n['noted_at'], 0, 10)) ?></td>
|
||||
<td><span class="badge"><?= h($n['kind'] ?? '') ?></span></td>
|
||||
<td>
|
||||
<?php if (!empty($n['reason'])): ?><b><?= h(t('pdf.reason')) ?>:</b>
|
||||
<?= h($n['reason']) ?><br><?php endif; ?>
|
||||
<?php if (!empty($n['weight_kg'])): ?><b><?= h(t('pdf.weight')) ?>:</b> <?= h($n['weight_kg']) ?>
|
||||
kg<br><?php endif; ?>
|
||||
<?php if (!empty($n['temperature_c'])): ?><b><?= h(t('pdf.temperature')) ?>:</b>
|
||||
<?= h($n['temperature_c']) ?> °C<br><?php endif; ?>
|
||||
<?php if (!empty($n['symptoms'])): ?><b><?= h(t('pdf.symptoms')) ?>:</b>
|
||||
<?= nl2br(h($n['symptoms'])) ?><br><?php endif; ?>
|
||||
<?php if (!empty($n['exam'])): ?><b><?= h(t('pdf.exam')) ?>:</b>
|
||||
<?= nl2br(h($n['exam'])) ?><br><?php endif; ?>
|
||||
<?php if (!empty($n['diagnosis'])): ?><b><?= h(t('pdf.diagnosis')) ?>:</b>
|
||||
<?= nl2br(h($n['diagnosis'])) ?><br><?php endif; ?>
|
||||
<?php if (!empty($n['plan'])): ?><b><?= h(t('pdf.plan')) ?>:</b>
|
||||
<?= nl2br(h($n['plan'])) ?><br><?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<?php if (!empty($n['clinic_name'])): ?><?= h($n['clinic_name']) ?><br><?php endif; ?>
|
||||
<?php if (!empty($n['vet_name'])): ?><?= h($n['vet_name']) ?><?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</table>
|
||||
<?php endif; ?>
|
||||
|
||||
<h2><?= h(t('pdf.treatments')) ?></h2>
|
||||
<?php if (empty($treatments)): ?>
|
||||
<div class="muted"><?= h(t('pdf.no_treatment')) ?></div>
|
||||
<?php else: ?>
|
||||
<table class="grid">
|
||||
<tr>
|
||||
<th style="width:60px;"><?= h(t('pdf.state')) ?></th>
|
||||
<th><?= h(t('pdf.medication')) ?></th>
|
||||
<th style="width:120px;"><?= h(t('pdf.start')) ?></th>
|
||||
<th style="width:120px;"><?= h(t('pdf.end')) ?></th>
|
||||
<th style="width:160px;"><?= h(t('pdf.clinic_vet')) ?></th>
|
||||
<th><?= h(t('common.notes')) ?></th>
|
||||
</tr>
|
||||
<?php foreach ($treatments as $t): ?>
|
||||
<tr>
|
||||
<td><?= h(((int)$t['ongoing'] === 1) ? t('pdf.ongoing') : t('pdf.finished')) ?></td>
|
||||
<td><?= h($t['medication_name'] ?? '') ?> <?= !empty($t['molecule']) ? '(' . h($t['molecule']) . ')' : '' ?>
|
||||
</td>
|
||||
<td><?= h($t['start_date'] ?? '') ?></td>
|
||||
<td><?= h($t['end_date'] ?? '') ?></td>
|
||||
<td>
|
||||
<?php if (!empty($t['clinic_name'])): ?><?= h($t['clinic_name']) ?><br><?php endif; ?>
|
||||
<?php if (!empty($t['vet_name'])): ?><?= h($t['vet_name']) ?><?php endif; ?>
|
||||
</td>
|
||||
<td class="small">
|
||||
<?php if (!empty($t['route'])): ?><b><?= h(t('pdf.route')) ?>:</b>
|
||||
<?= h($t['route']) ?><br><?php endif; ?>
|
||||
<?php if (!empty($t['dose_text'])): ?><b><?= h(t('pdf.dosage')) ?>:</b>
|
||||
<?= h($t['dose_text']) ?><br><?php endif; ?>
|
||||
<?= !empty($t['notes']) ? nl2br(h($t['notes'])) : '' ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</table>
|
||||
<?php endif; ?>
|
||||
|
||||
<h2><?= h(t('pdf.vaccines')) ?></h2>
|
||||
<?php if (empty($vaccinations)): ?>
|
||||
<div class="muted"><?= h(t('pdf.no_vaccine')) ?></div>
|
||||
<?php else: ?>
|
||||
<table class="grid">
|
||||
<tr>
|
||||
<th><?= h(t('pdf.vaccine')) ?></th>
|
||||
<th style="width:110px;"><?= h(t('pdf.done_on')) ?></th>
|
||||
<th style="width:110px;"><?= h(t('pdf.booster')) ?></th>
|
||||
<th style="width:160px;"><?= h(t('pdf.clinic_vet')) ?></th>
|
||||
<th><?= h(t('common.notes')) ?></th>
|
||||
</tr>
|
||||
<?php foreach ($vaccinations as $v): ?>
|
||||
<tr>
|
||||
<td><?= h($v['vaccine_name'] ?? '') ?></td>
|
||||
<td><?= h($v['done_date'] ?? '') ?></td>
|
||||
<td><?= h($v['due_date'] ?? '') ?></td>
|
||||
<td>
|
||||
<?php if (!empty($v['clinic_name'])): ?><?= h($v['clinic_name']) ?><br><?php endif; ?>
|
||||
<?php if (!empty($v['vet_name'])): ?><?= h($v['vet_name']) ?><?php endif; ?>
|
||||
</td>
|
||||
<td class="small">
|
||||
<?php if (!empty($v['lot'])): ?><b><?= h(t('pdf.lot')) ?>:</b> <?= h($v['lot']) ?><br><?php endif; ?>
|
||||
<?= !empty($v['notes']) ? nl2br(h($v['notes'])) : '' ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</table>
|
||||
<?php endif; ?>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
569
app/Views/animal_pdf_v2.php
Normal file
569
app/Views/animal_pdf_v2.php
Normal file
|
|
@ -0,0 +1,569 @@
|
|||
<?php
|
||||
if (!function_exists('h')) {
|
||||
function h($s)
|
||||
{
|
||||
return htmlspecialchars((string)$s, ENT_QUOTES);
|
||||
}
|
||||
}
|
||||
|
||||
$baseDir = realpath(__DIR__ . '/../../public');
|
||||
$photoDir = $baseDir . '/media/animals/' . (int)$animal['id'];
|
||||
|
||||
function fileToDataUri(string $path): ?string
|
||||
{
|
||||
if (!is_file($path)) {
|
||||
return null;
|
||||
}
|
||||
$data = file_get_contents($path);
|
||||
if ($data === false) {
|
||||
return null;
|
||||
}
|
||||
$ext = strtolower(pathinfo($path, PATHINFO_EXTENSION));
|
||||
$mime = match($ext) {
|
||||
'jpg','jpeg' => 'image/jpeg',
|
||||
'png' => 'image/png',
|
||||
'webp' => 'image/webp',
|
||||
'gif' => 'image/gif',
|
||||
default => 'application/octet-stream'
|
||||
};
|
||||
return "data:$mime;base64," . base64_encode($data);
|
||||
}
|
||||
|
||||
function labelStatus(string $s): string
|
||||
{
|
||||
return match ($s) {
|
||||
'refuge' => t('status.refuge'),
|
||||
'fa' => t('status.fa'),
|
||||
'fa_permanente' => t('status.fa_permanente'),
|
||||
'quarantaine' => t('status.quarantaine'),
|
||||
'soin' => t('status.soin'),
|
||||
'isolation' => t('status.isolation'),
|
||||
'hospitalise','hospitalisé' => t('status.hospitalise'),
|
||||
'reserve','réservé' => t('status.reserve'),
|
||||
'adopte','adopté' => t('status.adopte'),
|
||||
'decede','décédé' => t('status.decede'),
|
||||
'enfui' => t('status.enfui'),
|
||||
default => $s,
|
||||
};
|
||||
}
|
||||
|
||||
function fivFelvText(array $a): string
|
||||
{
|
||||
// adapte selon tes colonnes réelles (chez toi ça marche déjà côté liste)
|
||||
$fiv = (string)($a['fiv'] ?? '');
|
||||
$felv = (string)($a['felv'] ?? '');
|
||||
if ($fiv === '' && $felv === '') {
|
||||
return t('animals.test_unknown');
|
||||
}
|
||||
if ($fiv === '0' && $felv === '0') {
|
||||
return t('animals.test_negative');
|
||||
}
|
||||
if ($fiv === '1' && $felv === '1') {
|
||||
return 'FIV/FELV+';
|
||||
}
|
||||
if ($fiv === '1') {
|
||||
return 'FIV+';
|
||||
}
|
||||
if ($felv === '1') {
|
||||
return 'FELV+';
|
||||
}
|
||||
// fallback : si tu stockes déjà du texte
|
||||
return trim($fiv . ' ' . $felv);
|
||||
}
|
||||
|
||||
function kindClass(string $k): string
|
||||
{
|
||||
$k = strtolower(trim($k));
|
||||
return match($k) {
|
||||
'consult' => 'k-consult',
|
||||
'contrôle','controle' => 'k-control',
|
||||
'hospi','hospitalisation' => 'k-hospi',
|
||||
'chirurgie' => 'k-surgery',
|
||||
'analyse','analyses' => 'k-lab',
|
||||
'vaccin' => 'k-vaccine',
|
||||
default => 'k-default'
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
$mainPhotoUri = null;
|
||||
if (!empty($primary_photo)) {
|
||||
$mainPhotoUri = fileToDataUri($photoDir . '/' . $primary_photo);
|
||||
}
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="<?= h(I18n::locale()) ?>">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title><?= h(t('pdf.medical_record', ['name' => $animal['name'] ?? ''])) ?></title>
|
||||
<style>
|
||||
@page {
|
||||
size: A4 portrait;
|
||||
margin: 14mm 12mm 18mm;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
font-size: 11.5px;
|
||||
color: #111;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 18px;
|
||||
margin: 0 0 6px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 13px;
|
||||
margin: 18px 0 8px;
|
||||
border-bottom: 1px solid #ddd;
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
|
||||
.muted {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.small {
|
||||
font-size: 10.5px;
|
||||
}
|
||||
|
||||
.box {
|
||||
border: 1px solid #ddd;
|
||||
padding: 10px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.row {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.left {
|
||||
width: 34%;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.right {
|
||||
width: 66%;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
table.grid {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.grid td,
|
||||
.grid th {
|
||||
border: 1px solid #ddd;
|
||||
padding: 6px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.grid th {
|
||||
background: #f3f3f3;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 2px 6px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #bbb;
|
||||
font-size: 10.5px;
|
||||
}
|
||||
|
||||
.tag {
|
||||
display: inline-block;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
background: #f3f3f3;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.photo {
|
||||
width: 100%;
|
||||
height: 220px;
|
||||
object-fit: cover;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.sep {
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
.k {
|
||||
color: #444;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.note-kind {
|
||||
display: inline-block;
|
||||
padding: 2px 6px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #ddd;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 2px 6px;
|
||||
border-radius: 10px;
|
||||
font-size: 10px;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
|
||||
.k-consult {
|
||||
background: #e8f0ff;
|
||||
border-color: #9db8ff;
|
||||
}
|
||||
|
||||
.k-control {
|
||||
background: #e8fff1;
|
||||
border-color: #9dffc1;
|
||||
}
|
||||
|
||||
.k-hospi {
|
||||
background: #ffecec;
|
||||
border-color: #ff9d9d;
|
||||
}
|
||||
|
||||
.k-surgery {
|
||||
background: #fff4e5;
|
||||
border-color: #ffd29d;
|
||||
}
|
||||
|
||||
.k-lab {
|
||||
background: #f2e8ff;
|
||||
border-color: #c89dff;
|
||||
}
|
||||
|
||||
.k-vaccine {
|
||||
background: #e6fbff;
|
||||
border-color: #9de7ff;
|
||||
}
|
||||
|
||||
.k-default {
|
||||
background: #f3f3f3;
|
||||
border-color: #ddd;
|
||||
}
|
||||
|
||||
.print-toolbar {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
margin: 0 0 16px;
|
||||
padding: 10px;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.print-toolbar button,
|
||||
.print-toolbar a {
|
||||
border: 1px solid #1d273b;
|
||||
border-radius: 6px;
|
||||
padding: 8px 13px;
|
||||
background: #fff;
|
||||
color: #1d273b;
|
||||
font: 600 14px Arial, sans-serif;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.print-toolbar button {
|
||||
background: #1d273b;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.document-certification {
|
||||
margin: 0 0 12px;
|
||||
padding: 7px 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
background: #f7f7f7;
|
||||
color: #626976;
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
@media print {
|
||||
.print-toolbar {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
h2 {
|
||||
break-after: avoid;
|
||||
}
|
||||
|
||||
table.grid tr {
|
||||
break-inside: avoid;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div class="print-toolbar"><a href="/animal?id=<?= (int)$animal['id'] ?>">←
|
||||
<?= h(t('pdf.back_record')) ?></a><button type="button" onclick="window.print()">🖨
|
||||
<?= h(t('pdf.print')) ?></button></div>
|
||||
<div class="document-certification"><?= h($footerText ?? '') ?></div>
|
||||
|
||||
<h1><?= h(t('pdf.medical_record', ['name' => $animal['name'] ?? ''])) ?></h1>
|
||||
<div class="muted small">
|
||||
<?= h(t('pdf.generated_on', ['date' => date('d/m/Y H:i')])) ?> • <?= h(t('pdf.internal_code')) ?> :
|
||||
<?= h($animal['internal_code'] ?? '') ?>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
$fivPos = ((int)($animal['fiv'] ?? 0) === 1);
|
||||
$felvPos = ((int)($animal['felv'] ?? 0) === 1);
|
||||
?>
|
||||
|
||||
<?php if ($fivPos || $felvPos): ?>
|
||||
<div style="
|
||||
background:#b91c1c;
|
||||
color:white;
|
||||
padding:12px;
|
||||
margin-bottom:15px;
|
||||
border-radius:6px;
|
||||
font-weight:bold;
|
||||
font-size:14px;
|
||||
">
|
||||
⚠️ <?= h(t('pdf.health_alert')) ?> :
|
||||
<?= $fivPos ? 'FIV+ ' : '' ?>
|
||||
<?= $felvPos ? 'FeLV+' : '' ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<table class="row" cellspacing="0" cellpadding="0">
|
||||
<?php if (!empty($death)): ?>
|
||||
<caption
|
||||
style="caption-side:top;text-align:left;border:2px solid #222;padding:10px;margin:10px 0 15px;border-radius:6px;">
|
||||
<strong><?= h(t('pdf.death_recorded', ['date' => (($deathDate = DateTimeImmutable::createFromFormat('Y-m-d', (string)$death['deceased_date'])) ? $deathDate->format('d/m/Y') : t('pdf.unknown_date'))])) ?></strong><br>
|
||||
<?= h(t('animal.cause')) ?> :
|
||||
<?= h(DeathController::causeLabel((string)$death['cause_code'])) ?><?= !empty($death['euthanized']) ? ' · ' . h(t('animal.euthanasia')) : '' ?><br>
|
||||
<span
|
||||
class="small muted"><?= h(!empty($death['occurred_in_care']) ? t('pdf.death_in_care') : t('pdf.death_outside_care')) ?><?= $death['crematorium_name'] ? ' · ' . h(t('pdf.crematorium')) . ' : ' . h($death['crematorium_name']) : '' ?></span>
|
||||
</caption>
|
||||
<?php endif; ?>
|
||||
<tr>
|
||||
<td class="left">
|
||||
<div class="box">
|
||||
<?php if ($mainPhotoUri): ?>
|
||||
<img class="photo" src="<?= h($mainPhotoUri) ?>" alt="">
|
||||
<div class="sep"></div>
|
||||
<?php else: ?>
|
||||
<div class="muted"><?= h(t('pdf.no_main_photo')) ?></div>
|
||||
<div class="sep"></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div><span class="k"><?= h(t('animal.status')) ?> :</span> <span
|
||||
class="badge"><?= h(labelStatus((string)($animal['status'] ?? ''))) ?></span></div>
|
||||
<div><span class="k"><?= h(t('animal.species')) ?> :</span> <?= h(match (strtolower((string)($animal['species'] ?? ''))) {
|
||||
'cat', 'chat' => t('species.cat'), 'dog', 'chien' => t('species.dog'), 'nac' => t('species.nac'), default => (string)($animal['species'] ?? '')
|
||||
}) ?></div>
|
||||
<div><span class="k"><?= h(t('animal.sex')) ?> :</span> <?= h($animal['sex'] ?? '') ?></div>
|
||||
<div><span class="k"><?= h(t('animal.birth')) ?> :</span> <?= h($animal['birth_date'] ?? '') ?>
|
||||
</div>
|
||||
<div><span class="k"><?= h(t('animal.form.identification')) ?> :</span> <?= h(match((string)($animal['identification_type'] ?? 'unknown')) {
|
||||
'microchip' => t('animal.microchip'),'tattoo' => t('animal.tattoo'),default => t('common.not_provided')
|
||||
}) ?><?= !empty($animal['chip_id']) ? ' · ' . h($animal['chip_id']) : '' ?></div>
|
||||
<div><span class="k">FIV/FELV :</span> <?= h(fivFelvText($animal)) ?></div>
|
||||
|
||||
<?php if (!empty($animal['intake_date']) || !empty($animal['quarantine_until'])): ?>
|
||||
<div class="sep"></div>
|
||||
<div><span class="k"><?= h(t('pdf.entry')) ?> :</span> <?= h($animal['intake_date'] ?? '') ?></div>
|
||||
<div><span class="k"><?= h(t('animal.form.quarantine_exit')) ?> :</span>
|
||||
<?= h($animal['quarantine_until'] ?? '') ?></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td class="right" style="padding-left:12px;">
|
||||
<div class="box">
|
||||
<div class="k"><?= h(t('pdf.summary')) ?></div>
|
||||
<div class="sep"></div>
|
||||
|
||||
<div>
|
||||
<span class="tag"><?= h(t('pdf.last_medical')) ?></span>
|
||||
<?php if (empty($last_med)): ?>
|
||||
<span class="muted">—</span>
|
||||
<?php else: ?>
|
||||
<span><?= h(substr((string)$last_med['noted_at'], 0, 16)) ?></span>
|
||||
<?php if (!empty($last_med['kind'])): ?> • <span
|
||||
class="note-kind"><?= h($last_med['kind']) ?></span><?php endif; ?>
|
||||
<?php if (!empty($last_med['reason'])): ?><div class="small"><span
|
||||
class="k"><?= h(t('pdf.reason')) ?> :</span> <?= h($last_med['reason']) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($last_med['weight_kg'])): ?><div class="small"><span
|
||||
class="k"><?= h(t('pdf.weight')) ?> :</span> <?= h($last_med['weight_kg']) ?> kg</div>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($last_med['diagnosis'])): ?><div class="small"><span
|
||||
class="k"><?= h(t('pdf.diagnosis')) ?> :</span> <?= h($last_med['diagnosis']) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($last_med['plan'])): ?><div class="small"><span
|
||||
class="k"><?= h(t('pdf.plan')) ?> :</span> <?= h($last_med['plan']) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="sep"></div>
|
||||
|
||||
<div>
|
||||
<span class="tag"><?= h(t('pdf.next_vaccine')) ?></span>
|
||||
<?php if (empty($next_vaccine)): ?>
|
||||
<span class="muted">—</span>
|
||||
<?php else: ?>
|
||||
<span><?= h($next_vaccine['vaccine_name'] ?? '') ?></span>
|
||||
• <span class="k"><?= h($next_vaccine['due_date'] ?? '') ?></span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($animal['notes'])): ?>
|
||||
<div class="sep"></div>
|
||||
<div class="k"><?= h(t('common.notes')) ?></div>
|
||||
<div><?= nl2br(h($animal['notes'])) ?></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="sep"></div>
|
||||
<div class="muted small">
|
||||
<?= h(t('pdf.internal_document', ['association' => AppSettings::get('association_name')])) ?>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<h2><?= h(t('pdf.medical_notes')) ?></h2>
|
||||
<?php if (empty($medical_notes)): ?>
|
||||
<div class="muted"><?= h(t('pdf.no_medical_notes')) ?></div>
|
||||
<?php else: ?>
|
||||
<table class="grid">
|
||||
<tr>
|
||||
<th style="width:90px;"><?= h(t('common.date')) ?></th>
|
||||
<th style="width:80px;"><?= h(t('animal.form.type')) ?></th>
|
||||
<th><?= h(t('pdf.content')) ?></th>
|
||||
<th style="width:170px;"><?= h(t('pdf.clinic_vet')) ?></th>
|
||||
</tr>
|
||||
<?php foreach ($medical_notes as $n): ?>
|
||||
<tr>
|
||||
<td><?= h(substr((string)$n['noted_at'], 0, 10)) ?></td>
|
||||
<td><span class="badge <?= kindClass((string)($n['kind'] ?? '')) ?>">
|
||||
<?= h($n['kind'] ?? '') ?>
|
||||
</span></td>
|
||||
<td>
|
||||
<?php if (!empty($n['reason'])): ?><b><?= h(t('pdf.reason')) ?>:</b>
|
||||
<?= h($n['reason']) ?><br><?php endif; ?>
|
||||
<?php if (!empty($n['weight_kg'])): ?><b><?= h(t('pdf.weight')) ?>:</b> <?= h($n['weight_kg']) ?>
|
||||
kg<br><?php endif; ?>
|
||||
<?php if (!empty($n['symptoms'])): ?><b><?= h(t('pdf.symptoms')) ?>:</b>
|
||||
<?= nl2br(h($n['symptoms'])) ?><br><?php endif; ?>
|
||||
<?php if (!empty($n['exam'])): ?><b><?= h(t('pdf.exam')) ?>:</b>
|
||||
<?= nl2br(h($n['exam'])) ?><br><?php endif; ?>
|
||||
<?php if (!empty($n['diagnosis'])): ?><b><?= h(t('pdf.diagnosis')) ?>:</b>
|
||||
<?= nl2br(h($n['diagnosis'])) ?><br><?php endif; ?>
|
||||
<?php if (!empty($n['plan'])): ?><b><?= h(t('pdf.plan')) ?>:</b>
|
||||
<?= nl2br(h($n['plan'])) ?><br><?php endif; ?>
|
||||
<?php
|
||||
$notePhotos = $medicalPhotosByNote[$n['id']] ?? [];
|
||||
?>
|
||||
|
||||
<?php if (!empty($notePhotos)): ?>
|
||||
<div style="margin-top:8px;">
|
||||
<strong><?= h(t('pdf.photos')) ?> :</strong><br>
|
||||
|
||||
<?php foreach ($notePhotos as $p):
|
||||
$absolutePath = __DIR__ . '/../../data/private-media/animals/' . (int)$animal['id'] . '/meds/' . $p['filename'];
|
||||
if (file_exists($absolutePath)):
|
||||
|
||||
$uri = fileToDataUri($absolutePath);
|
||||
?>
|
||||
<img src="<?= $uri ?>" style="width:160px;">
|
||||
<?php endif; endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<?php if (!empty($n['clinic_name'])): ?><?= h($n['clinic_name']) ?><br><?php endif; ?>
|
||||
<?php if (!empty($n['vet_name'])): ?><?= h($n['vet_name']) ?><?php endif; ?>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</table>
|
||||
<?php endif; ?>
|
||||
|
||||
<h2><?= h(t('pdf.treatments')) ?></h2>
|
||||
<?php if (empty($treatments)): ?>
|
||||
<div class="muted"><?= h(t('pdf.no_treatment')) ?></div>
|
||||
<?php else: ?>
|
||||
<table class="grid">
|
||||
<tr>
|
||||
<th style="width:65px;"><?= h(t('pdf.state')) ?></th>
|
||||
<th><?= h(t('pdf.medication')) ?></th>
|
||||
<th style="width:95px;"><?= h(t('pdf.start')) ?></th>
|
||||
<th style="width:95px;"><?= h(t('pdf.end')) ?></th>
|
||||
<th style="width:170px;"><?= h(t('pdf.clinic_vet')) ?></th>
|
||||
<th><?= h(t('common.notes')) ?></th>
|
||||
</tr>
|
||||
<?php foreach ($treatments as $t): ?>
|
||||
<tr>
|
||||
<td><?= ((int)$t['ongoing'] === 1) ? t('pdf.ongoing') : t('pdf.finished') ?></td>
|
||||
<td><?= h($t['medication_name'] ?? '') ?></td>
|
||||
<td><?= h($t['start_date'] ?? '') ?></td>
|
||||
<td><?= h($t['end_date'] ?? '') ?></td>
|
||||
<td>
|
||||
<?php if (!empty($t['clinic_name'])): ?><?= h($t['clinic_name']) ?><br><?php endif; ?>
|
||||
<?php if (!empty($t['vet_name'])): ?><?= h($t['vet_name']) ?><?php endif; ?>
|
||||
</td>
|
||||
<td class="small">
|
||||
<?php if (!empty($t['route'])): ?><b><?= h(t('pdf.route')) ?>:</b>
|
||||
<?= h($t['route']) ?><br><?php endif; ?>
|
||||
<?php if (!empty($t['dose_text'])): ?><b><?= h(t('pdf.dosage')) ?>:</b>
|
||||
<?= h($t['dose_text']) ?><br><?php endif; ?>
|
||||
<?= !empty($t['notes']) ? nl2br(h($t['notes'])) : '' ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</table>
|
||||
<?php endif; ?>
|
||||
|
||||
<h2><?= h(t('pdf.vaccines')) ?></h2>
|
||||
<?php if (empty($vaccinations)): ?>
|
||||
<div class="muted"><?= h(t('pdf.no_vaccine')) ?></div>
|
||||
<?php else: ?>
|
||||
<table class="grid">
|
||||
<tr>
|
||||
<th><?= h(t('pdf.vaccine')) ?></th>
|
||||
<th style="width:95px;"><?= h(t('pdf.done_on')) ?></th>
|
||||
<th style="width:95px;"><?= h(t('pdf.booster')) ?></th>
|
||||
<th style="width:170px;"><?= h(t('pdf.clinic_vet')) ?></th>
|
||||
<th><?= h(t('common.notes')) ?></th>
|
||||
</tr>
|
||||
<?php foreach ($vaccinations as $v): ?>
|
||||
<tr>
|
||||
<td><?= h($v['vaccine_name'] ?? '') ?></td>
|
||||
<td><?= h($v['done_date'] ?? '') ?></td>
|
||||
<td><?= h($v['due_date'] ?? '') ?></td>
|
||||
<td>
|
||||
<?php if (!empty($v['clinic_name'])): ?><?= h($v['clinic_name']) ?><br><?php endif; ?>
|
||||
<?php if (!empty($v['vet_name'])): ?><?= h($v['vet_name']) ?><?php endif; ?>
|
||||
</td>
|
||||
<td class="small">
|
||||
<?php if (!empty($v['lot'])): ?><b><?= h(t('pdf.lot')) ?>:</b> <?= h($v['lot']) ?><br><?php endif; ?>
|
||||
<?= !empty($v['notes']) ? nl2br(h($v['notes'])) : '' ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</table>
|
||||
<?php endif; ?>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
488
app/Views/animal_show.php
Normal file
488
app/Views/animal_show.php
Normal file
|
|
@ -0,0 +1,488 @@
|
|||
<?php
|
||||
/** @var array $animal */
|
||||
/** @var array $medical_notes */
|
||||
/** @var array $treatments */
|
||||
/** @var array $vaccinations */
|
||||
/** @var array $weights */
|
||||
/** @var array adoption */
|
||||
|
||||
require_once __DIR__ . '/../Services/IcadService.php';
|
||||
$isCat = SpeciesService::isCat((string)($animal['species'] ?? ''));
|
||||
include __DIR__ . '/animal_adoption.php';
|
||||
$canViewMedical = PermissionService::can('medical');
|
||||
$isArchived = !empty($animal['archived_at']);
|
||||
|
||||
$icadSvc = new IcadService(DB::pdo());
|
||||
|
||||
$cached = $icadSvc->getCachedByAnimal((int)$animal['id']);
|
||||
$stale = $icadSvc->isStale($cached, 86400);
|
||||
|
||||
// variable attendue par ta vue
|
||||
$icad = [
|
||||
'cached' => $cached,
|
||||
'stale' => $stale,
|
||||
];
|
||||
|
||||
$statusClass = [
|
||||
'refuge' => 'bg-blue-lt text-blue',
|
||||
'fa' => 'bg-cyan-lt text-cyan',
|
||||
'fa_permanente' => 'bg-indigo-lt text-indigo',
|
||||
'chat_libre' => 'bg-green-lt text-green',
|
||||
'reserve' => 'bg-purple-lt text-purple',
|
||||
'adopte' => 'bg-green-lt text-green',
|
||||
'fugue' => 'bg-orange-lt text-orange',
|
||||
'decede' => 'bg-dark-lt text-dark',
|
||||
'quarantaine' => 'bg-yellow-lt text-yellow',
|
||||
'soin' => 'bg-red-lt text-red',
|
||||
][$animal['status']] ?? 'bg-secondary-lt text-secondary';
|
||||
|
||||
$statusLabel = status_badge((string)($animal['status'] ?? ''))[1];
|
||||
|
||||
$historyStyles = [
|
||||
'create' => ['bg' => 'bg-green', 'icon' => 'plus'],
|
||||
'update' => ['bg' => 'bg-blue', 'icon' => 'edit'],
|
||||
'status' => ['bg' => 'bg-purple', 'icon' => 'refresh'],
|
||||
'medical' => ['bg' => 'bg-red', 'icon' => 'heart'],
|
||||
'vaccine' => ['bg' => 'bg-yellow', 'icon' => 'shield-check'],
|
||||
'deworming' => ['bg' => 'bg-lime', 'icon' => 'pill'],
|
||||
'treatment' => ['bg' => 'bg-orange', 'icon' => 'pill'],
|
||||
'care_round' => ['bg' => 'bg-red', 'icon' => 'alert-triangle'],
|
||||
'death' => ['bg' => 'bg-dark', 'icon' => 'rainbow'],
|
||||
'photo' => ['bg' => 'bg-teal', 'icon' => 'camera'],
|
||||
];
|
||||
|
||||
switch ($animal['sex']) {
|
||||
case 'M': $animal['sex'] = t('sex.male');
|
||||
$sex_color = '367EB3';
|
||||
break;
|
||||
case 'F': $animal['sex'] = t('sex.female');
|
||||
$sex_color = 'A736B3';
|
||||
break;
|
||||
default: $animal['sex'] = t('common.unknown');
|
||||
$sex_color = '36B362';
|
||||
}
|
||||
|
||||
?>
|
||||
|
||||
<style>
|
||||
.animal-actions-menu form {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.animal-actions-menu .dropdown-item {
|
||||
width: 100%;
|
||||
min-height: 34px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: .45rem 1rem;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
color: var(--tblr-body-color, #1d273b) !important;
|
||||
background: transparent;
|
||||
font-family: inherit;
|
||||
font-size: .875rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.4;
|
||||
text-align: left;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.animal-actions-menu .dropdown-item:hover,
|
||||
.animal-actions-menu .dropdown-item:focus {
|
||||
color: var(--tblr-body-color, #1d273b) !important;
|
||||
background: var(--tblr-dropdown-link-hover-bg, #f1f3f5);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
<div class="row row-cards">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<?php if ($isArchived): ?><div
|
||||
class="alert alert-important alert-secondary rounded-0 mb-0 d-flex flex-wrap align-items-center gap-2"
|
||||
role="status"><span>📦</span>
|
||||
<div><strong><?= h(t('animal.archived_record')) ?></strong>
|
||||
<div class="small">
|
||||
<?= h(t('animal.archived_help', ['date' => !empty($animal['archived_at']) ? t('animal.archived_on', ['date' => date('d/m/Y H:i', strtotime((string)$animal['archived_at']))]) : ''])) ?>
|
||||
</div>
|
||||
</div>
|
||||
</div><?php endif; ?>
|
||||
<?php
|
||||
$fivStatus = (string)(($animal['fiv_status'] ?? '') ?: ((int)($animal['fiv'] ?? 0) === 1 ? 'pos' : 'neg'));
|
||||
$felvStatus = (string)(($animal['felv_status'] ?? '') ?: ((int)($animal['felv'] ?? 0) === 1 ? 'pos' : 'neg'));
|
||||
$fivPos = $fivStatus === 'pos';
|
||||
$felvPos = $felvStatus === 'pos';
|
||||
$testStatusMeta = [
|
||||
'unknown' => [t('animals.test_unknown'), 'bg-secondary-lt'],
|
||||
'neg' => [t('animals.test_negative'), 'bg-green-lt'],
|
||||
'pos' => [t('animals.test_positive'), 'bg-red-lt'],
|
||||
'doubtful' => [t('animals.test_doubtful'), 'bg-yellow-lt'],
|
||||
];
|
||||
|
||||
$alertText = [];
|
||||
if ($fivPos) {
|
||||
$alertText[] = 'FIV+';
|
||||
}
|
||||
if ($felvPos) {
|
||||
$alertText[] = 'FeLV+';
|
||||
}
|
||||
?>
|
||||
|
||||
<?php if ($fivPos || $felvPos): ?>
|
||||
<div class="alert alert-important alert-danger d-flex align-items-center mb-3" role="alert">
|
||||
<div class="me-3">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon alert-icon" width="24" height="24"
|
||||
viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round"
|
||||
stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M12 9v4" />
|
||||
<path d="M12 17h.01" />
|
||||
<path
|
||||
d="M10.29 3.86l-7.1 12.14a2 2 0 0 0 1.71 3h14.2a2 2 0 0 0 1.71 -3l-7.1 -12.14a2 2 0 0 0 -3.42 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="fw-bold"><?= h(t('animal.health_alert', ['status' => implode(' / ', $alertText)])) ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<div class="card-body d-flex align-items-center justify-content-between">
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<div class="text-center flex-shrink-0">
|
||||
<?php if (!empty($primary_photo)): ?>
|
||||
<img src="/media/animals/<?= (int)$animal['id'] ?>/<?= h($primary_photo) ?>"
|
||||
style="width:100px;height:100px;object-fit:cover;border-radius:12px" alt="">
|
||||
<?php else: ?>
|
||||
<div class="bg-muted-lt"
|
||||
style="width:100px;height:100px;border-radius:12px;display:flex;align-items:center;justify-content:center;">
|
||||
<span style="font-size:50px">🐾</span>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<div class="text-muted mt-1" style="font-size:.7rem"><?= h($animal['internal_code']) ?></div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="mb-0">
|
||||
<?= h($animal['name']) ?>
|
||||
<?php if ($lastWeight !== null): ?>
|
||||
<?php
|
||||
$arrow = '→';
|
||||
if ($prevWeight !== null) {
|
||||
if ((float)$lastWeight > (float)$prevWeight) {
|
||||
$arrow = '↑';
|
||||
} elseif ((float)$lastWeight < (float)$prevWeight) {
|
||||
$arrow = '↓';
|
||||
}
|
||||
}
|
||||
?>
|
||||
<span class="badge bg-indigo-lt ms-2"><?= number_format((float)$lastWeight, 2) ?> kg
|
||||
<?= $arrow ?></span>
|
||||
<?php endif; ?>
|
||||
</h3>
|
||||
|
||||
<div class="text-muted">
|
||||
<?= h(t('animal.species')) ?> :
|
||||
<strong><?= h(species_label($animal['species'] ?? '')) ?></strong>
|
||||
• <?= h(t('animal.sex')) ?> : <strong
|
||||
style="color:#<?php echo $sex_color; ?>;"><?= h($animal['sex']) ?></strong><br />
|
||||
<?= h(t('animal.breed')) ?> : <strong><?= h($animal['breed'] ?: '—') ?></strong>
|
||||
• <?= h(t('animal.coat')) ?> :
|
||||
<?php if (!empty($animal['color'])): ?>
|
||||
<span class="coat-swatch" style="<?= h(coat_swatch_style($animal['color'])) ?>"></span>
|
||||
<strong><?= h($animal['color']) ?></strong>
|
||||
<?php else: ?><strong>—</strong><?php endif; ?>
|
||||
• <?= h(t('animal.hair')) ?> : <strong><?= h(match ((string)($animal['hair_type'] ?? '')) {
|
||||
'court' => t('animal.hair_short'),
|
||||
'mi-long' => t('animal.hair_medium'),
|
||||
'long' => t('animal.hair_long'),
|
||||
'frise' => t('animal.hair_curly'),
|
||||
'sans-poil' => t('animal.hairless'),
|
||||
default => '—',
|
||||
}) ?></strong><br />
|
||||
<strong><?= h(match((string)($animal['identification_type'] ?? 'unknown')) {
|
||||
'microchip' => t('animal.microchip'),'tattoo' => t('animal.tattoo'),default => t('animal.identification_unknown')
|
||||
}) ?></strong><?= $animal['chip_id'] ? ' · ' . h($animal['chip_id']) : '' ?><?php if (!empty($animal['identification_date'])): ?>
|
||||
·
|
||||
<?= h(t('animal.placed_on', ['date' => date('d/m/Y', strtotime((string)$animal['identification_date']))])) ?><?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="text-muted">
|
||||
<?= h(t('animal.status')) ?> : <span class="badge <?= $statusClass ?>">
|
||||
<?= $statusLabel ?>
|
||||
</span>
|
||||
<?php if (in_array(($animal['status'] ?? ''), ['refuge','soin','quarantaine'], true)):$roomConfig = ShelterRoomService::byCode(true)[(string)($animal['refuge_room'] ?? '')] ?? null; ?>
|
||||
• <?= h(t('animal.room')) ?> : <span
|
||||
class="badge <?= $roomConfig ? '' : 'bg-secondary-lt' ?>"
|
||||
<?= $roomConfig ? 'style="color:' . h($roomConfig['color']) . ';background:color-mix(in srgb,' . h($roomConfig['color']) . ' 14%,transparent)"' : '' ?>><?= h($roomConfig['name'] ?? t('animal.assign')) ?></span>
|
||||
<?php endif; ?>
|
||||
<?php if (($animal['status'] ?? '') === 'chat_libre'): ?>
|
||||
• <?= h(t('animal.site')) ?> :
|
||||
<strong><?= h($animal['free_cat_site_name'] ?: ($animal['free_cat_city'] ?: t('animal.to_provide'))) ?></strong>
|
||||
<?php endif; ?>
|
||||
<?php if ($canViewMedical && $isCat): ?>
|
||||
• FIV:
|
||||
<?php [$fivLabel, $fivClass] = $testStatusMeta[$fivStatus] ?? $testStatusMeta['unknown']; ?>
|
||||
<span class="badge <?= $fivClass ?>"><?= h(strtoupper($fivLabel)) ?></span>
|
||||
|
||||
• FeLV:
|
||||
<?php [$felvLabel, $felvClass] = $testStatusMeta[$felvStatus] ?? $testStatusMeta['unknown']; ?>
|
||||
<span class="badge <?= $felvClass ?>"><?= h(strtoupper($felvLabel)) ?></span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php if (($animal['status'] ?? '') === 'chat_libre'): $trackingLabels = ['present' => t('animal.present_on_site'),'missing' => t('animal.missing'),'moved' => t('animal.moved'),'deceased' => t('status.decede')]; ?>
|
||||
<div class="card card-sm bg-green-lt border-green-lt mt-3">
|
||||
<div class="card-body py-3">
|
||||
<div class="d-flex flex-wrap align-items-center gap-2 mb-2"><strong>🌿
|
||||
<?= h(t('animal.free_cat_followup')) ?></strong><span
|
||||
class="badge bg-green text-white"><?= h($trackingLabels[$animal['free_cat_tracking_status'] ?? 'present'] ?? t('animal.present_on_site')) ?></span>
|
||||
</div>
|
||||
<div class="row g-2 small">
|
||||
<div class="col-md-4"><span
|
||||
class="text-muted"><?= h(t('animal.site_or_colony')) ?></span><br><strong><?= h($animal['free_cat_site_name'] ?: '—') ?></strong><?= !empty($animal['free_cat_city']) ? ' · ' . h($animal['free_cat_city']) : '' ?>
|
||||
</div>
|
||||
<div class="col-md-4"><span
|
||||
class="text-muted"><?= h(t('animal.caretaker')) ?></span><br><strong><?= h($freeCatCaretaker['name'] ?? '—') ?></strong>
|
||||
</div>
|
||||
<div class="col-md-4"><span
|
||||
class="text-muted"><?= h(t('animal.capture_release')) ?></span><br><?= !empty($animal['free_cat_captured_at']) ? h(date('d/m/Y', strtotime($animal['free_cat_captured_at']))) : '—' ?>
|
||||
→
|
||||
<?= !empty($animal['free_cat_released_at']) ? h(date('d/m/Y', strtotime($animal['free_cat_released_at']))) : '—' ?>
|
||||
</div><?php if (!empty($animal['free_cat_address'])): ?><div class="col-12"><span
|
||||
class="text-muted"><?= h(t('animal.internal_location')) ?></span><br><?= h($animal['free_cat_address']) ?>
|
||||
</div><?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if ($canViewMedical): ?>
|
||||
<div class="mt-2 d-flex flex-wrap align-items-center gap-2">
|
||||
<?php $sterilizationState = $animal['sterilization_status'] ?? (!empty($animal['sterilized']) ? 'yes' : 'unknown');
|
||||
$sterilizationMeta = ['yes' => [t('animal.sterilized'),'bg-green-lt'],'no' => [t('animals.not_sterilized'),'bg-yellow-lt'],'unknown' => [t('animals.sterilization_unknown'),'bg-secondary-lt']];
|
||||
[$sterilizationLabel,$sterilizationClass] = $sterilizationMeta[$sterilizationState] ?? $sterilizationMeta['unknown']; ?>
|
||||
<span class="badge <?= h($sterilizationClass) ?>">
|
||||
<?= h($sterilizationLabel) ?>
|
||||
</span>
|
||||
<?php if ($isCat): ?><span class="text-muted small"><?= h(t('animal.adoption_fee_hint')) ?>
|
||||
<strong
|
||||
class="text-body"><?= $sterilizationState === 'yes' ? AppSettings::int('adoption_fee_sterilized') . ' €' : AppSettings::int('adoption_fee_unsterilized') . ' €' ?></strong></span><?php endif; ?>
|
||||
<?php if ($sterilizationState === 'yes' && (!empty($animal['sterilization_date']) || !empty($sterilizationContacts))): ?><span
|
||||
class="text-muted small"><?= !empty($animal['sterilization_date']) ? h(date('d/m/Y', strtotime((string)$animal['sterilization_date']))) : '' ?><?= !empty($sterilizationContacts['vet']['name']) ? ' · ' . h($sterilizationContacts['vet']['name']) : '' ?><?= !empty($sterilizationContacts['clinic']['name']) ? ' · ' . h($sterilizationContacts['clinic']['name']) : '' ?></span><?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php $availability = $animal['adoption_availability'] ?? 'unknown';
|
||||
$availabilityMeta = ['unknown' => [t('animal.availability_define'),'bg-secondary-lt'],'not_available' => [t('animals.unavailable_title'),'bg-orange-lt'],'available' => [t('animals.available_title'),'bg-green-lt']];
|
||||
[$availabilityLabel,$availabilityClass] = $availabilityMeta[$availability] ?? $availabilityMeta['unknown'];
|
||||
$registrationLabels = ['unknown' => t('animal.registration_unknown'),'pending' => t('animal.registration_pending'),'registered' => t('animal.registration_done')];
|
||||
$registration = $animal['identification_registration_status'] ?? 'unknown';
|
||||
$registrationClass = $registration === 'registered' ? 'bg-green-lt' : ($registration === 'pending' ? 'bg-yellow-lt' : 'bg-secondary-lt'); ?>
|
||||
<div class="mt-2 d-flex flex-wrap align-items-center gap-1"><span
|
||||
class="badge <?= h($registrationClass) ?>"><?= h($registrationLabels[$registration] ?? $registrationLabels['unknown']) ?></span><span
|
||||
class="badge <?= h($availabilityClass) ?>"><?= h($availabilityLabel) ?></span><?php if (!empty($animal['website_published'])): ?><span
|
||||
class="badge <?= $availability === 'available' ? 'bg-cyan-lt' : 'bg-yellow-lt' ?>">🌐
|
||||
<?= h(t($availability === 'available' ? 'public_site.published' : 'public_site.waiting_publication')) ?></span><?php endif; ?><?php if ($availability === 'available' && !empty($animal['adoption_available_from'])): ?><span
|
||||
class="text-muted small ms-1"><?= h(t('animal.available_from', ['date' => date('d/m/Y', strtotime((string)$animal['adoption_available_from']))])) ?></span><?php elseif ($availability === 'not_available' && !empty($animal['adoption_unavailability_reason'])): ?><span
|
||||
class="text-muted small ms-1"><?= h($animal['adoption_unavailability_reason']) ?></span><?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-end d-flex justify-content-end gap-2">
|
||||
|
||||
<!-- Modifier -->
|
||||
<?php if (!$isArchived): ?>
|
||||
<a class="btn btn-primary d-flex align-items-center gap-1"
|
||||
href="/animal/edit?id=<?= (int)$animal['id'] ?>">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="18" height="18" viewBox="0 0 24 24"
|
||||
stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round"
|
||||
stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M12 20h9" />
|
||||
<path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4Z" />
|
||||
</svg>
|
||||
<?= h(t('common.edit')) ?>
|
||||
</a>
|
||||
|
||||
<!-- Ajouter -->
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-success d-flex align-items-center gap-1 dropdown-toggle" type="button"
|
||||
data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="18" height="18"
|
||||
viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none"
|
||||
stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M12 5v14" />
|
||||
<path d="M5 12h14" />
|
||||
</svg>
|
||||
<?= h(t('common.add')) ?>
|
||||
</button>
|
||||
|
||||
<div class="dropdown-menu dropdown-menu-end shadow">
|
||||
<?php if ($canViewMedical): ?>
|
||||
<a class="dropdown-item" href="#" data-bs-toggle="modal"
|
||||
data-bs-target="#modalMedicalStatus">
|
||||
🧬 <?= h(t('animal.medical_status')) ?>
|
||||
</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item" href="#" data-bs-toggle="modal" data-bs-target="#modalMedical">
|
||||
🩺 <?= h(t('animal.medical_note')) ?>
|
||||
</a>
|
||||
<a class="dropdown-item" href="#" data-bs-toggle="modal" data-bs-target="#modalTreatment">
|
||||
💊 <?= h(t('animal.treatment')) ?>
|
||||
</a>
|
||||
<a class="dropdown-item" href="#" data-bs-toggle="modal" data-bs-target="#modalVaccine">
|
||||
💉 <?= h(t('animal.vaccine')) ?>
|
||||
</a>
|
||||
<a class="dropdown-item" href="#" data-bs-toggle="modal"
|
||||
data-bs-target="#modalDewormingCatalog">
|
||||
🪱 <?= h(t('animals.dewormer')) ?>
|
||||
</a>
|
||||
<a class="dropdown-item" href="#" data-bs-toggle="modal"
|
||||
data-bs-target="#modalPrescription">📄
|
||||
<?= h(t('medical_document.menu_prescription')) ?></a>
|
||||
<a class="dropdown-item" href="#" data-bs-toggle="modal" data-bs-target="#modalLabReport">🧪
|
||||
<?= h(t('medical_document.menu_analysis')) ?></a>
|
||||
<a class="dropdown-item" href="#" data-bs-toggle="modal" data-bs-target="#modalExpense">💶
|
||||
<?= h(t('animal.veterinary_expense')) ?></a>
|
||||
<a class="dropdown-item" href="/animal/surgery/new?animal_id=<?= (int)$animal['id'] ?>">🏥
|
||||
Chirurgie</a>
|
||||
<?php endif; ?>
|
||||
<?php if (empty($bondedGroup)): ?>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item" href="/bonded/new?animal_id=<?= (int)$animal['id'] ?>">🔗
|
||||
<?= h(t('animal.bonded_group')) ?></a>
|
||||
<?php endif; ?>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item" href="#" data-bs-toggle="modal" data-bs-target="#modalPlacement">🏡
|
||||
<?= h(t('animal.placement_return')) ?></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions secondaires -->
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-outline-secondary dropdown-toggle" type="button"
|
||||
data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<?= h(t('common.actions')) ?>
|
||||
</button>
|
||||
|
||||
<div class="dropdown-menu dropdown-menu-end shadow animal-actions-menu">
|
||||
<a class="dropdown-item" href="#" data-bs-toggle="modal" data-bs-target="#modalAdoption">
|
||||
🏠 <?= h(t('animal.validate_adoption')) ?>
|
||||
</a>
|
||||
<a class="dropdown-item" href="/animal/pdf?id=<?= (int)$animal['id'] ?>" target="_blank">
|
||||
🖨 <?= h(t('common.print_record')) ?>
|
||||
</a>
|
||||
|
||||
<div class="dropdown-divider"></div>
|
||||
<?php if (!in_array((string)($animal['status'] ?? ''), ['decede','décédé'], true)): ?>
|
||||
<a class="dropdown-item text-dark" href="/animal/death?id=<?= (int)$animal['id'] ?>">🌈
|
||||
<?= h(t('animal.record_death')) ?>
|
||||
</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form method="post" action="/animal/archive"
|
||||
onsubmit="return confirm(<?= h(json_encode(t('animal.archive_confirm'), JSON_HEX_APOS | JSON_HEX_QUOT)) ?>);">
|
||||
<input type="hidden" name="id" value="<?= (int)$animal['id'] ?>">
|
||||
<button class="dropdown-item" type="submit">
|
||||
📦 <?= h(t('animal.archive')) ?>
|
||||
</button>
|
||||
</form>
|
||||
<form method="post" action="/animal/delete"
|
||||
onsubmit="const reason = prompt(<?= h(json_encode(t('animal.trash_reason'), JSON_HEX_APOS | JSON_HEX_QUOT)) ?>, <?= h(json_encode(t('animal.trash_default_reason'), JSON_HEX_APOS | JSON_HEX_QUOT)) ?>); if (reason === null) return false; this.elements.reason.value = reason; return confirm(<?= h(json_encode(t('animal.trash_confirm'), JSON_HEX_APOS | JSON_HEX_QUOT)) ?>);">
|
||||
<input type="hidden" name="id" value="<?= (int)$animal['id'] ?>">
|
||||
<input type="hidden" name="reason" value="">
|
||||
<button class="dropdown-item text-dark" type="submit">🗑
|
||||
<?= h(t('animal.move_to_trash')) ?></button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Retour -->
|
||||
<?php else: ?>
|
||||
<a class="btn btn-primary d-flex align-items-center gap-1"
|
||||
href="/animal/edit?id=<?= (int)$animal['id'] ?>">✏️ <?= h(t('common.edit')) ?></a>
|
||||
<a class="btn btn-outline-secondary" href="/animal/pdf?id=<?= (int)$animal['id'] ?>"
|
||||
target="_blank">🖨 <?= h(t('common.print_record')) ?></a>
|
||||
<form method="post" action="/animal/restore"
|
||||
onsubmit="return confirm(<?= h(json_encode(t('inactive.restore_confirm'), JSON_HEX_APOS | JSON_HEX_QUOT)) ?>);">
|
||||
<input type="hidden" name="csrf" value="<?= h(Auth::csrf()) ?>"><input type="hidden" name="id"
|
||||
value="<?= (int)$animal['id'] ?>"><button class="btn btn-success"
|
||||
type="submit"><?= h(t('common.restore')) ?></button></form>
|
||||
<?php endif; ?>
|
||||
<a class="btn btn-ghost-secondary" href="<?= $isArchived ? '/animals/archived' : '/animals' ?>">
|
||||
← <?= h(t('common.back')) ?>
|
||||
</a>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="col-12">
|
||||
|
||||
<div class="mt-">
|
||||
|
||||
<ul id="submenus" class="nav nav-tabs nav-tabs-alt mt-3 mb-0">
|
||||
<li class="nav-item"><a href="#tab-ident" class="nav-link active" data-bs-toggle="tab">🧬
|
||||
<?= h(t('animal.tab.identity')) ?></a></li>
|
||||
<li class="nav-item"><a href="#tab-activity" class="nav-link" data-bs-toggle="tab">📊
|
||||
<?= h(t('animal.tab.activity')) ?></a></li>
|
||||
<?php if ($canViewMedical): ?>
|
||||
<li class="nav-item"><a href="#tab-med" class="nav-link" data-bs-toggle="tab">❤️
|
||||
<?= h(t('animal.tab.medical')) ?></a></li>
|
||||
<li class="nav-item"><a href="#tab-trt" class="nav-link" data-bs-toggle="tab">💊
|
||||
<?= h(t('animal.tab.treatments')) ?></a></li>
|
||||
<li class="nav-item"><a href="#tab-vac" class="nav-link" data-bs-toggle="tab">💉
|
||||
<?= h(t('animal.tab.vaccines')) ?></a></li>
|
||||
<?php endif; ?>
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" id="animalMoreTabs" href="#" data-bs-toggle="dropdown"
|
||||
role="button" aria-expanded="false"><?= h(t('animal.tab.more')) ?></a>
|
||||
<div class="dropdown-menu shadow">
|
||||
<a href="#tab-locations" class="dropdown-item" data-bs-toggle="tab">📍
|
||||
<?= h(t('animal.tab.locations')) ?></a>
|
||||
<a href="#tab-placements" class="dropdown-item" data-bs-toggle="tab">🏡
|
||||
<?= h(t('animal.tab.placements')) ?></a>
|
||||
<?php if ($isCat): ?><a href="#tab-litters" class="dropdown-item" data-bs-toggle="tab">🐾
|
||||
<?= h(t('animal.tab.litters')) ?></a><?php endif; ?>
|
||||
<?php if ($canViewMedical): ?><a href="#tab-documents" class="dropdown-item"
|
||||
data-bs-toggle="tab">📄 <?= h(t('animal.tab.documents')) ?></a><?php endif; ?>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a href="#tab-hist" class="dropdown-item" data-bs-toggle="tab">🕒
|
||||
<?= h(t('animal.tab.history')) ?></a>
|
||||
<a href="#tab-photos" class="dropdown-item" data-bs-toggle="tab">📷
|
||||
<?= h(t('animal.tab.photos')) ?></a>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="card border-top-0 rounded-top-0">
|
||||
<div class="card-body">
|
||||
<div class="tab-content">
|
||||
|
||||
<?php require __DIR__ . '/_animal_show_identity.php'; ?>
|
||||
|
||||
<?php require __DIR__ . '/_animal_show_locations.php'; ?>
|
||||
|
||||
<?php require __DIR__ . '/_animal_show_activity.php'; ?>
|
||||
|
||||
<?php require __DIR__ . '/_animal_show_medical.php'; ?>
|
||||
|
||||
<?php require __DIR__ . '/_animal_show_litters.php'; ?>
|
||||
|
||||
<?php require __DIR__ . '/_animal_show_documents.php'; ?>
|
||||
|
||||
<?php require __DIR__ . '/_animal_show_history.php'; ?>
|
||||
|
||||
<?php require __DIR__ . '/_animal_show_photos.php'; ?>
|
||||
|
||||
<?php require __DIR__ . '/_animal_show_modals.php'; ?>
|
||||
168
app/Views/animals_inactive.php
Normal file
168
app/Views/animals_inactive.php
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
<?php
|
||||
$isTrash = $mode === 'trash';
|
||||
$basePath = $isTrash ? '/animals/trash' : '/animals/archived';
|
||||
$removedLabel = $isTrash ? t('inactive.deleted_on') : t('inactive.archived_on');
|
||||
$profileMark = static function (string $value, string $label): string {
|
||||
[$symbol,$class,$state] = match($value) {
|
||||
'yes' => ['✓','text-green',t('common.yes')],'no' => ['✕','text-red',t('common.no')],default => ['—','text-secondary',t('common.unknown')]
|
||||
};
|
||||
return '<span class="' . $class . '" title="' . h($label . ' : ' . $state) . '">' . $symbol . '</span>';
|
||||
};
|
||||
$testBadge = static function (string $value, string $label): string {
|
||||
[$symbol,$class,$title] = match($value) {
|
||||
'neg' => ['−','bg-green-lt',t('animals.test_negative')],'pos' => ['+','bg-red-lt',t('animals.test_positive')],'doubtful' => ['?','bg-yellow-lt',t('animals.test_doubtful')],default => ['?','bg-secondary-lt',t('animals.test_unknown')]
|
||||
};
|
||||
return '<span class="badge ' . $class . '" title="' . h($label . ' : ' . $title) . '">' . h($label . ' ' . $symbol) . '</span>';
|
||||
};
|
||||
?>
|
||||
<style>
|
||||
.inactive-age {
|
||||
width: 5.75rem;
|
||||
min-width: 5.75rem;
|
||||
white-space: nowrap
|
||||
}
|
||||
|
||||
.inactive-profile {
|
||||
width: 5.75rem;
|
||||
min-width: 5.75rem;
|
||||
text-align: center
|
||||
}
|
||||
|
||||
.inactive-profile-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: .15rem;
|
||||
align-items: center
|
||||
}
|
||||
|
||||
.inactive-profile-grid>* {
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
font-weight: 700
|
||||
}
|
||||
|
||||
.inactive-photo {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
object-fit: cover;
|
||||
border-radius: 10px
|
||||
}
|
||||
|
||||
.inactive-weight {
|
||||
min-width: 7.5rem;
|
||||
white-space: nowrap
|
||||
}
|
||||
|
||||
</style>
|
||||
<div class="card">
|
||||
<div class="card-header d-flex flex-wrap align-items-center gap-3">
|
||||
<div class="me-auto">
|
||||
<h3 class="card-title mb-1"><?= $isTrash ? t('inactive.trash') : t('inactive.archives') ?></h3>
|
||||
<div class="text-muted small"><?= $isTrash ? t('inactive.trash_help') : t('inactive.archive_help') ?></div>
|
||||
</div><a class="btn btn-outline-primary" href="/animals"><?= h(t('inactive.back')) ?></a>
|
||||
</div>
|
||||
<div class="card-body border-bottom">
|
||||
<form class="row g-2 align-items-center" method="get" action="<?= h($basePath) ?>">
|
||||
<div class="col"><input class="form-control" name="q" value="<?= h($q) ?>"
|
||||
placeholder="<?= h(t('inactive.search')) ?>"></div>
|
||||
<div class="col-12 col-md-3"><select class="form-select"
|
||||
name="status"><?php foreach ($statuses as $key => $label): ?><option value="<?= h($key) ?>"
|
||||
<?= $status === $key ? 'selected' : '' ?>><?= h($label) ?></option><?php endforeach; ?></select>
|
||||
</div>
|
||||
<div class="col-auto"><button class="btn btn-primary" type="submit"><?= h(t('common.search')) ?></button>
|
||||
</div><?php if ($q !== '' || $status !== ''): ?><div class="col-auto"><a class="btn btn-outline-secondary"
|
||||
href="<?= h($basePath) ?>"><?= h(t('common.clear')) ?></a></div><?php endif; ?>
|
||||
</form>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-vcenter card-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><?= h(t('animals.photo')) ?></th>
|
||||
<th><?= sort_th(t('animals.name'), 'name', $sort, $dir, $basePath) ?> <span
|
||||
class="text-muted mx-1">·</span>
|
||||
<?= sort_th(t('animals.sex'), 'sex', $sort, $dir, $basePath) ?></th>
|
||||
<th class="inactive-age"><?= sort_th(t('animals.age'), 'age', $sort, $dir, $basePath) ?></th>
|
||||
<th><?= sort_th(t('animals.status'), 'status', $sort, $dir, $basePath) ?></th>
|
||||
<th class="inactive-profile">
|
||||
<div class="inactive-profile-grid"><?php foreach (['compatibility_dogs' => ['🐶',t('animals.dogs')],'compatibility_cats' => ['🐱',t('animals.cats')],'house_trained' => ['🧼',t('animals.cleanliness')]] as $key => [$icon,$label]):$qs = $_GET;
|
||||
$qs['sort'] = $key;
|
||||
$qs['dir'] = $sort === $key && $dir === 'ASC' ? 'desc' : 'asc'; ?><a class="text-reset text-decoration-none"
|
||||
href="<?= h($basePath . '?' . http_build_query($qs)) ?>"
|
||||
title="<?= h(t('animals.sort_by', ['label' => $label])) ?>"><?= $icon ?><?= $sort === $key ? ($dir === 'ASC' ? '↑' : '↓') : '' ?></a><?php endforeach; ?>
|
||||
</div>
|
||||
</th>
|
||||
<th class="text-nowrap">FIV/FeLV</th>
|
||||
<th class="inactive-weight"><?= sort_th(t('common.weight'), 'weight', $sort, $dir, $basePath) ?>
|
||||
</th>
|
||||
<th><?= sort_th(t('animals.location'), 'rescue_location_name', $sort, $dir, $basePath) ?></th>
|
||||
<th class="text-nowrap"><?= sort_th($removedLabel, 'removed_at', $sort, $dir, $basePath) ?></th>
|
||||
<?php if ($isTrash): ?><th><?= h(t('inactive.reason')) ?></th><?php endif; ?><th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($animals as $animal):
|
||||
$src = !empty($animal['primary_photo']) ? '/media/animals/' . (int)$animal['id'] . '/' . rawurlencode($animal['primary_photo']) : '/assets/cat_placeholder.svg';
|
||||
$ageLabel = '—';
|
||||
if (!empty($animal['birth_date'])) {
|
||||
try {
|
||||
$age = (new DateTimeImmutable((string)$animal['birth_date']))->diff(new DateTimeImmutable('today'));
|
||||
$ageLabel = $age->y > 0 ? $age->y . ' a' . ($age->m ? ' · ' . $age->m . ' m' : '') : $age->m . ' m';
|
||||
if (!empty($animal['birth_is_estimated'])) {
|
||||
$ageLabel = '~ ' . $ageLabel;
|
||||
}
|
||||
} catch (Throwable) {
|
||||
}
|
||||
}
|
||||
$sex = match((string)($animal['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')]
|
||||
};
|
||||
$removed = $animal[$isTrash ? 'deleted_at' : 'archived_at'] ?? null;
|
||||
$removedDate = $removed ? (new DateTimeImmutable((string)$removed))->format('d/m/Y H:i') : '—';
|
||||
?>
|
||||
<tr>
|
||||
<td><img class="inactive-photo" src="<?= h($src) ?>" alt=""></td>
|
||||
<td>
|
||||
<div class="d-flex align-items-center gap-2"><span class="coat-swatch"
|
||||
style="<?= h(coat_swatch_style($animal['color'] ?? '')) ?>"></span><?php if (!$isTrash): ?><a
|
||||
class="fw-bold"
|
||||
href="/animal?id=<?= (int)$animal['id'] ?>"><?= h($animal['name']) ?></a><?php else: ?><strong><?= h($animal['name']) ?></strong><?php endif; ?><span
|
||||
class="badge bg-secondary-lt"><?= h(species_label($animal['species'] ?? '')) ?></span><span
|
||||
class="badge <?= h($sex[1]) ?>" title="<?= h($sex[2]) ?>"><?= h($sex[0]) ?></span></div>
|
||||
</td>
|
||||
<td class="inactive-age"><?= h($ageLabel) ?></td>
|
||||
<td><?php [$statusClass,$statusLabel] = status_badge((string)$animal['status']); ?><span
|
||||
class="badge <?= h($statusClass) ?>"><?= h($statusLabel) ?></span></td>
|
||||
<td class="inactive-profile">
|
||||
<div class="inactive-profile-grid">
|
||||
<?= $profileMark((string)($animal['compatibility_dogs'] ?? 'unknown'), t('animals.dogs')) ?><?= $profileMark((string)($animal['compatibility_cats'] ?? 'unknown'), t('animals.cats')) ?><?= $profileMark((string)($animal['house_trained'] ?? 'unknown'), t('animals.cleanliness')) ?>
|
||||
</div>
|
||||
</td>
|
||||
<td><?php if (SpeciesService::isCat((string)($animal['species'] ?? ''))): ?><div
|
||||
class="d-flex flex-nowrap gap-1">
|
||||
<?= $testBadge((string)($animal['fiv_status'] ?? 'unknown'), 'FIV') ?><?= $testBadge((string)($animal['felv_status'] ?? 'unknown'), 'FeLV') ?>
|
||||
</div><?php else: ?><span class="text-muted">—</span><?php endif; ?></td>
|
||||
<td class="inactive-weight"><?php if ($animal['last_weight'] === null): ?>—<?php else:$arrow = '→';
|
||||
if ($animal['prev_weight'] !== null) {
|
||||
$arrow = (float)$animal['last_weight'] > (float)$animal['prev_weight'] ? '↑' : ((float)$animal['last_weight'] < (float)$animal['prev_weight'] ? '↓' : '→');
|
||||
} ?><?= h(number_format((float)$animal['last_weight'], 2, ',', ' ')) ?> kg <span
|
||||
class="text-muted"><?= $arrow ?></span><?php endif; ?></td>
|
||||
<td><?= h(($animal['rescue_location_name'] ?? '') ?: ($animal['rescue_address'] ?? '')) ?></td>
|
||||
<td class="text-nowrap"><?= h($removedDate) ?></td><?php if ($isTrash): ?><td>
|
||||
<?= h($animal['deletion_reason'] ?: '—') ?></td><?php endif; ?>
|
||||
<td class="text-end">
|
||||
<form method="post" action="/animal/restore"
|
||||
onsubmit="return confirm(<?= h(json_encode(t('inactive.restore_confirm'), JSON_HEX_APOS | JSON_HEX_QUOT)) ?>);">
|
||||
<input type="hidden" name="csrf" value="<?= h(Auth::csrf()) ?>"><input type="hidden"
|
||||
name="id" value="<?= (int)$animal['id'] ?>"><button class="btn btn-success btn-sm"
|
||||
type="submit"><?= h(t('inactive.restore')) ?></button></form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?><?php if (!$animals): ?><tr>
|
||||
<td colspan="<?= $isTrash ? 11 : 10 ?>" class="text-center text-muted py-4">
|
||||
<?= $isTrash ? t('inactive.no_trash') : t('inactive.no_archive') ?></td>
|
||||
</tr><?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
918
app/Views/animals_list.php
Normal file
918
app/Views/animals_list.php
Normal file
|
|
@ -0,0 +1,918 @@
|
|||
<?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>
|
||||
229
app/Views/auth.php
Normal file
229
app/Views/auth.php
Normal file
|
|
@ -0,0 +1,229 @@
|
|||
<!doctype html>
|
||||
<html lang="<?= htmlspecialchars(I18n::locale()) ?>">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title><?= htmlspecialchars($title) ?> — Globinours</title>
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<link rel="manifest" href="/manifest.webmanifest">
|
||||
<link rel="stylesheet" href="/assets/vendor/tabler/tabler.min.css">
|
||||
<link rel="stylesheet" href="/assets/mobile.css">
|
||||
<script defer src="/assets/pwa.js"></script>
|
||||
<style>
|
||||
body {
|
||||
min-height: 100vh;
|
||||
background: radial-gradient(circle at 15% 15%, rgba(214, 174, 103, .22), transparent 32rem), radial-gradient(circle at 85% 85%, rgba(176, 128, 65, .16), transparent 30rem), #f7f1e6;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 2rem 0
|
||||
}
|
||||
|
||||
.login-shell {
|
||||
width: min(94vw, <?=$mode === 'setup' ? '980px' : '440px' ?>)
|
||||
}
|
||||
|
||||
.login-logo {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
object-fit: contain;
|
||||
filter: drop-shadow(0 10px 22px rgba(70, 52, 30, .12))
|
||||
}
|
||||
|
||||
.login-card {
|
||||
border: 1px solid rgba(145, 110, 65, .18);
|
||||
border-radius: 1.2rem;
|
||||
box-shadow: 0 18px 55px rgba(71, 53, 31, .12);
|
||||
overflow: hidden
|
||||
}
|
||||
|
||||
.login-card .card-body {
|
||||
padding: 2rem
|
||||
}
|
||||
|
||||
.brand-subtitle {
|
||||
color: #7b6b59
|
||||
}
|
||||
|
||||
.check-row {
|
||||
display: flex;
|
||||
gap: .75rem;
|
||||
padding: .6rem 0;
|
||||
border-bottom: 1px solid rgba(98, 105, 118, .12)
|
||||
}
|
||||
|
||||
.check-row:last-child {
|
||||
border-bottom: 0
|
||||
}
|
||||
|
||||
.check-icon {
|
||||
width: 1.5rem;
|
||||
flex: 0 0 1.5rem;
|
||||
text-align: center
|
||||
}
|
||||
|
||||
.setup-path {
|
||||
word-break: break-all
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<main class="login-shell">
|
||||
<div class="text-center mb-3"><img class="login-logo" src="/auth/logo" alt="Globinours">
|
||||
<h1 class="mb-1">Globinours</h1>
|
||||
<div class="brand-subtitle"><?= htmlspecialchars(t('auth.tagline')) ?></div>
|
||||
</div>
|
||||
<form id="authForm" method="post" action="<?= $mode === 'setup' ? '/setup' : '/login' ?>"
|
||||
class="card login-card" novalidate>
|
||||
<div class="card-body"><input type="hidden" name="csrf"
|
||||
value="<?= htmlspecialchars(Auth::csrf()) ?>"><?php if ($mode === 'login'): ?><input type="hidden"
|
||||
name="next" value="<?= htmlspecialchars((string)($_GET['next'] ?? '/dashboard')) ?>"><?php endif; ?>
|
||||
<h2 class="text-center mb-3"><?= htmlspecialchars(t($mode === 'setup' ? 'auth.setup' : 'auth.login')) ?>
|
||||
</h2><?php if ($mode === 'login' && isset($_GET['restored'])): ?><div class="alert alert-success">✅
|
||||
<?= htmlspecialchars(t('auth.restore_success')) ?></div><?php endif; ?><?php if ($error): ?><div
|
||||
class="alert alert-danger"><?= htmlspecialchars($error) ?></div><?php endif; ?><div
|
||||
id="formValidationError" class="alert alert-danger d-none">
|
||||
<?= htmlspecialchars(t('auth.fix_fields')) ?></div>
|
||||
<?php if ($mode === 'setup'): ?><div class="row g-4">
|
||||
<div class="col-lg-5">
|
||||
<h3><?= h(t('install.requirements')) ?></h3>
|
||||
<p class="text-muted small"><?= h(t('install.requirements_help')) ?></p>
|
||||
<?php foreach ($checks ?? [] as $check): ?><div class="check-row"><span
|
||||
class="check-icon"><?= $check['ok'] ? '✅' : ($check['level'] === 'required' ? '❌' : '⚠️') ?></span>
|
||||
<div><strong><?= h($check['label']) ?></strong>
|
||||
<div class="small text-muted"><?= h($check['detail']) ?></div>
|
||||
</div>
|
||||
</div><?php endforeach; ?><div class="alert alert-info mt-3 mb-0">
|
||||
<strong><?= h(t('install.public_directory')) ?></strong>
|
||||
<div class="setup-path small mt-1"><?= h($publicPath ?? '') ?></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-7">
|
||||
<h3><?= h(t('install.association')) ?></h3>
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-12"><label
|
||||
class="form-label required"><?= h(t('settings.association_name')) ?></label><input
|
||||
class="form-control" name="association_name"
|
||||
value="<?= h($_POST['association_name'] ?? '') ?>" required></div>
|
||||
<div class="col-md-6"><label class="form-label"><?= h(t('settings.rna')) ?></label><input
|
||||
class="form-control" name="association_rna"
|
||||
value="<?= h($_POST['association_rna'] ?? '') ?>"></div>
|
||||
<div class="col-md-6"><label class="form-label"><?= h(t('settings.siret')) ?></label><input
|
||||
class="form-control" name="association_siret"
|
||||
value="<?= h($_POST['association_siret'] ?? '') ?>"></div>
|
||||
<div class="col-12"><label class="form-label"><?= h(t('settings.address')) ?></label><input
|
||||
class="form-control" name="association_address"
|
||||
value="<?= h($_POST['association_address'] ?? '') ?>"></div>
|
||||
<div class="col-md-4"><label
|
||||
class="form-label"><?= h(t('settings.postcode')) ?></label><input
|
||||
class="form-control" name="association_postal_code"
|
||||
value="<?= h($_POST['association_postal_code'] ?? '') ?>"></div>
|
||||
<div class="col-md-8"><label class="form-label"><?= h(t('settings.city')) ?></label><input
|
||||
class="form-control" name="association_city"
|
||||
value="<?= h($_POST['association_city'] ?? '') ?>"></div>
|
||||
<div class="col-md-6"><label class="form-label"><?= h(t('settings.phone')) ?></label><input
|
||||
class="form-control" name="association_phone"
|
||||
value="<?= h($_POST['association_phone'] ?? '') ?>"></div>
|
||||
<div class="col-md-6"><label class="form-label"><?= h(t('settings.email')) ?></label><input
|
||||
class="form-control" type="email" name="association_email"
|
||||
value="<?= h($_POST['association_email'] ?? '') ?>"></div>
|
||||
<div class="col-md-6"><label
|
||||
class="form-label"><?= h(t('settings.language')) ?></label><select
|
||||
class="form-select"
|
||||
name="app_language"><?php foreach (I18n::LOCALES as $code => $label): ?><option
|
||||
value="<?= h($code) ?>"><?= h($label) ?></option><?php endforeach; ?></select>
|
||||
</div>
|
||||
<div class="col-md-6"><label
|
||||
class="form-label"><?= h(t('public_site.main_address')) ?></label><select
|
||||
class="form-select" name="public_site_routing">
|
||||
<option value="integrated"><?= h(t('public_site.routing_management')) ?></option>
|
||||
<option value="root"><?= h(t('public_site.routing_public')) ?></option>
|
||||
</select></div>
|
||||
</div>
|
||||
<h3><?= h(t('install.administrator')) ?></h3>
|
||||
<div class="mb-3"><label
|
||||
class="form-label"><?= htmlspecialchars(t('auth.display_name')) ?></label><input
|
||||
class="form-control form-control-lg" name="display_name" autocomplete="name" required
|
||||
autofocus>
|
||||
<div class="invalid-feedback"></div>
|
||||
</div><?php endif; ?>
|
||||
<div class="mb-3"><label
|
||||
class="form-label"><?= htmlspecialchars(t('auth.username')) ?></label><input
|
||||
class="form-control form-control-lg" name="username" autocomplete="username" required
|
||||
<?= $mode === 'login' ? 'autofocus' : '' ?>>
|
||||
<div class="invalid-feedback"></div>
|
||||
</div>
|
||||
<div class="mb-3"><label
|
||||
class="form-label"><?= htmlspecialchars(t('auth.password')) ?></label><input
|
||||
class="form-control form-control-lg" type="password" name="password"
|
||||
autocomplete="<?= $mode === 'setup' ? 'new-password' : 'current-password' ?>"
|
||||
minlength="10" required>
|
||||
<div class="invalid-feedback"></div>
|
||||
</div>
|
||||
<?php if ($mode === 'login'): ?><label class="form-check mb-2"><input class="form-check-input"
|
||||
type="checkbox" name="remember_device" value="1"><span
|
||||
class="form-check-label"><?= h(t('auth.remember_device')) ?></span><span
|
||||
class="form-hint"><?= h(t('auth.remember_device_help')) ?></span></label><input
|
||||
type="hidden" name="device_name" id="deviceName" value=""><?php endif; ?>
|
||||
<?php if ($mode === 'setup'): ?><div class="mb-3"><label
|
||||
class="form-label"><?= htmlspecialchars(t('auth.password_confirm')) ?></label><input
|
||||
class="form-control form-control-lg" type="password" name="password_confirm"
|
||||
autocomplete="new-password" minlength="10" required>
|
||||
<div class="invalid-feedback"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div><?php endif; ?>
|
||||
<button class="btn btn-primary btn-lg w-100 mt-3"
|
||||
<?= $mode === 'setup' && !($canInstall ?? false) ? 'disabled' : '' ?>><?= htmlspecialchars(t($mode === 'setup' ? 'auth.create' : 'auth.sign_in')) ?></button>
|
||||
</div>
|
||||
</form>
|
||||
</main>
|
||||
<script>
|
||||
const form = document.getElementById('authForm'),
|
||||
summary = document.getElementById('formValidationError');
|
||||
|
||||
function frenchMessage(field) {
|
||||
if (field.validity.valueMissing) return < ? = json_encode(t('auth.required'), JSON_UNESCAPED_UNICODE) ? > ;
|
||||
if (field.validity.tooShort) return < ? = json_encode(t('auth.password_min', ['count' => '__COUNT__']),
|
||||
JSON_UNESCAPED_UNICODE) ? > .replace('__COUNT__', field.minLength);
|
||||
if (field.validity.patternMismatch) return < ? = json_encode(t('auth.invalid_format'),
|
||||
JSON_UNESCAPED_UNICODE) ? > ;
|
||||
return < ? = json_encode(t('auth.check_field'), JSON_UNESCAPED_UNICODE) ? > ;
|
||||
}
|
||||
form.addEventListener('submit', event => {
|
||||
form.querySelectorAll('.is-invalid').forEach(el => el.classList.remove('is-invalid'));
|
||||
const password = form.elements.password,
|
||||
confirmation = form.elements.password_confirm;
|
||||
if (confirmation) confirmation.setCustomValidity(password.value !== confirmation.value ? < ? =
|
||||
json_encode(t('auth.password_mismatch'), JSON_UNESCAPED_UNICODE) ? > : '');
|
||||
if (!form.checkValidity()) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
summary.classList.remove('d-none');
|
||||
form.querySelectorAll(':invalid').forEach(field => {
|
||||
field.classList.add('is-invalid');
|
||||
const feedback = field.parentElement.querySelector('.invalid-feedback');
|
||||
if (feedback) feedback.textContent = field.validationMessage && field.validity
|
||||
.customError ? field.validationMessage : frenchMessage(field);
|
||||
});
|
||||
form.querySelector(':invalid')?.focus();
|
||||
}
|
||||
});
|
||||
form.querySelectorAll('input').forEach(field => field.addEventListener('input', () => {
|
||||
field.setCustomValidity('');
|
||||
field.classList.remove('is-invalid');
|
||||
summary.classList.add('d-none');
|
||||
}));
|
||||
const deviceName = document.getElementById('deviceName');
|
||||
if (deviceName && navigator.userAgent.includes('GlobinoursAndroid')) {
|
||||
deviceName.value = 'Application Android';
|
||||
const remember = form.elements.remember_device;
|
||||
if (remember) remember.checked = true;
|
||||
}
|
||||
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
47
app/Views/bonded_form.php
Normal file
47
app/Views/bonded_form.php
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
<form method="post" action="/bonded/save" class="card">
|
||||
<input type="hidden" name="animal_ids[]" value="<?= (int)$sourceAnimalId ?>">
|
||||
<div class="card-header d-flex align-items-center justify-content-between">
|
||||
<div>
|
||||
<h3 class="card-title mb-1"><?= h(t('bonded.create')) ?></h3>
|
||||
<div class="text-muted small"><?= h(t('bonded.intro')) ?></div>
|
||||
</div>
|
||||
<a class="btn btn-ghost-secondary"
|
||||
href="/animal?id=<?= (int)$sourceAnimalId ?>"><?= h(t('common.cancel')) ?></a>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-5">
|
||||
<label class="form-label"><?= h(t('bonded.group_name')) ?> <span
|
||||
class="text-muted"><?= h(t('common.optional_parentheses')) ?></span></label>
|
||||
<input class="form-control" name="name" placeholder="<?= h(t('bonded.group_example')) ?>">
|
||||
</div>
|
||||
<div class="col-md-7">
|
||||
<label class="form-label"><?= h(t('animal.note')) ?> <span
|
||||
class="text-muted"><?= h(t('common.optional_parentheses')) ?></span></label>
|
||||
<input class="form-control" name="notes" placeholder="<?= h(t('bonded.note_example')) ?>">
|
||||
</div>
|
||||
</div>
|
||||
<h4><?= h(t('bonded.members')) ?></h4>
|
||||
<div class="row g-2">
|
||||
<?php foreach ($animals as $candidate): ?>
|
||||
<?php $isSource = (int)$candidate['id'] === (int)$sourceAnimalId; ?>
|
||||
<div class="col-md-4">
|
||||
<label class="form-select d-flex align-items-center gap-2 h-100 <?= $isSource ? 'bg-blue-lt' : '' ?>"
|
||||
style="cursor:pointer">
|
||||
<input class="form-check-input mt-0" type="checkbox"
|
||||
<?= $isSource ? 'checked disabled' : 'name="animal_ids[]"' ?>
|
||||
value="<?= (int)$candidate['id'] ?>">
|
||||
<span class="coat-swatch" style="<?= h(coat_swatch_style($candidate['color'] ?? '')) ?>"></span>
|
||||
<span><strong><?= h($candidate['name']) ?></strong><br><small
|
||||
class="text-muted"><?= h(species_label($candidate['species'] ?? 'chat')) ?> ·
|
||||
<?= h($candidate['status']) ?></small></span>
|
||||
</label>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer d-flex justify-content-between align-items-center">
|
||||
<span class="text-muted"><?= h(t('bonded.select_two')) ?></span>
|
||||
<button class="btn btn-primary" type="submit"><?= h(t('bonded.save')) ?></button>
|
||||
</div>
|
||||
</form>
|
||||
415
app/Views/care_round.php
Normal file
415
app/Views/care_round.php
Normal file
|
|
@ -0,0 +1,415 @@
|
|||
<?php
|
||||
function care_boxes(array $layout): array
|
||||
{
|
||||
$top = match($layout['top_layout']) {
|
||||
'left2' => ['top_ab' => t('care.box_top_left_double'),'top_c' => t('care.box_top_right')],'right2' => ['top_a' => t('care.box_top_left'),'top_bc' => t('care.box_top_right_double')],'merged' => ['top_all' => t('care.box_top_full')],default => ['top_a' => t('care.box_top_left'),'top_b' => t('care.box_top_center'),'top_c' => t('care.box_top_right')]
|
||||
};
|
||||
$bottom = $layout['bottom_layout'] === 'merged' ? ['bottom_all' => t('care.box_bottom_full')] : ['bottom_a' => t('care.box_bottom_left'),'bottom_b' => t('care.box_bottom_right')];
|
||||
return $top + $bottom + ['' => t('care.box_unassigned')];
|
||||
}
|
||||
$roomsByCode = [];
|
||||
foreach ($rooms as $configuredRoom) {
|
||||
$roomsByCode[$configuredRoom['code']] = $configuredRoom;
|
||||
}
|
||||
$byRoom = [];
|
||||
foreach ($animals as $a) {
|
||||
$byRoom[ShelterRoomService::resolveAnimal($a, $roomsByCode)][] = $a;
|
||||
}
|
||||
$fieldOptions = [
|
||||
'food' => ['normal' => t('care.food_normal'),'little' => t('care.food_little'),'refused' => t('care.food_refused')],
|
||||
'water' => ['normal' => t('care.water_normal'),'renewed' => t('care.water_renewed'),'abnormal' => t('care.abnormal')],
|
||||
'urine' => ['normal' => t('care.urine_normal'),'absent' => t('care.absent_feminine'),'abnormal' => t('care.abnormal')],
|
||||
'stool' => ['normal' => t('care.stool_normal'),'absent' => t('care.absent_feminine'),'abnormal' => t('care.abnormal')],
|
||||
'general_state' => ['good' => t('care.state_good'),'watch' => t('care.watch'),'alert' => t('care.alert')],
|
||||
];
|
||||
?>
|
||||
<style>
|
||||
.care-mode .btn,
|
||||
.care-mode .form-control {
|
||||
min-height: 44px
|
||||
}
|
||||
|
||||
.care-room {
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-radius: 1rem;
|
||||
overflow: hidden
|
||||
}
|
||||
|
||||
.care-box-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(6, 1fr);
|
||||
gap: .75rem
|
||||
}
|
||||
|
||||
.care-box {
|
||||
grid-column: span 3;
|
||||
min-height: 120px;
|
||||
border: 2px solid rgba(98, 105, 118, .2);
|
||||
border-radius: .8rem;
|
||||
padding: .75rem;
|
||||
background: #fff
|
||||
}
|
||||
|
||||
.care-box.box-top_a,
|
||||
.care-box.box-top_b,
|
||||
.care-box.box-top_c {
|
||||
grid-column: span 2
|
||||
}
|
||||
|
||||
.care-box.box-top_ab,
|
||||
.care-box.box-top_bc {
|
||||
grid-column: span 4
|
||||
}
|
||||
|
||||
.care-box.box-top_all,
|
||||
.care-box.box-bottom_all {
|
||||
grid-column: span 6
|
||||
}
|
||||
|
||||
.care-collective {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: .65rem
|
||||
}
|
||||
|
||||
.care-collective-title {
|
||||
grid-column: 1/-1
|
||||
}
|
||||
|
||||
.care-box-complete {
|
||||
border-color: #2fb344;
|
||||
background: #f1fbf4
|
||||
}
|
||||
|
||||
.care-cat {
|
||||
min-width: 0;
|
||||
border: 1px solid rgba(98, 105, 118, .18);
|
||||
border-radius: .65rem;
|
||||
background: #fff
|
||||
}
|
||||
|
||||
.care-cat[open] {
|
||||
box-shadow: 0 8px 24px rgba(31, 42, 68, .1)
|
||||
}
|
||||
|
||||
.care-cat summary {
|
||||
list-style: none;
|
||||
cursor: pointer;
|
||||
padding: .65rem
|
||||
}
|
||||
|
||||
.care-cat summary::-webkit-details-marker {
|
||||
display: none
|
||||
}
|
||||
|
||||
.care-cat-photo {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
object-fit: cover;
|
||||
border-radius: .6rem
|
||||
}
|
||||
|
||||
.care-checked {
|
||||
border-color: #2fb344;
|
||||
background: #f1fbf4
|
||||
}
|
||||
|
||||
.care-choice input {
|
||||
position: absolute;
|
||||
opacity: 0
|
||||
}
|
||||
|
||||
.care-choice span {
|
||||
display: block;
|
||||
padding: .55rem .7rem;
|
||||
border: 1px solid #d6d9df;
|
||||
border-radius: .5rem;
|
||||
text-align: center
|
||||
}
|
||||
|
||||
.care-choice input:checked+span {
|
||||
background: #206bc4;
|
||||
color: #fff;
|
||||
border-color: #206bc4
|
||||
}
|
||||
|
||||
@media(max-width:800px) {
|
||||
.care-box {
|
||||
grid-column: span 6 !important
|
||||
}
|
||||
|
||||
.care-collective {
|
||||
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr))
|
||||
}
|
||||
}
|
||||
|
||||
@media(max-width:520px) {
|
||||
.care-collective {
|
||||
grid-template-columns: 1fr
|
||||
}
|
||||
}
|
||||
|
||||
.care-room-silver {
|
||||
border-color: #aeb5bd
|
||||
}
|
||||
|
||||
.care-room-silver>.card-header,
|
||||
.care-room-silver>.card-body {
|
||||
background: #edf0f2
|
||||
}
|
||||
|
||||
.care-room-silver .care-cat {
|
||||
background: #f8f9fa;
|
||||
border-color: #adb5bd
|
||||
}
|
||||
|
||||
.care-room-twix {
|
||||
border-color: #f0a35e
|
||||
}
|
||||
|
||||
.care-room-twix>.card-header,
|
||||
.care-room-twix>.card-body {
|
||||
background: #fff0df
|
||||
}
|
||||
|
||||
.care-room-twix .care-cat {
|
||||
background: #fff9f2;
|
||||
border-color: #f0a35e
|
||||
}
|
||||
|
||||
.care-room-care {
|
||||
border-color: #e58a8a
|
||||
}
|
||||
|
||||
.care-room-care>.card-header,
|
||||
.care-room-care>.card-body {
|
||||
background: #fdeaea
|
||||
}
|
||||
|
||||
.care-room-care .care-box {
|
||||
background: #fff6f6;
|
||||
border-color: #e7a0a0
|
||||
}
|
||||
|
||||
.care-room-quarantine {
|
||||
border-color: #d8bd52
|
||||
}
|
||||
|
||||
.care-room-quarantine>.card-header,
|
||||
.care-room-quarantine>.card-body {
|
||||
background: #fff5c9
|
||||
}
|
||||
|
||||
.care-room-quarantine .care-box {
|
||||
background: #fffbed;
|
||||
border-color: #ddc76d
|
||||
}
|
||||
|
||||
.care-room-configured {
|
||||
border-color: var(--room-color)
|
||||
}
|
||||
|
||||
.care-room-configured>.card-header,
|
||||
.care-room-configured>.card-body {
|
||||
background: color-mix(in srgb, var(--room-color) 13%, #fff)
|
||||
}
|
||||
|
||||
.care-room-configured .care-box,
|
||||
.care-room-configured .care-cat {
|
||||
border-color: color-mix(in srgb, var(--room-color) 65%, #d6d9df)
|
||||
}
|
||||
|
||||
.care-room .care-box.care-box-complete,
|
||||
.care-room .care-cat.care-checked {
|
||||
border-color: #2fb344;
|
||||
background: #f1fbf4
|
||||
}
|
||||
|
||||
.care-round-toolbar {
|
||||
flex-wrap: nowrap !important
|
||||
}
|
||||
|
||||
.care-round-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: .5rem;
|
||||
margin-left: auto;
|
||||
white-space: nowrap
|
||||
}
|
||||
|
||||
.care-round-actions .flatpickr-wrapper,
|
||||
.care-round-actions input[type="date"],
|
||||
.care-round-actions .form-control {
|
||||
width: 160px;
|
||||
flex: 0 0 160px
|
||||
}
|
||||
|
||||
@media(max-width:900px) {
|
||||
.care-round-toolbar {
|
||||
flex-wrap: wrap !important
|
||||
}
|
||||
|
||||
.care-round-actions {
|
||||
width: 100%;
|
||||
margin-left: 0;
|
||||
flex-wrap: wrap
|
||||
}
|
||||
|
||||
.care-round-actions .flatpickr-wrapper {
|
||||
width: 160px
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
<div class="care-mode">
|
||||
<div class="card mb-3">
|
||||
<div class="card-body d-flex align-items-center gap-2 care-round-toolbar">
|
||||
<strong
|
||||
class="me-2"><?= h(t('care.round_of', ['period' => t($period === 'morning' ? 'care.morning' : 'care.evening')])) ?></strong>
|
||||
<a class="btn <?= $period === 'morning' ? 'btn-primary' : 'btn-outline-primary' ?>"
|
||||
href="/care-round?period=morning&date=<?= h($date) ?>">☀️ <?= h(t('care.morning')) ?></a>
|
||||
<a class="btn <?= $period === 'evening' ? 'btn-primary' : 'btn-outline-primary' ?>"
|
||||
href="/care-round?period=evening&date=<?= h($date) ?>">🌙 <?= h(t('care.evening')) ?></a>
|
||||
<div class="care-round-actions"><input class="form-control" type="date" value="<?= h($date) ?>"
|
||||
onchange="location.href='/care-round?period=<?= h($period) ?>&date='+this.value">
|
||||
<button class="btn btn-outline-secondary" type="button"
|
||||
onclick="document.documentElement.requestFullscreen?.()">⛶ <?= h(t('care.fullscreen')) ?></button>
|
||||
<a class="btn btn-outline-secondary" href="/care-round/setup">⚙️ <?= h(t('care.configure_boxes')) ?></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php foreach ($rooms as $room):$roomKey = (string)$room['code'];
|
||||
$roomLabel = (string)$room['name'];
|
||||
$boxed = $room['room_type'] === 'boxes'; ?>
|
||||
<section class="card care-room care-room-configured mb-3" style="--room-color:<?= h($room['color']) ?>"
|
||||
id="room-<?= h($roomKey) ?>">
|
||||
<div class="card-header">
|
||||
<h2 class="card-title"><?= h($roomLabel) ?></h2><span
|
||||
class="badge bg-secondary-lt ms-2"><?= h(tn('care.cat', 'care.cats', count($byRoom[$roomKey] ?? []))) ?></span>
|
||||
<?php if (!empty($room['bulk_validation']) && !empty($byRoom[$roomKey])): ?><form method="post"
|
||||
action="/care-round/save-room" class="ms-auto"
|
||||
onsubmit="return confirm(<?= h(json_encode(t('care.validate_room_confirm'), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)) ?>)">
|
||||
<input type="hidden" name="round_id" value="<?= (int)$round['id'] ?>"><input type="hidden" name="room"
|
||||
value="<?= h($roomKey) ?>"><input type="hidden" name="period" value="<?= h($period) ?>"><input
|
||||
type="hidden" name="date" value="<?= h($date) ?>"><button class="btn btn-success" type="submit">✓
|
||||
<?= h(t('care.everyone_well')) ?></button></form><?php endif; ?>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<?php $roomAnimals = $byRoom[$roomKey] ?? [];
|
||||
$configuredBoxCodes = array_column($room['boxes'], 'code');
|
||||
$legacyLayout = $boxed && in_array($roomKey, ['care','quarantine'], true) && !array_diff($configuredBoxCodes, ['top_a','top_b','top_c','bottom_a','bottom_b']) && !array_diff(['top_a','top_b','top_c','bottom_a','bottom_b'], $configuredBoxCodes);
|
||||
$boxes = $boxed ? ($legacyLayout ? care_boxes($layouts[$roomKey] ?? []) : array_column($room['boxes'], 'name', 'code')) : ['room' => $roomLabel];
|
||||
if ($boxed && !array_key_exists('', $boxes)) {
|
||||
$boxes += ['' => t('care.box_unassigned')];
|
||||
} ?>
|
||||
<div class="<?= $boxed ? 'care-box-grid' : 'row g-2' ?>">
|
||||
<?php foreach ($boxes as $boxKey => $boxLabel): $knownBoxKeys = array_values(array_filter(array_keys($boxes), static fn ($key) => $key !== ''));
|
||||
$boxAnimals = array_values(array_filter($roomAnimals, fn ($a) => !$boxed || ($boxKey === '' ? !in_array((string)($a['care_box_key'] ?? ''), $knownBoxKeys, true) : ($a['care_box_key'] ?? '') === $boxKey))); ?>
|
||||
<?php $boxComplete = $boxAnimals && count(array_filter($boxAnimals, fn ($cat) => isset($observations[(int)$cat['id']]))) === count($boxAnimals); ?>
|
||||
<div
|
||||
class="<?= $boxed ? 'care-box box-' . $boxKey : 'col-12 care-collective' ?> <?= $boxComplete ? 'care-box-complete' : '' ?>">
|
||||
<div class="fw-bold mb-2 <?= !$boxed ? 'care-collective-title' : '' ?>"><?= h($boxLabel) ?> <span
|
||||
class="text-muted">·
|
||||
<?= count($boxAnimals) ?></span><?= $boxComplete ? ' <span class="badge bg-green-lt ms-1">' . h(t('care.finished')) . '</span>' : '' ?>
|
||||
</div>
|
||||
<?php foreach ($boxAnimals as $a): $obs = $observations[(int)$a['id']] ?? null;
|
||||
$src = $a['primary_photo'] ? '/media/animals/' . (int)$a['id'] . '/' . rawurlencode($a['primary_photo']) : '/assets/cat_placeholder.svg'; ?>
|
||||
<details class="care-cat mb-2 <?= $obs ? 'care-checked' : '' ?>" id="animal-<?= (int)$a['id'] ?>">
|
||||
<summary class="d-flex align-items-center gap-2"><img class="care-cat-photo"
|
||||
src="<?= h($src) ?>"
|
||||
alt=""><strong><?= h($a['name']) ?></strong><?php if (!empty($treatmentsByAnimal[(int)$a['id']])): ?><span
|
||||
class="badge bg-purple-lt" title="<?= h(t('care.treatment_planned')) ?>"
|
||||
aria-label="<?= h(t('care.treatment_planned')) ?>">💊</span><?php endif; ?><?php if (!empty($unconfiguredTreatments[(int)$a['id']])): ?><span
|
||||
class="badge bg-yellow-lt" title="<?= h(t('care.treatment_unconfigured')) ?>"
|
||||
aria-label="<?= h(t('care.treatment_unconfigured')) ?>">💊 ⚠</span><?php endif; ?><span
|
||||
class="ms-auto badge <?= $obs ? 'bg-green-lt' : 'bg-yellow-lt' ?>"><?= h(t($obs ? 'care.validated' : 'care.todo')) ?></span>
|
||||
</summary>
|
||||
<form method="post" action="/care-round/save" enctype="multipart/form-data"
|
||||
class="p-2 pt-0 care-observation-form"><input type="hidden" name="round_id"
|
||||
value="<?= (int)$round['id'] ?>"><input type="hidden" name="animal_id"
|
||||
value="<?= (int)$a['id'] ?>"><input type="hidden" name="period"
|
||||
value="<?= h($period) ?>"><input type="hidden" name="date" value="<?= h($date) ?>">
|
||||
<button class="btn btn-success w-100 mb-2 care-all-normal" type="button">✓
|
||||
<?= h(t('care.all_normal')) ?></button>
|
||||
<?php foreach ($fieldOptions as $field => $options): ?><div class="row g-1 mb-2">
|
||||
<?php foreach ($options as $value => $label): ?><label class="col care-choice"><input
|
||||
type="radio" name="<?= h($field) ?>" value="<?= h($value) ?>"
|
||||
<?= (($obs[$field] ?? array_key_first($options)) === $value) ? 'checked' : '' ?>><span><?= h($label) ?></span></label><?php endforeach; ?>
|
||||
</div><?php endforeach; ?>
|
||||
<?php foreach ($treatmentsByAnimal[(int)$a['id']] ?? [] as $t): ?><div
|
||||
class="card card-sm bg-purple-lt mb-2">
|
||||
<div class="card-body py-2"><strong>💊 <?= h($t['medication_name']) ?></strong>
|
||||
<div class="small mb-2"><?= h($t['dose_text']) ?></div>
|
||||
<div class="btn-group w-100"><label class="btn btn-outline-success"><input
|
||||
type="radio" class="btn-check" name="treatments[<?= (int)$t['id'] ?>]"
|
||||
value="given"
|
||||
<?= ($administrations[(int)$t['id']] ?? '') === 'given' ? 'checked' : '' ?>>
|
||||
<?= h(t('care.given')) ?></label><label
|
||||
class="btn btn-outline-danger"><input type="radio" class="btn-check"
|
||||
name="treatments[<?= (int)$t['id'] ?>]" value="skipped"
|
||||
<?= ($administrations[(int)$t['id']] ?? '') === 'skipped' ? 'checked' : '' ?>>
|
||||
<?= h(t('care.not_given')) ?></label></div>
|
||||
</div>
|
||||
</div><?php endforeach; ?>
|
||||
<div class="card card-sm bg-red-lt mb-2">
|
||||
<div class="card-body py-2"><label class="form-label fw-bold">📷
|
||||
<?= h(t('care.anomaly_photo')) ?></label><input
|
||||
class="form-control care-photo-input mb-2" type="file" name="care_photo"
|
||||
accept="image/*" capture="environment"><input
|
||||
class="form-control care-photo-comment" type="text" name="photo_comment"
|
||||
maxlength="500" placeholder="<?= h(t('care.photo_comment')) ?>">
|
||||
<div class="form-hint"><?= h(t('care.photo_hint')) ?></div>
|
||||
</div>
|
||||
</div>
|
||||
<textarea class="form-control mb-2" name="comment" rows="2"
|
||||
placeholder="<?= h(t('care.optional_comment')) ?>"><?= h($obs['comment'] ?? '') ?></textarea><button
|
||||
class="btn btn-primary w-100"
|
||||
type="submit"><?= h(t('care.validate_animal', ['name' => $a['name']])) ?></button>
|
||||
</form>
|
||||
<?php foreach ($unconfiguredTreatments[(int)$a['id']] ?? [] as $unconfigured): ?><div
|
||||
class="alert alert-warning m-2 py-2"><strong>⚠
|
||||
<?= h($unconfigured['medication_name']) ?></strong> <?= h(t('care.no_schedule')) ?><div
|
||||
class="small mb-2"><?= h($unconfigured['dose_text']) ?></div>
|
||||
<form method="post" action="/care-round/treatment-schedule" class="d-flex flex-wrap gap-1">
|
||||
<input type="hidden" name="treatment_id" value="<?= (int)$unconfigured['id'] ?>"><input
|
||||
type="hidden" name="period" value="<?= h($period) ?>"><button
|
||||
class="btn btn-sm btn-outline-primary" name="schedule"
|
||||
value="morning"><?= h(t('care.morning')) ?></button><button
|
||||
class="btn btn-sm btn-outline-primary" name="schedule"
|
||||
value="evening"><?= h(t('care.evening')) ?></button><button
|
||||
class="btn btn-sm btn-primary" name="schedule"
|
||||
value="both"><?= h(t('care.both')) ?></button><button
|
||||
class="btn btn-sm btn-outline-secondary" name="schedule"
|
||||
value="needed"><?= h(t('animal.as_needed')) ?></button></form>
|
||||
</div><?php endforeach; ?>
|
||||
</details>
|
||||
<?php endforeach; ?>
|
||||
</div><?php endforeach; ?>
|
||||
<?php if (!$roomAnimals): ?><div class="text-muted"><?= h(t('care.no_animal_room')) ?></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</section><?php endforeach; ?>
|
||||
</div>
|
||||
<script>
|
||||
document.querySelectorAll('.care-all-normal').forEach(b => b.addEventListener('click', () => {
|
||||
const f = b.form;
|
||||
[
|
||||
['food', 'normal'],
|
||||
['water', 'normal'],
|
||||
['urine', 'normal'],
|
||||
['stool', 'normal'],
|
||||
['general_state', 'good']
|
||||
].forEach(([n, v]) => {
|
||||
const e = f.querySelector(`[name="${n}"][value="${v}"]`);
|
||||
if (e) e.checked = true;
|
||||
});
|
||||
}));
|
||||
document.querySelectorAll('.care-photo-input').forEach(input => input.addEventListener('change', () => {
|
||||
const comment = input.form.querySelector('.care-photo-comment');
|
||||
comment.required = input.files.length > 0;
|
||||
if (input.files.length > 0) comment.focus();
|
||||
}));
|
||||
|
||||
</script>
|
||||
107
app/Views/care_setup.php
Normal file
107
app/Views/care_setup.php
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
<?php
|
||||
$boxOptions = ['top_a' => t('care.box_top_left'),'top_b' => t('care.box_top_center'),'top_c' => t('care.box_top_right'),'top_ab' => t('care.box_top_left_double'),'top_bc' => t('care.box_top_right_double'),'top_all' => t('care.box_top_full'),'bottom_a' => t('care.box_bottom_left'),'bottom_b' => t('care.box_bottom_right'),'bottom_all' => t('care.box_bottom_full')];
|
||||
$roomsByCode = [];
|
||||
foreach ($rooms as $room) {
|
||||
$roomsByCode[$room['code']] = $room;
|
||||
}
|
||||
?>
|
||||
<form method="post" action="/care-round/setup/save" class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h3 class="card-title mb-1"><?= h(t('care.configure_boxes')) ?></h3>
|
||||
<div class="text-muted small"><?= h(t('care.setup_help')) ?></div>
|
||||
</div>
|
||||
<div class="d-flex gap-2"><a class="btn btn-outline-primary" href="/settings/rooms">🚪
|
||||
<?= h(t('settings.rooms')) ?></a><a class="btn" href="/care-round"><?= h(t('care.back_round')) ?></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<?php foreach (['care' => ($roomsByCode['care']['name'] ?? t('care.medical_room')),'quarantine' => ($roomsByCode['quarantine']['name'] ?? t('care.quarantine_room'))] as $room => $label): if (!isset($layouts[$room])) {
|
||||
continue;
|
||||
}$layout = $layouts[$room]; ?>
|
||||
<div class="col-md-6">
|
||||
<div class="card card-sm h-100">
|
||||
<div class="card-header">
|
||||
<h4 class="card-title"><?= h($label) ?></h4>
|
||||
</div>
|
||||
<div class="card-body row g-2">
|
||||
<div class="col-6"><label class="form-label"><?= h(t('care.three_top_boxes')) ?></label><select
|
||||
class="form-select" name="<?= h($room) ?>_top">
|
||||
<option value="split" <?= $layout['top_layout'] === 'split' ? 'selected' : '' ?>>
|
||||
<?= h(t('care.three_separate')) ?></option>
|
||||
<option value="left2" <?= $layout['top_layout'] === 'left2' ? 'selected' : '' ?>>
|
||||
<?= h(t('care.double_left')) ?></option>
|
||||
<option value="right2" <?= $layout['top_layout'] === 'right2' ? 'selected' : '' ?>>
|
||||
<?= h(t('care.double_right')) ?></option>
|
||||
<option value="merged" <?= $layout['top_layout'] === 'merged' ? 'selected' : '' ?>>
|
||||
<?= h(t('care.one_large')) ?></option>
|
||||
</select></div>
|
||||
<div class="col-6"><label class="form-label"><?= h(t('care.two_bottom_boxes')) ?></label><select
|
||||
class="form-select" name="<?= h($room) ?>_bottom">
|
||||
<option value="split" <?= $layout['bottom_layout'] === 'split' ? 'selected' : '' ?>>
|
||||
<?= h(t('care.two_separate')) ?></option>
|
||||
<option value="merged" <?= $layout['bottom_layout'] === 'merged' ? 'selected' : '' ?>>
|
||||
<?= h(t('care.one_large')) ?></option>
|
||||
</select></div>
|
||||
</div>
|
||||
</div>
|
||||
</div><?php endforeach; ?>
|
||||
</div>
|
||||
<h3 class="mt-4"><?= h(t('care.current_assignment')) ?></h3>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-vcenter">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><?= h(t('common.animal')) ?></th>
|
||||
<th><?= h(t('care.room')) ?></th>
|
||||
<th><?= h(t('care.box')) ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($animals as $animal):$currentRoom = ShelterRoomService::resolveAnimal($animal, $roomsByCode); ?>
|
||||
<tr>
|
||||
<td class="fw-bold"><?= h($animal['name']) ?></td>
|
||||
<td><select class="form-select care-room-select" name="room[<?= (int)$animal['id'] ?>]"
|
||||
data-animal="<?= (int)$animal['id'] ?>"><?php foreach ($rooms as $room): ?><option
|
||||
value="<?= h($room['code']) ?>"
|
||||
<?= $currentRoom === $room['code'] ? 'selected' : '' ?>><?= h($room['name']) ?>
|
||||
</option><?php endforeach; ?></select></td>
|
||||
<td><select class="form-select care-box-select" name="box[<?= (int)$animal['id'] ?>]"
|
||||
data-animal="<?= (int)$animal['id'] ?>">
|
||||
<option value=""><?= h(t('care.box_unassigned')) ?></option><?php foreach ($rooms as $room):$codes = array_column($room['boxes'], 'code');
|
||||
$legacy = in_array($room['code'], ['care','quarantine'], true) && !array_diff($codes, ['top_a','top_b','top_c','bottom_a','bottom_b']) && !array_diff(['top_a','top_b','top_c','bottom_a','bottom_b'], $codes);
|
||||
$options = $legacy ? $boxOptions : array_column($room['boxes'], 'name', 'code');
|
||||
foreach ($options as $key => $label): ?><option value="<?= h($key) ?>" data-room="<?= h($room['code']) ?>"
|
||||
<?= $currentRoom === $room['code'] && ($animal['care_box_key'] ?? '') === $key ? 'selected' : '' ?>>
|
||||
<?= h($label) ?></option><?php endforeach;endforeach; ?>
|
||||
</select></td>
|
||||
</tr><?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="mt-3"><label class="form-label"><?= h(t('care.assignment_reason')) ?></label><input
|
||||
class="form-control" name="location_change_reason" maxlength="255"
|
||||
placeholder="<?= h(t('care.assignment_placeholder')) ?>"></div>
|
||||
</div>
|
||||
<div class="card-footer text-end"><button class="btn btn-primary"><?= h(t('care.save_setup')) ?></button></div>
|
||||
</form>
|
||||
<script>
|
||||
document.querySelectorAll('.care-room-select').forEach(room => {
|
||||
const sync = () => {
|
||||
const box = document.querySelector(`.care-box-select[data-animal="${room.dataset.animal}"]`);
|
||||
if (!box) return;
|
||||
let visibleSelected = false;
|
||||
box.querySelectorAll('option[data-room]').forEach(option => {
|
||||
option.hidden = option.dataset.room !== room.value;
|
||||
if (!option.hidden && option.selected) visibleSelected = true;
|
||||
});
|
||||
if (!visibleSelected) box.value = '';
|
||||
box.disabled = !Array.from(box.options).some(option => option.dataset.room === room.value && !
|
||||
option.hidden);
|
||||
};
|
||||
room.addEventListener('change', sync);
|
||||
sync();
|
||||
});
|
||||
|
||||
</script>
|
||||
591
app/Views/dashboard.php
Normal file
591
app/Views/dashboard.php
Normal file
|
|
@ -0,0 +1,591 @@
|
|||
<?php
|
||||
$stats = $stats ?? [];
|
||||
$activity = $activity ?? [];
|
||||
$quality = $quality ?? [];
|
||||
|
||||
$vaccines_overdue = $vaccines_overdue ?? [];
|
||||
$vaccines_7d = $vaccines_7d ?? [];
|
||||
$vaccines_30d = $vaccines_30d ?? [];
|
||||
|
||||
$treatments = $treatments ?? [];
|
||||
$treatments_overdue = $treatments_overdue ?? [];
|
||||
|
||||
$recent_notes = $recent_notes ?? [];
|
||||
$quarantine = $quarantine ?? [];
|
||||
$careToday = $careToday ?? [];
|
||||
$careAlertsToday = $careAlertsToday ?? [];
|
||||
$grantAlerts = $grantAlerts ?? [];
|
||||
$agendaDue = $agendaDue ?? [];
|
||||
|
||||
function badgeKind(string $kind): string
|
||||
{
|
||||
return match ($kind) {
|
||||
'consult' => 'bg-blue-lt',
|
||||
'controle' => 'bg-green-lt',
|
||||
'symptome' => 'bg-yellow-lt',
|
||||
'diagnostic' => 'bg-purple-lt',
|
||||
'acte' => 'bg-orange-lt',
|
||||
'hosp' => 'bg-red-lt',
|
||||
default => 'bg-secondary-lt',
|
||||
};
|
||||
}
|
||||
|
||||
function badgeStatus(string $status): array
|
||||
{
|
||||
// [class, label]
|
||||
return match ($status) {
|
||||
'refuge' => ['bg-blue-lt',t('status.refuge')],
|
||||
'fa' => ['bg-cyan-lt',t('status.fa')],
|
||||
'fa_permanente' => ['bg-indigo-lt',t('status.fa_permanente')],
|
||||
'quarantaine' => ['bg-yellow-lt',t('status.quarantaine')],
|
||||
'soin' => ['bg-red-lt',t('status.soin')],
|
||||
'isolation' => ['bg-orange-lt',t('status.isolation')],
|
||||
'hospitalise', 'hospitalisé' => ['bg-red-lt',t('status.hospitalise')],
|
||||
'reserve', 'réservé' => ['bg-purple-lt',t('status.reserve')],
|
||||
'adopte', 'adopté' => ['bg-green-lt',t('status.adopte')],
|
||||
'decede', 'décédé' => ['bg-dark-lt',t('status.decede')],
|
||||
'enfui' => ['bg-secondary-lt',t('status.enfui')],
|
||||
default => ['bg-secondary-lt', $status],
|
||||
};
|
||||
}
|
||||
?>
|
||||
<?php
|
||||
$expiredQuarantines = array_filter($quarantine, static fn (array $q): bool => $q['days_left'] !== null && (int)$q['days_left'] <= 0);
|
||||
$weightLosses = array_filter($weight_alerts ?? [], static fn (array $w): bool => (float)$w['pct'] < 0);
|
||||
$urgentCount = count($vaccines_overdue) + count($treatments_overdue) + count($expiredQuarantines) + count($weightLosses) + count($careAlertsToday) + count($agendaDue);
|
||||
$morning = $careToday['morning'] ?? ['expected' => 0,'checked' => 0,'alerts' => 0];
|
||||
$evening = $careToday['evening'] ?? ['expected' => 0,'checked' => 0,'alerts' => 0];
|
||||
?>
|
||||
<div class="card mb-3 overflow-hidden <?= $urgentCount ? 'border-danger' : '' ?>">
|
||||
<div class="card-status-top <?= $urgentCount ? 'bg-red' : 'bg-green' ?>"></div>
|
||||
<div class="card-body">
|
||||
<div class="d-flex flex-wrap align-items-center justify-content-between gap-3 mb-3">
|
||||
<div>
|
||||
<div class="subheader"><?= h(t('dashboard.today_priorities')) ?></div>
|
||||
<h2 class="mb-0">
|
||||
<?= h($urgentCount ? tn('dashboard.watch_point', 'dashboard.watch_points', $urgentCount) : t('dashboard.controlled')) ?>
|
||||
</h2>
|
||||
</div><a class="btn btn-primary" href="/care-round"><?= h(t('dashboard.open_round')) ?></a>
|
||||
</div>
|
||||
<div class="row g-2">
|
||||
<div class="col-6 col-lg"><a href="#medical-priorities"
|
||||
class="card card-sm text-reset text-decoration-none h-100">
|
||||
<div class="card-body">
|
||||
<div class="h2 mb-1 text-danger"><?= $urgentCount ?></div>
|
||||
<div class="text-muted small"><?= h(t('dashboard.active_alerts')) ?></div>
|
||||
</div>
|
||||
</a></div>
|
||||
<div class="col-6 col-lg"><a href="#active-treatments"
|
||||
class="card card-sm text-reset text-decoration-none h-100">
|
||||
<div class="card-body">
|
||||
<div class="h2 mb-1"><?= count($treatments) ?></div>
|
||||
<div class="text-muted small"><?= h(t('dashboard.active_treatments')) ?></div>
|
||||
</div>
|
||||
</a></div>
|
||||
<div class="col-6 col-lg"><a href="/care-round?period=morning"
|
||||
class="card card-sm text-reset text-decoration-none h-100">
|
||||
<div class="card-body">
|
||||
<div class="h2 mb-1"><?= (int)$morning['checked'] ?>/<?= (int)$morning['expected'] ?></div>
|
||||
<div class="text-muted small"><?= h(t('dashboard.morning_round')) ?></div>
|
||||
</div>
|
||||
</a></div>
|
||||
<div class="col-6 col-lg"><a href="/care-round?period=evening"
|
||||
class="card card-sm text-reset text-decoration-none h-100">
|
||||
<div class="card-body">
|
||||
<div class="h2 mb-1"><?= (int)$evening['checked'] ?>/<?= (int)$evening['expected'] ?></div>
|
||||
<div class="text-muted small"><?= h(t('dashboard.evening_round')) ?></div>
|
||||
</div>
|
||||
</a></div>
|
||||
<div class="col-12 col-lg"><a href="#vaccine-deadlines"
|
||||
class="card card-sm text-reset text-decoration-none h-100">
|
||||
<div class="card-body">
|
||||
<div class="h2 mb-1"><?= count($vaccines_7d) ?></div>
|
||||
<div class="text-muted small"><?= h(t('dashboard.vaccines_7')) ?></div>
|
||||
</div>
|
||||
</a></div>
|
||||
<?php if (PermissionService::can('agenda')): ?><div class="col-12 col-lg"><a href="/agenda"
|
||||
class="card card-sm text-reset text-decoration-none h-100">
|
||||
<div class="card-body">
|
||||
<div class="h2 mb-1 <?= $agendaDue ? 'text-danger' : '' ?>"><?= count($agendaDue) ?></div>
|
||||
<div class="text-muted small"><?= h(t('dashboard.agenda_due')) ?></div>
|
||||
</div>
|
||||
</a></div><?php endif; ?>
|
||||
</div><?php if ($careAlertsToday): ?><div class="alert alert-warning py-2 mt-3 mb-0">
|
||||
<strong><?= h(t('dashboard.round_anomalies')) ?></strong>
|
||||
<?php foreach ($careAlertsToday as $i => $alert): ?><?= $i ? ', ' : '' ?><a
|
||||
href="/animal?id=<?= (int)$alert['id'] ?>"><?= h($alert['name']) ?></a>
|
||||
(<?= h(t($alert['period'] === 'morning' ? 'dashboard.morning' : 'dashboard.evening')) ?>)<?php endforeach; ?>
|
||||
</div><?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-2 mb-3">
|
||||
<div class="col">
|
||||
<form method="get" action="/animals">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" name="q" placeholder="<?= h(t('dashboard.quick_search')) ?>">
|
||||
<button class="btn btn-outline-primary" type="submit"><?= h(t('common.search')) ?></button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- VUE D'ENSEMBLE -->
|
||||
<div class="d-flex align-items-center gap-2 mt-4 mb-3">
|
||||
<h2 class="m-0"><?= h(t('dashboard.overview')) ?></h2>
|
||||
<span class="text-muted"><?= h(t('dashboard.overview_help')) ?></span>
|
||||
</div>
|
||||
|
||||
<div class="row row-cards mb-4">
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="subheader"><?= h(t('dashboard.total')) ?></div>
|
||||
<div class="h1 mb-2"><?= (int)($stats['total'] ?? 0) ?></div>
|
||||
<div class="d-flex flex-wrap gap-1">
|
||||
<span
|
||||
class="badge bg-blue-lt"><?= h(tn('dashboard.adult', 'dashboard.adults', (int)($stats['adults'] ?? 0))) ?></span>
|
||||
<span
|
||||
class="badge bg-cyan-lt"><?= h(tn('dashboard.kitten', 'dashboard.kittens', (int)($stats['kittens'] ?? 0))) ?></span>
|
||||
<?php if ((int)($stats['age_unknown'] ?? 0) > 0): ?>
|
||||
<span class="badge bg-secondary-lt"><?= (int)$stats['age_unknown'] ?>
|
||||
<?= h(t('dashboard.unknown_age')) ?></span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="subheader"><?= h(t('dashboard.shelter')) ?></div>
|
||||
<div class="h1 mb-2"><?= (int)($stats['refuge'] ?? 0) ?></div>
|
||||
<div class="d-flex flex-wrap gap-1">
|
||||
<?php foreach (($shelterRoomCounts ?? []) as $room):if ($room['status'] !== 'refuge') {
|
||||
continue;
|
||||
} ?><span class="badge"
|
||||
style="color:<?= h($room['color']) ?>;background:color-mix(in srgb,<?= h($room['color']) ?> 14%,transparent)"><?= (int)$room['count'] ?>
|
||||
<?= h($room['name']) ?></span><?php endforeach; ?>
|
||||
<?php if ((int)($stats['refuge_unassigned'] ?? 0) > 0): ?>
|
||||
<span class="badge bg-secondary-lt"><?= (int)$stats['refuge_unassigned'] ?>
|
||||
<?= h(t('dashboard.unassigned')) ?></span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<?php $fosterTotal = (int)($stats['fa'] ?? 0) + (int)($stats['fa_permanente'] ?? 0); ?>
|
||||
<div class="subheader"><?= h(t('dashboard.foster')) ?></div>
|
||||
<div class="h1 mb-2"><?= $fosterTotal ?></div>
|
||||
<div class="d-flex flex-wrap gap-1">
|
||||
<span class="badge bg-cyan-lt"><?= (int)($stats['fa'] ?? 0) ?>
|
||||
<?= h(t('dashboard.temporary')) ?></span>
|
||||
<span class="badge bg-indigo-lt"><?= (int)($stats['fa_permanente'] ?? 0) ?>
|
||||
<?= h(t('dashboard.permanent')) ?></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<?php $medicalPoleTotal = (int)($stats['quarantaine'] ?? 0) + (int)($stats['soin'] ?? 0); ?>
|
||||
<div class="subheader"><?= h(t('dashboard.medical_pole')) ?></div>
|
||||
<div class="h1 mb-2"><?= $medicalPoleTotal ?></div>
|
||||
<div class="d-flex flex-wrap gap-1">
|
||||
<span class="badge bg-yellow-lt"><?= (int)($stats['quarantaine'] ?? 0) ?>
|
||||
<?= h(t('dashboard.in_quarantine')) ?></span>
|
||||
<span class="badge bg-red-lt"><?= (int)($stats['soin'] ?? 0) ?>
|
||||
<?= h(t('dashboard.in_medical')) ?></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php $qualityItems = [['birth_date','missing_birth_date',t('quality.birth'),'🎂'],['sex','missing_sex',t('quality.sex'),'⚥'],['color','missing_color',t('quality.color'),'🎨'],['breed','missing_breed',t('quality.breed'),'🐈'],['origin','missing_origin',t('quality.origin'),'📍'],['chip','missing_chip',t('quality.chip'),'🔎'],['identification_registration','unknown_identification_registration',t('quality.registration'),'🏷️'],['sterilization_unknown','unknown_sterilization',t('quality.sterilization'),'❔'],['adoption_availability','unknown_adoption_availability',t('quality.adoption'),'🏠'],['sterilized','unsterilized',t('quality.unsterilized'),'⚕️']]; ?>
|
||||
<div class="d-flex align-items-center gap-2 mt-4 mb-3">
|
||||
<h2 class="m-0"><?= h(t('dashboard.data_quality')) ?></h2><span
|
||||
class="text-muted"><?= h(t('dashboard.data_quality_help')) ?></span>
|
||||
</div>
|
||||
<div
|
||||
class="card mb-4 overflow-hidden <?= (int)($quality['incomplete'] ?? 0) > 0 ? 'border-warning' : 'border-success' ?>">
|
||||
<div class="card-status-top <?= (int)($quality['incomplete'] ?? 0) > 0 ? 'bg-orange' : 'bg-green' ?>"></div>
|
||||
<div class="card-body">
|
||||
<div class="d-flex flex-wrap align-items-center gap-3 mb-3">
|
||||
<div class="me-auto">
|
||||
<div class="subheader"><?= h(t('dashboard.current_records')) ?></div>
|
||||
<h3 class="mb-0">
|
||||
<?php if ((int)($quality['incomplete'] ?? 0) > 0): ?><?= h(tn('dashboard.record_to_complete', 'dashboard.records_to_complete', (int)$quality['incomplete'])) ?><?php else: ?><?= h(t('dashboard.records_complete')) ?><?php endif; ?>
|
||||
</h3>
|
||||
</div><a class="btn btn-outline-cyan" href="/statistics"><?= h(t('dashboard.see_statistics')) ?></a>
|
||||
</div>
|
||||
<div class="row g-2">
|
||||
<?php foreach ($qualityItems as [$filter,$key,$label,$icon]): $count = (int)($quality[$key] ?? 0); ?><div
|
||||
class="col-6 col-md-4 col-xl"><a
|
||||
class="card card-sm h-100 text-reset text-decoration-none <?= $count ? '' : 'bg-muted-lt' ?>"
|
||||
href="/animals?quality=<?= h($filter) ?>">
|
||||
<div class="card-body d-flex align-items-center gap-2"><span class="fs-2"><?= $icon ?></span>
|
||||
<div>
|
||||
<div class="h2 mb-0 <?= $count ? 'text-orange' : 'text-success' ?>"><?= $count ?></div>
|
||||
<div class="text-muted small"><?= h($label) ?></div>
|
||||
</div>
|
||||
</div>
|
||||
</a></div><?php endforeach; ?></div>
|
||||
<div class="text-muted small mt-3"><?= h(t('dashboard.quality_note')) ?></div>
|
||||
</div>
|
||||
</div>
|
||||
<?php $grantAlertTotal = array_sum(array_map('intval', $grantAlerts));
|
||||
if ($grantAlertTotal > 0): ?><div class="alert alert-warning d-flex flex-wrap align-items-center gap-3 mb-4">
|
||||
<div class="fs-2">📁</div>
|
||||
<div class="me-auto"><strong><?= h(t('dashboard.grants_check')) ?></strong>
|
||||
<div class="small">
|
||||
<?= h(t('dashboard.grants_summary', ['overdue' => (int)($grantAlerts['overdue'] ?? 0),'deadlines' => (int)($grantAlerts['deadlines'] ?? 0),'incomplete' => (int)($grantAlerts['incomplete'] ?? 0),'documents' => (int)($grantAlerts['documents'] ?? 0)])) ?>
|
||||
</div>
|
||||
</div><a class="btn btn-warning" href="/admin/grants"><?= h(t('dashboard.open_grants')) ?></a>
|
||||
</div><?php endif; ?>
|
||||
|
||||
<!-- SOINS ET MEDICAL -->
|
||||
<div class="d-flex align-items-center gap-2 mt-4 mb-3" id="medical-priorities">
|
||||
<h2 class="m-0"><?= h(t('dashboard.medical')) ?></h2>
|
||||
<span class="text-muted"><?= h(t('dashboard.medical_help')) ?></span>
|
||||
</div>
|
||||
|
||||
<div class="row row-cards mb-3" id="vaccine-deadlines">
|
||||
<div class="col-12 col-lg-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">🚨 <?= h(t('dashboard.vaccines_overdue')) ?></h3>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-vcenter card-table">
|
||||
<tbody>
|
||||
<?php if (empty($vaccines_overdue)): ?>
|
||||
<tr>
|
||||
<td class="text-muted"><?= h(t('dashboard.nothing')) ?></td>
|
||||
</tr>
|
||||
<?php else: foreach ($vaccines_overdue as $v): ?>
|
||||
<tr>
|
||||
<td class="text-danger fw-bold"><?= h($v['due_date']) ?></td>
|
||||
<td><a href="/animal?id=<?= (int)$v['id'] ?>"><?= h($v['name']) ?></a> <span
|
||||
class="text-muted"><?= h($v['code'] ?? '') ?></span></td>
|
||||
<td><?= h($v['vaccine_name']) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-lg-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">⏳ <?= h(t('dashboard.vaccines_soon')) ?></h3>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-vcenter card-table">
|
||||
<tbody>
|
||||
<?php if (empty($vaccines_7d)): ?>
|
||||
<tr>
|
||||
<td class="text-muted"><?= h(t('dashboard.nothing')) ?></td>
|
||||
</tr>
|
||||
<?php else: foreach ($vaccines_7d as $v): ?>
|
||||
<tr>
|
||||
<td class="text-warning fw-bold"><?= h($v['due_date']) ?></td>
|
||||
<td><a href="/animal?id=<?= (int)$v['id'] ?>"><?= h($v['name']) ?></a></td>
|
||||
<td><?= h($v['vaccine_name']) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-lg-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">📅 <?= h(t('dashboard.vaccines_month')) ?></h3>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-vcenter card-table">
|
||||
<tbody>
|
||||
<?php if (empty($vaccines_30d)): ?>
|
||||
<tr>
|
||||
<td class="text-muted"><?= h(t('dashboard.nothing')) ?></td>
|
||||
</tr>
|
||||
<?php else: foreach ($vaccines_30d as $v): ?>
|
||||
<tr>
|
||||
<td class="text-primary fw-bold"><?= h($v['due_date']) ?></td>
|
||||
<td><a href="/animal?id=<?= (int)$v['id'] ?>"><?= h($v['name']) ?></a> <span
|
||||
class="text-muted"><?= h($v['code'] ?? '') ?></span></td>
|
||||
<td><?= h($v['vaccine_name']) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- QUARANTAINE -->
|
||||
<div class="row row-cards mb-3">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">🟨 <?= h(t('dashboard.quarantine_followup')) ?></h3>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-vcenter card-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><?= h(t('common.animal')) ?></th>
|
||||
<th><?= h(t('dashboard.entry')) ?></th>
|
||||
<th><?= h(t('dashboard.expected_exit')) ?></th>
|
||||
<th><?= h(t('dashboard.exit_followup')) ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($quarantine)): ?>
|
||||
<tr>
|
||||
<td colspan="4" class="text-muted"><?= h(t('dashboard.no_quarantine')) ?></td>
|
||||
</tr>
|
||||
<?php else: foreach ($quarantine as $q): ?>
|
||||
<?php
|
||||
$days = isset($q['days_left']) ? (int)$q['days_left'] : null;
|
||||
$timerClass = 'text-muted';
|
||||
$timerText = '—';
|
||||
|
||||
if (!empty($q['quarantine_until'])) {
|
||||
if ($days < 0) {
|
||||
$timerClass = 'text-danger fw-bold';
|
||||
$timerText = t('dashboard.overdue_days', ['days' => abs($days)]);
|
||||
} elseif ($days === 0) {
|
||||
$timerClass = 'text-warning fw-bold';
|
||||
$timerText = t('dashboard.exit_today');
|
||||
} elseif ($days === 1) {
|
||||
$timerClass = 'text-warning fw-bold';
|
||||
$timerText = t('dashboard.exit_tomorrow');
|
||||
} elseif ($days <= 5) {
|
||||
$timerClass = 'text-warning fw-bold';
|
||||
$timerText = t('dashboard.exit_days', ['days' => $days]);
|
||||
} else {
|
||||
$timerClass = 'text-success';
|
||||
$timerText = t('dashboard.exit_days', ['days' => $days]);
|
||||
}
|
||||
}
|
||||
?>
|
||||
<tr>
|
||||
<td><a href="/animal?id=<?= (int)$q['id'] ?>"><?= h($q['name']) ?></a> <span
|
||||
class="text-muted"><?= h($q['code'] ?? '') ?></span></td>
|
||||
<td><?= h($q['intake_date'] ?? '—') ?></td>
|
||||
<td><?= h($q['quarantine_until'] ?? '—') ?></td>
|
||||
<td class="<?= $timerClass ?>"><?= h($timerText) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="card-footer text-muted small">
|
||||
<?= h(t('dashboard.quarantine_tip')) ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row row-cards mb-3">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">📉📈 <?= h(t('dashboard.weight_alerts')) ?></h3>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-vcenter card-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><?= h(t('common.animal')) ?></th>
|
||||
<th>J-30</th>
|
||||
<th><?= h(t('dashboard.current')) ?></th>
|
||||
<th><?= h(t('dashboard.variation')) ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($weight_alerts)): ?>
|
||||
<tr>
|
||||
<td colspan="4" class="text-muted"><?= h(t('dashboard.no_alert')) ?></td>
|
||||
</tr>
|
||||
<?php else: foreach ($weight_alerts as $w): ?>
|
||||
<?php
|
||||
$pct = (float)$w['pct'];
|
||||
$cls = $pct < 0 ? 'text-danger fw-bold' : 'text-warning fw-bold';
|
||||
?>
|
||||
<tr>
|
||||
<td><a href="/animal?id=<?= (int)$w['id'] ?>"><?= h($w['name']) ?></a> <span
|
||||
class="text-muted"><?= h($w['code'] ?? '') ?></span></td>
|
||||
<td><?= h($w['w30_weight']) ?> kg <span
|
||||
class="text-muted small">(<?= h(substr((string)$w['w30_date'], 0, 10)) ?>)</span>
|
||||
</td>
|
||||
<td><?= h($w['last_weight']) ?> kg</td>
|
||||
<td class="<?= $cls ?>"><?= ($pct > 0 ? '+' : '') . h($w['pct']) ?>%</td>
|
||||
</tr>
|
||||
<?php endforeach; endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- LISTES -->
|
||||
<div class="row row-cards" id="active-treatments">
|
||||
<div class="col-12 col-lg-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">⚠️ <?= h(t('dashboard.treatments_close')) ?></h3>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-vcenter card-table">
|
||||
<tbody>
|
||||
<?php if (empty($treatments_overdue)): ?>
|
||||
<tr>
|
||||
<td class="text-muted"><?= h(t('dashboard.nothing')) ?></td>
|
||||
</tr>
|
||||
<?php else: foreach ($treatments_overdue as $t): ?>
|
||||
<tr>
|
||||
<td class="text-warning fw-bold"><?= h($t['end_date']) ?></td>
|
||||
<td><a href="/animal?id=<?= (int)$t['id'] ?>"><?= h($t['name']) ?></a></td>
|
||||
<td><?= h($t['medication_name']) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-lg-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title"><?= h(t('dashboard.current_treatments')) ?></h3>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-vcenter card-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><?= h(t('common.animal')) ?></th>
|
||||
<th><?= h(t('dashboard.medication')) ?></th>
|
||||
<th><?= h(t('dashboard.start')) ?></th>
|
||||
<th><?= h(t('dashboard.end')) ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($treatments)): ?>
|
||||
<tr>
|
||||
<td colspan="4" class="text-muted"><?= h(t('dashboard.no_treatment')) ?></td>
|
||||
</tr>
|
||||
<?php else: foreach ($treatments as $t): ?>
|
||||
<tr>
|
||||
<td><a href="/animal?id=<?= (int)$t['id'] ?>"><?= h($t['name']) ?></a></td>
|
||||
<td>
|
||||
<?= h($t['medication_name']) ?>
|
||||
<?php if (!empty($t['dose_text']) || !empty($t['route'])): ?>
|
||||
<div class="text-muted small">
|
||||
<?= !empty($t['dose_text']) ? h($t['dose_text']) : '' ?>
|
||||
<?= (!empty($t['dose_text']) && !empty($t['route'])) ? ' • ' : '' ?>
|
||||
<?= !empty($t['route']) ? h($t['route']) : '' ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td><?= h($t['start_date'] ?? '') ?></td>
|
||||
<td><?= h($t['end_date'] ?? '') ?></td>
|
||||
</tr>
|
||||
<?php endforeach; endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- ACTIVITE RECENTE -->
|
||||
<div class="d-flex align-items-center gap-2 mt-4 mb-3">
|
||||
<h2 class="m-0"><?= h(t('dashboard.recent_activity')) ?></h2>
|
||||
<span class="text-muted"><?= h(t('dashboard.recent_activity_help')) ?></span>
|
||||
</div>
|
||||
|
||||
<div class="row row-cards mb-3">
|
||||
<div class="col-12 col-md-4">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="subheader"><?= h(t('dashboard.medical_notes_7')) ?></div>
|
||||
<div class="h1 mb-0"><?= (int)($activity['notes_7d'] ?? 0) ?></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-4">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="subheader"><?= h(t('dashboard.vaccines_30')) ?></div>
|
||||
<div class="h1 mb-0"><?= (int)($activity['vaccines_30d'] ?? 0) ?></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-4">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="subheader"><?= h(t('dashboard.treatments_30')) ?></div>
|
||||
<div class="h1 mb-0"><?= (int)($activity['treatments_30d'] ?? 0) ?></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row row-cards">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title"><?= h(t('dashboard.latest_notes')) ?></h3>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-vcenter card-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><?= h(t('dashboard.date')) ?></th>
|
||||
<th><?= h(t('common.animal')) ?></th>
|
||||
<th><?= h(t('dashboard.type')) ?></th>
|
||||
<th><?= h(t('dashboard.reason')) ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($recent_notes)): ?>
|
||||
<tr>
|
||||
<td colspan="4" class="text-muted"><?= h(t('dashboard.no_note')) ?></td>
|
||||
</tr>
|
||||
<?php else: foreach ($recent_notes as $n): ?>
|
||||
<?php $kind = (string)($n['kind'] ?? ''); ?>
|
||||
<tr>
|
||||
<td><?= h(substr((string)$n['noted_at'], 0, 16)) ?></td>
|
||||
<td><a href="/animal?id=<?= (int)$n['animal_id'] ?>"><?= h($n['name']) ?></a> <span
|
||||
class="text-muted"><?= h($n['code'] ?? '') ?></span></td>
|
||||
<td><span class="badge <?= badgeKind($kind) ?>"><?= h($kind) ?></span></td>
|
||||
<td><?= h($n['reason'] ?? '') ?></td>
|
||||
</tr>
|
||||
<?php endforeach; endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
71
app/Views/directory_form.php
Normal file
71
app/Views/directory_form.php
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
<form method="post" action="/directory/save" class="card"><input type="hidden" name="id"
|
||||
value="<?= (int)$contact['id'] ?>">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title"><?= h(t('directory.identity_details')) ?></h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-3"><label class="form-label"><?= h(t('directory.kind')) ?></label><select
|
||||
class="form-select" name="kind" id="directoryKind">
|
||||
<option value="person" <?= $contact['kind'] === 'person' ? 'selected' : '' ?>>
|
||||
<?= h(t('directory.person')) ?></option>
|
||||
<option value="organization" <?= $contact['kind'] === 'organization' ? 'selected' : '' ?>>
|
||||
<?= h(t('directory.organization')) ?></option>
|
||||
</select></div>
|
||||
<div class="col-md-9"><label class="form-label"><?= h(t('common.name')) ?></label><input
|
||||
class="form-control" name="name" value="<?= h($contact['name']) ?>" required></div>
|
||||
<div class="col-12"><label class="form-label"><?= h(t('directory.roles')) ?></label>
|
||||
<div class="d-flex flex-wrap gap-3"><?php foreach ($roles as $key => $label): ?><label
|
||||
class="form-check"><input class="form-check-input" type="checkbox" name="roles[]"
|
||||
value="<?= h($key) ?>" <?= in_array($key, $selectedRoles, true) ? 'checked' : '' ?>><span
|
||||
class="form-check-label"><?= h($label) ?></span></label><?php endforeach; ?></div>
|
||||
</div>
|
||||
<div class="col-md-4"><label class="form-label"><?= h(t('common.phone')) ?></label><input
|
||||
class="form-control" name="phone" value="<?= h($contact['phone']) ?>"></div>
|
||||
<div class="col-md-4"><label class="form-label"><?= h(t('common.email')) ?></label><input
|
||||
class="form-control" type="email" name="email" value="<?= h($contact['email']) ?>"></div>
|
||||
<div class="col-md-4" id="organizationLinkField"><label
|
||||
class="form-label"><?= h(t('directory.organization_link')) ?></label><select class="form-select"
|
||||
name="organization_id">
|
||||
<option value="">—</option><?php foreach ($organizations as $org): if ((int)$org['id'] === (int)$contact['id']) {
|
||||
continue;
|
||||
} ?><option value="<?= (int)$org['id'] ?>"
|
||||
<?= (int)$contact['organization_id'] === (int)$org['id'] ? 'selected' : '' ?>>
|
||||
<?= h($org['name']) ?></option><?php endforeach; ?>
|
||||
</select>
|
||||
<div class="form-hint"><?= h(t('directory.organization_hint')) ?></div>
|
||||
</div>
|
||||
<div class="col-md-6"><label class="form-label"><?= h(t('common.address')) ?></label><input
|
||||
class="form-control" name="address" value="<?= h($contact['address']) ?>"></div>
|
||||
<div class="col-md-2"><label class="form-label"><?= h(t('directory.postal_code')) ?></label><input
|
||||
class="form-control" name="postal_code" value="<?= h($contact['postal_code']) ?>"></div>
|
||||
<div class="col-md-2"><label class="form-label"><?= h(t('directory.city')) ?></label><input
|
||||
class="form-control" name="city" value="<?= h($contact['city']) ?>"></div>
|
||||
<div class="col-md-2"><label class="form-label"><?= h(t('directory.country')) ?></label><input
|
||||
class="form-control" name="country" value="<?= h($contact['country']) ?>"></div>
|
||||
<div class="col-12"><label class="form-label"><?= h(t('directory.internal_notes')) ?></label><textarea
|
||||
class="form-control" name="notes" rows="3"><?= h($contact['notes']) ?></textarea></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer d-flex justify-content-between"><a class="btn"
|
||||
href="/directory"><?= h(t('common.cancel')) ?></a><button
|
||||
class="btn btn-primary"><?= h(t('common.save')) ?></button></div>
|
||||
</form>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const kind = document.getElementById('directoryKind'),
|
||||
field = document.getElementById('organizationLinkField'),
|
||||
select = field?.querySelector('select');
|
||||
const sync = () => {
|
||||
const person = kind?.value === 'person';
|
||||
field?.classList.toggle('d-none', !person);
|
||||
if (select) {
|
||||
select.disabled = !person;
|
||||
if (!person) select.value = '';
|
||||
}
|
||||
};
|
||||
kind?.addEventListener('change', sync);
|
||||
sync();
|
||||
});
|
||||
|
||||
</script>
|
||||
130
app/Views/directory_list.php
Normal file
130
app/Views/directory_list.php
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
<?php
|
||||
$roleColors = ['fa' => 'cyan','benevole' => 'green','dirigeant' => 'purple','veterinaire' => 'red','cabinet' => 'orange','crematorium' => 'dark','adoptant' => 'blue','deposant' => 'yellow','fourriere' => 'azure'];
|
||||
?>
|
||||
<style>
|
||||
.directory-person>td {
|
||||
background: rgba(32, 107, 196, .035)
|
||||
}
|
||||
|
||||
.directory-organization>td {
|
||||
background: rgba(111, 66, 193, .055)
|
||||
}
|
||||
|
||||
.directory-person:hover>td {
|
||||
background: rgba(32, 107, 196, .075) !important
|
||||
}
|
||||
|
||||
.directory-organization:hover>td {
|
||||
background: rgba(111, 66, 193, .095) !important
|
||||
}
|
||||
|
||||
.directory-contact-row {
|
||||
cursor: pointer
|
||||
}
|
||||
|
||||
.directory-contact-row:focus {
|
||||
outline: 2px solid var(--tblr-primary);
|
||||
outline-offset: -2px
|
||||
}
|
||||
|
||||
</style>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<form class="row g-2 w-100 align-items-center" method="get" action="/directory">
|
||||
<div class="col"><input class="form-control" name="q" value="<?= h($q) ?>"
|
||||
placeholder="<?= h(t('directory.search')) ?>"></div>
|
||||
<div class="col-md-3"><select class="form-select" name="role">
|
||||
<option value=""><?= h(t('directory.all_roles')) ?></option>
|
||||
<?php foreach ($roles as $key => $label): ?><option value="<?= h($key) ?>"
|
||||
<?= $role === $key ? 'selected' : '' ?>><?= h($label) ?></option><?php endforeach; ?>
|
||||
</select></div>
|
||||
<div class="col-auto"><button class="btn btn-outline-primary"><?= h(t('common.filter')) ?></button></div>
|
||||
<div class="col-auto"><a class="btn btn-primary" href="/directory/new">+ <?= h(t('common.add')) ?></a></div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-vcenter card-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><?= h(t('directory.identity')) ?></th>
|
||||
<th><?= h(t('directory.roles')) ?></th>
|
||||
<th><?= h(t('directory.contact_details')) ?></th>
|
||||
<th><?= h(t('directory.city')) ?></th>
|
||||
<th><?= h(t('directory.organization')) ?></th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($contacts as $contact): ?><tr
|
||||
class="directory-contact-row <?= $contact['kind'] === 'organization' ? 'directory-organization' : 'directory-person' ?>"
|
||||
data-href="/directory/view?id=<?= (int)$contact['id'] ?>" tabindex="0" role="link"
|
||||
aria-label="<?= h(t('directory.open_contact', ['name' => $contact['name']])) ?>">
|
||||
<td><a class="fw-bold text-reset"
|
||||
href="/directory/view?id=<?= (int)$contact['id'] ?>"><?= h($contact['name']) ?></a>
|
||||
<div class="small text-muted">
|
||||
<?= h(t($contact['kind'] === 'organization' ? 'directory.organization' : 'directory.person')) ?><?php if ((int)$contact['adoption_count'] > 0): ?>
|
||||
· <span class="badge bg-blue-lt" title="<?= h(t('directory.adopted_animals')) ?>">🐾
|
||||
<?= (int)$contact['adoption_count'] ?></span><?php endif; ?><?php if ($contact['kind'] === 'organization' && (int)$contact['expense_total_cents'] > 0): ?>
|
||||
· <span
|
||||
class="text-orange"><?= h(number_format((int)$contact['expense_total_cents'] / 100, 2, ',', ' ') . ' €') ?></span><?php endif; ?>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="d-flex flex-wrap gap-1">
|
||||
<?php foreach (array_filter(explode(',', (string)$contact['roles'])) as $r): ?><span
|
||||
class="badge bg-<?= h($roleColors[$r] ?? 'secondary') ?>-lt"><?= h($roles[$r] ?? $r) ?></span><?php endforeach; ?>
|
||||
</div>
|
||||
</td>
|
||||
<td><?php if ($contact['phone']): ?><a
|
||||
href="tel:<?= h($contact['phone']) ?>"><?= h($contact['phone']) ?></a><?php endif; ?><?php if ($contact['email']): ?><br><a
|
||||
href="mailto:<?= h($contact['email']) ?>"><?= h($contact['email']) ?></a><?php endif; ?>
|
||||
</td>
|
||||
<td><?= h($contact['city'] ?: '—') ?></td>
|
||||
<td><?php if ($contact['organization_id'] && $contact['organization_name']): ?><a
|
||||
href="/directory/view?id=<?= (int)$contact['organization_id'] ?>"><?= h($contact['organization_name']) ?></a><?php else: ?>—<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<div class="d-flex gap-1 justify-content-end">
|
||||
<?php $contactRoles = array_filter(explode(',', (string)$contact['roles'])); ?><?php if (in_array('benevole', $contactRoles, true) || in_array('fa', $contactRoles, true)): ?>
|
||||
<div class="dropdown"><button class="btn btn-outline-orange btn-sm dropdown-toggle"
|
||||
data-bs-toggle="dropdown"><?= h(t('directory.documents')) ?></button>
|
||||
<div class="dropdown-menu dropdown-menu-end">
|
||||
<?php if (in_array('benevole', $contactRoles, true)): ?><a class="dropdown-item"
|
||||
href="/directory/documents?contact_id=<?= (int)$contact['id'] ?>&type=benevole&format=odt"><?= h(t('directory.volunteer_contract')) ?>
|
||||
— ODT</a><a class="dropdown-item" target="_blank"
|
||||
href="/directory/documents?contact_id=<?= (int)$contact['id'] ?>&type=benevole&format=pdf"><?= h(t('directory.volunteer_contract')) ?>
|
||||
— PDF</a><?php endif; ?><?php if (in_array('fa', $contactRoles, true)): ?><a
|
||||
class="dropdown-item"
|
||||
href="/directory/documents?contact_id=<?= (int)$contact['id'] ?>&type=fa&format=odt"><?= h(t('directory.foster_proposal')) ?>
|
||||
— ODT</a><a class="dropdown-item" target="_blank"
|
||||
href="/directory/documents?contact_id=<?= (int)$contact['id'] ?>&type=fa&format=pdf"><?= h(t('directory.foster_proposal')) ?>
|
||||
— PDF</a><?php endif; ?></div>
|
||||
</div><?php endif; ?><a class="btn btn-outline-primary btn-sm"
|
||||
href="/directory/edit?id=<?= (int)$contact['id'] ?>"><?= h(t('common.edit')) ?></a>
|
||||
</div>
|
||||
</td>
|
||||
</tr><?php endforeach; ?>
|
||||
<?php if (!$contacts): ?><tr>
|
||||
<td colspan="6" class="text-center text-muted py-4"><?= h(t('directory.no_contact')) ?></td>
|
||||
</tr><?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
document.querySelectorAll('.directory-contact-row').forEach(row => {
|
||||
const open = () => location.href = row.dataset.href;
|
||||
row.addEventListener('click', event => {
|
||||
if (event.target.closest('a,button,input,select,textarea,[data-bs-toggle]')) return;
|
||||
open();
|
||||
});
|
||||
row.addEventListener('keydown', event => {
|
||||
if ((event.key === 'Enter' || event.key === ' ') && !event.target.closest(
|
||||
'a,button,input,select,textarea')) {
|
||||
event.preventDefault();
|
||||
open();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
</script>
|
||||
135
app/Views/directory_show.php
Normal file
135
app/Views/directory_show.php
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
<?php $roleColors = ['fa' => 'cyan','benevole' => 'green','dirigeant' => 'purple','veterinaire' => 'red','cabinet' => 'orange','crematorium' => 'dark','adoptant' => 'blue','deposant' => 'yellow','fourriere' => 'azure'];
|
||||
$contactRoles = array_filter(explode(',', (string)$contact['roles']));
|
||||
$money = static fn (int$cents): string => number_format($cents / 100, 2, ',', ' ') . ' €';
|
||||
$frDate = static function (?string$date): string {
|
||||
if (!$date) {
|
||||
return'—';
|
||||
}try {
|
||||
return(new DateTimeImmutable($date))->format('d/m/Y');
|
||||
} catch (Throwable) {
|
||||
return$date;
|
||||
}
|
||||
};
|
||||
$animalRows = static function (array$rows, string$title, string$color, string$icon) use ($frDate): void {
|
||||
if (!$rows) {
|
||||
return;
|
||||
} ?><div class="card mb-3">
|
||||
<div class="card-status-top bg-<?= h($color) ?>"></div>
|
||||
<div class="card-header"><span class="fs-2 me-2"><?= $icon ?></span>
|
||||
<h3 class="card-title"><?= h($title) ?></h3><span
|
||||
class="badge bg-<?= h($color) ?>-lt ms-2"><?= count($rows) ?></span>
|
||||
</div>
|
||||
<div class="list-group list-group-flush"><?php foreach ($rows as $row): ?><a
|
||||
class="list-group-item list-group-item-action d-flex align-items-center justify-content-between gap-3"
|
||||
href="/animal?id=<?= (int)$row['id'] ?>"><span><strong><?= h($row['name']) ?></strong><span
|
||||
class="text-muted small ms-2"><?= h($row['internal_code']) ?> ·
|
||||
<?= h(species_label($row['species'])) ?></span></span><span
|
||||
class="text-muted small"><?= h($frDate($row['event_date'] ?? null)) ?></span></a><?php endforeach; ?>
|
||||
</div>
|
||||
</div><?php }; ?>
|
||||
<div class="d-flex flex-wrap justify-content-between align-items-center gap-2 mb-3">
|
||||
<div class="d-flex flex-wrap gap-2"><a class="btn" href="/directory">←
|
||||
<?= h(t('directory.back')) ?></a><?php if ($expensePeriodRequested): ?><a class="btn btn-outline-orange"
|
||||
href="/statistics?<?= h(http_build_query(['year' => $expensePeriodYear ?? 'all','municipality' => $expensePeriodMunicipality])) ?>">←
|
||||
<?= h(t('directory.back_statistics')) ?></a><?php endif; ?></div><a class="btn btn-primary"
|
||||
href="/directory/edit?id=<?= (int)$contact['id'] ?>"><?= h(t('common.edit')) ?></a>
|
||||
</div>
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-lg-7">
|
||||
<div class="card h-100">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-start justify-content-between gap-3">
|
||||
<div>
|
||||
<div class="text-muted">
|
||||
<?= h(t($contact['kind'] === 'organization' ? 'directory.organization' : 'directory.person')) ?>
|
||||
</div>
|
||||
<h2 class="mb-2"><?= h($contact['name']) ?></h2>
|
||||
<div class="d-flex flex-wrap gap-1"><?php foreach ($contactRoles as $role): ?><span
|
||||
class="badge bg-<?= h($roleColors[$role] ?? 'secondary') ?>-lt"><?= h($roles[$role] ?? $role) ?></span><?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fs-1"><?= $contact['kind'] === 'organization' ? '🏢' : '👤' ?></div>
|
||||
</div><?php if ($contact['organization_name']): ?><div class="mt-3"><span
|
||||
class="text-muted"><?= h(t('directory.organization')) ?> :</span>
|
||||
<?= h($contact['organization_name']) ?></div><?php endif; ?><?php if ($contact['notes']): ?>
|
||||
<hr>
|
||||
<div class="text-muted small"><?= nl2br(h($contact['notes'])) ?></div><?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-5">
|
||||
<div class="card h-100">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title"><?= h(t('directory.contact_details')) ?></h3>
|
||||
</div>
|
||||
<div class="card-body"><?php if ($contact['phone']): ?><div class="mb-2">☎ <a
|
||||
href="tel:<?= h(preg_replace('/[^0-9+]/', '', $contact['phone'])) ?>"><?= h($contact['phone']) ?></a>
|
||||
</div><?php endif; ?><?php if ($contact['email']): ?><div class="mb-2">✉ <a
|
||||
href="mailto:<?= h($contact['email']) ?>"><?= h($contact['email']) ?></a></div><?php endif; ?><?php $address = implode(', ', array_filter([$contact['address'],trim(($contact['postal_code'] ?? '') . ' ' . ($contact['city'] ?? '')),$contact['country']]));
|
||||
if ($address): ?><div>📍 <?= h($address) ?></div>
|
||||
<?php endif; ?><?php if (!$contact['phone'] && !$contact['email'] && !$address): ?><div
|
||||
class="text-muted"><?= h(t('directory.no_contact_details')) ?></div><?php endif; ?></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php if ($contact['kind'] === 'organization'): ?><div class="card mb-3">
|
||||
<div class="card-status-top bg-orange"></div>
|
||||
<div class="card-body">
|
||||
<div class="row align-items-center">
|
||||
<div class="col">
|
||||
<div class="subheader"><?= h(t('directory.total_invoiced')) ?><?php if ($expensePeriodRequested): ?> ·
|
||||
<?= h($expensePeriodYear ?? t('statistics.all_years')) ?><?php if ($expensePeriodMunicipality !== ''): ?>
|
||||
· <?= h($expensePeriodMunicipality) ?><?php endif; ?><?php endif; ?></div>
|
||||
<div class="h1 mb-0 text-orange"><?= h($money((int)$expenseTotal)) ?></div>
|
||||
<div class="text-muted small"><?= h(t('directory.expense_count', ['count' => (int)$expenseCount])) ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto fs-1">💶</div>
|
||||
</div>
|
||||
</div>
|
||||
</div><?php endif; ?>
|
||||
<?php $animalRows($adoptions, t('directory.adopted_animals'), 'blue', '🐾');
|
||||
$animalRows($fosters, t('directory.fostered_animals'), 'cyan', '🏠');
|
||||
$animalRows($deposited, t('directory.deposited_animals'), 'yellow', '📥'); ?>
|
||||
<?php if ($linkedPeople): ?><div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title"><?= h(t('directory.linked_people')) ?></h3><span
|
||||
class="badge bg-secondary-lt ms-2"><?= count($linkedPeople) ?></span>
|
||||
</div>
|
||||
<div class="list-group list-group-flush"><?php foreach ($linkedPeople as $person): ?><a
|
||||
class="list-group-item list-group-item-action"
|
||||
href="/directory/view?id=<?= (int)$person['id'] ?>"><strong><?= h($person['name']) ?></strong><?php if ($person['phone'] || $person['email']): ?><span
|
||||
class="text-muted small ms-2"><?= h($person['phone'] ?: $person['email']) ?></span><?php endif; ?></a><?php endforeach; ?>
|
||||
</div>
|
||||
</div><?php endif; ?>
|
||||
<?php if ($expenses): ?><div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title"><?= h(t('directory.expense_history')) ?></h3>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-vcenter card-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><?= h(t('common.date')) ?></th>
|
||||
<th><?= h(t('animal.animal')) ?></th>
|
||||
<th><?= h(t('common.label')) ?></th>
|
||||
<th class="text-end"><?= h(t('common.amount')) ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody><?php foreach ($expenses as $expense): ?><tr>
|
||||
<td><?= h($frDate($expense['occurred_on'])) ?></td>
|
||||
<td><a href="/animal?id=<?= (int)$expense['animal_id'] ?>"><?= h($expense['animal_name']) ?></a>
|
||||
<div class="small text-muted"><?= h($expense['internal_code']) ?></div>
|
||||
</td>
|
||||
<td><?= h($expense['label']) ?></td>
|
||||
<td class="text-end fw-bold"><?= h($money((int)$expense['total_cents'])) ?></td>
|
||||
</tr><?php endforeach; ?></tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<th colspan="3" class="text-end"><?= h(t('common.total')) ?></th>
|
||||
<th class="text-end"><?= h($money((int)$expenseTotal)) ?></th>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div><?php endif; ?>
|
||||
54
app/Views/directory_show_accounting.php
Normal file
54
app/Views/directory_show_accounting.php
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
<?php require __DIR__ . '/directory_show.php'; ?>
|
||||
<?php if ($contact['kind'] === 'organization' && !empty($contactInvoices)): $invoicePaid = (int)$contactInvoiceTotal - (int)$contactInvoiceDue; ?>
|
||||
<div class="card mt-3">
|
||||
<div class="card-status-top bg-teal"></div>
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">🧾 Facturation comptable</h3><a class="btn btn-sm btn-outline-teal ms-auto"
|
||||
href="/accounting?year=<?= $expensePeriodYear ?: 0 ?>">Ouvrir Comptabilité</a>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-3">
|
||||
<div class="subheader">Facturé net</div>
|
||||
<div class="h2 mb-0"><?= h($money((int)$contactInvoiceTotal)) ?></div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="subheader">Payé</div>
|
||||
<div class="h2 mb-0 text-green"><?= h($money($invoicePaid)) ?></div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="subheader">Reste dû</div>
|
||||
<div class="h2 mb-0 text-orange"><?= h($money((int)$contactInvoiceDue)) ?></div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="subheader">Pièces</div>
|
||||
<div class="h2 mb-0"><?= count($contactInvoices) ?></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-vcenter mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Référence</th>
|
||||
<th>Type</th>
|
||||
<th>Statut</th>
|
||||
<th class="text-end">TTC</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody><?php foreach (array_slice($contactInvoices, 0, 25) as $invoice):?><tr>
|
||||
<td><?= h($frDate($invoice['invoice_date'])) ?></td>
|
||||
<td><a href="/accounting/edit?id=<?= (int)$invoice['id'] ?>"><?= h($invoice['reference']) ?></a>
|
||||
</td>
|
||||
<td><?= h(['invoice' => 'Facture','credit' => 'Avoir','adjustment' => 'Régularisation'][$invoice['document_type']] ?? $invoice['document_type']) ?>
|
||||
</td>
|
||||
<td><span
|
||||
class="badge <?= $invoice['status'] === 'paid' ? 'bg-green-lt' : 'bg-orange-lt' ?>"><?= h(['paid' => 'Payée','to_pay' => 'À payer','partial' => 'Partielle','disputed' => 'Contestée','cancelled' => 'Annulée'][$invoice['status']] ?? $invoice['status']) ?></span>
|
||||
</td>
|
||||
<td class="text-end fw-bold"><?= h($money((int)$invoice['total_ttc_cents'])) ?></td>
|
||||
</tr><?php endforeach?></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
52
app/Views/evolutions.php
Normal file
52
app/Views/evolutions.php
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
<div class="card mb-4">
|
||||
<div class="card-body p-4">
|
||||
<span class="badge bg-blue-lt mb-2">Globinours · Version <?= h(app_version()) ?></span>
|
||||
<h2 class="mb-2">Évolutions du projet</h2>
|
||||
<p class="text-muted mb-0">Cette page présente les grandes étapes du produit. Elle ne reprend aucune donnée,
|
||||
statistique ni information issue d’une installation réelle.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-lg-6">
|
||||
<section class="card h-100">
|
||||
<div class="card-body">
|
||||
<span class="badge bg-green-lt mb-2">Socle livré</span>
|
||||
<h3>Gestion quotidienne du refuge</h3>
|
||||
<p class="text-muted">Fiches animales, annuaire, placements, adoptions, tournées, agenda, suivi médical,
|
||||
pharmacie, documents et statistiques.</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<section class="card h-100">
|
||||
<div class="card-body">
|
||||
<span class="badge bg-green-lt mb-2">Protection</span>
|
||||
<h3>Sécurité et continuité</h3>
|
||||
<p class="text-muted">Permissions, audit, médias privés, sauvegardes vérifiées, restauration contrôlée
|
||||
et outils de maintenance.</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<section class="card h-100">
|
||||
<div class="card-body">
|
||||
<span class="badge bg-blue-lt mb-2">1.0.0-rc.3</span>
|
||||
<h3>Stabilisation avant la 1.0</h3>
|
||||
<p class="text-muted">Découpage des fichiers volumineux, recherche universelle, correction de
|
||||
l’installation vierge, anonymisation du dépôt, fiche Twix fictive complète et harmonisation des
|
||||
sources PHP.</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<section class="card h-100">
|
||||
<div class="card-body">
|
||||
<span class="badge bg-secondary-lt mb-2">Après la 1.0</span>
|
||||
<h3>Prochaines étapes</h3>
|
||||
<p class="text-muted">Tests HTTP plus complets, validation des paquets d’installation et amélioration
|
||||
continue des parcours.</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
382
app/Views/layout.php
Normal file
382
app/Views/layout.php
Normal file
|
|
@ -0,0 +1,382 @@
|
|||
<?php
|
||||
/** @var string $title */
|
||||
require_once __DIR__ . '/_helpers.php';
|
||||
require_once __DIR__ . '/_settings_links.php';
|
||||
$hasRescue = $hasRescue ?? false;
|
||||
$hasCurrent = $hasCurrent ?? false;
|
||||
$rescueAddr = $rescueAddr ?? null;
|
||||
$currentAddr = $currentAddr ?? null;
|
||||
$currentPath = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH) ?: '/';
|
||||
$dashboardActive = $currentPath === '/' || $currentPath === '/dashboard';
|
||||
$statisticsActive = $currentPath === '/statistics';
|
||||
$animalsActive = $currentPath === '/animals' || str_starts_with($currentPath, '/animal');
|
||||
$adminActive = str_starts_with($currentPath, '/admin');
|
||||
$directoryActive = str_starts_with($currentPath, '/directory');
|
||||
$careRoundActive = str_starts_with($currentPath, '/care-round');
|
||||
$agendaActive = str_starts_with($currentPath, '/agenda');
|
||||
$accountingActive = str_starts_with($currentPath, '/accounting');
|
||||
$settingsActive = str_starts_with($currentPath, '/settings');
|
||||
$settingsMenuLinks = settingsNavigationLinks();
|
||||
$currentUser = Auth::user();
|
||||
$pageName = trim((string)($title ?? ''));
|
||||
$documentTitle = $pageName !== '' ? $pageName . ' — Globinours' : 'Globinours';
|
||||
$pageDescription = trim((string)($pageDescription ?? ''));
|
||||
if ($pageDescription === '') {
|
||||
$pageDescription = match (true) {
|
||||
$dashboardActive => t('meta.dashboard'),
|
||||
$statisticsActive => t('meta.statistics'),
|
||||
$adminActive => t('meta.administrative'),
|
||||
$animalsActive => t('meta.animals'),
|
||||
default => t('meta.default'),
|
||||
};
|
||||
}
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="<?= h(I18n::locale()) ?>">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title><?= h($documentTitle) ?></title>
|
||||
<meta name="description" content="<?= h($pageDescription) ?>">
|
||||
<meta name="application-name" content="Globinours">
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
<meta name="theme-color" content="#206bc4">
|
||||
<meta name="csrf-token" content="<?= h(Auth::csrf()) ?>">
|
||||
<link rel="icon" href="/favicon.ico" sizes="any">
|
||||
<link rel="manifest" href="/manifest.webmanifest">
|
||||
<link rel="stylesheet" href="/assets/mobile.css">
|
||||
|
||||
<link rel="stylesheet" href="/assets/vendor/tabler/tabler.min.css">
|
||||
<link rel="stylesheet" href="/assets/vendor/flatpickr/flatpickr.min.css">
|
||||
<link rel="stylesheet" href="/assets/app-shell.css">
|
||||
|
||||
<!-- Chart.js (pour les courbes poids) -->
|
||||
<script src="/assets/vendor/chartjs/chart.umd.min.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="page">
|
||||
<?php require __DIR__ . '/_layout_topbar.php'; ?>
|
||||
<div class="page-wrapper">
|
||||
<div class="page-header d-print-none">
|
||||
<div class="container-xl">
|
||||
<h2 class="page-title"><?= htmlspecialchars($title ?? '', ENT_QUOTES) ?></h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="page-body">
|
||||
<div class="container-xl">
|
||||
<?php require $viewFile; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php require __DIR__ . '/_layout_footer.php'; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/assets/vendor/tabler/tabler.min.js"></script>
|
||||
|
||||
<link rel="stylesheet" href="/assets/vendor/leaflet/leaflet.css">
|
||||
<script src="/assets/vendor/leaflet/leaflet.js"></script>
|
||||
<script>
|
||||
(function() {
|
||||
const rescue = {
|
||||
lat: < ? = $hasRescue ? (float) $rescueLat : 'null' ? > ,
|
||||
lng: < ? = $hasRescue ? (float) $rescueLng : 'null' ? > ,
|
||||
addr: < ? = json_encode($rescueAddr, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ? >
|
||||
};
|
||||
|
||||
const cur = {
|
||||
lat: < ? = $hasCurrent ? (float) $currentLat : 'null' ? > ,
|
||||
lng: < ? = $hasCurrent ? (float) $currentLng : 'null' ? > ,
|
||||
addr: < ? = json_encode($currentAddr, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ? >
|
||||
};
|
||||
|
||||
const el = document.getElementById('mapJourney');
|
||||
if (!el || typeof L === 'undefined') return;
|
||||
|
||||
const fallback = [48.5, -4.3];
|
||||
|
||||
// icônes “pastille” colorées
|
||||
const iconRescue = L.divIcon({
|
||||
className: '',
|
||||
iconSize: [18, 18],
|
||||
iconAnchor: [9, 9],
|
||||
html: '<div class="twix-pin rescue" title="' + < ? = json_encode(t('map.rescue'),
|
||||
JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ? > +'"></div>'
|
||||
});
|
||||
|
||||
const iconCurrent = L.divIcon({
|
||||
className: '',
|
||||
iconSize: [18, 18],
|
||||
iconAnchor: [9, 9],
|
||||
html: '<div class="twix-pin current" title="' + < ? = json_encode(t('map.current'),
|
||||
JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ? > +'"></div>'
|
||||
});
|
||||
|
||||
const map = L.map('mapJourney', {
|
||||
scrollWheelZoom: false
|
||||
});
|
||||
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
maxZoom: 19,
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright" target="_blank" rel="noopener noreferrer">OpenStreetMap</a>'
|
||||
}).addTo(map);
|
||||
|
||||
const pts = [];
|
||||
|
||||
// helper anti XSS + joli rendu
|
||||
const esc = (s) => String(s ?? '')
|
||||
.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>')
|
||||
.replaceAll('"', '"').replaceAll("'", "'");
|
||||
const safePopup = (label, address) => {
|
||||
const box = document.createElement('div');
|
||||
const strong = document.createElement('strong');
|
||||
strong.textContent = label;
|
||||
box.append(strong, document.createElement('br'), document.createTextNode(address || '—'));
|
||||
return box;
|
||||
};
|
||||
|
||||
if (rescue.lat !== null && rescue.lng !== null) {
|
||||
const p = [rescue.lat, rescue.lng];
|
||||
pts.push(p);
|
||||
L.marker(p, {
|
||||
icon: iconRescue
|
||||
})
|
||||
.addTo(map)
|
||||
.bindPopup(safePopup( < ? = json_encode(t('map.rescue'), JSON_UNESCAPED_UNICODE |
|
||||
JSON_UNESCAPED_SLASHES) ? > , rescue.addr), {
|
||||
autoPan: false, // <-- stop le déplacement
|
||||
closeButton: false
|
||||
});
|
||||
}
|
||||
|
||||
if (cur.lat !== null && cur.lng !== null) {
|
||||
const p = [cur.lat, cur.lng];
|
||||
pts.push(p);
|
||||
L.marker(p, {
|
||||
icon: iconCurrent
|
||||
})
|
||||
.addTo(map)
|
||||
.bindPopup(safePopup( < ? = json_encode(t('map.current'), JSON_UNESCAPED_UNICODE |
|
||||
JSON_UNESCAPED_SLASHES) ? > , cur.addr), {
|
||||
autoPan: false, // <-- stop le déplacement
|
||||
closeButton: false
|
||||
});
|
||||
}
|
||||
|
||||
if (pts.length === 0) {
|
||||
map.setView(fallback, 8);
|
||||
L.popup({
|
||||
autoPan: false
|
||||
}).setLatLng(fallback).setContent( < ? = json_encode(t('map.missing_coordinates'),
|
||||
JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ? > ).openOn(map);
|
||||
} else if (pts.length === 1) {
|
||||
map.setView(pts[0], 14);
|
||||
} else {
|
||||
// “vol d’oiseau” : ligne + fitBounds avec padding pour éviter les pins collés au bord
|
||||
L.polyline(pts, {
|
||||
weight: 3,
|
||||
opacity: 0.8,
|
||||
dashArray: "6 6"
|
||||
}).addTo(map);
|
||||
map.fitBounds(L.latLngBounds(pts), {
|
||||
padding: [60, 60]
|
||||
}); // <-- plus de marge => popup moins au bord
|
||||
}
|
||||
|
||||
// Légende (dans la carte)
|
||||
const legend = L.control({
|
||||
position: 'topright'
|
||||
});
|
||||
legend.onAdd = function() {
|
||||
const div = L.DomUtil.create('div', 'twix-map-legend');
|
||||
div.innerHTML = `
|
||||
<div class="row"><span class="dot rescue"></span> ${<?= json_encode(t('map.rescue'), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>}</div>
|
||||
<div class="row"><span class="dot current"></span> ${<?= json_encode(t('map.current'), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>}</div>
|
||||
<div class="row" style="opacity:.9"><span class="line"></span> ${<?= json_encode(t('map.flight_path'), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>}</div>
|
||||
`;
|
||||
return div;
|
||||
};
|
||||
legend.addTo(map);
|
||||
|
||||
// Leaflet + tabs : fix affichage
|
||||
setTimeout(() => map.invalidateSize(), 200);
|
||||
})();
|
||||
|
||||
</script>
|
||||
<script src="/assets/vendor/flatpickr/flatpickr.min.js"></script>
|
||||
<script src="/assets/vendor/flatpickr/l10n/fr.js"></script>
|
||||
<script defer src="/assets/pwa.js"></script>
|
||||
<script src="/assets/global-search.js"></script>
|
||||
<script>
|
||||
const settingsMenu = document.querySelector('.settings-dropdown .dropdown-menu');
|
||||
if (settingsMenu) {
|
||||
const groupHeaders = [...settingsMenu.children].filter(node => node.classList.contains('dropdown-header'));
|
||||
if (groupHeaders.length === 4) {
|
||||
const groups = groupHeaders.map(header => {
|
||||
const group = document.createElement('div');
|
||||
group.className = 'settings-menu-group';
|
||||
const nodes = [header];
|
||||
let next = header.nextElementSibling;
|
||||
while (next && !next.classList.contains('dropdown-divider') && !next.classList.contains(
|
||||
'dropdown-header') && next.tagName !== 'FORM') {
|
||||
nodes.push(next);
|
||||
next = next.nextElementSibling;
|
||||
}
|
||||
nodes.forEach(node => group.appendChild(node));
|
||||
return group;
|
||||
});
|
||||
const logout = settingsMenu.querySelector(':scope > form[action="/logout"]');
|
||||
const logoutDivider = logout?.previousElementSibling?.classList.contains('dropdown-divider') ? logout
|
||||
.previousElementSibling : null;
|
||||
[...settingsMenu.querySelectorAll(':scope > .dropdown-divider')].forEach((divider, index) => {
|
||||
if (index > 0 && divider !== logoutDivider) divider.remove();
|
||||
});
|
||||
const columns = document.createElement('div');
|
||||
columns.className = 'settings-menu-columns';
|
||||
const configuration = document.createElement('div');
|
||||
configuration.className = 'settings-menu-column';
|
||||
configuration.append(groups[0], groups[1]);
|
||||
const administration = document.createElement('div');
|
||||
administration.className = 'settings-menu-column';
|
||||
administration.append(groups[2], groups[3]);
|
||||
columns.append(configuration, administration);
|
||||
settingsMenu.insertBefore(columns, logoutDivider || logout || null);
|
||||
settingsMenu.classList.add('settings-menu-wide');
|
||||
}
|
||||
}
|
||||
const globinoursPermissions = < ? = json_encode(PermissionService::currentPermissions(),
|
||||
JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) ? > ;
|
||||
const permissionModuleForPath = path => {
|
||||
if (path === '/' || path.startsWith('/dashboard')) return 'dashboard';
|
||||
if (path.startsWith('/animal/documents')) return 'medical';
|
||||
if (path.startsWith('/animals') || path.startsWith('/animal') || path.startsWith('/litter') || path
|
||||
.startsWith('/bonded')) {
|
||||
if (['/animal/add-medical', '/animal/medical-status', '/animal/add-treatment',
|
||||
'/animal/add-vaccine', '/animal/death', '/animal/surgery'
|
||||
].some(prefix => path.startsWith(prefix))) return 'medical';
|
||||
return 'animals';
|
||||
}
|
||||
if (path.startsWith('/care-round')) return 'care';
|
||||
if (path.startsWith('/directory')) return 'directory';
|
||||
if (path.startsWith('/statistics')) return 'statistics';
|
||||
if (path.startsWith('/accounting')) return 'accounting';
|
||||
if (path.startsWith('/admin/grants')) return 'grants';
|
||||
if (path.startsWith('/admin') || path.startsWith('/documents')) return 'administrative';
|
||||
return null;
|
||||
};
|
||||
const permissionEditPages = new Set(['/animal/new', '/animal/edit', '/animal/death', '/litter/new',
|
||||
'/litter/add-existing', '/litter/add-existing/save', '/bonded/new', '/directory/new',
|
||||
'/directory/edit', '/care-round/setup'
|
||||
]);
|
||||
const permissionAllows = (url, action = 'view') => {
|
||||
let path;
|
||||
try {
|
||||
path = new URL(url, location.origin).pathname;
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
const module = permissionModuleForPath(path);
|
||||
if (!module) return true;
|
||||
if (action === 'view' && permissionEditPages.has(path)) action = 'edit';
|
||||
return Boolean(globinoursPermissions[module]?.[action]);
|
||||
};
|
||||
document.querySelectorAll('.page-body form').forEach(form => {
|
||||
if ((form.method || 'get').toLowerCase() !== 'post') return;
|
||||
if (!permissionAllows(form.action, 'edit')) {
|
||||
form.hidden = true;
|
||||
form.dataset.permissionHidden = 'true';
|
||||
}
|
||||
});
|
||||
document.querySelectorAll('.page-body a[href]').forEach(link => {
|
||||
const raw = link.getAttribute('href') || '';
|
||||
if (raw.startsWith('#') || raw.startsWith('javascript:')) return;
|
||||
if (!permissionAllows(link.href, 'view')) {
|
||||
link.hidden = true;
|
||||
link.dataset.permissionHidden = 'true';
|
||||
}
|
||||
});
|
||||
document.querySelectorAll('.page-body [data-bs-target]').forEach(trigger => {
|
||||
const target = document.querySelector(trigger.getAttribute('data-bs-target'));
|
||||
const actions = target ? [...target.querySelectorAll('form')].map(form => form.action) : [];
|
||||
if (actions.length && actions.every(action => !permissionAllows(action, 'edit'))) {
|
||||
trigger.hidden = true;
|
||||
trigger.dataset.permissionHidden = 'true';
|
||||
}
|
||||
});
|
||||
document.querySelectorAll('.page-body .dropdown-menu').forEach(menu => {
|
||||
const available = [...menu.querySelectorAll('a,button,form')].some(item => !item.hidden && item
|
||||
.dataset.permissionHidden !== 'true' && !item.closest('[data-permission-hidden="true"]'));
|
||||
if (!available) {
|
||||
const dropdown = menu.closest('.dropdown');
|
||||
if (dropdown) dropdown.hidden = true;
|
||||
}
|
||||
});
|
||||
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content;
|
||||
document.querySelectorAll('form[method="post" i]').forEach(form => {
|
||||
if (!csrfToken || form.querySelector('input[name="csrf"]')) return;
|
||||
const input = document.createElement('input');
|
||||
input.type = 'hidden';
|
||||
input.name = 'csrf';
|
||||
input.value = csrfToken;
|
||||
form.appendChild(input);
|
||||
});
|
||||
const validationMessages = {
|
||||
required: < ? = json_encode(t('validation.required'), JSON_UNESCAPED_UNICODE) ? > ,
|
||||
passwordMin: count => < ? = json_encode(t('validation.password_min'), JSON_UNESCAPED_UNICODE) ? >
|
||||
.replace(':count', count),
|
||||
invalidFormat: < ? = json_encode(t('validation.invalid_format'), JSON_UNESCAPED_UNICODE) ? > ,
|
||||
checkField: < ? = json_encode(t('validation.check_field'), JSON_UNESCAPED_UNICODE) ? >
|
||||
};
|
||||
document.querySelectorAll('form').forEach(form => {
|
||||
if (!form.querySelector('input[type="password"]')) return;
|
||||
form.noValidate = true;
|
||||
form.addEventListener('submit', event => {
|
||||
form.querySelectorAll('.is-invalid').forEach(field => field.classList.remove(
|
||||
'is-invalid'));
|
||||
if (form.checkValidity()) return;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
form.querySelectorAll(':invalid').forEach(field => {
|
||||
field.classList.add('is-invalid');
|
||||
let feedback = field.parentElement.querySelector('.invalid-feedback');
|
||||
if (!feedback) {
|
||||
feedback = document.createElement('div');
|
||||
feedback.className = 'invalid-feedback';
|
||||
field.insertAdjacentElement('afterend', feedback);
|
||||
}
|
||||
feedback.textContent = field.validity.valueMissing ? validationMessages
|
||||
.required : field.validity.tooShort ? validationMessages.passwordMin(
|
||||
field.minLength) : field.validity.typeMismatch ? validationMessages
|
||||
.invalidFormat : validationMessages.checkField;
|
||||
});
|
||||
form.querySelector(':invalid')?.focus();
|
||||
});
|
||||
form.querySelectorAll('input').forEach(field => field.addEventListener('input', () => field
|
||||
.classList.remove('is-invalid')));
|
||||
});
|
||||
document.querySelectorAll('input[type="date"]').forEach(input => {
|
||||
flatpickr(input, {
|
||||
locale: < ? = json_encode(I18n::locale() === 'fr' ? 'fr' : 'default') ? > ,
|
||||
altInput: true,
|
||||
altFormat: 'd/m/Y',
|
||||
dateFormat: 'Y-m-d',
|
||||
allowInput: true,
|
||||
disableMobile: true
|
||||
});
|
||||
});
|
||||
|
||||
</script>
|
||||
<script src="/assets/accounting.js"></script>
|
||||
<script>
|
||||
document.querySelectorAll('select option').forEach(o => {
|
||||
const s = o.textContent || '';
|
||||
if (s) o.textContent = s.charAt(0).toLocaleUpperCase(document.documentElement.lang || 'fr') + s
|
||||
.slice(1);
|
||||
});
|
||||
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
66
app/Views/litter_add_existing.php
Normal file
66
app/Views/litter_add_existing.php
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
<?php
|
||||
$dateLabel = t('common.unknown_date');
|
||||
if (!empty($litter['birth_date'])) {
|
||||
$date = DateTimeImmutable::createFromFormat('Y-m-d', (string)$litter['birth_date']);
|
||||
if ($date) {
|
||||
$dateLabel = $date->format('d/m/Y');
|
||||
}
|
||||
}
|
||||
?>
|
||||
<form method="post" action="/litter/add-existing/save" class="card">
|
||||
<input type="hidden" name="csrf" value="<?= h(Auth::csrf()) ?>">
|
||||
<input type="hidden" name="litter_id" value="<?= (int)$litter['id'] ?>">
|
||||
<input type="hidden" name="source_animal_id" value="<?= (int)$sourceAnimalId ?>">
|
||||
<div class="card-header d-flex align-items-center justify-content-between">
|
||||
<div>
|
||||
<h3 class="card-title mb-1"><?= h(t('litter.add_existing')) ?></h3>
|
||||
<div class="text-muted small">
|
||||
<?= h(t('litter.summary', ['date' => $dateLabel,'mother' => $litter['mother_name'] ?: t('common.unknown_f'),'father' => $litter['father_name'] ?: t('common.unknown_m')])) ?>
|
||||
</div>
|
||||
</div>
|
||||
<a class="btn btn-ghost-secondary"
|
||||
href="/animal?id=<?= (int)$sourceAnimalId ?>#tab-litters"><?= h(t('common.cancel')) ?></a>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="litterAnimalSearch"><?= h(t('litter.search_cat')) ?></label>
|
||||
<input class="form-control" id="litterAnimalSearch" type="search"
|
||||
placeholder="<?= h(t('litter.cat_name_code')) ?>" autocomplete="off">
|
||||
<div class="form-hint"><?= h(t('litter.attached_hidden')) ?></div>
|
||||
</div>
|
||||
<div class="row g-2" id="litterAnimalChoices">
|
||||
<?php foreach ($animals as $candidate): ?>
|
||||
<div class="col-md-4 litter-animal-choice"
|
||||
data-search="<?= h(mb_strtolower($candidate['name'] . ' ' . $candidate['internal_code'])) ?>">
|
||||
<label class="form-select d-flex align-items-center gap-2 h-100" style="cursor:pointer">
|
||||
<input class="form-check-input mt-0" type="checkbox" name="animal_ids[]"
|
||||
value="<?= (int)$candidate['id'] ?>">
|
||||
<span class="coat-swatch" style="<?= h(coat_swatch_style($candidate['color'] ?? '')) ?>"></span>
|
||||
<span class="min-w-0"><strong><?= h($candidate['name']) ?></strong><br><small
|
||||
class="text-muted"><?= h($candidate['internal_code'] ?: t('animal.no_code')) ?> ·
|
||||
<?= ($candidate['sex'] ?? 'U') === 'F' ? t('sex.female') : (($candidate['sex'] ?? 'U') === 'M' ? t('sex.male') : t('animal.sex_to_define')) ?></small></span>
|
||||
<?php if (!empty($candidate['archived_at'])): ?><span
|
||||
class="badge bg-secondary-lt ms-auto"><?= h(t('animal.archived_badge')) ?></span><?php endif; ?>
|
||||
</label>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php if (!$animals): ?><div class="empty">
|
||||
<div class="empty-description"><?= h(t('litter.none_available')) ?></div>
|
||||
</div><?php endif; ?>
|
||||
</div>
|
||||
<div class="card-footer d-flex justify-content-between align-items-center">
|
||||
<span class="text-muted"><?= h(t('litter.select_multiple')) ?></span>
|
||||
<button class="btn btn-primary" type="submit"><?= h(t('litter.add_to')) ?></button>
|
||||
</div>
|
||||
</form>
|
||||
<script>
|
||||
document.getElementById('litterAnimalSearch')?.addEventListener('input', event => {
|
||||
const query = event.target.value.trim().toLocaleLowerCase( < ? = json_encode(I18n::locale() === 'fr' ?
|
||||
'fr' : 'en') ? > );
|
||||
document.querySelectorAll('.litter-animal-choice').forEach(choice => {
|
||||
choice.classList.toggle('d-none', query !== '' && !choice.dataset.search.includes(query));
|
||||
});
|
||||
});
|
||||
|
||||
</script>
|
||||
226
app/Views/litter_form.php
Normal file
226
app/Views/litter_form.php
Normal file
|
|
@ -0,0 +1,226 @@
|
|||
<?php
|
||||
$coatColors = [
|
||||
'Bicolore Noir / Blanc' => t('coat.black_white'), 'Bicolore Gris / Blanc' => t('coat.grey_white'), 'Bicolore Roux / Blanc' => t('coat.ginger_white'), 'Bicolore Tigré et Blanc' => t('coat.tabby_white'),
|
||||
'Gris' => t('coat.grey'), 'Roux' => t('coat.ginger'), 'Noir' => t('coat.black'), 'Chocolat' => t('coat.chocolate'), 'Blanc' => t('coat.white'), 'Tricolore / Calico' => t('coat.calico'),
|
||||
'Écaille de tortue' => t('coat.tortoiseshell'), 'Colourpoint / Siamois' => t('coat.colourpoint'), 'Tigré / Tabby' => t('coat.tabby'),
|
||||
];
|
||||
$prefillCount = max(0, min(20, (int)($_GET['count'] ?? 4)));
|
||||
?>
|
||||
<form method="post" action="/litter/save" class="card">
|
||||
<input type="hidden" name="csrf" value="<?= h(Auth::csrf()) ?>">
|
||||
<div class="card-header d-flex align-items-center justify-content-between">
|
||||
<h3 class="card-title"><?= h(t('litter.create')) ?></h3>
|
||||
<a href="<?= $sourceAnimalId ? '/animal?id=' . (int)$sourceAnimalId . '#tab-litters' : '/animals' ?>"
|
||||
class="btn btn-ghost-secondary"><?= h(t('common.cancel')) ?></a>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<input type="hidden" name="source_animal_id" value="<?= (int)$sourceAnimalId ?>">
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label"><?= h(t('litter.mother')) ?> <span
|
||||
class="text-muted"><?= h(t('common.optional_parentheses')) ?></span></label>
|
||||
<select class="form-select" name="mother_id" id="motherId">
|
||||
<option value="">— <?= h(t('litter.unknown_parent')) ?> —</option>
|
||||
<?php foreach ($mothers as $parent): ?>
|
||||
<option value="<?= (int)$parent['id'] ?>"
|
||||
<?= (int)$motherId === (int)$parent['id'] ? 'selected' : '' ?>><?= h($parent['name']) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label"><?= h(t('litter.father')) ?> <span
|
||||
class="text-muted"><?= h(t('common.optional_parentheses')) ?></span></label>
|
||||
<select class="form-select" name="father_id" id="fatherId">
|
||||
<option value="">— <?= h(t('litter.unknown_parent')) ?> —</option>
|
||||
<?php foreach ($fathers as $parent): ?>
|
||||
<option value="<?= (int)$parent['id'] ?>"
|
||||
<?= (int)$fatherId === (int)$parent['id'] ? 'selected' : '' ?>><?= h($parent['name']) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label"><?= h(t('litter.birth_date')) ?> <span
|
||||
class="text-muted"><?= h(t('common.optional_parentheses')) ?></span></label>
|
||||
<input class="form-control" type="date" name="birth_date" placeholder="jj/mm/aaaa">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="hidden" name="kitten_mode" id="kittenMode" value="new">
|
||||
<div class="nav nav-tabs mb-4" role="tablist" aria-label="<?= h(t('litter.kitten_type')) ?>">
|
||||
<button class="nav-link active" type="button" data-kitten-mode="new" aria-selected="true">✨
|
||||
<?= h(t('litter.new_kittens')) ?></button>
|
||||
<button class="nav-link" type="button" data-kitten-mode="existing" aria-selected="false">🔎
|
||||
<?= h(t('litter.existing_cats')) ?></button>
|
||||
</div>
|
||||
|
||||
<div id="newKittensPane">
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label" for="kittenCount"><?= h(t('litter.kitten_count')) ?></label>
|
||||
<input class="form-control" id="kittenCount" type="number" min="1" max="20" step="1"
|
||||
value="<?= max(1, $prefillCount) ?>" required>
|
||||
<div class="form-hint"><?= h(t('litter.create_records')) ?></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<h4 class="mb-0"><?= h(t('litter.new_records')) ?></h4>
|
||||
<div class="text-muted small"><?= h(t('litter.auto_name')) ?></div>
|
||||
</div>
|
||||
<div id="kittens" class="vstack gap-2"></div>
|
||||
</div>
|
||||
|
||||
<div id="existingKittensPane" class="d-none">
|
||||
<div class="mb-3">
|
||||
<h4 class="mb-1"><?= h(t('litter.select_siblings')) ?></h4>
|
||||
<div class="text-muted small mb-3"><?= h(t('litter.no_new_record')) ?></div>
|
||||
<input class="form-control" id="existingKittenSearch" type="search"
|
||||
placeholder="<?= h(t('litter.search_name_code')) ?>" autocomplete="off">
|
||||
</div>
|
||||
<div class="row g-2" id="existingKittenChoices">
|
||||
<?php foreach (($existingKittens ?? []) as $candidate): ?>
|
||||
<div class="col-md-4 existing-kitten-choice"
|
||||
data-search="<?= h(mb_strtolower($candidate['name'] . ' ' . $candidate['internal_code'])) ?>"
|
||||
data-animal-id="<?= (int)$candidate['id'] ?>">
|
||||
<label class="form-select d-flex align-items-center gap-2 h-100" style="cursor:pointer">
|
||||
<input class="form-check-input mt-0 existing-kitten-checkbox" type="checkbox"
|
||||
name="existing_kitten_ids[]" value="<?= (int)$candidate['id'] ?>" disabled>
|
||||
<span class="coat-swatch" style="<?= h(coat_swatch_style($candidate['color'] ?? '')) ?>"></span>
|
||||
<span><strong><?= h($candidate['name']) ?></strong><br><small
|
||||
class="text-muted"><?= h($candidate['internal_code'] ?: t('animal.no_code')) ?></small></span>
|
||||
<?php if (!empty($candidate['archived_at'])): ?><span
|
||||
class="badge bg-secondary-lt ms-auto"><?= h(t('animal.archived_badge')) ?></span><?php endif; ?>
|
||||
</label>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer text-end">
|
||||
<button class="btn btn-primary" type="submit"><?= h(t('litter.create_litter')) ?></button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<template id="kittenTemplate">
|
||||
<div class="card card-sm kitten-row">
|
||||
<div class="card-body row g-2 align-items-end">
|
||||
<div class="col-md-1 fw-bold kitten-number"></div>
|
||||
<div class="col-md-4"><label class="form-label"><?= h(t('litter.name_optional')) ?></label><input
|
||||
class="form-control kitten-name" placeholder="<?= h(t('litter.auto_name_placeholder')) ?>"></div>
|
||||
<div class="col-md-3"><label class="form-label"><?= h(t('animal.coat')) ?></label><select
|
||||
class="form-select kitten-color">
|
||||
<option value=""><?= h(t('common.to_define')) ?></option>
|
||||
<?php foreach ($coatColors as $value => $label): ?><option value="<?= h($value) ?>"><?= h($label) ?>
|
||||
</option><?php endforeach; ?>
|
||||
</select></div>
|
||||
<div class="col-md-2"><label class="form-label"><?= h(t('animal.sex_label')) ?></label><select
|
||||
class="form-select kitten-sex">
|
||||
<option value="U"><?= h(t('common.to_define')) ?></option>
|
||||
<option value="F"><?= h(t('sex.female')) ?></option>
|
||||
<option value="M"><?= h(t('sex.male')) ?></option>
|
||||
</select></div>
|
||||
<div class="col-md-2"><button class="btn btn-outline-danger w-100 remove-kitten"
|
||||
type="button"><?= h(t('common.remove')) ?></button></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
(() => {
|
||||
const list = document.getElementById('kittens');
|
||||
const template = document.getElementById('kittenTemplate');
|
||||
const countInput = document.getElementById('kittenCount');
|
||||
const modeInput = document.getElementById('kittenMode');
|
||||
const newPane = document.getElementById('newKittensPane');
|
||||
const existingPane = document.getElementById('existingKittensPane');
|
||||
|
||||
function renumber() {
|
||||
[...list.children].forEach((row, index) => {
|
||||
row.querySelector('.kitten-number').textContent = 'N° ' + (index + 1);
|
||||
row.querySelector('.kitten-name').name = `kittens[${index}][name]`;
|
||||
row.querySelector('.kitten-color').name = `kittens[${index}][color]`;
|
||||
row.querySelector('.kitten-sex').name = `kittens[${index}][sex]`;
|
||||
});
|
||||
}
|
||||
|
||||
function add() {
|
||||
const row = template.content.firstElementChild.cloneNode(true);
|
||||
row.querySelector('.remove-kitten').addEventListener('click', () => {
|
||||
row.remove();
|
||||
countInput.value = list.children.length;
|
||||
renumber();
|
||||
});
|
||||
list.appendChild(row);
|
||||
renumber();
|
||||
}
|
||||
|
||||
function requestedCount() {
|
||||
const value = parseInt(countInput.value, 10);
|
||||
return Math.max(1, Math.min(20, Number.isNaN(value) ? 1 : value));
|
||||
}
|
||||
|
||||
function hasValues(row) {
|
||||
return row.querySelector('.kitten-name').value !== '' ||
|
||||
row.querySelector('.kitten-color').value !== '' ||
|
||||
row.querySelector('.kitten-sex').value !== 'U';
|
||||
}
|
||||
|
||||
function syncCount(confirmRemoval = false) {
|
||||
const target = requestedCount();
|
||||
if (target < list.children.length) {
|
||||
const removedRows = [...list.children].slice(target);
|
||||
if (confirmRemoval && removedRows.some(hasValues) &&
|
||||
!window.confirm( < ? = json_encode(t('litter.reduce_confirm'), JSON_UNESCAPED_UNICODE |
|
||||
JSON_UNESCAPED_SLASHES) ? > )) {
|
||||
countInput.value = list.children.length;
|
||||
return;
|
||||
}
|
||||
while (list.children.length > target) list.lastElementChild.remove();
|
||||
}
|
||||
while (list.children.length < target) add();
|
||||
countInput.value = target;
|
||||
renumber();
|
||||
}
|
||||
countInput.addEventListener('input', () => {
|
||||
if (requestedCount() > list.children.length) syncCount();
|
||||
});
|
||||
countInput.addEventListener('change', () => syncCount(true));
|
||||
syncCount();
|
||||
const existingSearch = document.getElementById('existingKittenSearch');
|
||||
const mother = document.getElementById('motherId');
|
||||
const father = document.getElementById('fatherId');
|
||||
const syncExistingChoices = () => {
|
||||
const query = existingSearch?.value.trim().toLocaleLowerCase( < ? = json_encode(I18n::locale() ===
|
||||
'fr' ? 'fr' : 'en') ? > ) || '';
|
||||
document.querySelectorAll('.existing-kitten-choice').forEach(choice => {
|
||||
const isParent = choice.dataset.animalId === mother?.value || choice.dataset
|
||||
.animalId === father?.value;
|
||||
choice.classList.toggle('d-none', isParent || (query !== '' && !choice.dataset.search
|
||||
.includes(query)));
|
||||
if (isParent) choice.querySelector('.existing-kitten-checkbox').checked = false;
|
||||
});
|
||||
};
|
||||
existingSearch?.addEventListener('input', syncExistingChoices);
|
||||
mother?.addEventListener('change', syncExistingChoices);
|
||||
father?.addEventListener('change', syncExistingChoices);
|
||||
syncExistingChoices();
|
||||
const setMode = mode => {
|
||||
const existing = mode === 'existing';
|
||||
modeInput.value = existing ? 'existing' : 'new';
|
||||
newPane.classList.toggle('d-none', existing);
|
||||
existingPane.classList.toggle('d-none', !existing);
|
||||
newPane.querySelectorAll('input,select').forEach(input => input.disabled = existing);
|
||||
existingPane.querySelectorAll('input[type="checkbox"]').forEach(input => input.disabled = !
|
||||
existing);
|
||||
document.querySelectorAll('[data-kitten-mode]').forEach(button => {
|
||||
const active = button.dataset.kittenMode === mode;
|
||||
button.classList.toggle('active', active);
|
||||
button.setAttribute('aria-selected', active ? 'true' : 'false');
|
||||
});
|
||||
};
|
||||
document.querySelectorAll('[data-kitten-mode]').forEach(button => button.addEventListener('click', () =>
|
||||
setMode(button.dataset.kittenMode)));
|
||||
setMode('new');
|
||||
})();
|
||||
|
||||
</script>
|
||||
30
app/Views/public_site_animal.php
Normal file
30
app/Views/public_site_animal.php
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
<section class="section">
|
||||
<div class="wrap"><a href="<?= h(('animals')) ?>">← <?= h(t('public_site.back_animals')) ?></a>
|
||||
<div class="content-card animal-detail" style="margin-top:20px">
|
||||
<div class="gallery"><?php if (!$photos): ?><div class="photo-placeholder">
|
||||
<?= h($animal['species_icon'] ?: '🐾') ?></div><?php else: ?><?php foreach ($photos as $p): ?><img
|
||||
src="/media/animals/<?= (int)$animal['id'] ?>/<?= h(rawurlencode($p['filename'])) ?>"
|
||||
alt="<?= h($p['caption'] ?: $animal['name']) ?>"><?php endforeach; ?><?php endif; ?></div>
|
||||
<div>
|
||||
<div class="meta"><?= h($animal['species_name'] ?: SpeciesService::label($animal['species'])) ?></div>
|
||||
<h1 class="page-title"><?= h($animal['name']) ?></h1>
|
||||
<div class="facts">
|
||||
<div class="fact"><strong><?= h(t('public_site.age')) ?></strong><br><?= h(public_age($animal)) ?>
|
||||
</div>
|
||||
<div class="fact">
|
||||
<strong><?= h(t('animal.sex')) ?></strong><br><?= h($animal['sex'] === 'F' ? t('sex.female') : ($animal['sex'] === 'M' ? t('sex.male') : t('common.unknown'))) ?>
|
||||
</div><?php if ($animal['breed']): ?><div class="fact">
|
||||
<strong><?= h(t('public_site.breed')) ?></strong><br><?= h($animal['breed']) ?></div>
|
||||
<?php endif; ?><?php if ($animal['color']): ?><div class="fact">
|
||||
<strong><?= h(t('public_site.coat')) ?></strong><br><?= h($animal['color']) ?></div>
|
||||
<?php endif; ?>
|
||||
</div><?php if ($animal['website_description']): ?><p><?= nl2br(h($animal['website_description'])) ?>
|
||||
</p><?php endif; ?><div class="chips">
|
||||
<?php foreach (['compatibility_dogs' => ['🐶',t('public_site.dogs')],'compatibility_cats' => ['🐱',t('public_site.cats')],'compatibility_children' => ['🧒',t('public_site.children')],'house_trained' => ['✨',t('public_site.clean')]] as $key => $meta): if (($animal[$key] ?? 'unknown') === 'yes'): ?><span
|
||||
class="chip"><?= $meta[0] . ' ' . h($meta[1]) ?></span><?php endif; endforeach; ?></div>
|
||||
<p style="margin-top:28px"><a class="button"
|
||||
href="<?= h(('contact')) ?>"><?= h(t('public_site.ask_adoption')) ?></a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
10
app/Views/public_site_animals.php
Normal file
10
app/Views/public_site_animals.php
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<section class="section">
|
||||
<div class="wrap">
|
||||
<h1 class="page-title"><?= h(t('public_site.animals')) ?></h1>
|
||||
<div class="filters"><a class="button <?= $selectedSpecies === '' ? '' : 'secondary' ?>"
|
||||
href="<?= h($siteUrl('animals')) ?>"><?= h(t('public_site.all_species')) ?></a><?php foreach ($publicSpecies as $s): ?><a
|
||||
class="button <?= $selectedSpecies === $s['code'] ? '' : 'secondary' ?>"
|
||||
href="<?= h($siteUrl('animals', ['espece' => $s['code']])) ?>"><?= h(($s['icon'] ?: '🐾') . ' ' . $s['name'] . ' (' . $s['count'] . ')') ?></a><?php endforeach; ?>
|
||||
</div><?php require __DIR__ . '/_public_animal_cards.php'; ?>
|
||||
</div>
|
||||
</section>
|
||||
49
app/Views/public_site_contact.php
Normal file
49
app/Views/public_site_contact.php
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
<?php $lat = is_numeric($settings['association_latitude'] ?? null) ? (float)$settings['association_latitude'] : null;
|
||||
$lng = is_numeric($settings['association_longitude'] ?? null) ? (float)$settings['association_longitude'] : null;
|
||||
$obfuscate = static fn (string$value): string => implode('', array_reverse(mb_str_split($value))); ?>
|
||||
<section class="section">
|
||||
<div class="wrap">
|
||||
<div class="content-card">
|
||||
<h1 class="page-title"><?= h(t('public_site.contact')) ?></h1>
|
||||
<p><?= nl2br(h($settings['public_site_contact_text'])) ?></p>
|
||||
<div class="contact-list"><?php if ($fullAddress !== ''): ?><div class="contact-item">📍
|
||||
<?= h($fullAddress) ?></div><?php endif; ?><?php if ($settings['association_phone']): ?><div
|
||||
class="contact-item">☎ <span
|
||||
class="contact-obfuscated"><?= h($obfuscate($settings['association_phone'])) ?></span></div>
|
||||
<?php endif; ?><?php if ($settings['association_email']): ?><div class="contact-item">✉ <span
|
||||
class="contact-obfuscated"><?= h($obfuscate($settings['association_email'])) ?></span></div>
|
||||
<?php endif; ?></div>
|
||||
<div class="chips" style="margin-top:25px"><?php if ($settings['public_site_facebook_url']): ?><a
|
||||
class="button secondary" href="<?= h($settings['public_site_facebook_url']) ?>"
|
||||
rel="noopener noreferrer">Facebook</a><?php endif; ?><?php if ($settings['public_site_instagram_url']): ?><a
|
||||
class="button secondary" href="<?= h($settings['public_site_instagram_url']) ?>"
|
||||
rel="noopener noreferrer">Instagram</a><?php endif; ?></div>
|
||||
<?php if ($lat !== null && $lng !== null): ?><h2 style="margin-top:35px">
|
||||
<?= h(t('public_site.map_title')) ?></h2>
|
||||
<div id="association-map" class="contact-map" aria-label="<?= h(t('public_site.map_title')) ?>"></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<div class="wrap" style="text-align:center"><a href="<?= h($siteUrl('legal')) ?>"><?= h(t('public_site.legal')) ?></a> ·
|
||||
<a href="<?= h($siteUrl('privacy')) ?>"><?= h(t('public_site.privacy')) ?></a></div>
|
||||
<?php if ($lat !== null && $lng !== null): ?><script src="/assets/vendor/leaflet/leaflet.js"></script>
|
||||
<script>
|
||||
(() => {
|
||||
const map = L.map('association-map', {
|
||||
scrollWheelZoom: false
|
||||
}).setView([ < ? = json_encode($lat) ? > , < ? = json_encode($lng) ? > ], 15);
|
||||
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
maxZoom: 19,
|
||||
attribution: '© OpenStreetMap'
|
||||
}).addTo(map);
|
||||
const popup = document.createElement('div');
|
||||
const name = document.createElement('strong');
|
||||
name.textContent = < ? = json_encode($association, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ? > ;
|
||||
popup.append(name, document.createElement('br'), document.createTextNode( < ? = json_encode($fullAddress,
|
||||
JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ? > ));
|
||||
L.marker([ < ? = json_encode($lat) ? > , < ? = json_encode($lng) ? > ]).addTo(map).bindPopup(popup)
|
||||
.openPopup();
|
||||
})();
|
||||
|
||||
</script><?php endif; ?>
|
||||
12
app/Views/public_site_home.php
Normal file
12
app/Views/public_site_home.php
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<section class="hero">
|
||||
<div class="wrap">
|
||||
<h1><?= h($settings['public_site_home_title']) ?></h1>
|
||||
<p><?= nl2br(h($settings['public_site_home_text'])) ?></p><a class="button"
|
||||
href="<?= h($siteUrl('animals')) ?>"><?= h(t('public_site.discover')) ?></a>
|
||||
</div>
|
||||
</section>
|
||||
<section class="section">
|
||||
<div class="wrap">
|
||||
<h2><?= h(t('public_site.latest_animals')) ?></h2><?php require __DIR__ . '/_public_animal_cards.php'; ?>
|
||||
</div>
|
||||
</section>
|
||||
469
app/Views/public_site_layout.php
Normal file
469
app/Views/public_site_layout.php
Normal file
|
|
@ -0,0 +1,469 @@
|
|||
<?php
|
||||
require_once __DIR__ . '/_helpers.php';
|
||||
$siteUrl = static fn (string$page = 'home', array$query = []): string => PublicSiteController::url($page, $query);
|
||||
$theme = in_array($settings['public_site_theme'] ?? 'warm', ['warm','nature','minimal'], true) ? $settings['public_site_theme'] : 'warm';
|
||||
$association = $settings['association_name'] ?: 'Association';
|
||||
$viewFile = __DIR__ . '/' . $view;
|
||||
$requestPath = rtrim((string)(parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH) ?: '/'), '/') ?: '/';
|
||||
$activePage = match($requestPath) {
|
||||
'/','/site' => 'home','/animaux','/site/animaux','/adoption','/site/animal' => 'animals','/presentation','/site/presentation' => 'about','/dons','/site/dons' => 'donation','/contact','/site/contact' => 'contact',default => ''
|
||||
};
|
||||
$activeSpecies = (string)($selectedSpecies ?? ($animal['species'] ?? ''));
|
||||
$navClass = static fn (bool$active): string => $active ? ' class="active" aria-current="page"' : '';
|
||||
$fullAddress = implode(', ', array_filter([$settings['association_address'] ?? '',trim(($settings['association_postal_code'] ?? '') . ' ' . ($settings['association_city'] ?? ''))]));
|
||||
$hasContactMap = $activePage === 'contact' && is_numeric($settings['association_latitude'] ?? null) && is_numeric($settings['association_longitude'] ?? null);
|
||||
if (!function_exists('public_age')) {
|
||||
function public_age(array$a): string
|
||||
{
|
||||
$months = null;
|
||||
if (!empty($a['birth_date'])) {
|
||||
$born = new DateTimeImmutable($a['birth_date']);
|
||||
$now = new DateTimeImmutable('today');
|
||||
$months = max(0, $born->diff($now)->y * 12 + $born->diff($now)->m);
|
||||
} elseif (($a['birth_estimated_months'] ?? '') !== '') {
|
||||
$months = (int)$a['birth_estimated_months'];
|
||||
}if ($months === null) {
|
||||
return t('public_site.age_unknown');
|
||||
}if ($months < 12) {
|
||||
return t('public_site.months', ['count' => $months]);
|
||||
}$years = intdiv($months, 12);
|
||||
$rest = $months % 12;
|
||||
return$rest ? t('public_site.years_months', ['years' => $years,'months' => $rest]) : t('public_site.years', ['count' => $years]);
|
||||
}
|
||||
}
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="<?= h(I18n::locale()) ?>" data-theme="<?= h($theme) ?>">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title><?= h($title . ' — ' . $association) ?></title>
|
||||
<meta name="description"
|
||||
content="<?= h(mb_substr(strip_tags((string)($settings['public_site_home_text'] ?? '')), 0, 155)) ?>">
|
||||
<meta name="robots"
|
||||
content="<?= ($settings['public_site_enabled'] ?? '0') === '1' ? 'index,follow' : 'noindex,nofollow' ?>">
|
||||
<?php if ($hasContactMap): ?>
|
||||
<link rel="stylesheet" href="/assets/vendor/leaflet/leaflet.css"><?php endif; ?><style>
|
||||
:root {
|
||||
--accent: #d87945;
|
||||
--accent-dark: #a84f29;
|
||||
--ink: #2d2926;
|
||||
--muted: #756d67;
|
||||
--paper: #fffaf5;
|
||||
--surface: #fff;
|
||||
--line: #eadfd5;
|
||||
--radius: 20px;
|
||||
--shadow: 0 12px 34px rgba(74, 49, 34, .09)
|
||||
}
|
||||
|
||||
[data-theme=nature] {
|
||||
--accent: #56856a;
|
||||
--accent-dark: #315e48;
|
||||
--ink: #24352c;
|
||||
--muted: #65756c;
|
||||
--paper: #f5f8f3;
|
||||
--surface: #fff;
|
||||
--line: #dce7dc;
|
||||
--radius: 14px;
|
||||
--shadow: 0 10px 28px rgba(38, 72, 51, .08)
|
||||
}
|
||||
|
||||
[data-theme=minimal] {
|
||||
--accent: #202020;
|
||||
--accent-dark: #000;
|
||||
--ink: #171717;
|
||||
--muted: #6d6d6d;
|
||||
--paper: #f5f5f5;
|
||||
--surface: #fff;
|
||||
--line: #dedede;
|
||||
--radius: 4px;
|
||||
--shadow: none
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
background: var(--paper);
|
||||
color: var(--ink);
|
||||
font: 16px/1.6 system-ui, -apple-system, Segoe UI, sans-serif
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit
|
||||
}
|
||||
|
||||
.wrap {
|
||||
width: min(1120px, calc(100% - 32px));
|
||||
margin: auto
|
||||
}
|
||||
|
||||
.site-head {
|
||||
background: var(--surface);
|
||||
border-bottom: 1px solid var(--line);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1000
|
||||
}
|
||||
|
||||
.head-row {
|
||||
min-height: 96px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24px
|
||||
}
|
||||
|
||||
.brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
text-decoration: none;
|
||||
font-weight: 800;
|
||||
font-size: 1.15rem
|
||||
}
|
||||
|
||||
.brand img {
|
||||
width: auto;
|
||||
height: 72px;
|
||||
max-width: 150px;
|
||||
object-fit: contain
|
||||
}
|
||||
|
||||
.nav {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap
|
||||
}
|
||||
|
||||
.nav a {
|
||||
padding: 8px 11px;
|
||||
border-radius: 999px;
|
||||
text-decoration: none
|
||||
}
|
||||
|
||||
.nav a:hover,
|
||||
.nav a.active {
|
||||
background: var(--paper);
|
||||
color: var(--accent-dark)
|
||||
}
|
||||
|
||||
.nav a.active {
|
||||
font-weight: 800
|
||||
}
|
||||
|
||||
.hero {
|
||||
padding: 72px 0 52px;
|
||||
background: linear-gradient(135deg, var(--surface), var(--paper))
|
||||
}
|
||||
|
||||
.hero h1,
|
||||
.page-title {
|
||||
font-size: clamp(2rem, 6vw, 4rem);
|
||||
line-height: 1.05;
|
||||
margin: 0 0 18px
|
||||
}
|
||||
|
||||
.hero p {
|
||||
max-width: 720px;
|
||||
font-size: 1.15rem;
|
||||
color: var(--muted)
|
||||
}
|
||||
|
||||
.button {
|
||||
display: inline-block;
|
||||
border: 0;
|
||||
border-radius: 999px;
|
||||
padding: 11px 18px;
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
font-weight: 700
|
||||
}
|
||||
|
||||
.button.secondary {
|
||||
background: transparent;
|
||||
color: var(--accent-dark);
|
||||
border: 1px solid var(--accent)
|
||||
}
|
||||
|
||||
main {
|
||||
min-height: 65vh
|
||||
}
|
||||
|
||||
.section {
|
||||
padding: 52px 0
|
||||
}
|
||||
|
||||
.section h2 {
|
||||
font-size: 2rem;
|
||||
margin: 0 0 22px
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 20px
|
||||
}
|
||||
|
||||
.animal-card {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
overflow: hidden;
|
||||
box-shadow: var(--shadow);
|
||||
text-decoration: none;
|
||||
transition: transform .18s
|
||||
}
|
||||
|
||||
.animal-card:hover {
|
||||
transform: translateY(-3px)
|
||||
}
|
||||
|
||||
.animal-card img,
|
||||
.photo-placeholder {
|
||||
display: block;
|
||||
width: 100%;
|
||||
aspect-ratio: 1/1;
|
||||
object-fit: cover;
|
||||
background: var(--line)
|
||||
}
|
||||
|
||||
.photo-placeholder {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
font-size: 3rem
|
||||
}
|
||||
|
||||
.animal-card-body {
|
||||
padding: 15px
|
||||
}
|
||||
|
||||
.animal-card h3 {
|
||||
margin: 0;
|
||||
font-size: 1.2rem
|
||||
}
|
||||
|
||||
.meta {
|
||||
color: var(--muted);
|
||||
font-size: .92rem
|
||||
}
|
||||
|
||||
.chips {
|
||||
display: flex;
|
||||
gap: 7px;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 10px
|
||||
}
|
||||
|
||||
.chip {
|
||||
padding: 3px 9px;
|
||||
border-radius: 999px;
|
||||
background: var(--paper);
|
||||
font-size: .8rem
|
||||
}
|
||||
|
||||
.filters {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 25px
|
||||
}
|
||||
|
||||
.content-card {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
padding: clamp(22px, 5vw, 50px);
|
||||
box-shadow: var(--shadow)
|
||||
}
|
||||
|
||||
.animal-detail {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(280px, 1fr) 1fr;
|
||||
gap: 38px
|
||||
}
|
||||
|
||||
.gallery {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 10px
|
||||
}
|
||||
|
||||
.gallery img {
|
||||
width: 100%;
|
||||
aspect-ratio: 1/1;
|
||||
object-fit: cover;
|
||||
border-radius: var(--radius)
|
||||
}
|
||||
|
||||
.gallery img:first-child {
|
||||
grid-column: 1/-1
|
||||
}
|
||||
|
||||
.facts {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 10px;
|
||||
margin: 22px 0
|
||||
}
|
||||
|
||||
.fact {
|
||||
padding: 12px;
|
||||
background: var(--paper);
|
||||
border-radius: 10px
|
||||
}
|
||||
|
||||
.contact-list {
|
||||
display: grid;
|
||||
gap: 12px
|
||||
}
|
||||
|
||||
.contact-item {
|
||||
padding: 14px 0;
|
||||
border-bottom: 1px solid var(--line)
|
||||
}
|
||||
|
||||
.contact-obfuscated {
|
||||
direction: rtl;
|
||||
unicode-bidi: bidi-override;
|
||||
display: inline-block
|
||||
}
|
||||
|
||||
.contact-map {
|
||||
height: 360px;
|
||||
margin-top: 28px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
overflow: hidden;
|
||||
background: var(--paper)
|
||||
}
|
||||
|
||||
footer {
|
||||
margin-top: 55px;
|
||||
border-top: 1px solid var(--line);
|
||||
padding: 32px 0;
|
||||
color: var(--muted)
|
||||
}
|
||||
|
||||
.footer-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 24px;
|
||||
flex-wrap: wrap
|
||||
}
|
||||
|
||||
.footer-identity {
|
||||
max-width: 650px
|
||||
}
|
||||
|
||||
.footer-meta {
|
||||
font-size: .9rem
|
||||
}
|
||||
|
||||
.footer-powered a {
|
||||
color: var(--accent-dark);
|
||||
font-weight: 800
|
||||
}
|
||||
|
||||
.empty {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
color: var(--muted)
|
||||
}
|
||||
|
||||
@media(max-width:900px) {
|
||||
.grid {
|
||||
grid-template-columns: repeat(2, 1fr)
|
||||
}
|
||||
|
||||
.animal-detail {
|
||||
grid-template-columns: 1fr
|
||||
}
|
||||
|
||||
.head-row {
|
||||
align-items: flex-start;
|
||||
padding: 12px 0;
|
||||
flex-wrap: wrap
|
||||
}
|
||||
|
||||
.brand img {
|
||||
height: 64px
|
||||
}
|
||||
|
||||
.nav {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
overflow: auto;
|
||||
flex-wrap: nowrap
|
||||
}
|
||||
}
|
||||
|
||||
@media(max-width:520px) {
|
||||
.grid {
|
||||
grid-template-columns: 1fr
|
||||
}
|
||||
|
||||
.nav a {
|
||||
white-space: nowrap
|
||||
}
|
||||
|
||||
.hero {
|
||||
padding: 45px 0
|
||||
}
|
||||
|
||||
.facts {
|
||||
grid-template-columns: 1fr
|
||||
}
|
||||
|
||||
.contact-map {
|
||||
height: 300px
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<header class="site-head">
|
||||
<div class="wrap head-row"><a class="brand"
|
||||
href="<?= h($siteUrl()) ?>"><?php if (AssociationBrand::path()): ?><img src="<?= h($siteUrl('logo')) ?>"
|
||||
alt=""><?php endif; ?><span><?= h($association) ?></span></a>
|
||||
<nav class="nav" aria-label="<?= h(t('public_site.navigation')) ?>">
|
||||
<a<?= $navClass($activePage === 'home') ?> href="<?= h($siteUrl()) ?>">
|
||||
<?= h(t('public_site.home')) ?></a><?php foreach ($publicSpecies as $species): ?>
|
||||
<a<?= $navClass($activePage === 'animals' && $activeSpecies === $species['code']) ?>
|
||||
href="<?= h($siteUrl('animals', ['espece' => $species['code']])) ?>">
|
||||
<?= h(($species['icon'] ?: '🐾') . ' ' . $species['name']) ?></a><?php endforeach; ?><?php if (($settings['public_site_about_enabled'] ?? '0') === '1'): ?>
|
||||
<a<?= $navClass($activePage === 'about') ?> href="<?= h($siteUrl('about')) ?>">
|
||||
<?= h($settings['public_site_about_title']) ?></a><?php endif; ?>
|
||||
<a<?= $navClass($activePage === 'donation') ?> href="<?= h($siteUrl('donation')) ?>">
|
||||
<?= h(t('public_site.donation')) ?></a>
|
||||
<a<?= $navClass($activePage === 'contact') ?> href="<?= h($siteUrl('contact')) ?>">
|
||||
<?= h(t('public_site.contact')) ?></a>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
<main><?php require $viewFile; ?></main>
|
||||
<footer>
|
||||
<div class="wrap footer-row">
|
||||
<div class="footer-identity">
|
||||
<strong><?= h($association) ?></strong><?php if ($fullAddress !== ''): ?><br><?= h($fullAddress) ?><?php endif; ?>
|
||||
<div class="footer-meta">
|
||||
<?php if (($settings['association_rna'] ?? '') !== ''): ?><span><?= h(t('public_site.rna')) ?> :
|
||||
<?= h($settings['association_rna']) ?></span><?php endif; ?><?php if (($settings['association_rna'] ?? '') !== '' && ($settings['association_siret'] ?? '') !== ''): ?>
|
||||
· <?php endif; ?><?php if (($settings['association_siret'] ?? '') !== ''): ?><span><?= h(t('public_site.siret')) ?>
|
||||
: <?= h($settings['association_siret']) ?></span><?php endif; ?></div>
|
||||
</div>
|
||||
<div class="footer-powered"><?= h(t('public_site.powered_by')) ?> <a
|
||||
href="https://nnsprod.com/git/oversu/Globinours" target="_blank"
|
||||
rel="noopener noreferrer">Globinours</a></div>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
11
app/Views/public_site_text.php
Normal file
11
app/Views/public_site_text.php
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<section class="section">
|
||||
<div class="wrap">
|
||||
<article class="content-card">
|
||||
<h1 class="page-title"><?= h($title) ?></h1>
|
||||
<div><?= nl2br(h($content)) ?></div>
|
||||
<?php if ($kind === 'donation' && !empty($settings['public_site_donation_url'])): ?><p
|
||||
style="margin-top:28px"><a class="button" href="<?= h($settings['public_site_donation_url']) ?>"
|
||||
rel="noopener noreferrer"><?= h(t('public_site.make_donation')) ?></a></p><?php endif; ?>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
8
app/Views/public_site_unavailable.php
Normal file
8
app/Views/public_site_unavailable.php
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
<section class="section">
|
||||
<div class="wrap">
|
||||
<div class="content-card empty">
|
||||
<h1><?= h($title) ?></h1>
|
||||
<p><?= h(t('public_site.unavailable_help')) ?></p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
27
app/Views/search.php
Normal file
27
app/Views/search.php
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
<form class="card mb-4" method="get" action="/search">
|
||||
<div class="card-body">
|
||||
<div class="input-group input-group-lg"><span class="input-group-text">⌕</span><input class="form-control"
|
||||
name="q" value="<?= h($searchQuery) ?>" minlength="2" maxlength="120" autofocus
|
||||
placeholder="<?= h(t('search.placeholder')) ?>"><button
|
||||
class="btn btn-primary"><?= h(t('search.action')) ?></button></div>
|
||||
</div>
|
||||
</form>
|
||||
<?php if (mb_strlen($searchQuery) < 2): ?><div class="empty">
|
||||
<div class="empty-description"><?= h(t('search.hint')) ?></div>
|
||||
</div><?php elseif (!$searchResults): ?><div class="empty">
|
||||
<div class="empty-description"><?= h(t('search.none')) ?></div>
|
||||
</div><?php else: ?>
|
||||
<div class="row row-cards"><?php foreach ($searchResults as $type => $items): ?><div class="col-12 col-lg-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title"><?= h(t('search.group.' . $type)) ?></h3><span
|
||||
class="badge bg-blue-lt ms-auto"><?= count($items) ?></span>
|
||||
</div>
|
||||
<div class="list-group list-group-flush"><?php foreach ($items as $item): ?><a
|
||||
class="list-group-item list-group-item-action"
|
||||
href="<?= h($item['url']) ?>"><strong><?= h($item['title']) ?></strong><?php if ($item['detail']): ?>
|
||||
<div class="text-muted small"><?= h($item['detail']) ?></div><?php endif; ?></a><?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div><?php endforeach; ?></div>
|
||||
<?php endif; ?>
|
||||
99
app/Views/settings_asm3.php
Normal file
99
app/Views/settings_asm3.php
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
<?php require __DIR__ . '/settings_nav.php'; ?>
|
||||
<?php if ($error): ?><div class="alert alert-danger"><?= h($error) ?></div><?php endif; ?>
|
||||
<?php if ($success === 'analyzed'): ?><div class="alert alert-success"><?= h(t('asm3.analyzed_success')) ?></div>
|
||||
<?php elseif ($success === 'imported'): ?><div class="alert alert-success"><?= h(t('asm3.imported_success')) ?></div>
|
||||
<?php endif; ?>
|
||||
<div class="card mb-4">
|
||||
<div class="card-status-top bg-blue"></div>
|
||||
<div class="card-header">
|
||||
<div>
|
||||
<h3 class="card-title"><?= h(t('asm3.new')) ?></h3>
|
||||
<div class="text-muted small"><?= h(t('asm3.new_help')) ?></div>
|
||||
</div>
|
||||
</div>
|
||||
<form method="post" action="/settings/asm3/upload" enctype="multipart/form-data"><input type="hidden" name="csrf"
|
||||
value="<?= h(Auth::csrf()) ?>">
|
||||
<div class="card-body row g-3 align-items-end">
|
||||
<div class="col-md"><label class="form-label"><?= h(t('asm3.dump')) ?></label><input class="form-control"
|
||||
type="file" name="dump" accept=".sql,.pgsql,text/plain,application/sql" required>
|
||||
<div class="form-hint">
|
||||
<?= h(t('asm3.upload_help', ['limit' => (string)ini_get('upload_max_filesize')])) ?></div>
|
||||
<?php if ((int)ini_get('upload_max_filesize') < 250): ?><div
|
||||
class="alert alert-warning py-2 mt-2 mb-0 small"> <?= h(t('asm3.php_limit')) ?><br><code>php -d
|
||||
upload_max_filesize=250M -d post_max_size=260M -d max_execution_time=600 -S 127.0.0.1:8000 -t
|
||||
public</code></div><?php endif; ?>
|
||||
</div>
|
||||
<div class="col-md-auto"><button class="btn btn-primary"><?= h(t('asm3.analyze')) ?></button></div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="vstack gap-3"><?php foreach ($jobs as $job):$r = json_decode($job['analysis_json'], true) ?: [];
|
||||
$result = json_decode((string)$job['result_json'], true) ?: []; ?><div class="card">
|
||||
<div class="card-header">
|
||||
<div class="me-auto">
|
||||
<h3 class="card-title"><?= h($job['original_name']) ?></h3>
|
||||
<div class="text-muted small">
|
||||
<?= h(t('asm3.analyzed_at', ['date' => date('d/m/Y H:i', strtotime($job['created_at']))])) ?><?= $job['actor'] ? ' ' . h(t('asm3.by', ['name' => $job['actor']])) : '' ?>
|
||||
</div>
|
||||
</div><span
|
||||
class="badge <?= $job['status'] === 'completed' ? 'bg-green-lt' : ($job['status'] === 'failed' ? 'bg-red-lt' : 'bg-blue-lt') ?>"><?= h(['analyzed' => t('asm3.status.analyzed'),'completed' => t('asm3.status.completed'),'failed' => t('asm3.status.failed')][$job['status']] ?? $job['status']) ?></span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-2 mb-3">
|
||||
<?php foreach ([[t('asm3.animals'),$r['animals']['create'] ?? 0,$r['animals']['review'] ?? 0],[t('asm3.contacts'),$r['contacts']['create'] ?? 0,$r['contacts']['review'] ?? 0],[t('asm3.movements'),$r['movements']['events'] ?? 0,$r['movements']['unsupported_movements'] ?? 0],[t('asm3.vaccinations'),$r['clinical']['vaccinations'] ?? 0,0],[t('asm3.medical_notes'),$r['clinical']['medical_notes'] ?? 0,0],[t('asm3.weights'),$r['clinical']['weights'] ?? 0,0],[t('asm3.litters'),$r['litters']['litters'] ?? 0,$r['litters']['unresolved'] ?? 0]] as [$label,$count,$warning]):?>
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="border rounded p-2 h-100">
|
||||
<div class="text-muted small"><?= h($label) ?></div>
|
||||
<div class="h2 mb-0"><?= (int)$count ?></div><?php if ($warning):?><span
|
||||
class="text-orange small"><?= h(t('asm3.to_review', ['count' => (int)$warning])) ?></span><?php endif;?>
|
||||
</div>
|
||||
</div><?php endforeach;?></div>
|
||||
<div class="alert alert-info py-2 small">
|
||||
<?= h(t('asm3.media', ['count' => (int)($r['media']['metadata'] ?? 0)])) ?></div>
|
||||
<?php if ($job['status'] === 'analyzed'): ?><form method="post" action="/settings/asm3/apply"
|
||||
class="border-top pt-3" id="asm3Form<?= (int)$job['id'] ?>"><input type="hidden" name="csrf"
|
||||
value="<?= h(Auth::csrf()) ?>"><input type="hidden" name="id" value="<?= (int)$job['id'] ?>">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6"><label class="form-label"><?= h(t('asm3.mode')) ?></label><label
|
||||
class="form-selectgroup-item d-block mb-2"><input type="radio" name="mode" value="merge"
|
||||
class="form-selectgroup-input" checked><span
|
||||
class="form-selectgroup-label"><strong><?= h(t('asm3.merge')) ?></strong><br><span
|
||||
class="text-muted small"><?= h(t('asm3.merge_help')) ?></span></span></label><label
|
||||
class="form-selectgroup-item d-block"><input type="radio" name="mode" value="replace"
|
||||
class="form-selectgroup-input"><span
|
||||
class="form-selectgroup-label border-danger"><strong
|
||||
class="text-danger"><?= h(t('asm3.replace')) ?></strong><br><span
|
||||
class="text-muted small"><?= h(t('asm3.replace_help')) ?></span></span></label>
|
||||
</div>
|
||||
<div class="col-md-6"><label class="form-label"><?= h(t('asm3.confirmation')) ?></label><input
|
||||
class="form-control" name="confirmation" placeholder="<?= h(t('asm3.type_import')) ?>"
|
||||
autocomplete="off" required>
|
||||
<div class="form-hint confirmation-hint"><?= h(t('asm3.expected')) ?>
|
||||
<strong><?= h(t('asm3.confirm_import_phrase')) ?></strong></div>
|
||||
<div class="alert alert-warning py-2 mt-3 small"><?= h(t('asm3.transaction_help')) ?></div>
|
||||
<button class="btn btn-danger w-100"><?= h(t('asm3.backup_import')) ?></button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<script>
|
||||
document.querySelectorAll('#asm3Form<?= (int)$job['
|
||||
id '] ?> input[name="mode"]').forEach(el => el.addEventListener('change', () => {
|
||||
const replace = el.form.mode.value === 'replace';
|
||||
el.form.confirmation.placeholder = replace ? < ? = json_encode(t('asm3.type_replace'),
|
||||
JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ? > : < ? = json_encode(t(
|
||||
'asm3.type_import'), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ? > ;
|
||||
el.form.querySelector('.confirmation-hint strong').textContent = replace ? < ? =
|
||||
json_encode(t('asm3.confirm_replace_phrase'), JSON_UNESCAPED_UNICODE |
|
||||
JSON_UNESCAPED_SLASHES) ? > : < ? = json_encode(t('asm3.confirm_import_phrase'),
|
||||
JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ? > ;
|
||||
}));
|
||||
|
||||
</script><?php elseif ($result):?><details>
|
||||
<summary><?= h(t('asm3.technical_report')) ?></summary>
|
||||
<pre
|
||||
class="mt-2 p-2 bg-muted-lt rounded small"><?= h(json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)) ?></pre>
|
||||
</details><?php endif;?>
|
||||
</div>
|
||||
</div><?php endforeach;?><?php if (!$jobs):?><div class="card">
|
||||
<div class="card-body text-muted"><?= h(t('asm3.none')) ?></div>
|
||||
</div><?php endif;?></div>
|
||||
29
app/Views/settings_audit.php
Normal file
29
app/Views/settings_audit.php
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
<?php require __DIR__ . '/settings_nav.php'; ?>
|
||||
<div class="mb-3">
|
||||
<h2 class="mb-1"><?= h(t('audit.heading')) ?></h2>
|
||||
<div class="text-muted"><?= h(t('audit.intro')) ?></div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-vcenter card-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><?= h(t('common.date')) ?></th>
|
||||
<th><?= h(t('audit.user')) ?></th>
|
||||
<th><?= h(t('audit.action')) ?></th>
|
||||
<th><?= h(t('audit.item')) ?></th>
|
||||
<th><?= h(t('audit.path')) ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody><?php foreach ($rows as $row): ?><tr>
|
||||
<td class="text-nowrap"><?= h($row['created_at']) ?></td>
|
||||
<td><strong><?= h($row['actor'] ?: t('audit.deleted_account')) ?></strong></td>
|
||||
<td><?= h($row['summary'] ?: $row['action']) ?></td>
|
||||
<td><?= h(trim((string)$row['entity_type'] . ' ' . (string)$row['entity_id'])) ?></td>
|
||||
<td class="small text-muted"><?= h($row['method'] . ' ' . $row['path']) ?></td>
|
||||
</tr><?php endforeach; ?><?php if (!$rows): ?><tr>
|
||||
<td colspan="5" class="text-center text-muted py-4"><?= h(t('audit.none')) ?></td>
|
||||
</tr><?php endif; ?></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
141
app/Views/settings_backups.php
Normal file
141
app/Views/settings_backups.php
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
<?php
|
||||
$configuredRetention = AppSettings::int('backup_retention', 1, 50);
|
||||
$formatSize = static function (int $bytes): string {
|
||||
$decimal = I18n::locale() === 'fr' ? ',' : '.';
|
||||
if ($bytes >= 1_048_576) {
|
||||
return number_format($bytes / 1_048_576, 1, $decimal, ' ') . ' ' . t('unit.mb');
|
||||
}
|
||||
return number_format($bytes / 1024, 0, $decimal, ' ') . ' ' . t('unit.kb');
|
||||
};
|
||||
?>
|
||||
<?php require __DIR__ . '/settings_nav.php'; ?>
|
||||
<div class="mb-3">
|
||||
<h2 class="mb-1"><?= h(t('backups.heading')) ?></h2>
|
||||
<div class="text-muted"><?= h(t('backups.intro')) ?></div>
|
||||
</div>
|
||||
|
||||
<?php if ($status === 'created'): ?><div class="alert alert-success">✅ <?= h(t('backups.created')) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if ($status === 'deleted'): ?><div class="alert alert-success">✅ <?= h(t('backups.deleted')) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if ($status === 'verified'): ?><div class="alert alert-success">✅ <?= h(t('backups.verified')) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if ($status === 'schedule'): ?><div class="alert alert-success">✅ <?= h(t('backups.schedule_saved')) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if ($error !== ''): ?><div class="alert alert-danger"><strong><?= h(t('backups.failed')) ?></strong>
|
||||
<?= h($error) ?></div><?php endif; ?>
|
||||
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-lg-7">
|
||||
<div class="card h-100">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title"><?= h(t('backups.create')) ?></h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p><?= h(t('backups.create_help')) ?></p>
|
||||
<div class="text-muted small"><?= h(t('backups.manifest_help', ['count' => $configuredRetention])) ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer text-end">
|
||||
<form method="post" action="/settings/backups/create"><button class="btn btn-primary">💾
|
||||
<?= h(t('backups.create_now')) ?></button></form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-5">
|
||||
<div class="card border-danger h-100 overflow-hidden">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title text-danger"><?= h(t('backups.restore')) ?></h3>
|
||||
</div>
|
||||
<form method="post" action="/settings/backups/restore" enctype="multipart/form-data"
|
||||
onsubmit="return confirm(<?= h(json_encode(t('backups.restore_confirm'), JSON_HEX_APOS | JSON_HEX_QUOT)) ?>);">
|
||||
<div class="card-body">
|
||||
<label class="form-label"><?= h(t('backups.archive')) ?></label>
|
||||
<input class="form-control" type="file" name="backup" accept=".zip,application/zip" required>
|
||||
<label class="form-check mt-3"><input class="form-check-input" type="checkbox" required><span
|
||||
class="form-check-label"><?= h(t('backups.understand')) ?></span></label>
|
||||
</div>
|
||||
<div class="card-footer text-end"><button class="btn btn-danger">♻
|
||||
<?= h(t('backups.restore_archive')) ?></button></div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">⏱ <?= h(t('backups.automatic')) ?></h3>
|
||||
</div>
|
||||
<form method="post" action="/settings/backups/schedule"><input type="hidden" name="csrf"
|
||||
value="<?= h(Auth::csrf()) ?>">
|
||||
<div class="card-body row g-3 align-items-end">
|
||||
<div class="col-md"><label class="form-check"><input class="form-check-input" type="checkbox"
|
||||
name="backup_schedule_enabled"
|
||||
<?= AppSettings::get('backup_schedule_enabled') === '1' ? 'checked' : '' ?>><span
|
||||
class="form-check-label"><?= h(t('backups.enable_schedule')) ?></span></label>
|
||||
<div class="form-hint"><?= h(t('backups.schedule_help')) ?></div>
|
||||
</div>
|
||||
<div class="col-md-3"><label class="form-label"><?= h(t('notification.frequency')) ?></label><select
|
||||
class="form-select" name="backup_schedule_frequency">
|
||||
<option value="daily"
|
||||
<?= AppSettings::get('backup_schedule_frequency') === 'daily' ? 'selected' : '' ?>>
|
||||
<?= h(t('notification.daily')) ?></option>
|
||||
<option value="weekly"
|
||||
<?= AppSettings::get('backup_schedule_frequency') === 'weekly' ? 'selected' : '' ?>>
|
||||
<?= h(t('notification.weekly')) ?></option>
|
||||
</select></div>
|
||||
<div class="col-auto"><button class="btn btn-primary"><?= h(t('common.save')) ?></button></div>
|
||||
<div class="col-12 small text-muted"><code>php <?= h(dirname(__DIR__, 2) . '/scripts/maintenance.php') ?>
|
||||
run</code> · <?= h(t('backups.last_verified')) ?> :
|
||||
<?= h(AppSettings::get('backup_last_verified_at') ?: '—') ?></div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title"><?= h(t('backups.available')) ?></h3>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-vcenter card-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><?= h(t('common.date')) ?></th>
|
||||
<th><?= h(t('backups.archive_column')) ?></th>
|
||||
<th><?= h(t('backups.size')) ?></th>
|
||||
<th class="w-1"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($backups as $backup): ?>
|
||||
<tr>
|
||||
<td class="text-nowrap"><?= h($backup['date']) ?></td>
|
||||
<td><strong><?= h($backup['name']) ?></strong><?php if (str_contains($backup['name'], 'avant-restauration')): ?>
|
||||
<span class="badge bg-orange-lt"><?= h(t('backups.emergency')) ?></span><?php endif; ?></td>
|
||||
<td><?= h($formatSize((int)$backup['size'])) ?></td>
|
||||
<td>
|
||||
<div class="d-flex gap-1">
|
||||
<form method="post" action="/settings/backups/verify"><input type="hidden" name="csrf"
|
||||
value="<?= h(Auth::csrf()) ?>"><input type="hidden" name="file"
|
||||
value="<?= h($backup['name']) ?>"><button class="btn btn-outline-success btn-sm">✓
|
||||
<?= h(t('backups.verify')) ?></button></form><a
|
||||
class="btn btn-outline-primary btn-sm text-nowrap"
|
||||
href="/settings/backups/download?file=<?= rawurlencode($backup['name']) ?>">⬇
|
||||
<?= h(t('backups.download')) ?></a>
|
||||
<form method="post" action="/settings/backups/delete"
|
||||
onsubmit="return confirm(<?= h(json_encode(t('backups.delete_confirm'), JSON_HEX_APOS | JSON_HEX_QUOT)) ?>);">
|
||||
<input type="hidden" name="csrf" value="<?= h(Auth::csrf()) ?>"><input type="hidden"
|
||||
name="file" value="<?= h($backup['name']) ?>"><button
|
||||
class="btn btn-outline-danger btn-sm" title="<?= h(t('common.delete')) ?>"
|
||||
aria-label="<?= h(t('common.delete')) ?>">🗑</button></form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php if (!$backups): ?><tr>
|
||||
<td colspan="4" class="text-center text-muted py-4"><?= h(t('backups.none')) ?></td>
|
||||
</tr><?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
68
app/Views/settings_data.php
Normal file
68
app/Views/settings_data.php
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
<?php require __DIR__ . '/settings_nav.php'; ?>
|
||||
<div class="mb-3">
|
||||
<h2 class="mb-1"><?= h(t('data.heading')) ?></h2>
|
||||
<div class="text-muted"><?= h(t('data.intro')) ?></div>
|
||||
</div>
|
||||
<?php if ($status === 'reset'): ?><div class="alert alert-success">
|
||||
<?= h(t('data.reset_success', ['backup' => $backup])) ?></div><?php elseif ($status === 'demo'): ?><div
|
||||
class="alert alert-success"><?= h(t('data.demo_success', ['backup' => $backup])) ?></div><?php endif; ?>
|
||||
<?php if ($error !== ''): ?><div class="alert alert-danger">⚠️ <?= h($error) ?></div><?php endif; ?>
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-6">
|
||||
<div class="card card-sm">
|
||||
<div class="card-body">
|
||||
<div class="text-muted"><?= h(t('data.animals')) ?></div>
|
||||
<div class="h1 mb-0"><?= (int)$counts['animals'] ?></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="card card-sm">
|
||||
<div class="card-body">
|
||||
<div class="text-muted"><?= h(t('data.contacts')) ?></div>
|
||||
<div class="h1 mb-0"><?= (int)$counts['contacts'] ?></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="alert alert-warning"><strong><?= h(t('data.destructive')) ?></strong> <?= h(t('data.destructive_help')) ?>
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
<div class="col-lg-6">
|
||||
<form class="card h-100 border-danger" method="post" action="/settings/data/reset"
|
||||
onsubmit="return confirm(<?= h(json_encode(t('data.reset_confirm'), JSON_HEX_APOS | JSON_HEX_QUOT)) ?>);">
|
||||
<input type="hidden" name="csrf" value="<?= h(Auth::csrf()) ?>">
|
||||
<div class="card-status-top bg-danger"></div>
|
||||
<div class="card-header">
|
||||
<h3 class="card-title"><?= h(t('data.reset')) ?></h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p><?= h(t('data.reset_help')) ?></p><label
|
||||
class="form-label"><?= h(t('data.confirmation')) ?></label><input class="form-control"
|
||||
name="confirmation" required autocomplete="off" placeholder="<?= h(t('data.type_reset')) ?>">
|
||||
<div class="form-hint"><?= h(t('data.exact_phrase')) ?>
|
||||
<strong><?= h(t('data.confirm_reset_phrase')) ?></strong></div>
|
||||
</div>
|
||||
<div class="card-footer"><button class="btn btn-danger w-100"><?= h(t('data.backup_reset')) ?></button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<form class="card h-100 border-purple" method="post" action="/settings/data/demo"
|
||||
onsubmit="return confirm(<?= h(json_encode(t('data.demo_confirm'), JSON_HEX_APOS | JSON_HEX_QUOT)) ?>);">
|
||||
<input type="hidden" name="csrf" value="<?= h(Auth::csrf()) ?>">
|
||||
<div class="card-status-top bg-purple"></div>
|
||||
<div class="card-header">
|
||||
<h3 class="card-title"><?= h(t('data.demo')) ?></h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p><?= h(t('data.demo_help')) ?></p><label
|
||||
class="form-label"><?= h(t('data.confirmation')) ?></label><input class="form-control"
|
||||
name="confirmation" required autocomplete="off" placeholder="<?= h(t('data.type_demo')) ?>">
|
||||
<div class="form-hint"><?= h(t('data.exact_phrase')) ?>
|
||||
<strong><?= h(t('data.confirm_demo_phrase')) ?></strong></div>
|
||||
</div>
|
||||
<div class="card-footer"><button class="btn btn-purple w-100"><?= h(t('data.backup_demo')) ?></button></div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
88
app/Views/settings_general.php
Normal file
88
app/Views/settings_general.php
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
<?php require __DIR__ . '/settings_nav.php'; ?>
|
||||
<div class="mb-3">
|
||||
<h2 class="mb-1"><?= h(t('settings.general_title')) ?></h2>
|
||||
<div class="text-muted"><?= h(t('settings.general_intro')) ?></div>
|
||||
</div>
|
||||
<?php if ($saved): ?><div class="alert alert-success">✅ <?= h(t('settings.saved')) ?></div><?php endif; ?>
|
||||
<?php if ($error !== ''): ?><div class="alert alert-danger">⚠️ <?= h($error) ?></div><?php endif; ?>
|
||||
<form method="post" action="/settings/general/save" enctype="multipart/form-data">
|
||||
<div class="card mb-3 overflow-hidden">
|
||||
<div class="card-status-top bg-primary"></div>
|
||||
<div class="card-header">
|
||||
<h3 class="card-title"><?= h(t('settings.association')) ?></h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6"><label
|
||||
class="form-label required"><?= h(t('settings.association_name')) ?></label><input
|
||||
class="form-control" name="association_name" value="<?= h($settings['association_name']) ?>"
|
||||
required></div>
|
||||
<div class="col-md-3"><label class="form-label"><?= h(t('settings.rna')) ?></label><input
|
||||
class="form-control" name="association_rna" value="<?= h($settings['association_rna']) ?>"
|
||||
placeholder="W…"></div>
|
||||
<div class="col-md-3"><label class="form-label"><?= h(t('settings.siret')) ?></label><input
|
||||
class="form-control" name="association_siret" value="<?= h($settings['association_siret']) ?>">
|
||||
</div>
|
||||
<div class="col-12"><label class="form-label"><?= h(t('settings.address')) ?></label><input
|
||||
class="form-control" name="association_address"
|
||||
value="<?= h($settings['association_address']) ?>"></div>
|
||||
<div class="col-md-3"><label class="form-label"><?= h(t('settings.postcode')) ?></label><input
|
||||
class="form-control" name="association_postal_code"
|
||||
value="<?= h($settings['association_postal_code']) ?>"></div>
|
||||
<div class="col-md-3"><label class="form-label"><?= h(t('settings.city')) ?></label><input
|
||||
class="form-control" name="association_city" value="<?= h($settings['association_city']) ?>">
|
||||
</div>
|
||||
<div class="col-md-3"><label class="form-label"><?= h(t('settings.phone')) ?></label><input
|
||||
class="form-control" name="association_phone" value="<?= h($settings['association_phone']) ?>">
|
||||
</div>
|
||||
<div class="col-md-3"><label class="form-label"><?= h(t('settings.email')) ?></label><input
|
||||
class="form-control" type="email" name="association_email"
|
||||
value="<?= h($settings['association_email']) ?>"></div>
|
||||
<div class="col-12">
|
||||
<hr class="my-1">
|
||||
</div>
|
||||
<div class="col-md-8"><label class="form-label"><?= h(t('settings.association_logo')) ?></label><input
|
||||
class="form-control" type="file" name="association_logo"
|
||||
accept="image/png,image/jpeg,image/webp">
|
||||
<div class="form-hint"><?= h(t('settings.logo_help')) ?></div><?php if ($associationLogo): ?><label
|
||||
class="form-check mt-2"><input class="form-check-input" type="checkbox"
|
||||
name="delete_association_logo" value="1"><span
|
||||
class="form-check-label text-danger"><?= h(t('settings.delete_logo')) ?></span></label><?php endif; ?>
|
||||
</div>
|
||||
<div class="col-md-4 d-flex align-items-center justify-content-md-end"><?php if ($associationLogo): ?>
|
||||
<div class="border rounded bg-white p-2 text-center"><img
|
||||
src="/settings/association-logo?v=<?= (int)filemtime($associationLogo) ?>"
|
||||
alt="<?= h(t('settings.logo_alt', ['name' => $settings['association_name']])) ?>"
|
||||
style="display:block;max-width:180px;max-height:90px;object-fit:contain">
|
||||
<div class="small text-muted mt-1"><?= h(t('settings.current_preview')) ?></div>
|
||||
</div><?php else: ?><div class="text-muted small"><?= h(t('settings.no_logo')) ?></div>
|
||||
<?php endif; ?></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card overflow-hidden">
|
||||
<div class="card-status-top bg-orange"></div>
|
||||
<div class="card-header">
|
||||
<h3 class="card-title"><?= h(t('settings.operation')) ?></h3>
|
||||
</div>
|
||||
<div class="card-body row g-3">
|
||||
<div class="col-md-4"><label class="form-label"><?= h(t('settings.language')) ?></label><select
|
||||
class="form-select" name="app_language"><?php foreach (I18n::LOCALES as $code => $label): ?><option
|
||||
value="<?= h($code) ?>" <?= ($settings['app_language'] ?? 'fr') === $code ? 'selected' : '' ?>>
|
||||
<?= h($label) ?></option><?php endforeach; ?></select>
|
||||
<div class="form-hint"><?= h(t('settings.language_help')) ?></div>
|
||||
</div>
|
||||
<div class="col-md-4"><label class="form-label"><?= h(t('settings.quarantine_duration')) ?></label>
|
||||
<div class="input-group"><input class="form-control" type="number" min="1" max="90"
|
||||
name="quarantine_days" value="<?= (int)$settings['quarantine_days'] ?>"><span
|
||||
class="input-group-text"><?= h(t('settings.days')) ?></span></div>
|
||||
</div>
|
||||
<div class="col-md-4"><label class="form-label"><?= h(t('settings.backups_kept')) ?></label>
|
||||
<div class="input-group"><input class="form-control" type="number" min="1" max="50"
|
||||
name="backup_retention" value="<?= (int)$settings['backup_retention'] ?>"><span
|
||||
class="input-group-text">ZIP</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-end mt-3"><button class="btn btn-primary"><?= h(t('settings.save')) ?></button></div>
|
||||
</form>
|
||||
56
app/Views/settings_health.php
Normal file
56
app/Views/settings_health.php
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
<?php require __DIR__ . '/settings_nav.php';
|
||||
$groups = ['environment' => t('health.group_environment'),'database' => t('health.group_database'),'storage' => t('health.group_storage'),'continuity' => t('health.group_continuity'),'files' => t('health.group_files'),'security' => t('health.group_security')]; ?>
|
||||
<?php if ($installed): ?><div class="alert alert-success">✅ <?= h(t('install.complete')) ?></div><?php endif; ?>
|
||||
<div class="d-flex flex-wrap align-items-end justify-content-between gap-3 mb-3">
|
||||
<div>
|
||||
<h2 class="mb-1"><?= h(t('health.title')) ?></h2>
|
||||
<div class="text-muted"><?= h(t('health.intro')) ?></div>
|
||||
</div><a class="btn btn-primary" href="/settings/health">↻ <?= h(t('health.refresh')) ?></a>
|
||||
</div>
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-4">
|
||||
<div class="card border-success">
|
||||
<div class="card-body">
|
||||
<div class="subheader"><?= h(t('health.ok')) ?></div>
|
||||
<div class="h1 text-success mb-0"><?= (int)$report['summary']['ok'] ?></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card border-warning">
|
||||
<div class="card-body">
|
||||
<div class="subheader"><?= h(t('health.warnings')) ?></div>
|
||||
<div class="h1 text-warning mb-0"><?= (int)$report['summary']['warning'] ?></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card border-danger">
|
||||
<div class="card-body">
|
||||
<div class="subheader"><?= h(t('health.errors')) ?></div>
|
||||
<div class="h1 text-danger mb-0"><?= (int)$report['summary']['error'] ?></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="alert alert-info d-flex flex-wrap justify-content-between gap-2">
|
||||
<span><?= h(t('health.versions', ['php' => $report['php'],'sqlite' => $report['sqlite']])) ?></span><span><?= h(t('health.generated', ['date' => $report['generated_at']])) ?></span>
|
||||
</div>
|
||||
<?php foreach ($groups as $group => $label): $rows = array_values(array_filter($report['checks'], static fn (array$c): bool => $c['group'] === $group));
|
||||
if (!$rows) {
|
||||
continue;
|
||||
} ?><div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title"><?= h($label) ?></h3>
|
||||
</div>
|
||||
<div class="list-group list-group-flush"><?php foreach ($rows as $row): ?><div class="list-group-item">
|
||||
<div class="d-flex gap-3"><span
|
||||
class="fs-2"><?= $row['status'] === 'ok' ? '✅' : ($row['status'] === 'warning' ? '⚠️' : '❌') ?></span>
|
||||
<div>
|
||||
<div class="fw-semibold"><?= h($row['title']) ?></div>
|
||||
<div class="text-muted small"><?= h($row['detail']) ?></div>
|
||||
</div>
|
||||
</div>
|
||||
</div><?php endforeach; ?></div>
|
||||
</div><?php endforeach; ?>
|
||||
<div class="text-muted small text-end"><?= h(t('health.read_only')) ?></div>
|
||||
169
app/Views/settings_inventory.php
Normal file
169
app/Views/settings_inventory.php
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
<?php require __DIR__ . '/settings_nav.php';
|
||||
$money = static fn ($c) => $c === null ? '—' : number_format((int)$c / 100, 2, ',', ' ') . ' €'; ?>
|
||||
<div class="mb-3">
|
||||
<h2 class="mb-1">💊 <?= h(t('inventory.title')) ?></h2>
|
||||
<div class="text-muted"><?= h(t('inventory.intro')) ?></div>
|
||||
</div>
|
||||
<?php if ($status): ?><div class="alert alert-success">✅ <?= h(t('inventory.saved')) ?></div>
|
||||
<?php endif; ?><?php if ($error): ?><div class="alert alert-danger"><?= h($error) ?></div><?php endif; ?>
|
||||
<?php if ($alerts['low'] || $alerts['expired']): ?><div class="alert alert-warning">
|
||||
<strong><?= h(t('inventory.alerts')) ?></strong> · <?= count($alerts['low']) ?> <?= h(t('inventory.low_stock')) ?> ·
|
||||
<?= count($alerts['expired']) ?> <?= h(t('inventory.expiring')) ?></div><?php endif; ?>
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-xl-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title"><?= h(t('inventory.add_product')) ?></h3>
|
||||
</div>
|
||||
<form method="post" action="/settings/inventory/product"><input type="hidden" name="csrf"
|
||||
value="<?= h(Auth::csrf()) ?>">
|
||||
<div class="card-body row g-2">
|
||||
<div class="col-12"><label class="form-label"><?= h(t('inventory.category')) ?></label><select
|
||||
class="form-select" name="category" required>
|
||||
<option value="medication"><?= h(t('inventory.medications')) ?></option>
|
||||
<option value="dewormer"><?= h(t('inventory.dewormers')) ?></option>
|
||||
<option value="vaccine"><?= h(t('inventory.vaccines')) ?></option>
|
||||
</select></div>
|
||||
<div class="col-12"><label class="form-label"><?= h(t('common.name')) ?></label><input
|
||||
class="form-control" name="name" required></div>
|
||||
<div class="col-6"><label class="form-label"><?= h(t('inventory.unit')) ?></label><input
|
||||
class="form-control" name="unit" value="unité" required></div>
|
||||
<div class="col-6"><label class="form-label"><?= h(t('inventory.minimum')) ?></label><input
|
||||
class="form-control" type="number" step="0.01" min="0" name="minimum_quantity" value="0">
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer text-end"><button class="btn btn-primary"><?= h(t('common.add')) ?></button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xl-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title"><?= h(t('inventory.receive')) ?></h3>
|
||||
</div>
|
||||
<form method="post" action="/settings/inventory/receive"><input type="hidden" name="csrf"
|
||||
value="<?= h(Auth::csrf()) ?>">
|
||||
<div class="card-body row g-2">
|
||||
<div class="col-12"><label class="form-label"><?= h(t('inventory.product')) ?></label><select
|
||||
class="form-select" name="product_id" required>
|
||||
<option value="">—</option><?php foreach ($products as $p): ?><option
|
||||
value="<?= (int)$p['id'] ?>"><?= h($p['name'] . ' · ' . $p['category']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select></div>
|
||||
<div class="col-6"><label class="form-label"><?= h(t('inventory.quantity')) ?></label><input
|
||||
class="form-control" type="number" min="0.01" step="0.01" name="quantity" required></div>
|
||||
<div class="col-6"><label class="form-label"><?= h(t('inventory.unit_cost')) ?></label><input
|
||||
class="form-control" type="number" min="0" step="0.01" name="unit_cost"></div>
|
||||
<div class="col-6"><label class="form-label"><?= h(t('animal.batch')) ?></label><input
|
||||
class="form-control" name="batch_number"></div>
|
||||
<div class="col-6"><label class="form-label"><?= h(t('inventory.expiry')) ?></label><input
|
||||
class="form-control js-date" type="text" name="expires_on" placeholder="jj/mm/aaaa"
|
||||
autocomplete="off"></div>
|
||||
<div class="col-12"><label class="form-label"><?= h(t('inventory.supplier')) ?></label><select
|
||||
class="form-select" name="supplier_contact_id">
|
||||
<option value="">—</option><?php foreach ($suppliers as $s): ?><option
|
||||
value="<?= (int)$s['id'] ?>"><?= h($s['name']) ?></option><?php endforeach; ?>
|
||||
</select></div>
|
||||
</div>
|
||||
<div class="card-footer text-end"><button
|
||||
class="btn btn-primary"><?= h(t('inventory.receive')) ?></button></div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xl-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title"><?= h(t('inventory.remove_stock')) ?></h3>
|
||||
</div>
|
||||
<form method="post" action="/settings/inventory/move"><input type="hidden" name="csrf"
|
||||
value="<?= h(Auth::csrf()) ?>">
|
||||
<div class="card-body row g-2">
|
||||
<div class="col-12"><label class="form-label"><?= h(t('inventory.batch')) ?></label><select
|
||||
class="form-select" name="batch_id" required>
|
||||
<option value="">—</option><?php foreach ($batches as $b) {
|
||||
if ((float)$b['quantity'] > 0): ?><option value="<?= (int)$b['id'] ?>">
|
||||
<?= h($b['product_name'] . ' · ' . ($b['batch_number'] ?: t('inventory.no_batch')) . ' · ' . $b['quantity'] . ' ' . $b['unit']) ?>
|
||||
</option><?php endif;
|
||||
} ?>
|
||||
</select></div>
|
||||
<div class="col-6"><label class="form-label"><?= h(t('inventory.movement')) ?></label><select
|
||||
class="form-select" name="movement_type">
|
||||
<option value="administration"><?= h(t('inventory.administration')) ?></option>
|
||||
<option value="loss"><?= h(t('inventory.loss')) ?></option>
|
||||
</select></div>
|
||||
<div class="col-6"><label class="form-label"><?= h(t('inventory.quantity')) ?></label><input
|
||||
class="form-control" type="number" min="0.01" step="0.01" name="quantity" required></div>
|
||||
<div class="col-12"><label class="form-label"><?= h(t('animal.animal')) ?></label><select
|
||||
class="form-select" name="animal_id">
|
||||
<option value="">—</option><?php foreach ($animals as $a): ?><option
|
||||
value="<?= (int)$a['id'] ?>"><?= h($a['name']) ?></option><?php endforeach; ?>
|
||||
</select></div>
|
||||
<div class="col-12"><label class="form-label"><?= h(t('privacy.reason')) ?></label><input
|
||||
class="form-control" name="reason"></div>
|
||||
</div>
|
||||
<div class="card-footer text-end"><button
|
||||
class="btn btn-primary"><?= h(t('inventory.record')) ?></button></div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title"><?= h(t('inventory.stock')) ?></h3>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-vcenter card-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><?= h(t('inventory.category')) ?></th>
|
||||
<th><?= h(t('inventory.product')) ?></th>
|
||||
<th><?= h(t('inventory.stock')) ?></th>
|
||||
<th><?= h(t('inventory.minimum')) ?></th>
|
||||
<th><?= h(t('inventory.next_expiry')) ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody><?php foreach ($products as $p): $low = (float)$p['stock'] <= (float)$p['minimum_quantity']; ?><tr
|
||||
class="<?= $low ? 'table-warning' : '' ?>">
|
||||
<td><?= h(t('inventory.' . $p['category'])) ?></td>
|
||||
<td><strong><?= h($p['name']) ?></strong></td>
|
||||
<td><?= h($p['stock'] . ' ' . $p['unit']) ?></td>
|
||||
<td><?= h($p['minimum_quantity'] . ' ' . $p['unit']) ?></td>
|
||||
<td><?= h($p['next_expiry'] ? date('d/m/Y', strtotime($p['next_expiry'])) : '—') ?></td>
|
||||
</tr><?php endforeach; ?><?php if (!$products): ?><tr>
|
||||
<td colspan="5" class="text-center text-muted py-4"><?= h(t('inventory.empty')) ?></td>
|
||||
</tr><?php endif; ?></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title"><?= h(t('inventory.history')) ?></h3>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-vcenter card-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><?= h(t('common.date')) ?></th>
|
||||
<th><?= h(t('inventory.product')) ?></th>
|
||||
<th><?= h(t('inventory.movement')) ?></th>
|
||||
<th><?= h(t('inventory.quantity')) ?></th>
|
||||
<th><?= h(t('animal.animal')) ?></th>
|
||||
<th><?= h(t('audit.user')) ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody><?php foreach ($movements as $m): ?><tr>
|
||||
<td><?= h(date('d/m/Y H:i', strtotime($m['occurred_at']))) ?></td>
|
||||
<td><?= h($m['product_name']) ?><div class="small text-muted"><?= h($m['batch_number'] ?: '—') ?>
|
||||
</div>
|
||||
</td>
|
||||
<td><?= h(t('inventory.' . $m['movement_type'])) ?></td>
|
||||
<td class="<?= (float)$m['quantity_delta'] < 0 ? 'text-danger' : 'text-success' ?>">
|
||||
<?= (float)$m['quantity_delta'] > 0 ? '+' : '' ?><?= h($m['quantity_delta'] . ' ' . $m['unit']) ?>
|
||||
</td>
|
||||
<td><?= h($m['animal_name'] ?: '—') ?></td>
|
||||
<td><?= h($m['actor'] ?: '—') ?></td>
|
||||
</tr><?php endforeach; ?></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
71
app/Views/settings_lab_references.php
Normal file
71
app/Views/settings_lab_references.php
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
<?php require __DIR__ . '/settings_nav.php'; ?>
|
||||
<div class="mb-3">
|
||||
<h2 class="mb-1"><?= h(t('lab_reference.heading')) ?></h2>
|
||||
<div class="text-muted"><?= h(t('lab_reference.intro')) ?></div>
|
||||
</div>
|
||||
<?php if ($saved): ?><div class="alert alert-success">✅ <?= h(t('lab_reference.saved')) ?></div>
|
||||
<?php endif; ?><?php if ($error !== ''): ?><div class="alert alert-danger">⚠️ <?= h($error) ?></div><?php endif; ?>
|
||||
<div class="alert alert-info"><strong><?= h(t('lab_reference.priority_title')) ?></strong>
|
||||
<?= h(t('lab_reference.priority_help')) ?></div>
|
||||
<form method="post" action="/settings/lab-references/save"><input type="hidden" name="csrf"
|
||||
value="<?= h(Auth::csrf()) ?>">
|
||||
<?php foreach ($groups as $analyzer => $speciesGroups): ?><div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<div>
|
||||
<h3 class="card-title mb-0">🩸 <?= h($analyzer) ?></h3>
|
||||
<div class="text-muted small"><?= h(t('lab_reference.manufacturer_ranges')) ?></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<ul class="nav nav-tabs px-3 pt-2" role="tablist">
|
||||
<?php foreach (array_keys($speciesGroups) as $i => $species): ?><li class="nav-item"><button
|
||||
class="nav-link <?= !$i ? 'active' : '' ?>" data-bs-toggle="tab"
|
||||
data-bs-target="#labref-<?= h(md5($analyzer . $species)) ?>"
|
||||
type="button"><?= h(SpeciesService::label($species)) ?></button></li><?php endforeach; ?></ul>
|
||||
<div class="tab-content">
|
||||
<?php foreach ($speciesGroups as $iSpecies => $rows): $species = $rows[0]['species_code']; ?><div
|
||||
class="tab-pane <?= $species === array_key_first($speciesGroups) ? 'active show' : '' ?>"
|
||||
id="labref-<?= h(md5($analyzer . $species)) ?>">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-vcenter mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><?= h(t('lab_reference.code')) ?></th>
|
||||
<th><?= h(t('medical_document.parameter')) ?></th>
|
||||
<th><?= h(t('medical_document.unit')) ?></th>
|
||||
<th><?= h(t('medical_document.reference_min')) ?></th>
|
||||
<th><?= h(t('medical_document.reference_max')) ?></th>
|
||||
<th><?= h(t('lab_reference.source')) ?></th>
|
||||
<th><?= h(t('settings.active_f')) ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody><?php foreach ($rows as $row): ?><tr>
|
||||
<td><strong><?= h($row['parameter_code']) ?></strong></td>
|
||||
<td><input class="form-control form-control-sm"
|
||||
name="ranges[<?= (int)$row['id'] ?>][name]"
|
||||
value="<?= h($row['parameter_name']) ?>" required></td>
|
||||
<td><input class="form-control form-control-sm"
|
||||
name="ranges[<?= (int)$row['id'] ?>][unit]" value="<?= h($row['unit']) ?>"
|
||||
required></td>
|
||||
<td><input class="form-control form-control-sm" type="number" step="any"
|
||||
name="ranges[<?= (int)$row['id'] ?>][min]"
|
||||
value="<?= h($row['reference_min'] ?? '') ?>"></td>
|
||||
<td><input class="form-control form-control-sm" type="number" step="any"
|
||||
name="ranges[<?= (int)$row['id'] ?>][max]"
|
||||
value="<?= h($row['reference_max'] ?? '') ?>"></td>
|
||||
<td><input class="form-control form-control-sm"
|
||||
name="ranges[<?= (int)$row['id'] ?>][source]"
|
||||
value="<?= h($row['source_note'] ?? '') ?>"></td>
|
||||
<td><label class="form-check"><input class="form-check-input" type="checkbox"
|
||||
name="ranges[<?= (int)$row['id'] ?>][active]"
|
||||
<?= $row['active'] ? 'checked' : '' ?>><span
|
||||
class="form-check-label"></span></label></td>
|
||||
</tr><?php endforeach; ?></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div><?php endforeach; ?></div>
|
||||
</div>
|
||||
</div><?php endforeach; ?>
|
||||
<div class="d-flex justify-content-end mb-4"><button class="btn btn-primary"><?= h(t('common.save')) ?></button>
|
||||
</div>
|
||||
</form>
|
||||
35
app/Views/settings_nav.php
Normal file
35
app/Views/settings_nav.php
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
$settingsPath = parse_url($_SERVER['REQUEST_URI'] ?? '/settings/general', PHP_URL_PATH);
|
||||
$settingsSection = match(true) {
|
||||
str_starts_with($settingsPath, '/settings/users') => 'users',
|
||||
str_starts_with($settingsPath, '/settings/permissions') => 'permissions',
|
||||
str_starts_with($settingsPath, '/settings/privacy') => 'privacy',
|
||||
str_starts_with($settingsPath, '/settings/species') => 'species',
|
||||
str_starts_with($settingsPath, '/settings/rooms') => 'rooms',
|
||||
str_starts_with($settingsPath, '/settings/pricing') => 'pricing',
|
||||
str_starts_with($settingsPath, '/settings/inventory') => 'inventory',
|
||||
str_starts_with($settingsPath, '/settings/lab-references') => 'lab-references',
|
||||
str_starts_with($settingsPath, '/settings/public-site') => 'public-site',
|
||||
str_starts_with($settingsPath, '/settings/backups') => 'backups',
|
||||
str_starts_with($settingsPath, '/settings/health') => 'health',
|
||||
str_starts_with($settingsPath, '/settings/notifications') => 'notifications',
|
||||
str_starts_with($settingsPath, '/settings/audit') => 'audit',
|
||||
str_starts_with($settingsPath, '/settings/asm3') => 'asm3',
|
||||
str_starts_with($settingsPath, '/settings/data') => 'data',
|
||||
default => 'general',
|
||||
};
|
||||
require_once __DIR__ . '/_settings_links.php';
|
||||
$settingsLinks = settingsNavigationLinks();
|
||||
?>
|
||||
<nav aria-label="<?= h(t('settings.navigation')) ?>" class="card card-sm mb-3 settings-section-nav">
|
||||
<div class="card-body py-3 d-flex flex-wrap align-items-stretch gap-3">
|
||||
<?php foreach (['general','business','access','maintenance'] as $group): ?><div class="settings-nav-group">
|
||||
<div class="subheader mb-2"><?= h(settingsGroupLabel($group)) ?></div>
|
||||
<div class="d-flex flex-wrap gap-2"><?php foreach ($settingsLinks as $key => [$href,$icon,$label,$linkGroup]): if ($linkGroup !== $group) {
|
||||
continue;
|
||||
} ?><a class="btn btn-sm <?= $settingsSection === $key ? 'btn-dark' : 'btn-outline-secondary' ?>"
|
||||
href="<?= h($href) ?>" <?= $settingsSection === $key ? 'aria-current="page"' : '' ?>><?= $icon ?>
|
||||
<?= h($label) ?></a><?php endforeach; ?></div>
|
||||
</div><?php endforeach; ?>
|
||||
</div>
|
||||
</nav>
|
||||
67
app/Views/settings_notifications.php
Normal file
67
app/Views/settings_notifications.php
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
<?php require __DIR__ . '/settings_nav.php'; ?>
|
||||
<div class="mb-3">
|
||||
<h2 class="mb-1"><?= h(t('notification.title')) ?></h2>
|
||||
<div class="text-muted"><?= h(t('notification.intro')) ?></div>
|
||||
</div>
|
||||
<?php if ($saved): ?><div class="alert alert-success">✅ <?= h(t('notification.saved')) ?></div><?php endif; ?>
|
||||
<?php if ($sent === 'sent'): ?><div class="alert alert-success">✅ <?= h(t('notification.sent')) ?></div>
|
||||
<?php elseif ($sent === 'mail_failed'): ?><div class="alert alert-danger">⚠️ <?= h(t('notification.mail_failed')) ?>
|
||||
</div><?php elseif ($sent === 'recipient' || $sent === 'invalid'): ?><div class="alert alert-danger">⚠️
|
||||
<?= h(t('notification.invalid_recipient')) ?></div><?php endif; ?>
|
||||
<div class="row g-3">
|
||||
<div class="col-lg-7">
|
||||
<form method="post" action="/settings/notifications/save"><input type="hidden" name="csrf"
|
||||
value="<?= h(Auth::csrf()) ?>">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title"><?= h(t('notification.email_digest')) ?></h3>
|
||||
</div>
|
||||
<div class="card-body"><label class="form-check form-switch mb-3"><input class="form-check-input"
|
||||
type="checkbox" name="notification_email_enabled"
|
||||
<?= $settings['notification_email_enabled'] === '1' ? 'checked' : '' ?>><span
|
||||
class="form-check-label"><?= h(t('notification.enable_email')) ?></span></label>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-7"><label
|
||||
class="form-label"><?= h(t('notification.recipient')) ?></label><input
|
||||
class="form-control" type="email" name="notification_email_recipient"
|
||||
value="<?= h($settings['notification_email_recipient']) ?>"
|
||||
placeholder="<?= h($settings['association_email']) ?>">
|
||||
<div class="form-hint"><?= h(t('notification.recipient_help')) ?></div>
|
||||
</div>
|
||||
<div class="col-md-5"><label
|
||||
class="form-label"><?= h(t('notification.frequency')) ?></label><select
|
||||
class="form-select" name="notification_email_frequency">
|
||||
<option value="daily"
|
||||
<?= $settings['notification_email_frequency'] === 'daily' ? 'selected' : '' ?>>
|
||||
<?= h(t('notification.daily')) ?></option>
|
||||
<option value="weekly"
|
||||
<?= $settings['notification_email_frequency'] === 'weekly' ? 'selected' : '' ?>>
|
||||
<?= h(t('notification.weekly')) ?></option>
|
||||
</select></div>
|
||||
</div>
|
||||
<div class="text-end mt-4"><button class="btn btn-primary"><?= h(t('settings.save')) ?></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="col-lg-5">
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title"><?= h(t('notification.current_alerts')) ?></h3>
|
||||
</div>
|
||||
<div class="list-group list-group-flush"><?php $alerts = NotificationService::alerts();
|
||||
if (!$alerts): ?><div class="list-group-item text-muted"><?= h(t('notification.no_alert')) ?></div>
|
||||
<?php else: foreach ($alerts as $alert): ?><a
|
||||
class="list-group-item list-group-item-action d-flex justify-content-between"
|
||||
href="<?= h($alert['url']) ?>"><span><?= h($alert['label']) ?></span><span
|
||||
class="badge bg-<?= $alert['severity'] === 'danger' ? 'red' : 'yellow' ?>-lt"><?= (int)$alert['count'] ?></span></a><?php endforeach;endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<form method="post" action="/settings/notifications/send"><input type="hidden" name="csrf"
|
||||
value="<?= h(Auth::csrf()) ?>"><button class="btn btn-outline-primary w-100"
|
||||
type="submit"><?= h(t('notification.send_test')) ?></button></form>
|
||||
<div class="text-muted small mt-2"><?= h(t('notification.cron_help')) ?> <code>php
|
||||
scripts/send-notifications.php</code></div>
|
||||
</div>
|
||||
</div>
|
||||
88
app/Views/settings_permissions.php
Normal file
88
app/Views/settings_permissions.php
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
<?php require __DIR__ . '/settings_nav.php'; ?>
|
||||
<?php if ($saved): ?><div class="alert alert-success"><?= h(t('settings.permissions_saved')) ?></div><?php endif; ?>
|
||||
<div class="mb-3">
|
||||
<h2 class="mb-1"><?= h(t('settings.permissions')) ?></h2>
|
||||
<div class="text-muted"><?= h(t('settings.permissions_intro')) ?></div>
|
||||
</div>
|
||||
<div class="alert alert-info"><strong><?= h(t('settings.safeguard')) ?></strong> <?= h(t('settings.safeguard_help')) ?>
|
||||
</div>
|
||||
<style>
|
||||
.permissions-table .role-start {
|
||||
border-left: 2px solid var(--tblr-border-color) !important
|
||||
}
|
||||
|
||||
.permissions-table .role-shade {
|
||||
background: rgba(98, 105, 118, .035)
|
||||
}
|
||||
|
||||
.permissions-table thead .role-start {
|
||||
border-left-color: rgba(98, 105, 118, .38) !important
|
||||
}
|
||||
|
||||
.permissions-table thead th {
|
||||
vertical-align: middle
|
||||
}
|
||||
|
||||
</style>
|
||||
<form method="post" action="/settings/permissions/save"><input type="hidden" name="csrf" value="<?= h(Auth::csrf()) ?>">
|
||||
<div class="card">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-vcenter mb-0 permissions-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="min-width:250px"><?= h(t('settings.module')) ?></th><?php $roleIndex = 0;
|
||||
foreach ($roles as $role => $roleLabel): ?><th
|
||||
class="text-center role-start <?= $roleIndex++ % 2 ? 'role-shade' : '' ?>" colspan="2">
|
||||
<?= h(PermissionService::roleLabel($role)) ?></th><?php endforeach; ?>
|
||||
</tr>
|
||||
<tr>
|
||||
<th></th><?php $roleIndex = 0;
|
||||
foreach ($roles as $role => $roleLabel): $shade = $roleIndex++ % 2 ? 'role-shade' : ''; ?><th
|
||||
class="text-center small text-muted role-start <?= $shade ?>"><?= h(t('settings.view')) ?>
|
||||
</th>
|
||||
<th class="text-center small text-muted <?= $shade ?>"><?= h(t('settings.edit')) ?></th>
|
||||
<?php endforeach; ?>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody><?php foreach ($modules as $module => [$label,$description]): ?><tr>
|
||||
<td>
|
||||
<div class="fw-bold"><?= h(PermissionService::moduleLabel($module)) ?></div>
|
||||
<div class="text-muted small"><?= h(PermissionService::moduleDescription($module)) ?></div>
|
||||
</td><?php $roleIndex = 0;
|
||||
foreach ($roles as $role => $roleLabel): $locked = $role === 'admin';
|
||||
$shade = $roleIndex++ % 2 ? 'role-shade' : ''; ?><td class="text-center role-start <?= $shade ?>"><input
|
||||
class="form-check-input permission-view" type="checkbox"
|
||||
name="permissions[<?= h($role) ?>][<?= h($module) ?>][view]" value="1"
|
||||
<?= $matrix[$role][$module]['view'] ? 'checked' : '' ?> <?= $locked ? 'disabled' : '' ?>
|
||||
data-role="<?= h($role) ?>" data-module="<?= h($module) ?>"
|
||||
aria-label="<?= h(t('settings.permission_view', ['module' => PermissionService::moduleLabel($module),'role' => PermissionService::roleLabel($role)])) ?>">
|
||||
</td>
|
||||
<td class="text-center <?= $shade ?>"><input class="form-check-input permission-edit"
|
||||
type="checkbox" name="permissions[<?= h($role) ?>][<?= h($module) ?>][edit]" value="1"
|
||||
<?= $matrix[$role][$module]['edit'] ? 'checked' : '' ?> <?= $locked ? 'disabled' : '' ?>
|
||||
data-role="<?= h($role) ?>" data-module="<?= h($module) ?>"
|
||||
aria-label="<?= h(t('settings.permission_edit', ['module' => PermissionService::moduleLabel($module),'role' => PermissionService::roleLabel($role)])) ?>">
|
||||
</td><?php endforeach; ?>
|
||||
</tr><?php endforeach; ?></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="card-footer d-flex justify-content-between align-items-center"><span
|
||||
class="text-muted small"><?= h(t('settings.edit_implies_view')) ?></span><button
|
||||
class="btn btn-dark"><?= h(t('settings.save_permissions')) ?></button></div>
|
||||
</div>
|
||||
</form>
|
||||
<script>
|
||||
document.querySelectorAll('.permission-edit').forEach(edit => edit.addEventListener('change', () => {
|
||||
if (!edit.checked) return;
|
||||
const view = document.querySelector('.permission-view[data-role="' + edit.dataset.role +
|
||||
'"][data-module="' + edit.dataset.module + '"]');
|
||||
if (view) view.checked = true;
|
||||
}));
|
||||
document.querySelectorAll('.permission-view').forEach(view => view.addEventListener('change', () => {
|
||||
if (view.checked) return;
|
||||
const edit = document.querySelector('.permission-edit[data-role="' + view.dataset.role +
|
||||
'"][data-module="' + view.dataset.module + '"]');
|
||||
if (edit) edit.checked = false;
|
||||
}));
|
||||
|
||||
</script>
|
||||
172
app/Views/settings_pricing.php
Normal file
172
app/Views/settings_pricing.php
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
<?php require __DIR__ . '/settings_nav.php'; ?>
|
||||
<div class="mb-3">
|
||||
<h2 class="mb-1"><?= h(t('pricing.heading')) ?></h2>
|
||||
<div class="text-muted"><?= h(t('pricing.intro')) ?></div>
|
||||
</div>
|
||||
<?php if ($saved): ?><div class="alert alert-success">✅ <?= h(t('pricing.saved')) ?></div>
|
||||
<?php endif; ?><?php if ($error !== ''): ?><div class="alert alert-danger"><?= h($error) ?></div><?php endif; ?>
|
||||
<div class="row row-cards mb-4">
|
||||
<div class="col-lg-5">
|
||||
<form class="card h-100" method="post" action="/settings/pricing/adoption">
|
||||
<div class="card-status-top bg-green"></div>
|
||||
<div class="card-header">
|
||||
<h3 class="card-title"><?= h(t('pricing.adoption_fees')) ?></h3>
|
||||
</div>
|
||||
<div class="card-body row g-3">
|
||||
<div class="col-6"><label class="form-label"><?= h(t('pricing.sterilized')) ?></label>
|
||||
<div class="input-group"><input class="form-control" type="number" min="0"
|
||||
name="adoption_fee_sterilized"
|
||||
value="<?= (int)$settings['adoption_fee_sterilized'] ?>"><span
|
||||
class="input-group-text">€</span></div>
|
||||
</div>
|
||||
<div class="col-6"><label class="form-label"><?= h(t('pricing.unsterilized')) ?></label>
|
||||
<div class="input-group"><input class="form-control" type="number" min="0"
|
||||
name="adoption_fee_unsterilized"
|
||||
value="<?= (int)$settings['adoption_fee_unsterilized'] ?>"><span
|
||||
class="input-group-text">€</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer text-end"><button class="btn btn-primary"><?= h(t('common.save')) ?></button></div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="col-lg-7">
|
||||
<div class="card h-100">
|
||||
<div class="card-status-top bg-orange"></div>
|
||||
<div class="card-header">
|
||||
<div>
|
||||
<h3 class="card-title"><?= h(t('pricing.discounts')) ?></h3>
|
||||
<div class="text-muted small"><?= h(t('pricing.discounts_help')) ?></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="list-group list-group-flush">
|
||||
<?php foreach ($clinics as $c):$clinicRules = $discounts[(int)$c['id']] ?? []; ?><div
|
||||
class="list-group-item py-3">
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<div class="me-auto text-truncate">
|
||||
<div class="fw-bold text-truncate"><?= h($c['name']) ?></div>
|
||||
<div class="d-flex flex-wrap gap-1 mt-1"><?php foreach ($groups as $code => $label):if ($code === 'none') {
|
||||
continue;
|
||||
}$value = (float)($clinicRules[$code]['percent'] ?? 0);
|
||||
if ($value <= 0) {
|
||||
continue;
|
||||
}?><span class="badge bg-orange-lt"><?= h(PricingService::groupLabel($code)) ?> ·
|
||||
<?= number_format($value, 0, ',', ' ') ?>
|
||||
%</span><?php endforeach; ?><?php if (!array_filter($clinicRules, static fn ($r) => (float)$r['percent'] > 0)): ?><span
|
||||
class="text-muted small"><?= h(t('pricing.no_discount')) ?></span><?php endif; ?>
|
||||
</div>
|
||||
</div><button class="btn btn-outline-secondary btn-sm" type="button" data-bs-toggle="modal"
|
||||
data-bs-target="#discountModal<?= (int)$c['id'] ?>"><?= h(t('pricing.configure')) ?></button>
|
||||
</div>
|
||||
</div><?php endforeach; ?><?php if (!$clinics): ?><div class="card-body text-muted">
|
||||
<?= h(t('pricing.no_structure')) ?></div><?php endif; ?></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php foreach ($clinics as $c):$clinicRules = $discounts[(int)$c['id']] ?? []; ?><div class="modal modal-blur fade"
|
||||
id="discountModal<?= (int)$c['id'] ?>" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<form class="modal-content" method="post" action="/settings/pricing/discount"><input type="hidden"
|
||||
name="clinic_contact_id" value="<?= (int)$c['id'] ?>">
|
||||
<div class="modal-header">
|
||||
<div>
|
||||
<h5 class="modal-title"><?= h(t('pricing.discount_for', ['name' => $c['name']])) ?></h5>
|
||||
<div class="text-muted small"><?= h(t('pricing.future_help')) ?></div>
|
||||
</div><button class="btn-close" type="button" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="vstack gap-3"><?php foreach ($groups as $code => $label):if ($code === 'none') {
|
||||
continue;
|
||||
}$value = $clinicRules[$code]['percent'] ?? 0; ?><div class="row align-items-center"><label
|
||||
class="col form-label mb-0"><?= h(PricingService::groupLabel($code)) ?></label>
|
||||
<div class="col-5">
|
||||
<div class="input-group"><input class="form-control" name="discounts[<?= h($code) ?>]"
|
||||
type="number" min="0" max="100" step="0.01" value="<?= h((string)$value) ?>"><span
|
||||
class="input-group-text">%</span></div>
|
||||
</div>
|
||||
</div><?php endforeach; ?></div>
|
||||
<div class="alert alert-info py-2 mt-3 mb-0 small"><?= h(t('pricing.antibiotic_help')) ?></div>
|
||||
</div>
|
||||
<div class="modal-footer"><button class="btn me-auto" type="button"
|
||||
data-bs-dismiss="modal"><?= h(t('common.cancel')) ?></button><button
|
||||
class="btn btn-primary"><?= h(t('pricing.save_discounts')) ?></button></div>
|
||||
</form>
|
||||
</div>
|
||||
</div><?php endforeach; ?>
|
||||
<div class="card mb-4">
|
||||
<div class="card-status-top bg-blue"></div>
|
||||
<div class="card-header">
|
||||
<div>
|
||||
<h3 class="card-title"><?= h(t('pricing.add_label')) ?></h3>
|
||||
<div class="text-muted small"><?= h(t('pricing.add_label_help')) ?></div>
|
||||
</div>
|
||||
</div>
|
||||
<form method="post" action="/settings/pricing/tariff">
|
||||
<div class="card-body row g-3">
|
||||
<div class="col-md-4"><label class="form-label required"><?= h(t('pricing.structure')) ?></label><select
|
||||
class="form-select" name="clinic_contact_id" required>
|
||||
<option value="">—</option><?php foreach ($clinics as $c): ?><option value="<?= (int)$c['id'] ?>">
|
||||
<?= h($c['name']) ?></option><?php endforeach; ?>
|
||||
</select></div>
|
||||
<div class="col-md-5"><label class="form-label required"><?= h(t('pricing.label')) ?></label><input
|
||||
class="form-control" name="label" required placeholder="<?= h(t('pricing.label_example')) ?>"></div>
|
||||
<div class="col-md-3"><label class="form-label"><?= h(t('pricing.catalog_price')) ?></label>
|
||||
<div class="input-group"><input class="form-control" name="amount" inputmode="decimal" required><span
|
||||
class="input-group-text">€</span></div>
|
||||
</div>
|
||||
<div class="col-md-3"><label class="form-label"><?= h(t('settings.category')) ?></label><select
|
||||
class="form-select" name="category"><?php foreach ($categories as $k => $v): ?><option
|
||||
value="<?= h($k) ?>"><?= h(PricingService::categoryLabel($k)) ?></option>
|
||||
<?php endforeach; ?></select></div>
|
||||
<div class="col-md-3"><label class="form-label"><?= h(t('pricing.discount_group')) ?></label><select
|
||||
class="form-select" name="discount_group"><?php foreach ($groups as $k => $v): ?><option
|
||||
value="<?= h($k) ?>"><?= h(PricingService::groupLabel($k)) ?></option>
|
||||
<?php endforeach; ?></select></div>
|
||||
<div class="col-md-2"><label class="form-label"><?= h(t('pricing.unit')) ?></label><input
|
||||
class="form-control" name="unit_label" value="acte"></div>
|
||||
<div class="col-md-4"><label class="form-label"><?= h(t('pricing.linked_medication')) ?> <span
|
||||
class="text-muted">(<?= h(t('common.optional')) ?>)</span></label><select class="form-select"
|
||||
name="medication_id">
|
||||
<option value="">—</option><?php foreach ($medications as $m): ?><option
|
||||
value="<?= (int)$m['id'] ?>"><?= h($m['name']) ?></option><?php endforeach; ?>
|
||||
</select></div>
|
||||
<div class="col-12"><label class="form-check"><input class="form-check-input" type="checkbox" name="active"
|
||||
checked><span class="form-check-label"><?= h(t('pricing.active_tariff')) ?></span></label></div>
|
||||
</div>
|
||||
<div class="card-footer text-end"><button class="btn btn-primary"><?= h(t('pricing.add_tariff')) ?></button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title"><?= h(t('pricing.grid')) ?></h3>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-vcenter mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><?= h(t('pricing.structure')) ?></th>
|
||||
<th><?= h(t('pricing.label')) ?></th>
|
||||
<th><?= h(t('settings.category')) ?></th>
|
||||
<th><?= h(t('pricing.discount')) ?></th>
|
||||
<th class="text-end"><?= h(t('pricing.catalog')) ?></th>
|
||||
<th class="text-end"><?= h(t('pricing.estimated_cost')) ?></th>
|
||||
<th><?= h(t('pricing.state')) ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody><?php foreach ($tariffs as $t): ?><tr>
|
||||
<td><?= h($t['clinic_name']) ?></td>
|
||||
<td class="fw-semibold"><?= h($t['label']) ?><div class="text-muted small">
|
||||
<?= h(t('pricing.per_unit', ['unit' => $t['unit_label']])) ?></div>
|
||||
</td>
|
||||
<td><?= h(PricingService::categoryLabel($t['category'])) ?></td>
|
||||
<td><?= h(PricingService::groupLabel($t['discount_group'])) ?> ·
|
||||
<?= number_format((float)$t['discount_percent'], 2, ',', ' ') ?> %</td>
|
||||
<td class="text-end"><?= number_format($t['amount_cents'] / 100, 2, ',', ' ') ?> €</td>
|
||||
<td class="text-end fw-bold"><?= number_format($t['effective_cents'] / 100, 2, ',', ' ') ?> €</td>
|
||||
<td><span
|
||||
class="badge <?= $t['active'] ? 'bg-green-lt' : 'bg-secondary-lt' ?>"><?= $t['active'] ? t('pricing.active') : t('pricing.inactive') ?></span>
|
||||
</td>
|
||||
</tr><?php endforeach; ?></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
106
app/Views/settings_privacy.php
Normal file
106
app/Views/settings_privacy.php
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
<?php require __DIR__ . '/settings_nav.php'; ?>
|
||||
<div class="mb-3">
|
||||
<h2 class="mb-1">🔏 <?= h(t('privacy.heading')) ?></h2>
|
||||
<div class="text-muted"><?= h(t('privacy.intro')) ?></div>
|
||||
</div>
|
||||
<?php if ($saved): ?><div class="alert alert-success">✅ <?= h(t('settings.saved')) ?></div>
|
||||
<?php endif; ?><?php if ($error): ?><div class="alert alert-danger"><?= h($error) ?></div><?php endif; ?>
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title"><?= h(t('privacy.retention')) ?></h3>
|
||||
</div>
|
||||
<form method="post" action="/settings/privacy/save"><input type="hidden" name="csrf" value="<?= h(Auth::csrf()) ?>">
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<?php foreach (['privacy_retention_years' => t('privacy.other_contacts'),'privacy_retention_adopter_years' => t('privacy.adopters'),'privacy_retention_foster_years' => t('privacy.fosters'),'privacy_retention_volunteer_years' => t('privacy.volunteers')] as $key => $label): ?>
|
||||
<div class="col-sm-6 col-xl-3"><label class="form-label"><?= h($label) ?></label>
|
||||
<div class="input-group"><input class="form-control" type="number" min="1" max="30"
|
||||
name="<?= h($key) ?>" value="<?= (int)$settings[$key] ?>"><span
|
||||
class="input-group-text"><?= h(t('privacy.years')) ?></span></div>
|
||||
</div><?php endforeach; ?><div class="col-sm-6 col-xl-3"><label
|
||||
class="form-label"><?= h(t('privacy.access_retention')) ?></label>
|
||||
<div class="input-group"><input class="form-control" type="number" min="1" max="120"
|
||||
name="privacy_access_log_months"
|
||||
value="<?= (int)$settings['privacy_access_log_months'] ?>"><span
|
||||
class="input-group-text"><?= h(t('privacy.months')) ?></span></div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="alert alert-info mb-0 py-2"><?= h(t('privacy.no_auto_delete')) ?></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer text-end"><button class="btn btn-primary"><?= h(t('common.save')) ?></button></div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title"><?= h(t('privacy.review')) ?></h3><span
|
||||
class="badge bg-yellow-lt ms-2"><?= count($review) ?></span>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-vcenter card-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><?= h(t('directory.identity')) ?></th>
|
||||
<th><?= h(t('directory.roles')) ?></th>
|
||||
<th><?= h(t('privacy.last_activity')) ?></th>
|
||||
<th><?= h(t('privacy.applied_policy')) ?></th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody><?php foreach ($review as $row): ?><tr>
|
||||
<td><a href="/directory/view?id=<?= (int)$row['id'] ?>"><?= h($row['name']) ?></a>
|
||||
<div class="small text-muted"><?= h($row['email'] ?: $row['phone']) ?></div>
|
||||
</td>
|
||||
<td><?= h($row['roles'] ?: '—') ?></td>
|
||||
<td><?= h($row['last_activity'] ? date('d/m/Y', strtotime($row['last_activity'])) : '—') ?></td>
|
||||
<td><?= (int)$row['policy_years'] ?> <?= h(t('privacy.years')) ?><?php if ($row['blocked']): ?><div
|
||||
class="badge bg-red-lt"><?= h(t('privacy.active_link')) ?></div><?php endif; ?></td>
|
||||
<td class="text-end"><a class="btn btn-sm btn-outline-primary"
|
||||
href="/settings/privacy/export?id=<?= (int)$row['id'] ?>">⬇
|
||||
<?= h(t('privacy.export')) ?></a><?php if (!$row['blocked']): ?><form
|
||||
class="d-inline-flex flex-wrap gap-1 justify-content-end" method="post"
|
||||
action="/settings/privacy/anonymize"
|
||||
onsubmit="return confirm(<?= h(json_encode(t('privacy.anonymize_confirm'), JSON_HEX_APOS | JSON_HEX_QUOT)) ?>)">
|
||||
<input type="hidden" name="csrf" value="<?= h(Auth::csrf()) ?>"><input type="hidden"
|
||||
name="id" value="<?= (int)$row['id'] ?>"><input class="form-control form-control-sm"
|
||||
type="text" name="reason" required maxlength="255"
|
||||
placeholder="<?= h(t('privacy.reason')) ?>"><label class="form-check small"><input
|
||||
class="form-check-input" type="checkbox" name="confirm_reviewed" required><span
|
||||
class="form-check-label"><?= h(t('privacy.reviewed')) ?></span></label><button
|
||||
class="btn btn-sm btn-outline-danger"><?= h(t('privacy.anonymize')) ?></button></form>
|
||||
<?php endif; ?></td>
|
||||
</tr><?php endforeach; ?><?php if (!$review): ?><tr>
|
||||
<td colspan="5" class="text-center text-muted py-4"><?= h(t('privacy.none_review')) ?></td>
|
||||
</tr><?php endif; ?></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title"><?= h(t('privacy.access_register')) ?></h3>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-vcenter card-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><?= h(t('common.date')) ?></th>
|
||||
<th><?= h(t('audit.user')) ?></th>
|
||||
<th><?= h(t('privacy.resource')) ?></th>
|
||||
<th><?= h(t('animal.animal')) ?></th>
|
||||
<th><?= h(t('privacy.action')) ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody><?php foreach ($accesses as $row): ?><tr>
|
||||
<td><?= h(date('d/m/Y H:i', strtotime($row['accessed_at']))) ?></td>
|
||||
<td><?= h($row['actor'] ?: '—') ?></td>
|
||||
<td><?= h($row['resource_type'] . ' · ' . ($row['original_name'] ?: '#' . $row['resource_id'])) ?>
|
||||
</td>
|
||||
<td><?= h($row['animal_name'] ?: '—') ?></td>
|
||||
<td><?= h($row['action']) ?></td>
|
||||
</tr><?php endforeach; ?><?php if (!$accesses): ?><tr>
|
||||
<td colspan="5" class="text-center text-muted py-4"><?= h(t('privacy.no_access')) ?></td>
|
||||
</tr><?php endif; ?></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
131
app/Views/settings_public_site.php
Normal file
131
app/Views/settings_public_site.php
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
<?php require __DIR__ . '/settings_nav.php'; ?>
|
||||
<div class="d-flex flex-wrap justify-content-between align-items-start gap-2 mb-3">
|
||||
<div>
|
||||
<h2 class="mb-1"><?= h(t('public_site.settings_heading')) ?></h2>
|
||||
<div class="text-muted"><?= h(t('public_site.settings_intro')) ?></div>
|
||||
</div><a class="btn btn-outline-primary" href="<?= h(PublicSiteController::url()) ?>" target="_blank">↗
|
||||
<?= h(t('public_site.preview')) ?></a>
|
||||
</div>
|
||||
<?php if ($saved): ?><div class="alert alert-success">✅ <?= h(t('public_site.saved')) ?></div>
|
||||
<?php endif; ?><?php if ($error): ?><div class="alert alert-danger">⚠️ <?= h($error) ?></div><?php endif; ?>
|
||||
<form method="post" action="/settings/public-site/save"><input type="hidden" name="csrf" value="<?= h(Auth::csrf()) ?>">
|
||||
<div class="card mb-3">
|
||||
<div class="card-status-top bg-green"></div>
|
||||
<div class="card-header">
|
||||
<h3 class="card-title"><?= h(t('public_site.activation_style')) ?></h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-12"><label class="form-check form-switch"><input class="form-check-input"
|
||||
type="checkbox" name="public_site_enabled" value="1"
|
||||
<?= ($settings['public_site_enabled'] ?? '0') === '1' ? 'checked' : '' ?>><span
|
||||
class="form-check-label fw-bold"><?= h(t('public_site.enable')) ?></span></label>
|
||||
<div class="form-hint"><?= h(t('public_site.enable_help')) ?></div>
|
||||
</div>
|
||||
<div class="col-12"><label class="form-label"><?= h(t('public_site.main_address')) ?></label>
|
||||
<div class="row g-2">
|
||||
<div class="col-md-6"><label class="form-selectgroup-item w-100"><input
|
||||
class="form-selectgroup-input" type="radio" name="public_site_routing"
|
||||
value="integrated"
|
||||
<?= ($settings['public_site_routing'] ?? 'integrated') === 'integrated' ? 'checked' : '' ?>><span
|
||||
class="form-selectgroup-label d-block"><strong><?= h(t('public_site.routing_management')) ?></strong><span
|
||||
class="d-block text-muted small"><?= h(t('public_site.routing_management_help')) ?></span></span></label>
|
||||
</div>
|
||||
<div class="col-md-6"><label class="form-selectgroup-item w-100"><input
|
||||
class="form-selectgroup-input" type="radio" name="public_site_routing" value="root"
|
||||
<?= ($settings['public_site_routing'] ?? 'integrated') === 'root' ? 'checked' : '' ?>><span
|
||||
class="form-selectgroup-label d-block"><strong><?= h(t('public_site.routing_public')) ?></strong><span
|
||||
class="d-block text-muted small"><?= h(t('public_site.routing_public_help')) ?></span></span></label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-hint mt-2"><?= h(t('public_site.routing_no_server')) ?></div>
|
||||
</div>
|
||||
<div class="col-12"><label class="form-label"><?= h(t('public_site.theme')) ?></label>
|
||||
<div class="row g-2">
|
||||
<?php foreach (['warm' => ['🧡',t('public_site.theme_warm')],'nature' => ['🌿',t('public_site.theme_nature')],'minimal' => ['◻️',t('public_site.theme_minimal')]] as $key => $theme): ?>
|
||||
<div class="col-md-4"><label class="form-selectgroup-item w-100"><input type="radio"
|
||||
name="public_site_theme" value="<?= h($key) ?>" class="form-selectgroup-input"
|
||||
<?= ($settings['public_site_theme'] ?? 'warm') === $key ? 'checked' : '' ?>><span
|
||||
class="form-selectgroup-label d-flex align-items-center gap-2"><?= $theme[0] ?>
|
||||
<strong><?= h($theme[1]) ?></strong></span></label></div><?php endforeach; ?></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">🏠 <?= h(t('public_site.home')) ?></h3>
|
||||
</div>
|
||||
<div class="card-body row g-3">
|
||||
<div class="col-12"><label class="form-label"><?= h(t('common.title')) ?></label><input class="form-control"
|
||||
name="public_site_home_title" value="<?= h($settings['public_site_home_title']) ?>" required></div>
|
||||
<div class="col-12"><label class="form-label"><?= h(t('common.text')) ?></label><textarea
|
||||
class="form-control" name="public_site_home_text"
|
||||
rows="4"><?= h($settings['public_site_home_text']) ?></textarea></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">📖 <?= h(t('public_site.free_page')) ?></h3>
|
||||
</div>
|
||||
<div class="card-body row g-3">
|
||||
<div class="col-12"><label class="form-check form-switch"><input class="form-check-input" type="checkbox"
|
||||
name="public_site_about_enabled" value="1"
|
||||
<?= ($settings['public_site_about_enabled'] ?? '0') === '1' ? 'checked' : '' ?>><span
|
||||
class="form-check-label"><?= h(t('public_site.show_free_page')) ?></span></label></div>
|
||||
<div class="col-md-4"><label class="form-label"><?= h(t('common.title')) ?></label><input
|
||||
class="form-control" name="public_site_about_title"
|
||||
value="<?= h($settings['public_site_about_title']) ?>"></div>
|
||||
<div class="col-md-8"><label class="form-label"><?= h(t('common.text')) ?></label><textarea
|
||||
class="form-control" name="public_site_about_text"
|
||||
rows="5"><?= h($settings['public_site_about_text']) ?></textarea></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">💝 <?= h(t('public_site.donation')) ?></h3>
|
||||
</div>
|
||||
<div class="card-body row g-3">
|
||||
<div class="col-md-4"><label class="form-label"><?= h(t('common.title')) ?></label><input
|
||||
class="form-control" name="public_site_donation_title"
|
||||
value="<?= h($settings['public_site_donation_title']) ?>"></div>
|
||||
<div class="col-md-8"><label class="form-label"><?= h(t('public_site.donation_url')) ?></label><input
|
||||
class="form-control" type="url" name="public_site_donation_url"
|
||||
value="<?= h($settings['public_site_donation_url']) ?>" placeholder="https://..."></div>
|
||||
<div class="col-12"><label class="form-label"><?= h(t('common.text')) ?></label><textarea
|
||||
class="form-control" name="public_site_donation_text"
|
||||
rows="4"><?= h($settings['public_site_donation_text']) ?></textarea></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">✉️ <?= h(t('public_site.contact')) ?></h3>
|
||||
</div>
|
||||
<div class="card-body row g-3">
|
||||
<div class="col-12">
|
||||
<div class="alert alert-info py-2"><?= h(t('public_site.contact_reuse')) ?></div><label
|
||||
class="form-label"><?= h(t('common.text')) ?></label><textarea class="form-control"
|
||||
name="public_site_contact_text" rows="3"><?= h($settings['public_site_contact_text']) ?></textarea>
|
||||
</div>
|
||||
<div class="col-md-6"><label class="form-label">Facebook</label><input class="form-control" type="url"
|
||||
name="public_site_facebook_url" value="<?= h($settings['public_site_facebook_url']) ?>"></div>
|
||||
<div class="col-md-6"><label class="form-label">Instagram</label><input class="form-control" type="url"
|
||||
name="public_site_instagram_url" value="<?= h($settings['public_site_instagram_url']) ?>"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">⚖️ <?= h(t('public_site.legal_privacy')) ?></h3>
|
||||
</div>
|
||||
<div class="card-body row g-3">
|
||||
<div class="col-md-6"><label class="form-label"><?= h(t('public_site.legal')) ?></label><textarea
|
||||
class="form-control" name="public_site_legal_text"
|
||||
rows="16"><?= h(PublicSiteController::legalText($settings)) ?></textarea></div>
|
||||
<div class="col-md-6"><label class="form-label"><?= h(t('public_site.privacy')) ?></label><textarea
|
||||
class="form-control" name="public_site_privacy_text"
|
||||
rows="16"><?= h(PublicSiteController::privacyText($settings)) ?></textarea></div>
|
||||
<div class="col-12 form-hint"><?= h(t('public_site.legal_hint')) ?></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-end mb-4"><button class="btn btn-primary"><?= h(t('common.save')) ?></button></div>
|
||||
</form>
|
||||
86
app/Views/settings_rooms.php
Normal file
86
app/Views/settings_rooms.php
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
<?php require __DIR__ . '/settings_nav.php'; ?>
|
||||
<?php if ($saved): ?><div class="alert alert-success"><?= h(t('rooms.saved')) ?></div><?php endif; ?>
|
||||
<?php if ($error): ?><div class="alert alert-danger"><?= h($error) ?></div><?php endif; ?>
|
||||
<form method="post" action="/settings/rooms/save"><input type="hidden" name="csrf" value="<?= h(Auth::csrf()) ?>">
|
||||
<div class="alert alert-info"><?= h(t('rooms.help')) ?></div>
|
||||
<div class="row row-cards">
|
||||
<?php foreach ($rooms as $room):$boxNames = implode("\n", array_column(array_filter($room['boxes'], static fn ($box) => (int)$box['active'] === 1), 'name')); ?>
|
||||
<div class="col-lg-6">
|
||||
<div class="card h-100">
|
||||
<div class="card-status-top" style="background:<?= h($room['color']) ?>"></div>
|
||||
<div class="card-header">
|
||||
<div>
|
||||
<h3 class="card-title"><?= h($room['name']) ?></h3>
|
||||
<div class="text-muted small"><code><?= h($room['code']) ?></code></div>
|
||||
</div><label class="form-check form-switch ms-auto"><input class="form-check-input" type="checkbox"
|
||||
name="rooms[<?= (int)$room['id'] ?>][active]" <?= $room['active'] ? 'checked' : '' ?>><span
|
||||
class="form-check-label"><?= h(t('common.active')) ?></span></label>
|
||||
</div>
|
||||
<div class="card-body row g-3">
|
||||
<div class="col-md-8"><label class="form-label"><?= h(t('common.name')) ?></label><input
|
||||
class="form-control" name="rooms[<?= (int)$room['id'] ?>][name]"
|
||||
value="<?= h($room['name']) ?>" required></div>
|
||||
<div class="col-md-4"><label class="form-label"><?= h(t('rooms.color')) ?></label><input
|
||||
class="form-control form-control-color w-100" type="color"
|
||||
name="rooms[<?= (int)$room['id'] ?>][color]" value="<?= h($room['color']) ?>"></div>
|
||||
<div class="col-md-4"><label class="form-label"><?= h(t('rooms.type')) ?></label><select
|
||||
class="form-select" name="rooms[<?= (int)$room['id'] ?>][room_type]">
|
||||
<option value="collective" <?= $room['room_type'] === 'collective' ? 'selected' : '' ?>>
|
||||
<?= h(t('rooms.collective')) ?></option>
|
||||
<option value="boxes" <?= $room['room_type'] === 'boxes' ? 'selected' : '' ?>>
|
||||
<?= h(t('rooms.with_boxes')) ?></option>
|
||||
</select></div>
|
||||
<div class="col-md-4"><label class="form-label"><?= h(t('animal.status')) ?></label><select
|
||||
class="form-select"
|
||||
name="rooms[<?= (int)$room['id'] ?>][status_code]"><?php foreach (['refuge' => t('status.refuge'),'soin' => t('status.soin'),'quarantaine' => t('status.quarantaine')] as $key => $label): ?>
|
||||
<option value="<?= h($key) ?>" <?= $room['status_code'] === $key ? 'selected' : '' ?>>
|
||||
<?= h($label) ?></option><?php endforeach; ?></select></div>
|
||||
<div class="col-md-4"><label class="form-label"><?= h(t('rooms.order')) ?></label><input
|
||||
class="form-control" type="number" min="0" name="rooms[<?= (int)$room['id'] ?>][sort_order]"
|
||||
value="<?= (int)$room['sort_order'] ?>"></div>
|
||||
<div class="col-12"><label class="form-label"><?= h(t('rooms.boxes')) ?></label><textarea
|
||||
class="form-control" rows="5" name="rooms[<?= (int)$room['id'] ?>][boxes]"
|
||||
placeholder="<?= h(t('rooms.one_box_per_line')) ?>"><?= h($boxNames) ?></textarea>
|
||||
<div class="form-hint"><?= h(t('rooms.boxes_help')) ?></div>
|
||||
</div>
|
||||
<div class="col-12"><label class="form-check"><input class="form-check-input" type="checkbox"
|
||||
name="rooms[<?= (int)$room['id'] ?>][bulk_validation]"
|
||||
<?= $room['bulk_validation'] ? 'checked' : '' ?>><span
|
||||
class="form-check-label"><?= h(t('rooms.bulk_validation')) ?></span></label></div>
|
||||
</div>
|
||||
</div>
|
||||
</div><?php endforeach; ?>
|
||||
<div class="col-12">
|
||||
<div class="card border-primary">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title"><?= h(t('rooms.add')) ?></h3>
|
||||
</div>
|
||||
<div class="card-body row g-3">
|
||||
<div class="col-md-4"><label class="form-label"><?= h(t('common.name')) ?></label><input
|
||||
class="form-control" name="new_room[name]"></div>
|
||||
<div class="col-md-3"><label class="form-label"><?= h(t('rooms.type')) ?></label><select
|
||||
class="form-select" name="new_room[room_type]">
|
||||
<option value="collective"><?= h(t('rooms.collective')) ?></option>
|
||||
<option value="boxes"><?= h(t('rooms.with_boxes')) ?></option>
|
||||
</select></div>
|
||||
<div class="col-md-3"><label class="form-label"><?= h(t('animal.status')) ?></label><select
|
||||
class="form-select" name="new_room[status_code]">
|
||||
<option value="refuge"><?= h(t('status.refuge')) ?></option>
|
||||
<option value="soin"><?= h(t('status.soin')) ?></option>
|
||||
<option value="quarantaine"><?= h(t('status.quarantaine')) ?></option>
|
||||
</select></div>
|
||||
<div class="col-md-2"><label class="form-label"><?= h(t('rooms.color')) ?></label><input
|
||||
class="form-control form-control-color w-100" type="color" name="new_room[color]"
|
||||
value="#6c757d"></div>
|
||||
<div class="col-12"><label class="form-label"><?= h(t('rooms.boxes')) ?></label><textarea
|
||||
class="form-control" name="new_room[boxes]" rows="3"
|
||||
placeholder="<?= h(t('rooms.one_box_per_line')) ?>"></textarea></div>
|
||||
<div class="col-12"><label class="form-check"><input class="form-check-input" type="checkbox"
|
||||
name="new_room[bulk_validation]"><span
|
||||
class="form-check-label"><?= h(t('rooms.bulk_validation')) ?></span></label></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 text-end"><button class="btn btn-primary"><?= h(t('common.save')) ?></button></div>
|
||||
</form>
|
||||
69
app/Views/settings_species.php
Normal file
69
app/Views/settings_species.php
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
<?php require __DIR__ . '/settings_nav.php'; ?>
|
||||
<div class="mb-3">
|
||||
<h2 class="mb-1"><?= h(t('settings.species_heading')) ?></h2>
|
||||
<div class="text-muted"><?= h(t('settings.species_intro')) ?></div>
|
||||
</div>
|
||||
<?php if ($saved): ?><div class="alert alert-success">✅ <?= h(t('settings.species_saved')) ?></div><?php endif; ?>
|
||||
<?php if ($error !== ''): ?><div class="alert alert-danger">⚠️ <?= h($error) ?></div><?php endif; ?>
|
||||
<div class="card mb-3">
|
||||
<div class="card-status-top bg-primary"></div>
|
||||
<div class="card-header">
|
||||
<h3 class="card-title"><?= h(t('settings.add_species')) ?></h3>
|
||||
</div>
|
||||
<form method="post" action="/settings/species/save"><input type="hidden" name="csrf" value="<?= h(Auth::csrf()) ?>">
|
||||
<div class="card-body row g-2 align-items-end">
|
||||
<div class="col-md-3"><label class="form-label"><?= h(t('settings.name')) ?></label><input
|
||||
class="form-control" name="name" required placeholder="<?= h(t('settings.species_example')) ?>">
|
||||
</div>
|
||||
<div class="col-md-2"><label class="form-label"><?= h(t('settings.code')) ?></label><input
|
||||
class="form-control" name="code" required pattern="[a-z0-9][a-z0-9-]{1,49}"
|
||||
placeholder="tortue-hermann"></div>
|
||||
<div class="col-md-2"><label class="form-label"><?= h(t('settings.category')) ?></label><select
|
||||
class="form-select" name="category"><?php foreach ($categories as $key => $label): ?><option
|
||||
value="<?= h($key) ?>"><?= h(SpeciesService::categoryLabel($key)) ?></option>
|
||||
<?php endforeach; ?></select></div>
|
||||
<div class="col-md-1"><label class="form-label"><?= h(t('settings.icon')) ?></label><input
|
||||
class="form-control" name="icon" value="🐾" maxlength="12"></div>
|
||||
<div class="col-md-1"><label class="form-label"><?= h(t('settings.color')) ?></label><input
|
||||
class="form-control form-control-color w-100" type="color" name="color" value="#6c757d"></div>
|
||||
<div class="col-md-1"><label class="form-label"><?= h(t('settings.order')) ?></label><input
|
||||
class="form-control" type="number" name="sort_order" value="100" min="0" max="999"></div>
|
||||
<div class="col-md-1"><label class="form-check mb-2"><input class="form-check-input" type="checkbox"
|
||||
name="active" checked><span
|
||||
class="form-check-label"><?= h(t('settings.active_f')) ?></span></label></div>
|
||||
<div class="col-md-1"><button class="btn btn-primary w-100"><?= h(t('common.add')) ?></button></div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="vstack gap-2"><?php foreach ($species as $item): ?>
|
||||
<form method="post" action="/settings/species/save" class="card card-sm"><input type="hidden" name="csrf"
|
||||
value="<?= h(Auth::csrf()) ?>"><input type="hidden" name="id" value="<?= (int)$item['id'] ?>">
|
||||
<div class="card-body row g-2 align-items-end">
|
||||
<div class="col-md-3"><label class="form-label"><?= h(t('settings.name')) ?></label><input
|
||||
class="form-control" name="name" value="<?= h($item['name']) ?>" required></div>
|
||||
<div class="col-md-2"><label class="form-label"><?= h(t('settings.stable_code')) ?></label><input
|
||||
class="form-control" name="code" value="<?= h($item['code']) ?>" required
|
||||
pattern="[a-z0-9][a-z0-9-]{1,49}" <?= !empty($item['is_system']) ? 'readonly' : '' ?>></div>
|
||||
<div class="col-md-2"><label class="form-label"><?= h(t('settings.category')) ?></label><select
|
||||
class="form-select" name="category"><?php foreach ($categories as $key => $label): ?><option
|
||||
value="<?= h($key) ?>" <?= $item['category'] === $key ? 'selected' : '' ?>>
|
||||
<?= h(SpeciesService::categoryLabel($key)) ?></option><?php endforeach; ?></select></div>
|
||||
<div class="col-md-1"><label class="form-label"><?= h(t('settings.icon')) ?></label><input
|
||||
class="form-control" name="icon" value="<?= h($item['icon']) ?>" maxlength="12"></div>
|
||||
<div class="col-md-1"><label class="form-label"><?= h(t('settings.color')) ?></label><input
|
||||
class="form-control form-control-color w-100" type="color" name="color"
|
||||
value="<?= h($item['color']) ?>"></div>
|
||||
<div class="col-md-1"><label class="form-label"><?= h(t('settings.order')) ?></label><input
|
||||
class="form-control" type="number" name="sort_order" value="<?= (int)$item['sort_order'] ?>" min="0"
|
||||
max="999"></div>
|
||||
<div class="col-md-1"><label class="form-check mb-2"><input class="form-check-input" type="checkbox"
|
||||
name="active" <?= (int)$item['active'] === 1 ? 'checked' : '' ?>><span
|
||||
class="form-check-label"><?= h(t('settings.active_f')) ?></span></label>
|
||||
<div class="small text-muted">
|
||||
<?= h(t('settings.animal_count', ['count' => (int)$item['animal_count']])) ?></div>
|
||||
</div>
|
||||
<div class="col-md-1"><button
|
||||
class="btn btn-outline-primary w-100"><?= h(t('common.save_short')) ?></button></div>
|
||||
</div>
|
||||
</form><?php endforeach; ?>
|
||||
</div>
|
||||
55
app/Views/settings_users.php
Normal file
55
app/Views/settings_users.php
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
<?php require __DIR__ . '/settings_nav.php'; ?>
|
||||
<div class="mb-3 d-flex flex-wrap align-items-center gap-3">
|
||||
<div class="me-auto">
|
||||
<h2 class="mb-1"><?= h(t('settings.users')) ?></h2>
|
||||
<div class="text-muted"><?= h(t('settings.users_intro')) ?></div>
|
||||
</div><a class="btn btn-dark" href="/settings/permissions">🔐 <?= h(t('settings.configure_permissions')) ?></a>
|
||||
</div>
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title"><?= h(t('settings.add_user')) ?></h3>
|
||||
</div>
|
||||
<form method="post" action="/settings/users/save">
|
||||
<div class="card-body row g-3">
|
||||
<div class="col-md-3"><label class="form-label"><?= h(t('settings.display_name')) ?></label><input
|
||||
class="form-control" name="display_name" required></div>
|
||||
<div class="col-md-3"><label class="form-label"><?= h(t('settings.username')) ?></label><input
|
||||
class="form-control" name="username" required></div>
|
||||
<div class="col-md-2"><label class="form-label"><?= h(t('settings.role')) ?></label><select
|
||||
class="form-select" name="role"><?php foreach ($roles as $key => $label): ?><option
|
||||
value="<?= h($key) ?>"><?= h(PermissionService::roleLabel($key)) ?></option>
|
||||
<?php endforeach; ?></select></div>
|
||||
<div class="col-md-3"><label class="form-label"><?= h(t('settings.initial_password')) ?></label><input
|
||||
class="form-control" type="password" name="password" minlength="10" required></div>
|
||||
<div class="col-md-1 d-flex align-items-end"><label class="form-check mb-2"><input class="form-check-input"
|
||||
type="checkbox" name="active" checked><span
|
||||
class="form-check-label"><?= h(t('settings.active')) ?></span></label></div>
|
||||
</div>
|
||||
<div class="card-footer text-end"><button class="btn btn-primary"><?= h(t('settings.create')) ?></button></div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="vstack gap-2"><?php foreach ($users as $user): ?><form method="post" action="/settings/users/save"
|
||||
class="card card-sm"><input type="hidden" name="id" value="<?= (int)$user['id'] ?>">
|
||||
<div class="card-body row g-2 align-items-end">
|
||||
<div class="col-md-3"><label class="form-label"><?= h(t('settings.display_name')) ?></label><input
|
||||
class="form-control" name="display_name"
|
||||
value="<?= h($user['display_name'] ?: $user['username']) ?>" required></div>
|
||||
<div class="col-md-2"><label class="form-label"><?= h(t('settings.username')) ?></label><input
|
||||
class="form-control" name="username" value="<?= h($user['username']) ?>" required></div>
|
||||
<div class="col-md-2"><label class="form-label"><?= h(t('settings.role')) ?></label><select
|
||||
class="form-select" name="role"><?php foreach ($roles as $key => $label): ?><option
|
||||
value="<?= h($key) ?>" <?= $user['role'] === $key ? 'selected' : '' ?>>
|
||||
<?= h(PermissionService::roleLabel($key)) ?></option><?php endforeach; ?></select></div>
|
||||
<div class="col-md-2"><label class="form-label"><?= h(t('settings.new_password')) ?></label><input
|
||||
class="form-control" type="password" name="password" minlength="10"
|
||||
placeholder="<?= h(t('settings.unchanged')) ?>"></div>
|
||||
<div class="col-md-1"><label class="form-check mb-2"><input class="form-check-input" type="checkbox"
|
||||
name="active" <?= (int)$user['active'] === 1 ? 'checked' : '' ?>><span
|
||||
class="form-check-label"><?= h(t('settings.active')) ?></span></label></div>
|
||||
<div class="col-md-2 text-end"><button class="btn btn-outline-primary"><?= h(t('common.save')) ?></button>
|
||||
<div class="small text-muted mt-1">
|
||||
<?= $user['last_login_at'] ? t('settings.seen', ['date' => $user['last_login_at']]) : t('settings.never_logged') ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form><?php endforeach; ?></div>
|
||||
389
app/Views/statistics.php
Normal file
389
app/Views/statistics.php
Normal file
|
|
@ -0,0 +1,389 @@
|
|||
<?php
|
||||
$headline = ['handled' => [t('statistics.handled'),'teal'],'entries' => [t('statistics.entries'),'green'],'exits' => [t('statistics.exits'),'red'],'adoptions' => [t('statistics.adoptions'),'blue'],'deaths' => [t('statistics.deaths'),'dark'],'births' => [t('statistics.births'),'purple'],'vaccinations' => [t('statistics.vaccinations'),'cyan'],'treatments' => [t('statistics.treatments'),'orange'],'medical_notes' => [t('statistics.medical_notes'),'pink']];
|
||||
$comparisonLabels = ['handled' => t('statistics.handled'),'entries' => t('statistics.entries'),'exits' => t('statistics.exits'),'adoptions' => t('statistics.adoptions'),'deaths' => t('statistics.deaths'),'births' => t('statistics.births'),'vaccinations' => t('statistics.vaccinations'),'treatments' => t('statistics.treatments_started'),'medical_notes' => t('statistics.medical_notes'),'avg' => t('statistics.avg_adoption_days')];
|
||||
$intakeTypeLabels = [];
|
||||
foreach (['unknown','found','owner_surrender','transfer','born_in_care','return','seizure','other'] as $key) {
|
||||
$intakeTypeLabels[$key] = t('statistics.intake.' . $key);
|
||||
}
|
||||
$intakeReasonLabels = [];
|
||||
foreach (['unknown','stray_found','unwanted_litter','owner_health','owner_death','owner_care_home','allergy','housing','financial','behavior','abandonment','danger','transfer','birth','other'] as $key) {
|
||||
$intakeReasonLabels[$key] = t('statistics.reason.' . $key);
|
||||
}
|
||||
$distributionLabels = ['Femelles' => t('statistics.value.females'),'Mâles' => t('statistics.value.males'),'Inconnu' => t('statistics.value.unknown'),'Non renseigné' => t('statistics.value.not_provided'),'Non renseignée' => t('statistics.value.not_provided_f'),'Moins d’un an' => t('statistics.value.under_year'),'1 à 2 ans' => t('statistics.value.one_two_years'),'3 à 7 ans' => t('statistics.value.three_seven_years'),'8 ans et plus' => t('statistics.value.eight_plus'),'Moins d’un mois' => t('statistics.value.under_month'),'1 à 3 mois' => t('statistics.value.one_three_months'),'3 à 6 mois' => t('statistics.value.three_six_months'),'6 à 12 mois' => t('statistics.value.six_twelve_months'),'Plus d’un an' => t('statistics.value.over_year'),'Chats' => t('statistics.value.cats'),'Chiens' => t('statistics.value.dogs'),'Inconnue' => t('statistics.value.unknown_f'),'Refuge' => t('status.refuge'),'Famille d’accueil' => t('status.fa'),'Accueil permanent' => t('statistics.value.permanent_foster'),'Chats libres suivis' => t('statistics.free_cats'),'Quarantaine' => t('status.quarantaine'),'Infirmerie' => t('status.soin'),'Réservé' => t('status.reserve'),'Site non renseigné' => t('statistics.value.unprovided_site')];
|
||||
$groups = [
|
||||
[t('statistics.intake_modes'),$distributions['intakeTypes'],$intakeTypeLabels,'green'],
|
||||
[t('statistics.intake_reasons'),$distributions['intakeReasons'],$intakeReasonLabels,'orange'],
|
||||
[t('statistics.colors'),$distributions['colors'],[],'blue'],
|
||||
[t('statistics.breeds'),$distributions['breeds'],[],'purple'],
|
||||
[t('statistics.species'),$distributions['species'],[],'cyan'],
|
||||
[t('statistics.sex'),$distributions['sex'],[],'pink'],
|
||||
[t('statistics.age_at_entry'),$distributions['ages'],[],'orange'],
|
||||
[t('statistics.origins'),$distributions['origins'],[],'green','municipality'],
|
||||
[t('statistics.adoption_delay'),$distributions['stays'],[],'azure'],
|
||||
[t('statistics.death_causes'),$distributions['deathCauses'],array_combine(array_keys(DeathController::CAUSES), array_map(static fn ($key) => DeathController::causeLabel($key), array_keys(DeathController::CAUSES))),'dark'],
|
||||
[t('statistics.current_distribution'),$distributions['currentStatus'],[],'teal'],
|
||||
[t('statistics.free_cat_sites'),$distributions['freeCatSites'] ?? [],[],'green'],
|
||||
[$allYears ? t('statistics.releases_year') : t('statistics.releases_site'),$distributions['freeCatReleases'] ?? [],[],'lime'],
|
||||
];
|
||||
?>
|
||||
<style>
|
||||
@media print {
|
||||
form[action="/statistics"] {
|
||||
display: none !important
|
||||
}
|
||||
|
||||
.page-body {
|
||||
padding-top: 0
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
<div class="card mb-4 overflow-hidden">
|
||||
<div class="card-status-top bg-cyan"></div>
|
||||
<div class="card-body d-flex flex-wrap align-items-end gap-3">
|
||||
<div class="me-auto">
|
||||
<div class="subheader"><?= h(t('statistics.memory')) ?></div>
|
||||
<h2 class="mb-1">
|
||||
<?= $municipality !== '' ? t('statistics.city_report', ['city' => $municipality]) : t('statistics.figures') ?>
|
||||
</h2>
|
||||
<div class="text-muted"><?= $municipality !== '' ? t('statistics.city_help') : t('statistics.help') ?></div>
|
||||
</div>
|
||||
<form method="get" action="/statistics" class="d-flex flex-wrap align-items-end gap-2">
|
||||
<div><label class="form-label"><?= h(t('statistics.period')) ?></label><select class="form-select"
|
||||
name="year">
|
||||
<option value="all" <?= $allYears ? 'selected' : '' ?>><?= h(t('statistics.all_years')) ?></option>
|
||||
<?php foreach (array_reverse($years) as $y): ?><option value="<?= $y ?>"
|
||||
<?= !$allYears && $y === $year ? 'selected' : '' ?>><?= $y ?></option><?php endforeach; ?>
|
||||
</select></div>
|
||||
<div><label class="form-label"><?= h(t('statistics.origin_city')) ?></label><select class="form-select"
|
||||
name="municipality">
|
||||
<option value=""><?= h(t('statistics.all_cities')) ?></option>
|
||||
<?php foreach (($municipalities ?? []) as $city => $total): ?><option value="<?= h($city) ?>"
|
||||
<?= $municipality === $city ? 'selected' : '' ?>><?= h($city) ?> · <?= (int)$total ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select></div><button
|
||||
class="btn btn-cyan"><?= h(t('common.show')) ?></button><?php if ($municipality !== ''): ?><a
|
||||
class="btn btn-outline-secondary"
|
||||
href="/statistics?year=<?= h($allYears ? 'all' : (string)$year) ?>"><?= h(t('common.clear')) ?></a><button
|
||||
class="btn btn-outline-primary" type="button" onclick="window.print()">🖨
|
||||
<?= h(t('common.print')) ?></button><?php endif; ?>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if ($municipality !== ''): ?><div class="alert alert-info d-flex flex-wrap align-items-center gap-2">
|
||||
<strong><?= h($municipality) ?> ·
|
||||
<?= h($allYears ? t('statistics.all_years') : (string)$year) ?></strong><span><?= h(t('statistics.city_filter_summary', ['count' => (int)$stats['handled']])) ?></span><span
|
||||
class="ms-auto small"><?= h(t('statistics.filter_applied')) ?></span></div><?php endif; ?>
|
||||
<?php if ($municipality === '' && !empty($municipalityCoverage)): ?><div class="alert alert-info mb-4">
|
||||
<div class="d-flex flex-wrap align-items-center gap-2">
|
||||
<strong><?= h(t('statistics.location_coverage')) ?></strong><span
|
||||
class="badge bg-green-lt"><?= h(t('statistics.location_documented', ['count' => $municipalityCoverage['documented']])) ?></span><span
|
||||
class="badge bg-azure-lt"><?= h(t('statistics.location_inferred', ['count' => $municipalityCoverage['inferred']])) ?></span><span
|
||||
class="badge bg-purple-lt"><?= h(t('statistics.location_born_in_care', ['count' => $municipalityCoverage['born_in_care']])) ?></span><span
|
||||
class="badge bg-secondary-lt"><?= h(t('statistics.location_unknown', ['count' => $municipalityCoverage['unknown']])) ?></span>
|
||||
</div>
|
||||
<div class="small mt-1"><?= h(t('statistics.location_coverage_help')) ?></div>
|
||||
</div><?php endif; ?>
|
||||
<?php if ($allYears && $undatedDeaths): ?><div class="alert alert-info">
|
||||
<?= h(t('statistics.undated_deaths', ['total' => (int)$stats['deaths'],'count' => (int)$undatedDeaths])) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if ($allYears): ?><div class="alert alert-info"><strong><?= h(t('statistics.global_memory')) ?></strong>
|
||||
<?= h(t('statistics.undated_summary', ['entries' => (int)$undatedHandled,'exits' => (int)$undatedExits,'births' => (int)$undatedBirths,'vaccinations' => (int)$stats['vaccinations'],'vaccinated' => (int)($scopeCounts['vaccinated_animals'] ?? 0),'notes' => (int)$stats['medical_notes'],'medical' => (int)($scopeCounts['medical_animals'] ?? 0)])) ?>
|
||||
</div><?php endif; ?>
|
||||
|
||||
<div class="row row-cards mb-4"><?php foreach ($headline as $key => [$label,$color]): ?><div class="col-6 col-md-3">
|
||||
<div class="card h-100">
|
||||
<div class="card-status-top bg-<?= $color ?>"></div>
|
||||
<div class="card-body">
|
||||
<div class="text-muted small text-uppercase"><?= h($label) ?></div>
|
||||
<div class="display-6 fw-bold"><?= (int)$stats[$key] ?></div>
|
||||
<div class="small text-muted">
|
||||
<?= $allYears ? t('statistics.since_start') : t('statistics.in_year', ['year' => (int)$year]) ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div><?php endforeach; ?></div>
|
||||
|
||||
<div class="card mb-4 overflow-hidden">
|
||||
<div class="card-status-top bg-orange"></div>
|
||||
<div class="card-header">
|
||||
<div>
|
||||
<h3 class="card-title"><?= h(t('statistics.vet_costs')) ?></h3>
|
||||
<div class="text-muted small"><?= h(t('statistics.vet_costs_help')) ?></div>
|
||||
</div>
|
||||
<div class="ms-auto text-end">
|
||||
<div class="h2 mb-0"><?= number_format(($costs['total_cents'] ?? 0) / 100, 2, ',', ' ') ?> €</div>
|
||||
<div class="text-muted small">
|
||||
<?= h(t('statistics.animals_concerned', ['count' => (int)($costs['animals'] ?? 0)])) ?></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3 mb-3">
|
||||
<?php foreach ([[t('statistics.cost_scope_animals'),(int)($costs['scope_animals'] ?? 0),null],[t('statistics.cost_with_expense'),(int)($costs['animals'] ?? 0),null],[t('statistics.cost_avg_all'),(int)($costs['average_all_cents'] ?? 0),'money'],[t('statistics.cost_avg_spending'),(int)($costs['average_expense_cents'] ?? 0),'money'],[t('statistics.cost_median'),(int)($costs['median_expense_cents'] ?? 0),'money']] as [$label,$value,$format]): ?>
|
||||
<div class="col-6 col-lg">
|
||||
<div class="border rounded p-3 h-100">
|
||||
<div class="text-muted small"><?= h($label) ?></div>
|
||||
<div class="h3 mb-0">
|
||||
<?= $format === 'money' ? number_format($value / 100, 2, ',', ' ') . ' €' : $value ?></div>
|
||||
</div>
|
||||
</div><?php endforeach;?></div>
|
||||
<div class="alert alert-info py-2 small"><?= h(t('statistics.cost_metric_help')) ?></div>
|
||||
<div class="row g-4">
|
||||
<div class="col-md-6">
|
||||
<h4 class="mb-1"><?= h(t('statistics.by_structure')) ?></h4>
|
||||
<div class="text-muted small mb-2"><?= h(t('statistics.structure_cost_help')) ?></div>
|
||||
<?php foreach (($costs['by_clinic'] ?? []) as $r): $structureUrl = !empty($r['contact_id']) ? '/directory/view?' . http_build_query(['id' => (int)$r['contact_id'],'year' => $allYears ? 'all' : $year,'municipality' => $municipality]) : null; ?>
|
||||
<div class="d-flex justify-content-between align-items-start gap-3 border-bottom py-2">
|
||||
<span><?php if ($structureUrl): ?><a class="fw-semibold" href="<?= h($structureUrl) ?>"
|
||||
title="<?= h(t('statistics.open_structure_cost', ['structure' => $r['label']])) ?>"><?= h($r['label']) ?></a><?php else: ?><?= h($r['label']) ?><?php endif; ?><span
|
||||
class="d-block text-muted small"><?= h(t('statistics.animals_short', ['count' => (int)$r['animals']])) ?>
|
||||
· <?= number_format(((int)$r['average_cents']) / 100, 2, ',', ' ') ?> € /
|
||||
<?= h(mb_strtolower(t('statistics.animals'))) ?></span></span><strong
|
||||
class="text-nowrap"><?= number_format($r['total_cents'] / 100, 2, ',', ' ') ?> €</strong></div><?php endforeach;
|
||||
if (empty($costs['by_clinic'])):?><span class="text-muted"><?= h(t('statistics.no_expense')) ?></span><?php endif;?>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h4 class="mb-1"><?= h(t('statistics.by_category')) ?></h4>
|
||||
<div class="text-muted small mb-2"><?= h(t('statistics.category_cost_help')) ?></div>
|
||||
<?php foreach (($costs['by_category'] ?? []) as $r): ?><div
|
||||
class="d-flex justify-content-between align-items-start gap-3 border-bottom py-2">
|
||||
<span><?= h(PricingService::categoryLabel($r['label'])) ?><span
|
||||
class="d-block text-muted small"><?= h(t('statistics.animals_short', ['count' => (int)$r['animals']])) ?>
|
||||
· <?= number_format(((int)$r['average_cents']) / 100, 2, ',', ' ') ?> € /
|
||||
<?= h(mb_strtolower(t('statistics.animals'))) ?></span></span><strong
|
||||
class="text-nowrap"><?= number_format($r['total_cents'] / 100, 2, ',', ' ') ?> €</strong></div>
|
||||
<?php endforeach;?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if ($municipality === ''): ?><div class="card mb-4 overflow-hidden">
|
||||
<div class="card-status-top bg-green"></div>
|
||||
<div class="card-header">
|
||||
<div>
|
||||
<h3 class="card-title"><?= h(t('statistics.cost_by_city')) ?></h3>
|
||||
<div class="text-muted small"><?= h(t('statistics.cost_by_city_help')) ?></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-vcenter card-table mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><?= h(t('statistics.municipality')) ?></th>
|
||||
<th class="text-end"><?= h(t('statistics.cost_scope_animals')) ?></th>
|
||||
<th class="text-end"><?= h(t('statistics.expense_animals')) ?></th>
|
||||
<th class="text-end"><?= h(t('statistics.total_cost')) ?></th>
|
||||
<th class="text-end"><?= h(t('statistics.average_all')) ?></th>
|
||||
<th class="text-end"><?= h(t('statistics.average_with_expense')) ?></th>
|
||||
<th class="text-end"><?= h(t('statistics.median')) ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach (($municipalityCosts ?? []) as $row): $cityUrl = '/statistics?' . http_build_query(['year' => $allYears ? 'all' : $year,'municipality' => $row['municipality']]); ?>
|
||||
<tr>
|
||||
<td><a class="fw-semibold" href="<?= h($cityUrl) ?>"
|
||||
title="<?= h(t('statistics.cost_city_open', ['city' => $row['municipality']])) ?>"><?= h($row['municipality']) ?></a>
|
||||
</td>
|
||||
<td class="text-end"><?= (int)$row['animals'] ?></td>
|
||||
<td class="text-end"><?= (int)$row['animals_with_expense'] ?></td>
|
||||
<td class="text-end fw-bold"><?= number_format($row['total_cents'] / 100, 2, ',', ' ') ?> €</td>
|
||||
<td class="text-end"><?= number_format($row['average_all_cents'] / 100, 2, ',', ' ') ?> €</td>
|
||||
<td class="text-end"><?= number_format($row['average_expense_cents'] / 100, 2, ',', ' ') ?> €</td>
|
||||
<td class="text-end"><?= number_format($row['median_expense_cents'] / 100, 2, ',', ' ') ?> €</td>
|
||||
</tr><?php endforeach;?><?php if (empty($municipalityCosts)): ?><tr>
|
||||
<td colspan="7" class="text-muted text-center py-4"><?= h(t('statistics.cost_no_city')) ?></td>
|
||||
</tr><?php endif;?></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div><?php endif; ?>
|
||||
|
||||
<div class="row row-cards mb-4">
|
||||
<div class="col-lg-8"><?php if ($allYears): ?><div class="card h-100">
|
||||
<div class="card-header">
|
||||
<div>
|
||||
<h3 class="card-title"><?= h(t('statistics.annual_activity')) ?></h3>
|
||||
<div class="text-muted small"><?= h(t('statistics.annual_activity_help')) ?></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div style="height:280px"><canvas id="activityHistoryChart"
|
||||
aria-label="<?= h(t('statistics.chart_label')) ?>"></canvas></div><?php if ($undatedHandled): ?>
|
||||
<div class="text-muted small mt-3">
|
||||
<?= h(t('statistics.undated_handled', ['count' => (int)$undatedHandled])) ?></div><?php endif; ?>
|
||||
</div>
|
||||
<div class="table-responsive border-top">
|
||||
<table class="table table-vcenter mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><?= h(t('statistics.year')) ?></th>
|
||||
<th class="text-end"><?= h(t('statistics.handled')) ?></th>
|
||||
<th class="text-end"><?= h(t('statistics.entries')) ?></th>
|
||||
<th class="text-end"><?= h(t('statistics.exits')) ?></th>
|
||||
<th class="text-end"><?= h(t('statistics.adoptions')) ?></th>
|
||||
<th class="text-end"><?= h(t('statistics.deaths')) ?></th>
|
||||
<th class="text-end"><?= h(t('statistics.births')) ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody><?php foreach (array_reverse($timeline, true) as $timelineYear => $annual): ?><tr>
|
||||
<td class="fw-bold"><?= (int)$timelineYear ?></td>
|
||||
<?php foreach (['handled','entries','exits','adoptions','deaths','births'] as $key): ?><td
|
||||
class="text-end"><?= (int)$annual[$key] ?></td><?php endforeach; ?>
|
||||
</tr><?php endforeach; ?><?php if ($undatedHandled): ?><tr class="text-muted">
|
||||
<td class="fw-bold"><?= h(t('statistics.unknown_date')) ?></td>
|
||||
<td class="text-end"><?= (int)$undatedHandled ?></td>
|
||||
<td class="text-end" colspan="5">—</td>
|
||||
</tr><?php endif; ?></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div><?php else: ?><div class="card h-100">
|
||||
<div class="card-header">
|
||||
<div>
|
||||
<h3 class="card-title"><?= h(t('grant.comparison', ['year' => $year - 1])) ?></h3>
|
||||
<div class="text-muted small"><?= h(t('statistics.no_previous_percent')) ?></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-vcenter mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><?= h(t('grant.indicator')) ?></th>
|
||||
<th class="text-end"><?= $year - 1 ?></th>
|
||||
<th class="text-end"><?= $year ?></th>
|
||||
<th class="text-end"><?= h(t('grant.evolution')) ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody><?php foreach ($comparisonLabels as $key => $label): $c = $comparison[$key]; ?><tr>
|
||||
<td><?= h($label) ?></td>
|
||||
<td class="text-end"><?= (int)round($c['previous']) ?></td>
|
||||
<td class="text-end fw-bold"><?= (int)round($c['current']) ?></td>
|
||||
<td class="text-end"><span
|
||||
class="badge <?= $c['difference'] > 0 ? 'bg-blue-lt' : ($c['difference'] < 0 ? 'bg-orange-lt' : 'bg-secondary-lt') ?>"><?= $c['difference'] > 0 ? '+' : '' ?><?= (int)round($c['difference']) ?><?= $c['percent'] !== null ? ' · ' . ($c['percent'] > 0 ? '+' : '') . number_format($c['percent'], 1, ',', ' ') . ' %' : '' ?></span>
|
||||
</td>
|
||||
</tr><?php endforeach; ?></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div><?php endif; ?></div>
|
||||
<div class="col-lg-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-header">
|
||||
<div>
|
||||
<h3 class="card-title"><?= h(t('statistics.current_snapshot')) ?></h3>
|
||||
<div class="text-muted small"><?= h(t('statistics.current_snapshot_help')) ?></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="display-5 fw-bold mb-3"><?= (int)($health['total'] ?? 0) ?> <span
|
||||
class="fs-3 fw-normal text-muted"><?= h(t('statistics.animals')) ?></span></div>
|
||||
<?php foreach ([[t('statistics.free_cats'),$health['free_cats'] ?? 0,'lime'],[t('statistics.sterilized'),$health['sterilized'] ?? 0,'green'],[t('statistics.chipped'),$health['chipped'] ?? 0,'blue'],[t('statistics.fiv_positive'),$health['fiv_positive'] ?? 0,'orange'],[t('statistics.felv_positive'),$health['felv_positive'] ?? 0,'red']] as [$label,$total,$color]): $pct = ($health['total'] ?? 0) > 0 ? round($total / $health['total'] * 100) : 0; ?>
|
||||
<div class="mb-3">
|
||||
<div class="d-flex justify-content-between mb-1">
|
||||
<span><?= h($label) ?></span><strong><?= (int)$total ?> · <?= $pct ?> %</strong></div>
|
||||
<div class="progress progress-sm">
|
||||
<div class="progress-bar bg-<?= $color ?>" style="width:<?= $pct ?>%"></div>
|
||||
</div>
|
||||
</div><?php endforeach; ?><div class="alert alert-info py-2 mb-0 small">
|
||||
<?= h(t('statistics.free_cat_help')) ?></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-wrap align-items-baseline gap-2 mb-3">
|
||||
<h2 class="mb-0"><?= h(t('statistics.profiles')) ?></h2><span
|
||||
class="text-muted"><?= h($allYears ? t('statistics.profiles_help_all') : t('statistics.profiles_help_year', ['year' => (int)$year])) ?></span>
|
||||
</div>
|
||||
<div class="row row-cards"><?php foreach ($groups as $group): [$title,$items,$labels,$color] = $group;
|
||||
$linkType = $group[4] ?? null;
|
||||
$maximum = $items ? max($items) : 0;
|
||||
$sum = array_sum($items); ?><div class="col-md-6 col-xl-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-header">
|
||||
<div>
|
||||
<h3 class="card-title"><?= h($title) ?></h3>
|
||||
<div class="text-muted small">
|
||||
<?= h(t('statistics.observations', ['count' => $sum])) ?><?= $linkType === 'municipality' && $municipality === '' ? ' · ' . t('statistics.click_city') : '' ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body"><?php if (!$items): ?><div class="text-muted">
|
||||
<?= h(t('statistics.no_period_data')) ?></div><?php else: ?><div class="vstack gap-3">
|
||||
<?php foreach ($items as $label => $total): $display = $labels[$label] ?? $distributionLabels[$label] ?? $label;
|
||||
$width = $maximum > 0 ? max(4, round($total / $maximum * 100)) : 0;
|
||||
$pct = $sum > 0 ? round($total / $sum * 100) : 0;
|
||||
$cityUrl = '/statistics?' . http_build_query(['year' => $allYears ? 'all' : $year,'municipality' => $label]); ?><div>
|
||||
<div class="d-flex justify-content-between gap-3 mb-1">
|
||||
<?php if ($linkType === 'municipality' && $municipality === ''): ?><a
|
||||
class="text-truncate fw-semibold" href="<?= h($cityUrl) ?>"
|
||||
title="<?= h(t('statistics.show_city', ['city' => $display])) ?>"><?= h($display) ?></a><?php else: ?><span
|
||||
class="text-truncate"
|
||||
title="<?= h($display) ?>"><?= h($display) ?></span><?php endif; ?><strong
|
||||
class="text-nowrap"><?= (int)$total ?> <span
|
||||
class="text-muted fw-normal small"><?= $pct ?> %</span></strong></div>
|
||||
<div class="progress progress-sm">
|
||||
<div class="progress-bar bg-<?= $color ?>" style="width:<?= $width ?>%"></div>
|
||||
</div>
|
||||
</div><?php endforeach; ?></div><?php endif; ?></div>
|
||||
</div>
|
||||
</div><?php endforeach; ?></div>
|
||||
<?php if ($allYears): ?><script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const canvas = document.getElementById('activityHistoryChart');
|
||||
if (!canvas || typeof Chart === 'undefined') return;
|
||||
new Chart(canvas, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: < ? = json_encode(array_map('strval', array_keys($timeline)),
|
||||
JSON_UNESCAPED_UNICODE) ? > ,
|
||||
datasets: [{
|
||||
label: < ? = json_encode(t('statistics.handled'), JSON_UNESCAPED_UNICODE) ?
|
||||
> ,
|
||||
data: < ? = json_encode(array_values(array_map(static fn($s) => (int) $s[
|
||||
'handled'], $timeline))) ? > ,
|
||||
borderColor: '#0ca678',
|
||||
backgroundColor: 'rgba(12,166,120,.12)',
|
||||
tension: .3
|
||||
}, {
|
||||
label: < ? = json_encode(t('statistics.adoptions'),
|
||||
JSON_UNESCAPED_UNICODE) ? > ,
|
||||
data: < ? = json_encode(array_values(array_map(static fn($s) => (int) $s[
|
||||
'adoptions'], $timeline))) ? > ,
|
||||
borderColor: '#206bc4',
|
||||
backgroundColor: 'rgba(32,107,196,.12)',
|
||||
tension: .3
|
||||
}, {
|
||||
label: < ? = json_encode(t('statistics.deaths'), JSON_UNESCAPED_UNICODE) ? >
|
||||
,
|
||||
data: < ? = json_encode(array_values(array_map(static fn($s) => (int) $s[
|
||||
'deaths'], $timeline))) ? > ,
|
||||
borderColor: '#343a40',
|
||||
backgroundColor: 'rgba(52,58,64,.10)',
|
||||
tension: .3
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
interaction: {
|
||||
mode: 'index',
|
||||
intersect: false
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'bottom'
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
ticks: {
|
||||
precision: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
</script><?php endif; ?>
|
||||
68
app/Views/surgery_form.php
Normal file
68
app/Views/surgery_form.php
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
<div class="row justify-content-center">
|
||||
<div class="col-xl-9">
|
||||
<form method="post" action="/animal/surgery/save" class="card"><input type="hidden" name="csrf"
|
||||
value="<?= h(Auth::csrf()) ?>"><input type="hidden" name="animal_id" value="<?= (int)$animal['id'] ?>">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">🏥 Chirurgie — <?= h($animal['name']) ?></h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4"><label class="form-label">Catégorie</label><select class="form-select"
|
||||
name="category">
|
||||
<option value="orthopedic">Orthopédie</option>
|
||||
<option value="dental">Dentaire</option>
|
||||
<option value="soft_tissue">Viscérale / tissus mous</option>
|
||||
<option value="reproductive">Appareil reproducteur</option>
|
||||
<option value="ophthalmology">Ophtalmologie</option>
|
||||
<option value="oncology">Oncologie</option>
|
||||
<option value="imaging_guided">Radiologie / intervention guidée</option>
|
||||
<option value="wound">Plaie / reconstruction</option>
|
||||
<option value="other">Autre</option>
|
||||
</select></div>
|
||||
<div class="col-md-8"><label class="form-label">Intitulé de l’intervention *</label><input
|
||||
class="form-control" name="procedure_name" required
|
||||
placeholder="Ex. ovariectomie, détartrage, ostéosynthèse…"></div>
|
||||
<div class="col-md-4"><label class="form-label">Date *</label><input class="form-control"
|
||||
type="date" name="surgery_date" value="<?= h(date('Y-m-d')) ?>" required></div>
|
||||
<div class="col-md-4"><label class="form-label">Statut</label><select class="form-select"
|
||||
name="status">
|
||||
<option value="completed">Réalisée</option>
|
||||
<option value="planned">Planifiée</option>
|
||||
<option value="cancelled">Annulée</option>
|
||||
</select></div>
|
||||
<div class="col-md-4"><label class="form-label">Montant réel (€)</label><input class="form-control"
|
||||
inputmode="decimal" name="amount" placeholder="Calculé depuis le tarif si vide"></div>
|
||||
<div class="col-md-6"><label class="form-label">Structure</label><select class="form-select"
|
||||
name="clinic_contact_id">
|
||||
<option value="">—</option><?php foreach ($clinics as $c):?><option
|
||||
value="<?= (int)$c['id'] ?>"><?= h($c['name']) ?></option><?php endforeach?>
|
||||
</select></div>
|
||||
<div class="col-md-6"><label class="form-label">Vétérinaire</label><select class="form-select"
|
||||
name="veterinarian_contact_id">
|
||||
<option value="">—</option><?php foreach ($vets as $v):?><option
|
||||
value="<?= (int)$v['id'] ?>"
|
||||
data-organization="<?= (int)($v['organization_id'] ?? 0) ?>"><?= h($v['name']) ?>
|
||||
</option><?php endforeach?>
|
||||
</select></div>
|
||||
<div class="col-12"><label class="form-label">Tarif de référence (facultatif)</label><select
|
||||
class="form-select" name="tariff_id">
|
||||
<option value="">— Saisie libre —</option>
|
||||
<?php foreach ($tariffs as $t):$net = (int)round($t['amount_cents'] * (1 - (float)$t['discount'] / 100));?>
|
||||
<option value="<?= (int)$t['id'] ?>" data-clinic="<?= (int)$t['clinic_contact_id'] ?>">
|
||||
<?= h($t['label']) ?> — <?= number_format($net / 100, 2, ',', ' ') ?> €</option>
|
||||
<?php endforeach?>
|
||||
</select></div>
|
||||
<div class="col-md-6"><label class="form-label">Anesthésie / protocole</label><textarea
|
||||
class="form-control" name="anesthesia" rows="3"></textarea></div>
|
||||
<div class="col-md-6"><label class="form-label">Compte rendu / annotations</label><textarea
|
||||
class="form-control" name="notes" rows="3"></textarea></div>
|
||||
<div class="col-12"><label class="form-label">Suites et contrôles prévus</label><textarea
|
||||
class="form-control" name="follow_up" rows="2"></textarea></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer text-end"><a class="btn btn-link"
|
||||
href="/animal?id=<?= (int)$animal['id'] ?>">Annuler</a><button class="btn btn-primary">Enregistrer
|
||||
la chirurgie</button></div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
Loading…
Add table
Add a link
Reference in a new issue