Corriger les dossiers animaux et le suivi médical

This commit is contained in:
Alexandre NOEL 2026-10-01 15:22:09 +02:00
commit 7b89fd5dfa
34 changed files with 440 additions and 39 deletions

View file

@ -4,7 +4,7 @@
<!-- IDENTITÉ -->
<div class="tab-pane active show" id="tab-ident">
<?php if ($canViewMedical && !empty($death)): ?>
<?php if ($canViewMedical && !empty($death) && in_array(strtolower((string)$animal['status']), ['decede','décédé'], true)): ?>
<?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)));
@ -221,11 +221,15 @@ $hasCurrent = ($currentLat !== null && $currentLng !== null);
</div>
<?php if ($animal['status'] === 'adopte'): ?>
<?php if (in_array($animal['status'], ['adopte','adopté'], true) && !empty($adoption)): ?>
<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'] . ')') ?>
<?php if (!empty($adoption['adopter_contact_id'])): ?><a class="fw-bold" href="/directory/show?id=<?= (int)$adoption['adopter_contact_id'] ?>"><?= h($adoption['adopter_name']) ?></a><?php else: ?><span class="fw-bold"><?= h($adoption['adopter_name']) ?></span><?php endif; ?>
<div class="small mt-1"><?= h(trim(implode(' · ', array_filter([
trim((string)($adoption['adopter_address'] ?? '') . ' ' . (string)($adoption['adopter_postal_code'] ?? '') . ' ' . (string)($adoption['adopter_city'] ?? '')),
$adoption['adopter_phone'] ?? '', $adoption['adopter_email'] ?? ''
])))) ?></div>
</div>
<?php endif; ?>

View file

@ -22,6 +22,10 @@
<div class="col-12 col-md-3">
<div class="subheader mb-1">FeLV</div>
<span class="badge <?= $felvClass ?>"><?= h($felvLabel) ?></span>
</div>
<div class="col-12 col-md-3">
<div class="subheader mb-1"><?= h(t('animal.test_date')) ?></div>
<strong><?= !empty($animal['fiv_felv_test_date']) ? h(date('d/m/Y', strtotime((string)$animal['fiv_felv_test_date']))) : '—' ?></strong>
</div><?php endif; ?>
<div class="col-12 <?= $isCat ? 'col-md-6' : '' ?>">
<div class="subheader mb-1"><?= h(t('animal.conditions_short')) ?></div>
@ -423,6 +427,9 @@ ORDER BY created_at ASC
<!-- VACCINS -->
<div class="tab-pane" id="tab-vac">
<div class="d-flex justify-content-end mb-3">
<button class="btn btn-primary" type="button" data-bs-toggle="modal" data-bs-target="#modalVaccine">+ <?= h(t('animal.add_vaccine')) ?></button>
</div>
<?php if (!$vaccinations): ?>
<div class="text-muted"><?= h(t('animal.no_vaccine_recorded')) ?></div>
<?php else: ?>

View file

@ -300,7 +300,11 @@ $currentTime = date('H:i');
<div class="col-md-4">
<label class="form-label"><?= h(t('animal.manufacturer')) ?></label>
<input class="form-control" name="manufacturer" placeholder="Ex : Boehringer Ingelheim">
<input class="form-control" name="manufacturer" list="vaccineManufacturerList"
placeholder="Ex : Boehringer Ingelheim">
<datalist id="vaccineManufacturerList">
<?php foreach (($vaccineManufacturers ?? []) as $manufacturer): ?><option value="<?= h($manufacturer['name']) ?>"></option><?php endforeach; ?>
</datalist>
</div>
<div class="col-md-4">
@ -619,13 +623,29 @@ document.addEventListener('DOMContentLoaded', () => {
</div>
<div class="col-md-4">
<label class="form-label"><?= h(t('animal.test_date')) ?></label>
<input class="form-control" type="date" name="test_date" value="<?= date('Y-m-d') ?>">
<input class="form-control" type="date" name="test_date"
value="<?= h($animal['fiv_felv_test_date'] ?: date('Y-m-d')) ?>">
</div>
<div class="col-md-8">
<label class="form-label"><?= h(t('animal.test_note')) ?></label>
<input class="form-control" name="test_notes"
placeholder="<?= h(t('animal.test_notes_placeholder')) ?>">
</div>
<div class="col-md-6">
<label class="form-label"><?= h(t('animal.veterinary_clinic')) ?></label>
<select class="form-select" name="clinic_contact_id">
<option value="">— <?= h(t('common.choose')) ?> —</option>
<?php foreach (($directoryClinics ?? []) as $clinic): ?><option value="<?= (int)$clinic['id'] ?>"><?= h($clinic['name']) ?></option><?php endforeach; ?>
</select>
</div>
<div class="col-md-6">
<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="test_tariff_id">
<option value=""><?= h(t('animal.no_expense')) ?></option>
<?php foreach (($expenseTariffs ?? []) as $tariff): if (($tariff['category'] ?? '') !== 'test') continue; ?><option
value="<?= (int)$tariff['id'] ?>" data-clinic="<?= (int)$tariff['clinic_contact_id'] ?>"><?= h($tariff['clinic_name'] . ' · ' . $tariff['label']) ?> · <?= number_format((int)$tariff['effective_cents']/100, 2, ',', ' ') ?> €</option><?php endforeach; ?>
</select>
</div>
</div>
<hr class="my-4">
@ -694,6 +714,22 @@ document.addEventListener('DOMContentLoaded', () => {
</form>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
const modal = document.getElementById('modalMedicalStatus');
const clinic = modal?.querySelector('select[name="clinic_contact_id"]');
const tariff = modal?.querySelector('select[name="test_tariff_id"]');
const sync = () => {
[...(tariff?.options || [])].forEach(option => {
if (!option.dataset.clinic) return;
option.hidden = !clinic?.value || option.dataset.clinic !== clinic.value;
option.disabled = option.hidden;
});
if (tariff?.selectedOptions[0]?.hidden) tariff.value = '';
};
clinic?.addEventListener('change', sync); sync();
});
</script>
<div class="modal modal-blur fade" id="modalMedical" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-centered" role="document">

View file

@ -3,9 +3,9 @@
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;
</optgroup><?php endif;
$expenseClinic = $t['clinic_name'];?><optgroup label="<?= h($expenseClinic) ?>"><?php endif;?><option
value="<?= (int)$t['id'] ?>"><?= h($t['label']) ?> ·
value="<?= (int)$t['id'] ?>" data-clinic="<?= (int)$t['clinic_contact_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;
@ -13,3 +13,27 @@ if ($expenseClinic !== ''):?></optgroup><?php endif;?>
</select>
<div class="form-hint"><?= h(t('animal.expense_hint')) ?></div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('form').forEach(form => {
const clinic = form.querySelector('select[name="clinic_contact_id"]');
const tariff = form.querySelector('select[name="tariff_id"]');
if (!clinic || !tariff || tariff.dataset.clinicFilterReady) return;
tariff.dataset.clinicFilterReady = '1';
const sync = () => {
const clinicId = clinic.value;
[...tariff.options].forEach(option => {
if (!option.dataset.clinic) return;
option.hidden = !clinicId || option.dataset.clinic !== clinicId;
option.disabled = option.hidden;
});
[...tariff.querySelectorAll('optgroup')].forEach(group => {
group.hidden = ![...group.querySelectorAll('option')].some(option => !option.hidden);
});
if (tariff.selectedOptions[0]?.hidden) tariff.value = '';
};
clinic.addEventListener('change', sync);
sync();
});
});
</script>

View file

@ -16,6 +16,7 @@ if (!function_exists('settingsNavigationLinks')) {
'rooms' => ['/settings/rooms','🚪',t('settings.rooms'),'business'],
'pricing' => ['/settings/pricing','💶',t('settings.pricing'),'business'],
'inventory' => ['/settings/inventory','💊',t('settings.inventory'),'business'],
'catalogs' => ['/settings/catalogs','📚',t('settings.catalogs'),'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'],

View file

@ -27,7 +27,8 @@
<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>
<input type="text" name="adopter_name" id="adopterName" class="form-control"
placeholder="Prénom NOM" required>
</div>
<div class="mb-3">

View file

@ -184,7 +184,7 @@ if ($felvPos) {
<?php endif; ?>
<div class="text-muted mt-1" style="font-size:.7rem"><?= h($animal['internal_code']) ?></div>
<div class="text-muted small mt-1"><?= h(t('animal.form.birth_date')) ?> :
<?= !empty($animal['birth_date']) ? h(date('d/m/Y', strtotime((string)$animal['birth_date']))) : '—' ?></div>
<?= !empty($animal['birth_date']) ? (!empty($animal['birth_is_estimated']) ? '~ ' : '') . h(date('d/m/Y', strtotime((string)$animal['birth_date']))) : '—' ?></div>
</div>
<div>

View file

@ -118,6 +118,28 @@ $evening = $careToday['evening'] ?? ['expected' => 0,'checked' => 0,'alerts' =>
</div><?php endif; ?>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('.reminder-list').forEach(table => {
const rows = [...table.tBodies[0].rows];
const limit = Number(table.dataset.visibleRows || 6);
if (rows.length <= limit) return;
rows.slice(limit).forEach(row => row.classList.add('d-none'));
const button = document.createElement('button');
button.type = 'button'; button.className = 'btn btn-sm btn-link w-100';
button.textContent = <?= json_encode(t('common.show_more'), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
button.addEventListener('click', () => {
const expanded = button.dataset.expanded === '1';
rows.slice(limit).forEach(row => row.classList.toggle('d-none', expanded));
button.dataset.expanded = expanded ? '0' : '1';
button.textContent = expanded
? <?= json_encode(t('common.show_more'), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>
: <?= json_encode(t('common.show_less'), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
});
table.closest('.table-responsive')?.after(button);
});
});
</script>
<div class="row g-2 mb-3">
<div class="col">
@ -264,7 +286,7 @@ if ($grantAlertTotal > 0): ?><div class="alert alert-warning d-flex flex-wrap al
<h3 class="card-title">🚨 <?= h(t('dashboard.vaccines_overdue')) ?></h3>
</div>
<div class="table-responsive">
<table class="table table-vcenter card-table">
<table class="table table-vcenter card-table reminder-list" data-visible-rows="6">
<tbody>
<?php if (empty($vaccines_overdue)): ?>
<tr>
@ -290,7 +312,7 @@ if ($grantAlertTotal > 0): ?><div class="alert alert-warning d-flex flex-wrap al
<h3 class="card-title">⏳ <?= h(t('dashboard.vaccines_soon')) ?></h3>
</div>
<div class="table-responsive">
<table class="table table-vcenter card-table">
<table class="table table-vcenter card-table reminder-list" data-visible-rows="6">
<tbody>
<?php if (empty($vaccines_7d)): ?>
<tr>
@ -315,7 +337,7 @@ if ($grantAlertTotal > 0): ?><div class="alert alert-warning d-flex flex-wrap al
<h3 class="card-title">📅 <?= h(t('dashboard.vaccines_month')) ?></h3>
</div>
<div class="table-responsive">
<table class="table table-vcenter card-table">
<table class="table table-vcenter card-table reminder-list" data-visible-rows="6">
<tbody>
<?php if (empty($vaccines_30d)): ?>
<tr>

View file

@ -31,10 +31,10 @@
<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.28</span>
<h3>Saisies médicales corrigibles</h3>
<p class="text-muted">Vaccinations et traitements antiparasitaires peuvent être rectifiés sans
déclencher une seconde sortie de stock ou une nouvelle dépense.</p>
<span class="badge bg-blue-lt mb-2">1.0.0-rc.30</span>
<h3>Données fiabilisées sur le terrain</h3>
<p class="text-muted">Décès historiques corrigibles, synchronisation I-CAD réparée, catalogues
médicaux administrables et tarifs filtrés par clinique.</p>
</div>
</section>
</div>
@ -43,8 +43,8 @@
<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>
<p class="text-muted">Réinitialisation planifiée des démonstrations, normalisation assistée des
contacts, validation des paquets YunoHost, ZimaOS et CasaOS, puis amélioration continue des parcours.</p>
</div>
</section>
</div>

View file

@ -0,0 +1,17 @@
<?php require __DIR__ . '/settings_nav.php';
$labels = ['medication'=>t('animal.medication'),'vaccine'=>t('animal.vaccine'),'dewormer'=>t('animals.dewormer'),'manufacturer'=>t('animal.manufacturer')];
$extras = ['medication'=>t('animal.molecule'),'vaccine'=>t('catalog.code'),'dewormer'=>t('catalog.active_ingredient'),'manufacturer'=>'']; ?>
<?php if ($saved): ?><div class="alert alert-success"><?= h(t('catalog.saved')) ?></div><?php endif; ?>
<?php if ($error !== ''): ?><div class="alert alert-danger"><?= h($error) ?></div><?php endif; ?>
<div class="mb-3"><h2 class="mb-1"><?= h(t('settings.catalogs')) ?></h2><div class="text-muted"><?= h(t('catalog.admin_help')) ?></div></div>
<div class="row g-3">
<?php foreach ($catalogs as $type => $rows): ?><div class="col-12 col-xl-6"><div class="card h-100">
<div class="card-header"><h3 class="card-title"><?= h($labels[$type]) ?></h3></div><div class="card-body">
<form class="row g-2 mb-3" method="post" action="/settings/catalogs/save"><input type="hidden" name="csrf" value="<?= h(Auth::csrf()) ?>"><input type="hidden" name="type" value="<?= h($type) ?>">
<div class="<?= $type === 'manufacturer' ? 'col-9' : 'col-6' ?>"><input class="form-control" name="name" placeholder="<?= h(t('common.name')) ?>" required></div>
<?php if ($type !== 'manufacturer'): ?><div class="col-3"><input class="form-control" name="extra" placeholder="<?= h($extras[$type]) ?>"></div><?php endif; ?><div class="col-3"><button class="btn btn-primary w-100">+ <?= h(t('common.add')) ?></button></div></form>
<div class="table-responsive"><table class="table table-sm table-vcenter"><tbody><?php foreach ($rows as $row): ?><tr><td>
<form class="d-flex gap-2" method="post" action="/settings/catalogs/save"><input type="hidden" name="csrf" value="<?= h(Auth::csrf()) ?>"><input type="hidden" name="type" value="<?= h($type) ?>"><input type="hidden" name="id" value="<?= (int)$row['id'] ?>"><input class="form-control form-control-sm" name="name" value="<?= h($row['name']) ?>" required><?php if ($type !== 'manufacturer'): ?><input class="form-control form-control-sm" name="extra" value="<?= h($row['extra'] ?? '') ?>" placeholder="<?= h($extras[$type]) ?>"><?php endif; ?><button class="btn btn-sm btn-outline-primary">✓</button></form>
</td><td class="w-1"><form method="post" action="/settings/catalogs/delete" onsubmit="return confirm(<?= h(json_encode(t('catalog.delete_confirm'), JSON_HEX_APOS|JSON_HEX_QUOT)) ?>)"><input type="hidden" name="csrf" value="<?= h(Auth::csrf()) ?>"><input type="hidden" name="type" value="<?= h($type) ?>"><input type="hidden" name="id" value="<?= (int)$row['id'] ?>"><button class="btn btn-sm btn-outline-danger">×</button></form></td></tr><?php endforeach; ?></tbody></table></div>
</div></div></div><?php endforeach; ?>
</div>

View file

@ -8,6 +8,7 @@ $settingsSection = match(true) {
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/catalogs') => 'catalogs',
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',

View file

@ -173,6 +173,14 @@ const insertImage = (button) => {
textarea.setRangeText(`![${safeAlt}](${button.dataset.url})\n`, textarea.selectionStart, textarea.selectionEnd, 'end');
textarea.focus();
};
document.querySelectorAll('textarea').forEach((textarea) => {
const resize = () => {
textarea.style.height = 'auto';
textarea.style.height = Math.max(140, textarea.scrollHeight + 8) + 'px';
};
textarea.addEventListener('input', resize);
resize();
});
document.querySelectorAll('.public-rich-text').forEach((textarea) => {
textarea.addEventListener('focus', () => { activePublicEditor = textarea; });
const toolbar = document.createElement('div');

View file

@ -8,7 +8,7 @@
<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">
name="category" id="surgeryCategory">
<option value="orthopedic">Orthopédie</option>
<option value="dental">Dentaire</option>
<option value="soft_tissue">Viscérale / tissus mous</option>
@ -20,8 +20,9 @@
<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
class="form-control" name="procedure_name" id="surgeryProcedure" list="surgeryProcedureList" required
placeholder="Ex. ovariectomie, détartrage, ostéosynthèse…"></div>
<datalist id="surgeryProcedureList"></datalist>
<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"
@ -29,7 +30,7 @@
<option value="completed">Réalisée</option>
<option value="planned">Planifiée</option>
<option value="cancelled">Annulée</option>
</select></div>
</select><div class="form-hint">Les tarifs affichés correspondent à la structure sélectionnée.</div></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"
@ -66,3 +67,37 @@
</form>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
const clinic = document.querySelector('select[name="clinic_contact_id"]');
const tariff = document.querySelector('select[name="tariff_id"]');
const category = document.getElementById('surgeryCategory');
const list = document.getElementById('surgeryProcedureList');
const procedures = {
orthopedic: ['Amputation','Arthrodèse','Ostéosynthèse','Réduction de fracture','Retrait de matériel'],
dental: ['Avulsion dentaire','Détartrage','Extraction dentaire'],
soft_tissue: ['Cystotomie','Entérectomie','Gastrotomie','Hernie','Laparotomie','Splénectomie'],
reproductive: ['Castration','Césarienne','Ovariectomie','Ovario-hystérectomie'],
ophthalmology: ['Énucléation','Entropion'], oncology: ['Exérèse de masse','Mastectomie'],
imaging_guided: ['Biopsie guidée','Ponction guidée'], wound: ['Parage de plaie','Suture','Reconstruction cutanée'],
other: ['Autre']
};
const syncTariffs = () => {
const id = clinic?.value || '';
[...(tariff?.options || [])].forEach(option => {
if (!option.dataset.clinic) return;
option.hidden = !id || option.dataset.clinic !== id;
option.disabled = option.hidden;
});
if (tariff?.selectedOptions[0]?.hidden) tariff.value = '';
};
const syncProcedures = () => {
if (!list) return;
list.replaceChildren(...(procedures[category?.value] || ['Autre']).map(label => {
const option = document.createElement('option'); option.value = label; return option;
}));
};
clinic?.addEventListener('change', syncTariffs); category?.addEventListener('change', syncProcedures);
syncTariffs(); syncProcedures();
});
</script>