Corriger les liens et la synchronisation i-CAD
This commit is contained in:
parent
7b89fd5dfa
commit
9ff26ad9be
10 changed files with 83 additions and 23 deletions
10
CHANGELOG.md
10
CHANGELOG.md
|
|
@ -2,6 +2,16 @@
|
|||
|
||||
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.31] — 2026-10-01
|
||||
|
||||
### Navigation et synchronisation des dossiers animaux
|
||||
|
||||
- correction du lien ouvrant la fiche d’un adoptant depuis la fiche animale ;
|
||||
- ajout d’une colonne « Actions » et d’un bouton « Modifier » explicite dans la liste des vaccins ;
|
||||
- transformation du rafraîchissement i-CAD en formulaire utilisable même sans JavaScript ;
|
||||
- contrôle des erreurs HTTP et maintien de l’actualisation dynamique de la fiche i-CAD ;
|
||||
- validation réelle du connecteur avec une fiche i-CAD, complétée par des tests de non-régression.
|
||||
|
||||
## [1.0.0-rc.30] — 2026-10-01
|
||||
|
||||
### Fiabilisation des dossiers et du suivi médical
|
||||
|
|
|
|||
|
|
@ -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.31` — Parcours de correction plus fiables
|
||||
|
||||
Les liens entre animaux et adoptants conduisent désormais directement à la bonne fiche d’annuaire. La modification des vaccins devient explicite et la synchronisation i-CAD ne dépend plus entièrement du JavaScript : un formulaire serveur assure le rafraîchissement même lorsque l’interface dynamique rencontre un problème.
|
||||
|
||||
## `1.0.0-rc.30` — Corrections issues de la validation terrain
|
||||
|
||||
Une passe croisée avec les sauvegardes ASM3 a corrigé une collision historique entre Gribouille et Lucci. Le suivi médical gagne une date de test FIV/FeLV, des catalogues administrables et une meilleure association entre tarifs et cliniques. La synchronisation I-CAD, la navigation mobile et l’identité Android ont également été adaptées aux conditions réelles d’utilisation.
|
||||
|
|
|
|||
|
|
@ -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.30`. 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.31`. 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).
|
||||
|
||||
|
|
|
|||
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
|||
1.0.0-rc.30
|
||||
1.0.0-rc.31
|
||||
|
|
|
|||
|
|
@ -448,6 +448,7 @@ final class AnimalsController
|
|||
}
|
||||
$db = DB::pdo();
|
||||
$id = (int) ($_POST['id'] ?? 0);
|
||||
$wantsJson = str_contains(strtolower((string) ($_SERVER['HTTP_ACCEPT'] ?? '')), 'application/json');
|
||||
if ($id <= 0) {
|
||||
http_response_code(400);
|
||||
echo h(t('error.bad_request'));
|
||||
|
|
@ -468,6 +469,10 @@ final class AnimalsController
|
|||
|
||||
$chipId = preg_replace('/\D+/', '', (string) ($animal['chip_id'] ?? ''));
|
||||
if ($chipId === '') {
|
||||
if (!$wantsJson) {
|
||||
header('Location: /animal?id=' . $id . '#tab-ident');
|
||||
return;
|
||||
}
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode(['cached' => null, 'stale' => true, 'error' => 'chip_id vide'], JSON_UNESCAPED_UNICODE);
|
||||
return;
|
||||
|
|
@ -489,6 +494,10 @@ final class AnimalsController
|
|||
$stale = $svc->isStale($cached, 86400);
|
||||
}
|
||||
|
||||
if (!$wantsJson) {
|
||||
header('Location: /animal?id=' . $id . '#tab-ident');
|
||||
return;
|
||||
}
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode(['cached' => $cached, 'stale' => $stale], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -225,7 +225,7 @@ $hasCurrent = ($currentLat !== null && $currentLng !== null);
|
|||
|
||||
<div class="alert alert-success mt-3">
|
||||
<strong><?= h(t('animal.adopted_by')) ?> :</strong><br>
|
||||
<?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; ?>
|
||||
<?php if (!empty($adoption['adopter_contact_id'])): ?><a class="fw-bold" href="/directory/view?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'] ?? ''
|
||||
|
|
@ -316,10 +316,14 @@ $err = (string)($cache['error'] ?? '');
|
|||
</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>
|
||||
<form method="post" action="/animal/icad" id="form-icad-refresh" class="m-0">
|
||||
<input type="hidden" name="csrf" value="<?= h(Auth::csrf()) ?>">
|
||||
<input type="hidden" name="id" value="<?= (int)$animal['id'] ?>">
|
||||
<input type="hidden" name="force" value="1">
|
||||
<button class="btn btn-sm btn-outline-secondary" type="submit" id="btn-icad-refresh">
|
||||
<?= h(t('common.refresh')) ?>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -443,6 +443,7 @@ ORDER BY created_at ASC
|
|||
<th><?= h(t('animal.batch_traceability')) ?></th>
|
||||
<th><?= h(t('animal.veterinary_clinic')) ?></th>
|
||||
<th><?= h(t('common.notes')) ?></th>
|
||||
<th class="text-end"><?= h(t('common.actions')) ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
|
@ -457,7 +458,7 @@ 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"
|
||||
<td class="text-end"><button type="button" class="btn btn-sm btn-outline-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'] ?? '') ?>"
|
||||
|
|
@ -465,8 +466,7 @@ ORDER BY created_at ASC
|
|||
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>
|
||||
data-notes="<?= h($v['notes'] ?? '') ?>"><?= h(t('common.edit')) ?></button></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
|
|
|
|||
|
|
@ -1160,6 +1160,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
</script>
|
||||
<script>
|
||||
(function() {
|
||||
const form = document.getElementById('form-icad-refresh');
|
||||
const btn = document.getElementById('btn-icad-refresh');
|
||||
const body = document.getElementById('icad-card-body');
|
||||
const tr = <?= json_encode(['error' => t('animal.icad_error'), 'empty' => t('animal.no_icad'), 'date' =>
|
||||
|
|
@ -1170,7 +1171,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
'appearance' => t('animal.appearance'), 'history' => t('animal.icad_history'), 'fetching' => t(
|
||||
'animal.icad_fetching'), 'fetchError' => t('animal.icad_fetch_error')
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?> ;
|
||||
if (!btn || !body) return;
|
||||
if (!form || !btn || !body) return;
|
||||
|
||||
function esc(s) {
|
||||
return (s ?? '').toString()
|
||||
|
|
@ -1240,8 +1241,8 @@ ${ownerHtml}
|
|||
`;
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
const id = btn.dataset.animalId;
|
||||
async function refresh(event) {
|
||||
event.preventDefault();
|
||||
btn.disabled = true;
|
||||
const oldTxt = btn.innerText;
|
||||
btn.innerText = '…';
|
||||
|
|
@ -1256,12 +1257,12 @@ ${ownerHtml}
|
|||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
id,
|
||||
force: '1',
|
||||
csrf: <?= json_encode(Auth::csrf()) ?>
|
||||
})
|
||||
credentials: 'same-origin',
|
||||
body: new URLSearchParams(new FormData(form))
|
||||
});
|
||||
if (!r.ok) {
|
||||
throw new Error(`HTTP ${r.status}: ${await r.text()}`);
|
||||
}
|
||||
const j = await r.json();
|
||||
body.innerHTML = renderIcad(j);
|
||||
} catch (e) {
|
||||
|
|
@ -1274,7 +1275,7 @@ ${ownerHtml}
|
|||
}
|
||||
}
|
||||
|
||||
btn.addEventListener('click', refresh);
|
||||
form.addEventListener('submit', refresh);
|
||||
})();
|
||||
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -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.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>
|
||||
<span class="badge bg-blue-lt mb-2">1.0.0-rc.31</span>
|
||||
<h3>Parcours de correction plus fiables</h3>
|
||||
<p class="text-muted">Liens d’adoptants corrigés, modification des vaccins plus explicite et
|
||||
synchronisation i-CAD utilisable même sans JavaScript.</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -215,6 +215,18 @@ try {
|
|||
$assert(strpos($vaccines, 'if ($entryId > 0)') < strpos($vaccines, 'InventoryService::consumeBatch'));
|
||||
$assert(strpos($dewormings, 'if ($entryId > 0)') < strpos($dewormings, 'InventoryService::consumeBatch'));
|
||||
});
|
||||
$test('La liste des vaccins affiche une action de modification explicite', function () use ($assert): void {
|
||||
$medical = file_get_contents(__DIR__ . '/../app/Views/_animal_show_medical.php');
|
||||
$assert(str_contains($medical, "h(t('common.actions'))"), 'L’en-tête Actions manque aux vaccins');
|
||||
$assert(
|
||||
str_contains($medical, 'btn btn-sm btn-outline-primary edit-vaccine'),
|
||||
'Le bouton Modifier des vaccins est absent',
|
||||
);
|
||||
$assert(
|
||||
!preg_match('/class="[^"]*edit-vaccine[^"]*"[\s\S]*?>✏️<\/button>/', $medical),
|
||||
'Le crayon est encore utilisé pour modifier un vaccin',
|
||||
);
|
||||
});
|
||||
$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');
|
||||
|
|
@ -491,6 +503,26 @@ try {
|
|||
$assert($structure !== null && $structure['label'] === 'Structure test');
|
||||
$assert((int) $structure['total_cents'] === 1234);
|
||||
});
|
||||
$test('Le nom de l’adoptant ouvre la bonne route annuaire', function () use ($assert): void {
|
||||
$view = file_get_contents(__DIR__ . '/../app/Views/_animal_show_identity.php');
|
||||
$routes = file_get_contents(__DIR__ . '/../app/Routes/web.php');
|
||||
$assert(
|
||||
str_contains($view, 'href="/directory/view?id=<?= (int)$adoption[\'adopter_contact_id\'] ?>"'),
|
||||
'Le lien de l’adoptant ne cible pas /directory/view',
|
||||
);
|
||||
$assert(!str_contains($view, '/directory/show?id='), 'L’ancienne route annuaire est encore utilisée');
|
||||
$assert(str_contains($routes, "case '/directory/view':"), 'La route /directory/view n’est pas déclarée');
|
||||
});
|
||||
$test('Le rafraîchissement i-CAD fonctionne avec et sans JavaScript', function () use ($assert): void {
|
||||
$identity = file_get_contents(__DIR__ . '/../app/Views/_animal_show_identity.php');
|
||||
$scripts = file_get_contents(__DIR__ . '/../app/Views/_animal_show_modals.php');
|
||||
$controller = file_get_contents(__DIR__ . '/../app/Controllers/AnimalsController.php');
|
||||
$assert(str_contains($identity, 'action="/animal/icad"'), 'Le bouton i-CAD n’a pas de formulaire de secours');
|
||||
$assert(str_contains($identity, 'name="csrf"'), 'Le formulaire i-CAD ne contient pas le jeton CSRF');
|
||||
$assert(str_contains($scripts, "form.addEventListener('submit', refresh)"), 'L’actualisation dynamique i-CAD est absente');
|
||||
$assert(str_contains($scripts, 'if (!r.ok)'), 'Les erreurs HTTP i-CAD ne sont pas contrôlées');
|
||||
$assert(str_contains($controller, "header('Location: /animal?id=' . \$id . '#tab-ident')"), 'Le repli sans JavaScript est absent');
|
||||
});
|
||||
$test('La recherche globale retrouve les dossiers accessibles', function () use ($assert): void {
|
||||
$animals = GlobalSearchService::search('Animal Test');
|
||||
$assert(($animals['animals'][0]['title'] ?? '') === 'Animal Test', 'Animal absent de la recherche');
|
||||
|
|
|
|||
Loading…
Reference in a new issue