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

@ -2,6 +2,17 @@
Toutes les modifications importantes du projet sont consignées ici. Le format s’inspire de [Keep a Changelog](https://keepachangelog.com/fr/1.1.0/) et le projet suit le [versionnage sémantique](https://semver.org/lang/fr/).
## [1.0.0-rc.28] — 2026-09-15
### Saisies médicales corrigibles
- possibilité de modifier une vaccination ou une administration de vermifuge / anti-puce directement depuis la fiche animale ;
- préremplissage du formulaire et contrôle de l’appartenance de l’entrée à l’animal ;
- absence de nouveau déstockage, de nouvelle dépense ou d’application à la portée pendant une correction ;
- ajout d’une trace de correction dans l’historique ;
- harmonisation du libellé « Vermifuge / anti-puce » dans toute l’interface ;
- sélection de « Chat Européen » comme race initiale lors de la création d’une fiche de chat.
## [1.0.0-rc.27] — 2026-09-15
### Recherche globale des animaux

View file

@ -10,6 +10,10 @@ L’import depuis Animal Shelter Manager a été conçu comme un parcours pruden
Avant la version 1.0, le projet a fait l’objet d’un nettoyage structurel et d’un découpage de ses fichiers les plus volumineux. La recherche universelle, les salles configurables, les traductions française et anglaise et une suite de contrôles isolés ont complété cette phase de stabilisation.
## `1.0.0-rc.28` — Corrections des saisies médicales
Les vaccinations et administrations de vermifuges ou d’anti-puces peuvent être corrigées sans provoquer une seconde sortie de stock ni une nouvelle dépense. Les nouvelles fiches de chats proposent également la race Chat Européen par défaut.
## `1.0.0-rc.27` — Recherche tolérante
La recherche globale retrouve les animaux par leur numéro de puce ou de tatouage et rapproche les termes indépendamment de leur casse et de leurs accents.

View file

@ -200,7 +200,7 @@ L’option `--apply` ne doit être utilisée qu’après lecture des rapports. L
## État du projet
La candidate actuelle est `1.0.0-rc.27`. La branche 1.0 est gelée fonctionnellement : seuls les défauts bloquants découverts pendant les validations terrain sont corrigés avant `1.0.0`.
La candidate actuelle est `1.0.0-rc.28`. La branche 1.0 est gelée fonctionnellement : seuls les défauts bloquants découverts pendant les validations terrain sont corrigés avant `1.0.0`.
Le contrôle de stabilisation est disponible dans [docs/AUDIT-1.0.md](docs/AUDIT-1.0.md).

View file

@ -1 +1 @@
1.0.0-rc.27
1.0.0-rc.28

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,7 +687,23 @@ trait AnimalMedicalActions
':clinic_contact_id' => $clinicContact['id'] ?? null,
':vet_contact_id' => $vetContact['id'] ?? null,
':created_by' => Auth::id(),
]);
];
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)),
@ -686,11 +713,12 @@ trait AnimalMedicalActions
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>

View file

@ -210,6 +210,24 @@ return [
'litter.detach'=>'Detach',
'litter.detach_confirm'=>'Remove this cat from the litter? Its record and data will be preserved.',
'litter.parent_is_kitten'=>'A parent cannot also be a kitten in this litter.',
'animals.dewormer'=>'Deworming / flea treatment',
'animal.deworming_record'=>'Record deworming / flea treatment',
'animal.no_deworming'=>'No deworming / flea treatment recorded.',
'animal.latest_deworming'=>'Latest deworming / flea treatment: :date',
'pricing.category.dewormer'=>'Deworming / flea treatment',
'deworm.catalog_record'=>'Record deworming / flea treatment',
'deworm.product'=>'Deworming / flea treatment',
'deworm.add_catalog'=>'Add a new deworming / flea treatment product to the catalogue',
'deworm.name'=>'Deworming / flea treatment product name',
'deworm.invalid_date'=>'Invalid deworming / flea treatment date.',
'deworm.name_required'=>'The deworming / flea treatment product name is required.',
'notification.dewormings_due'=>'Deworming / flea treatment due or within 7 days',
'inventory.intro'=>'Track medicines, deworming / flea treatments and vaccines separately, including batches, expiry dates, costs and movements.',
'inventory.dewormers'=>'Deworming / flea treatments',
'inventory.dewormer'=>'Deworming / flea treatment',
'agenda.deworming'=>'Deworming / flea treatment',
'agenda.deworming_reminder'=>'Deworming / flea treatment: :product',
'agenda.type_deworming'=>'Next deworming / flea treatment',
'animal.form.change_pending'=>'Ownership change pending',
'animal.form.owner_changed'=>'Owner changed',
'animal.registration_change_pending'=>'I-CAD: ownership change pending',

View file

@ -210,6 +210,24 @@ return [
'litter.detach'=>'Détacher',
'litter.detach_confirm'=>'Retirer ce chat de la portée ? Sa fiche et ses données seront conservées.',
'litter.parent_is_kitten'=>'Un parent ne peut pas être également chaton de cette portée.',
'animals.dewormer'=>'Vermifuge / anti-puce',
'animal.deworming_record'=>'Enregistrer un vermifuge / anti-puce',
'animal.no_deworming'=>'Aucun vermifuge / anti-puce enregistré.',
'animal.latest_deworming'=>'Dernier vermifuge / anti-puce : :date',
'pricing.category.dewormer'=>'Vermifuge / anti-puce',
'deworm.catalog_record'=>'Enregistrer un vermifuge / anti-puce',
'deworm.product'=>'Vermifuge / anti-puce',
'deworm.add_catalog'=>'Ajouter un vermifuge / anti-puce au référentiel',
'deworm.name'=>'Nom du vermifuge / anti-puce',
'deworm.invalid_date'=>'Date de vermifuge / anti-puce invalide.',
'deworm.name_required'=>'Le nom du vermifuge / anti-puce est obligatoire.',
'notification.dewormings_due'=>'Vermifuges / anti-puces échus ou sous 7 jours',
'inventory.intro'=>'Suivez séparément médicaments, vermifuges / anti-puces et vaccins, leurs lots, péremptions, coûts et mouvements.',
'inventory.dewormers'=>'Vermifuges / anti-puces',
'inventory.dewormer'=>'Vermifuge / anti-puce',
'agenda.deworming'=>'Vermifuge / anti-puce',
'agenda.deworming_reminder'=>'Vermifuge / anti-puce : :product',
'agenda.type_deworming'=>'Prochain vermifuge / anti-puce',
'animal.form.change_pending'=>'En attente de changement',
'animal.form.owner_changed'=>'Propriétaire changé',
'animal.registration_change_pending'=>'I-CAD : en attente de changement',

View file

@ -153,6 +153,10 @@ try {
'Les cinq box initiaux ne sont pas disponibles',
);
});
$test('Une nouvelle fiche de chat propose la race Chat Européen', function () use ($assert): void {
$controller = (string) file_get_contents(dirname(__DIR__) . '/app/Controllers/AnimalRecordActions.php');
$assert(str_contains($controller, "'breed' => 'Chat Européen'"));
});
$test('Les tournées acceptent quatre créneaux configurables', function () use ($db, $assert): void {
foreach (ShelterRoomService::PERIODS as $period) {
$db->prepare('INSERT INTO care_rounds(round_date,period) VALUES(?,?)')->execute(['2099-01-01', $period]);
@ -198,6 +202,19 @@ try {
$assert(!str_contains($modal, 'name="administered_by_name"'));
$assert(str_contains($summary, "\$v['clinic_name']"));
});
$test('Vaccins et vermifuges peuvent être corrigés sans nouveau déstockage', function () use ($assert): void {
$modals = (string) file_get_contents(dirname(__DIR__) . '/app/Views/_animal_show_modals.php');
$medical = (string) file_get_contents(dirname(__DIR__) . '/app/Views/_animal_show_medical.php');
$vaccines = (string) file_get_contents(dirname(__DIR__) . '/app/Controllers/AnimalMedicalActions.php');
$dewormings = (string) file_get_contents(dirname(__DIR__) . '/app/Controllers/DewormingController.php');
$assert(substr_count($modals, 'name="entry_id"') >= 2);
$assert(str_contains($medical, 'edit-vaccine'));
$assert(str_contains($medical, 'edit-deworming'));
$assert(str_contains($vaccines, 'if ($entryId > 0)'));
$assert(str_contains($dewormings, 'if ($entryId > 0)'));
$assert(strpos($vaccines, 'if ($entryId > 0)') < strpos($vaccines, 'InventoryService::consumeBatch'));
$assert(strpos($dewormings, 'if ($entryId > 0)') < strpos($dewormings, 'InventoryService::consumeBatch'));
});
$test('Les paramètres sont enregistrés et relus', function () use ($assert): void {
AppSettings::save(['association_name' => 'Refuge Test', 'app_language' => 'en'], null);
$assert(AppSettings::get('association_name') === 'Refuge Test');