fix: traiter le premier lot de tickets
Refs #4 #5 #6 #9 #12 #16 #18 #20
This commit is contained in:
parent
3f82acbbd8
commit
04cbe54dd1
14 changed files with 87 additions and 12 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.7] — 2026-09-07
|
||||
|
||||
### Premier lot de tickets
|
||||
|
||||
- création et réparation automatique du cache de géocodage afin d’éviter les erreurs lors de l’enregistrement d’un animal, des paramètres ou d’un logo (`#5`, `#16`) ;
|
||||
- messages explicites lorsqu’une espèce ou une salle existe déjà (`#9`, `#12`) ;
|
||||
- retour visuel immédiat sur le choix « Donné » ou « Non donné » pendant une tournée (`#20`) ;
|
||||
- publication de plusieurs animaux adoptables dans le jeu de démonstration (`#18`) ;
|
||||
- validation française des adresses électroniques et confirmation des URL HTTPS invalides (`#4`, `#6`).
|
||||
|
||||
## [1.0.0-rc.6] — 2026-09-07
|
||||
|
||||
### Compatibilité des ressources
|
||||
|
|
|
|||
|
|
@ -33,3 +33,7 @@ Ajout d’une bibliothèque d’images pour les contenus publics, simplification
|
|||
## `1.0.0-rc.6` — Compatibilité des ressources
|
||||
|
||||
Remplacement de la police emoji par un format vectoriel accepté par les navigateurs et renforcement de la publication des médias par le paquet YunoHost.
|
||||
|
||||
## `1.0.0-rc.7` — Premier lot de tickets
|
||||
|
||||
Fiabilisation du géocodage et des formulaires de configuration, amélioration des tournées de soins et publication d’animaux adoptables dans le jeu de démonstration.
|
||||
|
|
|
|||
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
|||
1.0.0-rc.6
|
||||
1.0.0-rc.7
|
||||
|
|
|
|||
|
|
@ -266,6 +266,11 @@ final class SettingsController
|
|||
$color = preg_match('/^#[0-9a-fA-F]{6}$/', (string) ($new['color'] ?? ''))
|
||||
? (string) $new['color']
|
||||
: '#6c757d';
|
||||
$duplicate = $db->prepare('SELECT 1 FROM shelter_rooms WHERE code=? OR name=? COLLATE NOCASE');
|
||||
$duplicate->execute([$code, $newName]);
|
||||
if ($duplicate->fetchColumn()) {
|
||||
throw new RuntimeException(t('rooms.duplicate'));
|
||||
}
|
||||
$db->prepare(
|
||||
'INSERT INTO shelter_rooms(code,name,room_type,status_code,color,bulk_validation,sort_order) VALUES(?,?,?,?,?,?,?)',
|
||||
)->execute([$code, $newName, $type, $status, $color, isset($new['bulk_validation']) ? 1 : 0, 100]);
|
||||
|
|
@ -351,6 +356,13 @@ final class SettingsController
|
|||
exit();
|
||||
}
|
||||
try {
|
||||
$duplicate = $db->prepare(
|
||||
'SELECT 1 FROM ref_species WHERE id<>:id AND (code=:code OR name=:name COLLATE NOCASE)',
|
||||
);
|
||||
$duplicate->execute([':id' => $id, ':code' => $code, ':name' => $name]);
|
||||
if ($duplicate->fetchColumn()) {
|
||||
throw new RuntimeException(t('settings.species_duplicate'));
|
||||
}
|
||||
if ($id > 0) {
|
||||
$old = $db->prepare('SELECT code,is_system FROM ref_species WHERE id=:id');
|
||||
$old->execute([':id' => $id]);
|
||||
|
|
|
|||
|
|
@ -375,6 +375,16 @@ final class DemoDataService
|
|||
$animalIds[$name] = (int) $db->lastInsertId();
|
||||
}
|
||||
|
||||
$publish = $db->prepare(
|
||||
"UPDATE animals SET website_published=1,website_description=? WHERE id=? AND adoption_availability='available'",
|
||||
);
|
||||
foreach (['Moka', 'Pixel', 'Caramel', 'Plume', 'Lune', 'Soleil'] as $name) {
|
||||
$publish->execute([
|
||||
'Fiche fictive publiée pour découvrir le fonctionnement du site public de démonstration.',
|
||||
$animalIds[$name],
|
||||
]);
|
||||
}
|
||||
|
||||
$db->prepare('INSERT INTO litters(mother_id,birth_date,notes) VALUES(?,?,?)')->execute([
|
||||
$animalIds['Plume'],
|
||||
$animals[6][3],
|
||||
|
|
|
|||
|
|
@ -9,7 +9,12 @@ final class GeoService
|
|||
return null;
|
||||
}
|
||||
|
||||
// Cache DB
|
||||
// Garde-fou pour les bases anciennes dans lesquelles la table de cache
|
||||
// n'avait jamais été créée par une migration dédiée.
|
||||
$db->exec(
|
||||
"CREATE TABLE IF NOT EXISTS geocode_cache (q TEXT PRIMARY KEY, lat REAL, lng REAL, raw_json TEXT, fetched_at TEXT NOT NULL DEFAULT (datetime('now')))",
|
||||
);
|
||||
|
||||
$st = $db->prepare('SELECT lat,lng FROM geocode_cache WHERE q=:q LIMIT 1');
|
||||
$st->execute([':q' => $q]);
|
||||
$row = $st->fetch(PDO::FETCH_ASSOC);
|
||||
|
|
@ -17,7 +22,6 @@ final class GeoService
|
|||
return ['lat' => (float) $row['lat'], 'lng' => (float) $row['lng']];
|
||||
}
|
||||
|
||||
// Nominatim (OpenStreetMap) — respecter l'usage: User-Agent + requêtes raisonnables
|
||||
$url = 'https://nominatim.openstreetmap.org/search?format=jsonv2&limit=1&q=' . rawurlencode($q);
|
||||
|
||||
$ch = curl_init($url);
|
||||
|
|
@ -43,7 +47,6 @@ final class GeoService
|
|||
|
||||
$json = json_decode($body, true);
|
||||
if (!is_array($json) || empty($json[0]['lat']) || empty($json[0]['lon'])) {
|
||||
// Stocker "vide" en cache évite de spammer
|
||||
$db->prepare(
|
||||
"INSERT OR REPLACE INTO geocode_cache(q,lat,lng,raw_json,fetched_at) VALUES(:q,NULL,NULL,:raw,datetime('now'))",
|
||||
)->execute([':q' => $q, ':raw' => $body]);
|
||||
|
|
|
|||
|
|
@ -186,6 +186,7 @@
|
|||
|
||||
function frenchMessage(field) {
|
||||
if (field.validity.valueMissing) return <?= json_encode(t('auth.required'), JSON_UNESCAPED_UNICODE) ?> ;
|
||||
if (field.validity.typeMismatch) return <?= json_encode(t('settings.invalid_email'), 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'),
|
||||
|
|
|
|||
|
|
@ -134,6 +134,17 @@ $fieldOptions = [
|
|||
border-color: #206bc4
|
||||
}
|
||||
|
||||
.care-treatment-choice:has(input:checked) {
|
||||
color: #fff;
|
||||
border-color: var(--tblr-primary);
|
||||
background: var(--tblr-primary)
|
||||
}
|
||||
|
||||
.care-treatment-choice.btn-outline-danger:has(input:checked) {
|
||||
border-color: var(--tblr-danger);
|
||||
background: var(--tblr-danger)
|
||||
}
|
||||
|
||||
@media(max-width:800px) {
|
||||
.care-box {
|
||||
grid-column: span 6 !important
|
||||
|
|
@ -341,12 +352,12 @@ $fieldOptions = [
|
|||
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
|
||||
<div class="btn-group w-100"><label class="btn btn-outline-success care-treatment-choice"><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"
|
||||
class="btn btn-outline-danger care-treatment-choice"><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>
|
||||
|
|
|
|||
|
|
@ -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.6</span>
|
||||
<h3>Compatibilité des ressources</h3>
|
||||
<p class="text-muted">Police emoji compatible avec les navigateurs et publication des médias
|
||||
renforcée pour les installations YunoHost.</p>
|
||||
<span class="badge bg-blue-lt mb-2">1.0.0-rc.7</span>
|
||||
<h3>Premier lot de tickets</h3>
|
||||
<p class="text-muted">Géocodage fiabilisé, doublons expliqués, tournées plus lisibles et animaux
|
||||
adoptables publiés dans la démonstration.</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
</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">
|
||||
<form method="post" action="/settings/general/save" enctype="multipart/form-data" novalidate>
|
||||
<div class="card mb-3 overflow-hidden">
|
||||
<div class="card-status-top bg-primary"></div>
|
||||
<div class="card-header">
|
||||
|
|
@ -37,7 +37,9 @@
|
|||
</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>
|
||||
value="<?= h($settings['association_email']) ?>"
|
||||
oninvalid="this.setCustomValidity(<?= h(json_encode(t('settings.invalid_email'))) ?>)"
|
||||
oninput="this.setCustomValidity('')"></div>
|
||||
<div class="col-12">
|
||||
<hr class="my-1">
|
||||
</div>
|
||||
|
|
|
|||
7
migrations/091_geocode_cache.sql
Normal file
7
migrations/091_geocode_cache.sql
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
CREATE TABLE IF NOT EXISTS geocode_cache (
|
||||
q TEXT PRIMARY KEY,
|
||||
lat REAL,
|
||||
lng REAL,
|
||||
raw_json TEXT,
|
||||
fetched_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
|
@ -121,4 +121,5 @@ return [
|
|||
'search.title'=>'Global search','search.placeholder'=>'Animal, contact, appointment or invoice…','search.action'=>'Search','search.hint'=>'Enter at least two characters.','search.none'=>'No accessible result.','search.open'=>'Open global search','search.shortcut'=>'Ctrl K','search.group.animals'=>'Animals','search.group.directory'=>'Directory','search.group.agenda'=>'Calendar','search.group.accounting'=>'Accounting','search.loading'=>'Searching…','search.all_results'=>'Show all results','search.error'=>'Search is temporarily unavailable.',
|
||||
'public_site.settings_heading'=>'Public website','public_site.theme_custom'=>'Custom','public_site.site_name'=>'Website name','public_site.custom_css'=>'Custom CSS','public_site.custom_css_help'=>'Used by the custom theme. Imports and external resources are blocked.','public_site.invalid_css'=>'The stylesheet contains a forbidden instruction.','public_site.routing_management_help'=>'Management stays at / and the public website at /site.','public_site.publish_animal'=>'Publish this animal on the public website','public_site.publish_animal_help'=>'The profile will only be visible when the animal is also available for adoption. Only public photos and the text below are used.','lab_reference.add_species'=>'Add a species to lab reports','lab_reference.copy_from'=>'Copy reference ranges from','lab_reference.target_species'=>'New species','lab_reference.copy_help'=>'Ranges are copied as a starting point and remain editable.','lab_reference.invalid_species'=>'Choose two different valid species.','lab_reference.nothing_copied'=>'This species already has these reference ranges.','statistics.hair_types'=>'Hair types','animals.filtered_by'=>'Filter: :value','service.icad.home_unavailable'=>'The I-CAD website is unavailable','service.icad.search_unavailable'=>'I-CAD search is unavailable','service.icad.record_unavailable'=>'The I-CAD record is unavailable',
|
||||
'public_site.image_library'=>'Image library','public_site.image_file'=>'Image to add','public_site.image_upload'=>'Upload image','public_site.image_help'=>'Upload an image, place the cursor in a text field, then choose “Insert”. Accepted formats: JPEG, PNG and WebP.','public_site.image_none'=>'No image uploaded.','public_site.image_insert'=>'Insert','public_site.image_invalid'=>'Choose a valid image.',
|
||||
'rooms.duplicate'=>'A room with this name or code already exists.','settings.species_duplicate'=>'A species with this name or code already exists.',
|
||||
];
|
||||
|
|
|
|||
|
|
@ -121,4 +121,5 @@ return [
|
|||
'search.title'=>'Recherche globale','search.placeholder'=>'Animal, contact, rendez-vous ou facture…','search.action'=>'Rechercher','search.hint'=>'Saisissez au moins deux caractères.','search.none'=>'Aucun résultat accessible.','search.open'=>'Ouvrir la recherche globale','search.shortcut'=>'Ctrl K','search.group.animals'=>'Animaux','search.group.directory'=>'Annuaire','search.group.agenda'=>'Agenda','search.group.accounting'=>'Comptabilité','search.loading'=>'Recherche…','search.all_results'=>'Afficher tous les résultats','search.error'=>'La recherche est momentanément indisponible.',
|
||||
'public_site.settings_heading'=>'Site public','public_site.theme_custom'=>'Personnalisé','public_site.site_name'=>'Nom du site','public_site.custom_css'=>'CSS personnalisé','public_site.custom_css_help'=>'Actif avec le thème personnalisé. Les imports et les ressources externes sont bloqués.','public_site.invalid_css'=>'La feuille de style contient une instruction interdite.','public_site.routing_management_help'=>'La gestion reste sur / et le site public sur /site.','public_site.publish_animal'=>'Publier cet animal sur le site public','public_site.publish_animal_help'=>'La fiche ne sera visible que si l’animal est également disponible à l’adoption. Seules les photos publiques et le texte ci-dessous seront utilisés.','lab_reference.add_species'=>'Ajouter une espèce aux bilans','lab_reference.copy_from'=>'Copier les références de','lab_reference.target_species'=>'Nouvelle espèce','lab_reference.copy_help'=>'Les intervalles sont copiés comme point de départ, puis restent modifiables.','lab_reference.invalid_species'=>'Choisissez deux espèces différentes et valides.','lab_reference.nothing_copied'=>'Cette espèce possède déjà ces références.','statistics.hair_types'=>'Types de poil','animals.filtered_by'=>'Filtre : :value','service.icad.home_unavailable'=>'Le site I-CAD est indisponible','service.icad.search_unavailable'=>'La recherche I-CAD est indisponible','service.icad.record_unavailable'=>'La fiche I-CAD est indisponible',
|
||||
'public_site.image_library'=>'Bibliothèque d’images','public_site.image_file'=>'Image à ajouter','public_site.image_upload'=>'Ajouter l’image','public_site.image_help'=>'Ajoutez une image, placez le curseur dans un texte puis choisissez « Insérer ». Formats acceptés : JPEG, PNG et WebP.','public_site.image_none'=>'Aucune image ajoutée.','public_site.image_insert'=>'Insérer','public_site.image_invalid'=>'Choisissez une image valide.',
|
||||
'rooms.duplicate'=>'Une salle portant ce nom ou ce code existe déjà.','settings.species_duplicate'=>'Une espèce portant ce nom ou ce code existe déjà.',
|
||||
];
|
||||
|
|
|
|||
|
|
@ -499,6 +499,19 @@ try {
|
|||
);
|
||||
}
|
||||
$assert(is_file($root . '/public/media/animals/' . $twix . '/twix.webp'), 'Photo de Twix absente');
|
||||
$assert(
|
||||
(int) $db
|
||||
->query(
|
||||
"SELECT COUNT(*) FROM animals WHERE website_published=1 AND adoption_availability='available' AND archived_at IS NULL",
|
||||
)
|
||||
->fetchColumn() >= 1,
|
||||
'Aucun animal de démonstration publié sur le site public',
|
||||
);
|
||||
$assert(
|
||||
(int) $db->query("SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='geocode_cache'")
|
||||
->fetchColumn() === 1,
|
||||
'Cache de géocodage absent',
|
||||
);
|
||||
DemoDataService::clearFiles();
|
||||
});
|
||||
} finally {
|
||||
|
|
|
|||
Loading…
Reference in a new issue