feat: permettre la correction des soins antiparasitaires

This commit is contained in:
Alexandre NOEL 2026-09-15 22:15:59 +02:00
commit f20d7cc965
14 changed files with 233 additions and 33 deletions

View file

@ -562,15 +562,30 @@ trait AnimalMedicalActions
$db = DB::pdo();
$id = (int) ($_POST['animal_id'] ?? 0);
$entryId = (int) ($_POST['entry_id'] ?? 0);
$vacId = (int) ($_POST['vaccine_id'] ?? 0);
if ($id <= 0 || $vacId <= 0) {
http_response_code(400);
echo h(t('error.bad_request'));
return;
}
if ($entryId > 0) {
$stmt = $db->prepare('SELECT 1 FROM vaccinations WHERE id=:entry AND animal_id=:animal');
$stmt->execute([':entry' => $entryId, ':animal' => $id]);
if (!$stmt->fetchColumn()) {
http_response_code(404);
echo h(t('error.not_found'));
return;
}
}
$done = trim((string) ($_POST['done_date'] ?? date('Y-m-d')));
$due = trim((string) ($_POST['due_date'] ?? ''));
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $done) || ($due !== '' && !preg_match('/^\d{4}-\d{2}-\d{2}$/', $due))) {
http_response_code(400);
echo h(t('error.bad_request'));
return;
}
// Si due_date est fourni, on respecte.
// Sinon auto selon protocole.
if ($due === '') {
@ -602,9 +617,9 @@ trait AnimalMedicalActions
SELECT COUNT(*)
FROM vaccinations v
JOIN ref_vaccines rv ON rv.id = v.vaccine_id
WHERE v.animal_id = :aid AND upper(coalesce(rv.family,'')) = 'L'
WHERE v.animal_id = :aid AND v.id <> :entry AND upper(coalesce(rv.family,'')) = 'L'
");
$stmt->execute([':aid' => $id]);
$stmt->execute([':aid' => $id, ':entry' => $entryId]);
$countL = (int) $stmt->fetchColumn();
$isPrimoL = $countL === 0;
}
@ -658,11 +673,7 @@ trait AnimalMedicalActions
}
$vet_name = $vetContact['name'] ?? $vet_name;
$stmt = $db->prepare('
INSERT INTO vaccinations(animal_id, vaccine_id, done_date, due_date, lot,manufacturer,batch_expires_on, notes, clinic_id, vet_name, clinic_contact_id, vet_contact_id, created_by)
VALUES(:aid,:vid,:done,:due,:lot,:manufacturer,:batch_expiry,:notes,:clinic_id,:vet_name,:clinic_contact_id,:vet_contact_id,:created_by)
');
$stmt->execute([
$values = [
':aid' => $id,
':vid' => $vacId,
':done' => $done,
@ -676,21 +687,38 @@ trait AnimalMedicalActions
':clinic_contact_id' => $clinicContact['id'] ?? null,
':vet_contact_id' => $vetContact['id'] ?? null,
':created_by' => Auth::id(),
]);
InventoryService::consumeBatch(
(int) ($_POST['inventory_batch_id'] ?? 0),
(float) str_replace(',', '.', (string) ($_POST['inventory_quantity'] ?? 0)),
$id,
'Vaccination du ' . $done,
);
if ((int) ($_POST['tariff_id'] ?? 0) > 0) {
PricingService::record($db, $id, (int) $_POST['tariff_id'], 1, $done, 'Ajoutée avec la vaccination');
];
if ($entryId > 0) {
unset($values[':aid'], $values[':created_by']);
$values[':entry'] = $entryId;
$stmt = $db->prepare('
UPDATE vaccinations SET vaccine_id=:vid,done_date=:done,due_date=:due,lot=:lot,
manufacturer=:manufacturer,batch_expires_on=:batch_expiry,notes=:notes,clinic_id=:clinic_id,
vet_name=:vet_name,clinic_contact_id=:clinic_contact_id,vet_contact_id=:vet_contact_id
WHERE id=:entry
');
$stmt->execute($values);
} else {
$stmt = $db->prepare('
INSERT INTO vaccinations(animal_id, vaccine_id, done_date, due_date, lot,manufacturer,batch_expires_on, notes, clinic_id, vet_name, clinic_contact_id, vet_contact_id, created_by)
VALUES(:aid,:vid,:done,:due,:lot,:manufacturer,:batch_expiry,:notes,:clinic_id,:vet_name,:clinic_contact_id,:vet_contact_id,:created_by)
');
$stmt->execute($values);
InventoryService::consumeBatch(
(int) ($_POST['inventory_batch_id'] ?? 0),
(float) str_replace(',', '.', (string) ($_POST['inventory_quantity'] ?? 0)),
$id,
'Vaccination du ' . $done,
);
if ((int) ($_POST['tariff_id'] ?? 0) > 0) {
PricingService::record($db, $id, (int) $_POST['tariff_id'], 1, $done, 'Ajoutée avec la vaccination');
}
}
self::logHistory(
$id,
'vaccine',
'Vaccin ajouté',
$entryId > 0 ? 'Vaccin corrigé' : 'Vaccin ajouté',
"Vaccin #$vacId\nFait le : $done" .
($due !== '' ? "\nRappel : $due" : '') .
($manufacturer !== '' ? "\nFabricant : $manufacturer" : '') .

View file

@ -182,6 +182,7 @@ trait AnimalRecordActions
'adoption_unavailability_reason' => '',
'website_published' => 0,
'website_description' => '',
'breed' => 'Chat Européen',
'color' => '',
'hair_type' => '',
'birth_date' => '',

View file

@ -17,6 +17,7 @@ final class DewormingController
}
$db = DB::pdo();
$animalId = (int) ($_POST['animal_id'] ?? 0);
$entryId = (int) ($_POST['entry_id'] ?? 0);
$date = trim((string) ($_POST['administered_on'] ?? ''));
$dewormer = (int) ($_POST['dewormer_id'] ?? 0);
if (!$animalId || !self::date($date)) {
@ -31,6 +32,15 @@ final class DewormingController
echo h(t('error.animal_not_found'));
return;
}
if ($entryId > 0) {
$s = $db->prepare('SELECT 1 FROM dewormings WHERE id=? AND animal_id=?');
$s->execute([$entryId, $animalId]);
if (!$s->fetchColumn()) {
http_response_code(404);
echo h(t('error.not_found'));
return;
}
}
if (isset($_POST['create_dewormer'])) {
$name = trim((string) ($_POST['new_dewormer_name'] ?? ''));
if ($name === '') {
@ -59,7 +69,7 @@ final class DewormingController
}
$ids = [$animalId];
$litterId = null;
if (isset($_POST['apply_litter'])) {
if ($entryId === 0 && isset($_POST['apply_litter'])) {
$s = $db->prepare(
'SELECT l.id FROM litters l LEFT JOIN litter_kittens lk ON lk.litter_id=l.id WHERE l.mother_id=? OR lk.animal_id=? ORDER BY l.id DESC LIMIT 1',
);
@ -85,8 +95,36 @@ final class DewormingController
'INSERT INTO dewormings(animal_id,dewormer_id,administered_on,dose_text,weight_kg,next_due_date,notes,source_litter_id,created_by) VALUES(?,?,?,?,?,?,?,?,?)',
);
$history = $db->prepare(
"INSERT INTO animal_history(animal_id,action,type,label,details,user_id) VALUES(?,'deworming','deworming','Vermifuge administré',?,?)",
"INSERT INTO animal_history(animal_id,action,type,label,details,user_id) VALUES(?,'deworming','deworming','Vermifuge / anti-puce administré',?,?)",
);
if ($entryId > 0) {
$s = $db->prepare('SELECT birth_date FROM animals WHERE id=?');
$s->execute([$animalId]);
$birthDate = $s->fetchColumn() ?: null;
$next = $manual ?: self::nextDue(is_string($birthDate) ? $birthDate : null, $date);
$db->prepare(
"UPDATE dewormings SET dewormer_id=?,administered_on=?,dose_text=?,weight_kg=?,next_due_date=?,notes=?,updated_at=datetime('now') WHERE id=? AND animal_id=?",
)->execute([
$dewormer ?: null,
$date,
trim((string) ($_POST['dose_text'] ?? '')) ?: null,
($_POST['weight_kg'] ?? '') !== '' ? (float) $_POST['weight_kg'] : null,
$next,
trim((string) ($_POST['notes'] ?? '')) ?: null,
$entryId,
$animalId,
]);
$db->prepare(
"INSERT INTO animal_history(animal_id,action,type,label,details,user_id) VALUES(?,'deworming_updated','deworming','Vermifuge / anti-puce corrigé',?,?)",
)->execute([
$animalId,
'Administration : ' . self::fr($date) . ($next ? ' · Prochaine échéance : ' . self::fr($next) : ''),
Auth::id(),
]);
$db->commit();
header('Location: /animal?id=' . $animalId . '#tab-med');
exit();
}
foreach ($ids as $id) {
$s = $db->prepare('SELECT name,birth_date FROM animals WHERE id=? AND deleted_at IS NULL');
$s->execute([$id]);
@ -117,7 +155,7 @@ final class DewormingController
(int) ($_POST['inventory_batch_id'] ?? 0),
$perAnimal * count($ids),
$animalId,
'Vermifuge du ' . $date . ($litterId ? ' · portée #' . $litterId : ''),
'Vermifuge / anti-puce du ' . $date . ($litterId ? ' · portée #' . $litterId : ''),
);
$db->commit();
header('Location: /animal?id=' . $animalId . '#tab-med');

View file

@ -12,7 +12,7 @@ final class PricingService
'euthanasia' => 'Euthanasie',
'cremation' => 'Crémation',
'medication' => 'Médicament',
'dewormer' => 'Vermifuge',
'dewormer' => 'Vermifuge / anti-puce',
'other' => 'Autre',
];
public const GROUPS = [

View file

@ -218,6 +218,7 @@
<th><?= h(t('animal.dose')) ?></th>
<th><?= h(t('common.weight')) ?></th>
<th><?= h(t('animal.recorded_by')) ?></th>
<th><span class="visually-hidden"><?= h(t('common.actions')) ?></span></th>
</tr>
</thead>
<tbody><?php foreach ($dewormings as $d): ?><tr>
@ -227,6 +228,16 @@
<td><?= $d['weight_kg'] !== null ? h(number_format((float)$d['weight_kg'], 2, ',', ' ')) . ' kg' : '—' ?>
</td>
<td><?= h($d['actor_name'] ?: '—') ?></td>
<td class="text-end"><button type="button" class="btn btn-sm btn-ghost-primary edit-deworming"
data-bs-toggle="modal" data-bs-target="#modalDeworming"
data-entry-id="<?= (int)$d['id'] ?>"
data-dewormer-id="<?= (int)($d['dewormer_id'] ?? 0) ?>"
data-administered-on="<?= h($d['administered_on']) ?>"
data-dose-text="<?= h($d['dose_text'] ?? '') ?>"
data-weight-kg="<?= h($d['weight_kg'] ?? '') ?>"
data-next-due-date="<?= h($d['next_due_date'] ?? '') ?>"
data-notes="<?= h($d['notes'] ?? '') ?>"
aria-label="<?= h(t('common.edit')) ?>">✏️</button></td>
</tr><?php endforeach; ?></tbody>
</table>
</div><?php endif; ?>
@ -362,6 +373,7 @@ ORDER BY created_at ASC
<th><?= h(t('animal.start')) ?></th>
<th><?= h(t('dashboard.end')) ?></th>
<th><?= h(t('common.notes')) ?></th>
<th><span class="visually-hidden"><?= h(t('common.actions')) ?></span></th>
</tr>
</thead>
<tbody>
@ -426,6 +438,16 @@ ORDER BY created_at ASC
</div><?php endif; ?></td>
<td><?= !empty($v['clinic_name']) ? h($v['clinic_name']) : '—' ?></td>
<td><?= $v['notes'] ? h($v['notes']) : '—' ?></td>
<td class="text-end"><button type="button" class="btn btn-sm btn-ghost-primary edit-vaccine"
data-bs-toggle="modal" data-bs-target="#modalVaccine"
data-entry-id="<?= (int)$v['id'] ?>" data-vaccine-id="<?= (int)$v['vaccine_id'] ?>"
data-done-date="<?= h($v['done_date']) ?>" data-due-date="<?= h($v['due_date'] ?? '') ?>"
data-clinic-contact-id="<?= (int)($v['clinic_contact_id'] ?? 0) ?>"
data-vet-contact-id="<?= (int)($v['vet_contact_id'] ?? 0) ?>"
data-lot="<?= h($v['lot'] ?? '') ?>" data-manufacturer="<?= h($v['manufacturer'] ?? '') ?>"
data-batch-expires-on="<?= h($v['batch_expires_on'] ?? '') ?>"
data-notes="<?= h($v['notes'] ?? '') ?>"
aria-label="<?= h(t('common.edit')) ?>">✏️</button></td>
</tr>
<?php endforeach; ?>
</tbody>

View file

@ -156,6 +156,7 @@ $currentTime = date('H:i');
class="btn-close" data-bs-dismiss="modal" aria-label="<?= h(t('common.close')) ?>"></button>
</div>
<div class="modal-body"><input type="hidden" name="animal_id" value="<?= (int)$animal['id'] ?>">
<input type="hidden" name="entry_id" value="">
<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') ?>"
@ -173,7 +174,7 @@ $currentTime = date('H:i');
<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-md-8"><label class="form-label"><?= h(t('inventory.batch')) ?></label><select
<div class="col-md-8" data-new-entry-only><label class="form-label"><?= h(t('inventory.batch')) ?></label><select
class="form-select" name="inventory_batch_id">
<option value="">—</option><?php foreach (($inventoryBatches ?? []) as $b) {
if ($b['category'] === 'dewormer'): ?><option value="<?= (int)$b['id'] ?>">
@ -181,13 +182,13 @@ $currentTime = date('H:i');
</option><?php endif;
} ?>
</select></div>
<div class="col-md-4"><label class="form-label"><?= h(t('inventory.quantity')) ?></label><input
<div class="col-md-4" data-new-entry-only><label class="form-label"><?= h(t('inventory.quantity')) ?></label><input
class="form-control" type="number" min="0" step="0.01" name="inventory_quantity"></div>
<div class="col-12"><label class="form-label"><?= h(t('animal.next_due')) ?> <span
class="text-muted">(<?= h(mb_strtolower(t('common.optional'))) ?>)</span></label><input
class="form-control" type="date" name="next_due_date">
<div class="form-hint"><?= h(t('animal.next_due_hint')) ?></div>
</div><?php if (!empty($litters)): ?><div class="col-12"><label class="form-check"><input
</div><?php if (!empty($litters)): ?><div class="col-12" data-new-entry-only><label class="form-check"><input
class="form-check-input" type="checkbox" name="apply_litter" value="1"><span
class="form-check-label"><?= h(t('animal.apply_litter')) ?></span></label></div>
<?php endif; ?><div class="col-12"><label
@ -257,6 +258,7 @@ $currentTime = date('H:i');
<div class="modal-body">
<input type="hidden" name="animal_id" value="<?= (int)$animal['id'] ?>">
<input type="hidden" name="entry_id" value="">
<div class="row g-2">
<div class="col-md-6">
@ -284,7 +286,7 @@ $currentTime = date('H:i');
<?php $directoryFieldId = 'vaccine';
require __DIR__ . '/_directory_medical_fields.php'; ?>
<?php require __DIR__ . '/_expense_tariff_field.php'; ?>
<div data-new-entry-only style="display: contents"><?php require __DIR__ . '/_expense_tariff_field.php'; ?></div>
<div class="col-md-4">
<label class="form-label"><?= h(t('animal.batch')) ?></label>
@ -301,7 +303,7 @@ $currentTime = date('H:i');
<input class="form-control" type="date" name="batch_expires_on">
</div>
<div class="col-md-8"><label class="form-label"><?= h(t('inventory.batch')) ?></label><select
<div class="col-md-8" data-new-entry-only><label class="form-label"><?= h(t('inventory.batch')) ?></label><select
class="form-select" name="inventory_batch_id">
<option value="">—</option><?php foreach (($inventoryBatches ?? []) as $b) {
if ($b['category'] === 'vaccine'): ?><option value="<?= (int)$b['id'] ?>">
@ -309,7 +311,7 @@ $currentTime = date('H:i');
</option><?php endif;
} ?>
</select></div>
<div class="col-md-4"><label class="form-label"><?= h(t('inventory.quantity')) ?></label><input
<div class="col-md-4" data-new-entry-only><label class="form-label"><?= h(t('inventory.quantity')) ?></label><input
class="form-control" type="number" min="0" step="0.01" name="inventory_quantity"></div>
<div class="col-md-12">
@ -328,6 +330,47 @@ $currentTime = date('H:i');
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
const prepareEditor = (modalId, triggerClass, fields) => {
const modal = document.getElementById(modalId);
if (!modal) return;
modal.addEventListener('show.bs.modal', event => {
const form = modal.querySelector('form');
const trigger = event.relatedTarget;
if (!form) return;
form.reset();
const editing = trigger?.classList.contains(triggerClass) ?? false;
form.elements.entry_id.value = editing ? (trigger.dataset.entryId || '') : '';
fields.forEach(([name, dataName]) => {
if (editing && form.elements[name]) form.elements[name].value = trigger.dataset[dataName] || '';
});
modal.querySelectorAll('[data-new-entry-only]').forEach(element => {
element.classList.toggle('d-none', editing);
element.querySelectorAll('input,select,textarea').forEach(input => input.disabled = editing);
});
if (editing) {
modal.querySelectorAll('.directory-contact-select').forEach(select => {
select.dispatchEvent(new Event('change', {bubbles: true}));
});
}
});
};
prepareEditor('modalDeworming', 'edit-deworming', [
['dewormer_id', 'dewormerId'], ['administered_on', 'administeredOn'],
['dose_text', 'doseText'], ['weight_kg', 'weightKg'],
['next_due_date', 'nextDueDate'], ['notes', 'notes'],
]);
prepareEditor('modalVaccine', 'edit-vaccine', [
['vaccine_id', 'vaccineId'], ['done_date', 'doneDate'], ['due_date', 'dueDate'],
['clinic_contact_id', 'clinicContactId'], ['vet_contact_id', 'vetContactId'],
['lot', 'lot'], ['manufacturer', 'manufacturer'],
['batch_expires_on', 'batchExpiresOn'], ['notes', 'notes'],
]);
});
</script>
<div class="modal modal-blur fade" id="modalPrescription" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-centered">
<form class="modal-content" method="post" action="/animal/prescription/save" enctype="multipart/form-data">

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.27</span>
<h3>Recherche tolérante</h3>
<p class="text-muted">Les fiches sont retrouvées par nom, code ou identification sans que les
accents ni les différences de casse empêchent le résultat.</p>
<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>
</div>
</section>
</div>