parent
a21f3f2bb4
commit
e7176b6371
17 changed files with 189 additions and 25 deletions
|
|
@ -2,6 +2,15 @@
|
|||
|
||||
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.11] — 2026-09-08
|
||||
|
||||
### Configuration des salles et des tournées
|
||||
|
||||
- critères facultatifs d’accueil par salle : espèce, sexe et statut FIV (`#10`) ;
|
||||
- contrôle de ces critères depuis la fiche animale et la configuration des déplacements ;
|
||||
- créneaux de tournée configurables par salle parmi matin, midi, après-midi et soir (`#21`) ;
|
||||
- conservation de matin et soir comme réglage par défaut des salles existantes.
|
||||
|
||||
## [1.0.0-rc.10] — 2026-09-08
|
||||
|
||||
### Correction des fiches animales
|
||||
|
|
|
|||
|
|
@ -49,3 +49,7 @@ Extension des liens sociaux du site public, clarification des salles et des tari
|
|||
## `1.0.0-rc.10` — Statuts des animaux
|
||||
|
||||
Correction du champ de statut du formulaire animal, stabilisation des codes internes et translittération des noms accentués.
|
||||
|
||||
## `1.0.0-rc.11` — Salles et tournées
|
||||
|
||||
Ajout de critères d’accueil par salle et de quatre créneaux de tournée configurables sans modifier les réglages existants.
|
||||
|
|
|
|||
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
|||
1.0.0-rc.10
|
||||
1.0.0-rc.11
|
||||
|
|
|
|||
|
|
@ -617,6 +617,15 @@ trait AnimalRecordActions
|
|||
|
||||
$fiv = $fiv ?? (int) ($old['fiv'] ?? 0);
|
||||
$felv = $felv ?? (int) ($old['felv'] ?? 0);
|
||||
if ($refugeRoom !== null && !ShelterRoomService::accepts($availableRooms[$refugeRoom], [
|
||||
'species' => $species,
|
||||
'sex' => $sex,
|
||||
'fiv' => $fiv,
|
||||
])) {
|
||||
http_response_code(400);
|
||||
echo h(t('rooms.animal_not_allowed'));
|
||||
return;
|
||||
}
|
||||
$careBox = in_array($status, ['soin', 'quarantaine'], true) ? $old['care_box_key'] ?? null : null;
|
||||
if ($isArchivedCorrection) {
|
||||
$refugeRoom = $old['refuge_room'] ?? null;
|
||||
|
|
|
|||
|
|
@ -4,10 +4,16 @@ declare(strict_types=1);
|
|||
|
||||
final class CareRoundsController
|
||||
{
|
||||
private static function period(mixed $value): string
|
||||
{
|
||||
$value = (string) $value;
|
||||
return in_array($value, ShelterRoomService::PERIODS, true) ? $value : 'morning';
|
||||
}
|
||||
|
||||
public static function index(): void
|
||||
{
|
||||
$db = DB::pdo();
|
||||
$period = ($_GET['period'] ?? 'morning') === 'evening' ? 'evening' : 'morning';
|
||||
$period = self::period($_GET['period'] ?? 'morning');
|
||||
$date = preg_match('/^\d{4}-\d{2}-\d{2}$/', (string) ($_GET['date'] ?? ''))
|
||||
? (string) $_GET['date']
|
||||
: date('Y-m-d');
|
||||
|
|
@ -65,6 +71,21 @@ final class CareRoundsController
|
|||
foreach ($rows as $r) {
|
||||
$unconfigured[(int) $r['animal_id']][] = $r;
|
||||
}
|
||||
$allRooms = ShelterRoomService::all(true);
|
||||
$rooms = array_values(array_filter(
|
||||
$allRooms,
|
||||
static fn(array $room): bool => in_array($period, ShelterRoomService::periods($room), true),
|
||||
));
|
||||
$allRoomsByCode = array_column($allRooms, null, 'code');
|
||||
$visibleRoomCodes = array_column($rooms, 'code');
|
||||
$animals = array_values(array_filter(
|
||||
$animals,
|
||||
static fn(array $animal): bool => in_array(
|
||||
ShelterRoomService::resolveAnimal($animal, $allRoomsByCode),
|
||||
$visibleRoomCodes,
|
||||
true,
|
||||
),
|
||||
));
|
||||
render('care_round.php', [
|
||||
'title' => t('care.title'),
|
||||
'pageDescription' => t('care.description'),
|
||||
|
|
@ -72,7 +93,7 @@ final class CareRoundsController
|
|||
'date' => $date,
|
||||
'round' => $round,
|
||||
'layouts' => $layouts,
|
||||
'rooms' => ShelterRoomService::all(true),
|
||||
'rooms' => $rooms,
|
||||
'animals' => $animals,
|
||||
'treatmentsByAnimal' => $treatments,
|
||||
'unconfiguredTreatments' => $unconfigured,
|
||||
|
|
@ -95,10 +116,14 @@ final class CareRoundsController
|
|||
return;
|
||||
}
|
||||
$animalCheck = $db->prepare(
|
||||
"SELECT 1 FROM animals WHERE id=? AND deleted_at IS NULL AND archived_at IS NULL AND lower(status) IN ('refuge','soin','quarantaine')",
|
||||
"SELECT * FROM animals WHERE id=? AND deleted_at IS NULL AND archived_at IS NULL AND lower(status) IN ('refuge','soin','quarantaine')",
|
||||
);
|
||||
$animalCheck->execute([$animalId]);
|
||||
if (!$animalCheck->fetchColumn()) {
|
||||
$animal = $animalCheck->fetch(PDO::FETCH_ASSOC);
|
||||
$roomsByCode = ShelterRoomService::byCode(true);
|
||||
$roomCode = $animal ? ShelterRoomService::resolveAnimal($animal, $roomsByCode) : '';
|
||||
$period = self::period($_POST['period'] ?? 'morning');
|
||||
if (!$animal || !isset($roomsByCode[$roomCode]) || !in_array($period, ShelterRoomService::periods($roomsByCode[$roomCode]), true)) {
|
||||
http_response_code(404);
|
||||
return;
|
||||
}
|
||||
|
|
@ -255,7 +280,7 @@ final class CareRoundsController
|
|||
$roundStmt->execute([':id' => $roundId]);
|
||||
$roundInfo = $roundStmt->fetch();
|
||||
$roundDate = $roundInfo ? date('d/m/Y', strtotime((string) $roundInfo['round_date'])) : date('d/m/Y');
|
||||
$roundPeriod = ($roundInfo['period'] ?? 'morning') === 'evening' ? 'soir' : 'matin';
|
||||
$roundPeriod = t('care.' . self::period($roundInfo['period'] ?? 'morning'));
|
||||
$history = $db->prepare(
|
||||
"INSERT INTO animal_history(animal_id,type,label,details,user_id) VALUES(:animal,'care_round',:label,:details,:user)",
|
||||
);
|
||||
|
|
@ -277,7 +302,7 @@ final class CareRoundsController
|
|||
}
|
||||
throw $e;
|
||||
}
|
||||
$period = ($_POST['period'] ?? 'morning') === 'evening' ? 'evening' : 'morning';
|
||||
$period = self::period($_POST['period'] ?? 'morning');
|
||||
$date = (string) ($_POST['date'] ?? date('Y-m-d'));
|
||||
header('Location: /care-round?period=' . $period . '&date=' . rawurlencode($date) . '#animal-' . $animalId);
|
||||
exit();
|
||||
|
|
@ -293,7 +318,8 @@ final class CareRoundsController
|
|||
$roundId = self::ensureRound($db);
|
||||
$room = (string) ($_POST['room'] ?? '');
|
||||
$rooms = ShelterRoomService::byCode(true);
|
||||
if (!$roundId || !isset($rooms[$room]) || empty($rooms[$room]['bulk_validation'])) {
|
||||
$period = self::period($_POST['period'] ?? 'morning');
|
||||
if (!$roundId || !isset($rooms[$room]) || empty($rooms[$room]['bulk_validation']) || !in_array($period, ShelterRoomService::periods($rooms[$room]), true)) {
|
||||
http_response_code(400);
|
||||
return;
|
||||
}
|
||||
|
|
@ -312,7 +338,6 @@ final class CareRoundsController
|
|||
}
|
||||
}
|
||||
|
||||
$period = ($_POST['period'] ?? 'morning') === 'evening' ? 'evening' : 'morning';
|
||||
$date = preg_match('/^\d{4}-\d{2}-\d{2}$/', (string) ($_POST['date'] ?? ''))
|
||||
? (string) $_POST['date']
|
||||
: date('Y-m-d');
|
||||
|
|
@ -329,7 +354,7 @@ final class CareRoundsController
|
|||
}
|
||||
$animals = $db
|
||||
->query(
|
||||
"SELECT id,name,status,refuge_room,care_box_key,current_address FROM animals WHERE deleted_at IS NULL AND archived_at IS NULL AND lower(status) IN ('refuge','soin','quarantaine') ORDER BY name COLLATE NOCASE",
|
||||
"SELECT id,name,species,sex,fiv,fiv_status,status,refuge_room,care_box_key,current_address FROM animals WHERE deleted_at IS NULL AND archived_at IS NULL AND lower(status) IN ('refuge','soin','quarantaine') ORDER BY name COLLATE NOCASE",
|
||||
)
|
||||
->fetchAll();
|
||||
render('care_setup.php', [
|
||||
|
|
@ -384,6 +409,11 @@ final class CareRoundsController
|
|||
continue;
|
||||
}
|
||||
$room = $rooms[$roomCode];
|
||||
$animalRules = $db->prepare('SELECT species,sex,fiv,fiv_status FROM animals WHERE id=:id');
|
||||
$animalRules->execute([':id' => $animalId]);
|
||||
if (!ShelterRoomService::accepts($room, $animalRules->fetch(PDO::FETCH_ASSOC) ?: [])) {
|
||||
continue;
|
||||
}
|
||||
$allowedBoxes = array_column(
|
||||
array_filter($room['boxes'], static fn($box) => (int) $box['active'] === 1),
|
||||
'code',
|
||||
|
|
@ -434,7 +464,7 @@ final class CareRoundsController
|
|||
DB::pdo()
|
||||
->prepare('UPDATE treatments SET give_morning=?,give_evening=?,give_as_needed=? WHERE id=?')
|
||||
->execute([$values[0], $values[1], $values[2], $id]);
|
||||
$period = ($_POST['period'] ?? 'morning') === 'evening' ? 'evening' : 'morning';
|
||||
$period = self::period($_POST['period'] ?? 'morning');
|
||||
header('Location: /care-round?period=' . $period);
|
||||
exit();
|
||||
}
|
||||
|
|
@ -444,7 +474,7 @@ final class CareRoundsController
|
|||
$date = preg_match('/^\d{4}-\d{2}-\d{2}$/', (string) ($_POST['date'] ?? ''))
|
||||
? (string) $_POST['date']
|
||||
: date('Y-m-d');
|
||||
$period = ($_POST['period'] ?? 'morning') === 'evening' ? 'evening' : 'morning';
|
||||
$period = self::period($_POST['period'] ?? 'morning');
|
||||
$requested = (int) ($_POST['round_id'] ?? 0);
|
||||
if ($requested) {
|
||||
$s = $db->prepare('SELECT id FROM care_rounds WHERE id=? AND round_date=? AND period=?');
|
||||
|
|
|
|||
|
|
@ -215,6 +215,7 @@ final class SettingsController
|
|||
render('settings_rooms.php', [
|
||||
'title' => t('rooms.title'),
|
||||
'rooms' => ShelterRoomService::all(),
|
||||
'species' => SpeciesService::all(),
|
||||
'saved' => isset($_GET['saved']),
|
||||
'error' => (string) ($_GET['error'] ?? ''),
|
||||
]);
|
||||
|
|
@ -236,6 +237,18 @@ final class SettingsController
|
|||
$type = (string) ($row['room_type'] ?? 'collective');
|
||||
$status = (string) ($row['status_code'] ?? 'refuge');
|
||||
$color = (string) ($row['color'] ?? '#6c757d');
|
||||
$allowedSpecies = SpeciesService::normalize((string) ($row['allowed_species'] ?? ''));
|
||||
if (!SpeciesService::exists($allowedSpecies, false)) {
|
||||
$allowedSpecies = null;
|
||||
}
|
||||
$allowedSex = in_array($row['allowed_sex'] ?? '', ['M', 'F'], true) ? (string) $row['allowed_sex'] : null;
|
||||
$requiredFiv = in_array($row['required_fiv'] ?? '', ['positive', 'negative'], true)
|
||||
? (string) $row['required_fiv']
|
||||
: 'any';
|
||||
$periods = array_values(array_intersect(ShelterRoomService::PERIODS, (array) ($row['care_periods'] ?? [])));
|
||||
if (!$periods) {
|
||||
throw new RuntimeException(t('rooms.period_required'));
|
||||
}
|
||||
if (
|
||||
$name === '' ||
|
||||
mb_strlen($name) > 100 ||
|
||||
|
|
@ -246,7 +259,7 @@ final class SettingsController
|
|||
throw new RuntimeException(t('rooms.invalid'));
|
||||
}
|
||||
$db->prepare(
|
||||
"UPDATE shelter_rooms SET name=?,room_type=?,status_code=?,color=?,bulk_validation=?,active=?,sort_order=?,updated_at=datetime('now') WHERE id=?",
|
||||
"UPDATE shelter_rooms SET name=?,room_type=?,status_code=?,color=?,bulk_validation=?,active=?,sort_order=?,allowed_species=?,allowed_sex=?,required_fiv=?,care_periods=?,updated_at=datetime('now') WHERE id=?",
|
||||
)->execute([
|
||||
$name,
|
||||
$type,
|
||||
|
|
@ -255,6 +268,10 @@ final class SettingsController
|
|||
isset($row['bulk_validation']) ? 1 : 0,
|
||||
isset($row['active']) ? 1 : 0,
|
||||
max(0, (int) ($row['sort_order'] ?? 100)),
|
||||
$allowedSpecies,
|
||||
$allowedSex,
|
||||
$requiredFiv,
|
||||
implode(',', $periods),
|
||||
$id,
|
||||
]);
|
||||
self::syncBoxes($db, $id, (string) ($row['boxes'] ?? ''));
|
||||
|
|
@ -278,14 +295,24 @@ final class SettingsController
|
|||
$color = preg_match('/^#[0-9a-fA-F]{6}$/', (string) ($new['color'] ?? ''))
|
||||
? (string) $new['color']
|
||||
: '#6c757d';
|
||||
$allowedSpecies = SpeciesService::normalize((string) ($new['allowed_species'] ?? ''));
|
||||
if (!SpeciesService::exists($allowedSpecies, false)) {
|
||||
$allowedSpecies = null;
|
||||
}
|
||||
$allowedSex = in_array($new['allowed_sex'] ?? '', ['M', 'F'], true) ? (string) $new['allowed_sex'] : null;
|
||||
$requiredFiv = in_array($new['required_fiv'] ?? '', ['positive', 'negative'], true)
|
||||
? (string) $new['required_fiv']
|
||||
: 'any';
|
||||
$periods = array_values(array_intersect(ShelterRoomService::PERIODS, (array) ($new['care_periods'] ?? [])));
|
||||
$periods = $periods ?: ['morning', 'evening'];
|
||||
$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]);
|
||||
'INSERT INTO shelter_rooms(code,name,room_type,status_code,color,bulk_validation,sort_order,allowed_species,allowed_sex,required_fiv,care_periods) VALUES(?,?,?,?,?,?,?,?,?,?,?)',
|
||||
)->execute([$code, $newName, $type, $status, $color, isset($new['bulk_validation']) ? 1 : 0, 100, $allowedSpecies, $allowedSex, $requiredFiv, implode(',', $periods)]);
|
||||
self::syncBoxes($db, (int) $db->lastInsertId(), (string) ($new['boxes'] ?? ''));
|
||||
}
|
||||
$db->commit();
|
||||
|
|
|
|||
|
|
@ -65,14 +65,27 @@ final class Migrations
|
|||
}
|
||||
}
|
||||
|
||||
$relaxesForeignKeys = $name === '093_room_rules_and_care_periods.sql';
|
||||
if ($relaxesForeignKeys) {
|
||||
$db->exec('PRAGMA foreign_keys = OFF');
|
||||
}
|
||||
$db->beginTransaction();
|
||||
try {
|
||||
$db->exec($sql);
|
||||
$ins = $db->prepare("INSERT INTO _migrations(name, applied_at) VALUES(:n, datetime('now'))");
|
||||
$ins->execute([':n' => $name]);
|
||||
if ($relaxesForeignKeys && $db->query('PRAGMA foreign_key_check')->fetchColumn() !== false) {
|
||||
throw new RuntimeException('Foreign key check failed after room migration');
|
||||
}
|
||||
$db->commit();
|
||||
if ($relaxesForeignKeys) {
|
||||
$db->exec('PRAGMA foreign_keys = ON');
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
$db->rollBack();
|
||||
if ($relaxesForeignKeys) {
|
||||
$db->exec('PRAGMA foreign_keys = ON');
|
||||
}
|
||||
throw new RuntimeException('Migration ' . $name . ' : ' . $e->getMessage(), 0, $e);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ declare(strict_types=1);
|
|||
|
||||
final class ShelterRoomService
|
||||
{
|
||||
public const PERIODS = ['morning', 'noon', 'afternoon', 'evening'];
|
||||
|
||||
public static function all(bool $activeOnly = false): array
|
||||
{
|
||||
$sql =
|
||||
|
|
@ -56,4 +58,28 @@ final class ShelterRoomService
|
|||
$value = iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $value) ?: $value;
|
||||
return trim(preg_replace('/[^a-z0-9]+/', '-', $value) ?? '', '-');
|
||||
}
|
||||
|
||||
public static function periods(array $room): array
|
||||
{
|
||||
$periods = array_values(array_intersect(
|
||||
self::PERIODS,
|
||||
explode(',', (string) ($room['care_periods'] ?? 'morning,evening')),
|
||||
));
|
||||
return $periods ?: ['morning', 'evening'];
|
||||
}
|
||||
|
||||
public static function accepts(array $room, array $animal): bool
|
||||
{
|
||||
$species = trim((string) ($room['allowed_species'] ?? ''));
|
||||
$sex = trim((string) ($room['allowed_sex'] ?? ''));
|
||||
$fiv = (string) ($room['required_fiv'] ?? 'any');
|
||||
if ($species !== '' && SpeciesService::normalize((string) ($animal['species'] ?? '')) !== $species) {
|
||||
return false;
|
||||
}
|
||||
if ($sex !== '' && (string) ($animal['sex'] ?? '') !== $sex) {
|
||||
return false;
|
||||
}
|
||||
$positive = (int) ($animal['fiv'] ?? 0) === 1 || (string) ($animal['fiv_status'] ?? '') === 'positive';
|
||||
return $fiv === 'any' || ($fiv === 'positive' ? $positive : !$positive);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -140,6 +140,7 @@ $estMonths = $birth_estimated_months > 0 ? ($birth_estimated_months % 12) : '';
|
|||
<?php foreach (($shelterRooms ?? []) as $room): ?><option
|
||||
value="<?= h($room['code']) ?>"
|
||||
data-status="<?= h($room['status_code']) ?>"
|
||||
<?= $isEdit && !ShelterRoomService::accepts($room, $animal) ? 'disabled' : '' ?>
|
||||
<?= (($animal['refuge_room'] ?? '') === $room['code']) ? 'selected' : '' ?>>
|
||||
<?= h($room['name']) ?></option><?php endforeach; ?>
|
||||
</select>
|
||||
|
|
|
|||
|
|
@ -276,12 +276,11 @@ $fieldOptions = [
|
|||
<div class="care-mode">
|
||||
<div class="card mb-3">
|
||||
<div class="card-body d-flex align-items-center gap-2 care-round-toolbar">
|
||||
<strong
|
||||
class="me-2"><?= h(t('care.round_of', ['period' => t($period === 'morning' ? 'care.morning' : 'care.evening')])) ?></strong>
|
||||
<a class="btn <?= $period === 'morning' ? 'btn-primary' : 'btn-outline-primary' ?>"
|
||||
href="/care-round?period=morning&date=<?= h($date) ?>">☀️ <?= h(t('care.morning')) ?></a>
|
||||
<a class="btn <?= $period === 'evening' ? 'btn-primary' : 'btn-outline-primary' ?>"
|
||||
href="/care-round?period=evening&date=<?= h($date) ?>">🌙 <?= h(t('care.evening')) ?></a>
|
||||
<strong class="me-2"><?= h(t('care.round_of', ['period' => t('care.' . $period)])) ?></strong>
|
||||
<?php foreach (['morning' => '☀️','noon' => '🕛','afternoon' => '🌤️','evening' => '🌙'] as $periodCode => $icon): ?>
|
||||
<a class="btn <?= $period === $periodCode ? 'btn-primary' : 'btn-outline-primary' ?>"
|
||||
href="/care-round?period=<?= h($periodCode) ?>&date=<?= h($date) ?>"><?= $icon ?> <?= h(t('care.' . $periodCode)) ?></a>
|
||||
<?php endforeach; ?>
|
||||
<div class="care-round-actions"><input class="form-control" type="date" value="<?= h($date) ?>"
|
||||
onchange="location.href='/care-round?period=<?= h($period) ?>&date='+this.value">
|
||||
<button class="btn btn-outline-secondary" type="button"
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@ foreach ($rooms as $room) {
|
|||
<td><select class="form-select care-room-select" name="room[<?= (int)$animal['id'] ?>]"
|
||||
data-animal="<?= (int)$animal['id'] ?>"><?php foreach ($rooms as $room): ?><option
|
||||
value="<?= h($room['code']) ?>"
|
||||
<?= !ShelterRoomService::accepts($room, $animal) ? 'disabled' : '' ?>
|
||||
<?= $currentRoom === $room['code'] ? 'selected' : '' ?>><?= h($room['name']) ?>
|
||||
</option><?php endforeach; ?></select></td>
|
||||
<td><select class="form-select care-box-select" name="box[<?= (int)$animal['id'] ?>]"
|
||||
|
|
|
|||
|
|
@ -31,10 +31,9 @@
|
|||
<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.10</span>
|
||||
<h3>Fiabilité des fiches animales</h3>
|
||||
<p class="text-muted">Enregistrement du statut rétabli, codes internes stabilisés et noms accentués
|
||||
correctement translittérés.</p>
|
||||
<span class="badge bg-blue-lt mb-2">1.0.0-rc.11</span>
|
||||
<h3>Salles et tournées configurables</h3>
|
||||
<p class="text-muted">Critères d’accueil par salle et créneaux de soins adaptés à l’organisation du refuge.</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -47,6 +47,11 @@
|
|||
name="rooms[<?= (int)$room['id'] ?>][bulk_validation]"
|
||||
<?= $room['bulk_validation'] ? 'checked' : '' ?>><span
|
||||
class="form-check-label"><?= h(t('rooms.bulk_validation')) ?></span></label></div>
|
||||
<div class="col-12"><hr><div class="form-label"><?= h(t('rooms.admission_rules')) ?></div></div>
|
||||
<div class="col-md-4"><label class="form-label"><?= h(t('animal.species')) ?></label><select class="form-select" name="rooms[<?= (int)$room['id'] ?>][allowed_species]"><option value=""><?= h(t('common.all')) ?></option><?php foreach ($species as $option): ?><option value="<?= h($option['code']) ?>" <?= ($room['allowed_species'] ?? '') === $option['code'] ? 'selected' : '' ?>><?= h($option['name']) ?></option><?php endforeach; ?></select></div>
|
||||
<div class="col-md-4"><label class="form-label"><?= h(t('animal.sex')) ?></label><select class="form-select" name="rooms[<?= (int)$room['id'] ?>][allowed_sex]"><option value=""><?= h(t('common.all')) ?></option><option value="M" <?= ($room['allowed_sex'] ?? '') === 'M' ? 'selected' : '' ?>><?= h(t('sex.male')) ?></option><option value="F" <?= ($room['allowed_sex'] ?? '') === 'F' ? 'selected' : '' ?>><?= h(t('sex.female')) ?></option></select></div>
|
||||
<div class="col-md-4"><label class="form-label">FIV</label><select class="form-select" name="rooms[<?= (int)$room['id'] ?>][required_fiv]"><option value="any"><?= h(t('common.all')) ?></option><option value="positive" <?= ($room['required_fiv'] ?? 'any') === 'positive' ? 'selected' : '' ?>><?= h(t('animals.test_positive')) ?></option><option value="negative" <?= ($room['required_fiv'] ?? 'any') === 'negative' ? 'selected' : '' ?>><?= h(t('animals.test_negative')) ?></option></select></div>
|
||||
<div class="col-12"><label class="form-label"><?= h(t('rooms.care_periods')) ?></label><div class="d-flex flex-wrap gap-3"><?php foreach (ShelterRoomService::PERIODS as $period): ?><label class="form-check"><input class="form-check-input" type="checkbox" name="rooms[<?= (int)$room['id'] ?>][care_periods][]" value="<?= h($period) ?>" <?= in_array($period, ShelterRoomService::periods($room), true) ? 'checked' : '' ?>><span class="form-check-label"><?= h(t('care.' . $period)) ?></span></label><?php endforeach; ?></div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div><?php endforeach; ?>
|
||||
|
|
@ -78,6 +83,10 @@
|
|||
<div class="col-12"><label class="form-check"><input class="form-check-input" type="checkbox"
|
||||
name="new_room[bulk_validation]"><span
|
||||
class="form-check-label"><?= h(t('rooms.bulk_validation')) ?></span></label></div>
|
||||
<div class="col-md-4"><label class="form-label"><?= h(t('animal.species')) ?></label><select class="form-select" name="new_room[allowed_species]"><option value=""><?= h(t('common.all')) ?></option><?php foreach ($species as $option): ?><option value="<?= h($option['code']) ?>"><?= h($option['name']) ?></option><?php endforeach; ?></select></div>
|
||||
<div class="col-md-4"><label class="form-label"><?= h(t('animal.sex')) ?></label><select class="form-select" name="new_room[allowed_sex]"><option value=""><?= h(t('common.all')) ?></option><option value="M"><?= h(t('sex.male')) ?></option><option value="F"><?= h(t('sex.female')) ?></option></select></div>
|
||||
<div class="col-md-4"><label class="form-label">FIV</label><select class="form-select" name="new_room[required_fiv]"><option value="any"><?= h(t('common.all')) ?></option><option value="positive"><?= h(t('animals.test_positive')) ?></option><option value="negative"><?= h(t('animals.test_negative')) ?></option></select></div>
|
||||
<div class="col-12"><label class="form-label"><?= h(t('rooms.care_periods')) ?></label><div class="d-flex flex-wrap gap-3"><?php foreach (ShelterRoomService::PERIODS as $period): ?><label class="form-check"><input class="form-check-input" type="checkbox" name="new_room[care_periods][]" value="<?= h($period) ?>" <?= in_array($period, ['morning','evening'], true) ? 'checked' : '' ?>><span class="form-check-label"><?= h(t('care.' . $period)) ?></span></label><?php endforeach; ?></div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
19
migrations/093_room_rules_and_care_periods.sql
Normal file
19
migrations/093_room_rules_and_care_periods.sql
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
ALTER TABLE shelter_rooms ADD COLUMN allowed_species TEXT;
|
||||
ALTER TABLE shelter_rooms ADD COLUMN allowed_sex TEXT;
|
||||
ALTER TABLE shelter_rooms ADD COLUMN required_fiv TEXT NOT NULL DEFAULT 'any' CHECK(required_fiv IN ('any','positive','negative'));
|
||||
ALTER TABLE shelter_rooms ADD COLUMN care_periods TEXT NOT NULL DEFAULT 'morning,evening';
|
||||
|
||||
CREATE TABLE care_rounds_new (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
round_date TEXT NOT NULL,
|
||||
period TEXT NOT NULL CHECK(period IN ('morning','noon','afternoon','evening')),
|
||||
performed_by TEXT,
|
||||
started_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
completed_at TEXT,
|
||||
performed_by_user_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
|
||||
UNIQUE(round_date, period)
|
||||
);
|
||||
INSERT INTO care_rounds_new(id,round_date,period,performed_by,started_at,completed_at,performed_by_user_id)
|
||||
SELECT id,round_date,period,performed_by,started_at,completed_at,performed_by_user_id FROM care_rounds;
|
||||
DROP TABLE care_rounds;
|
||||
ALTER TABLE care_rounds_new RENAME TO care_rounds;
|
||||
|
|
@ -124,4 +124,5 @@ return [
|
|||
'rooms.duplicate'=>'A room with this name or code already exists.','settings.species_duplicate'=>'A species with this name or code already exists.',
|
||||
'settings.invalid_rna'=>'The RNA number must start with W followed by 9 digits.','settings.invalid_siret'=>'The SIRET number must contain 14 digits and be valid.','rooms.name_too_long'=>'A room name cannot exceed 100 characters.','rooms.box_name_too_long'=>'A box name cannot exceed 100 characters.','animal.name_too_long'=>'An animal name cannot exceed 150 characters.',
|
||||
'pricing.adoption_fees_help'=>'Indicative amounts shared by all animals, according to their sterilisation status.','rooms.inactive_help'=>'An unchecked room becomes inactive, while its previous assignments remain in the history. Its internal code never changes.',
|
||||
'care.noon'=>'noon','care.afternoon'=>'afternoon','rooms.admission_rules'=>'Optional admission criteria','rooms.care_periods'=>'Rounds scheduled for this room','rooms.period_required'=>'Select at least one care-round period per room.','rooms.animal_not_allowed'=>'This animal does not match the admission criteria of the selected room.',
|
||||
];
|
||||
|
|
|
|||
|
|
@ -124,4 +124,5 @@ return [
|
|||
'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à.',
|
||||
'settings.invalid_rna'=>'Le numéro RNA doit commencer par W et être suivi de 9 chiffres.','settings.invalid_siret'=>'Le numéro SIRET doit contenir 14 chiffres et être valide.','rooms.name_too_long'=>'Le nom d’une salle ne peut pas dépasser 100 caractères.','rooms.box_name_too_long'=>'Le nom d’un box ne peut pas dépasser 100 caractères.','animal.name_too_long'=>'Le nom d’un animal ne peut pas dépasser 150 caractères.',
|
||||
'pricing.adoption_fees_help'=>'Montants indicatifs communs à tous les animaux, selon leur état de stérilisation.','rooms.inactive_help'=>'Une salle décochée devient inactive, mais ses anciennes affectations restent dans l’historique. Son code interne ne change jamais.',
|
||||
'care.noon'=>'midi','care.afternoon'=>'après-midi','rooms.admission_rules'=>'Critères d’accueil facultatifs','rooms.care_periods'=>'Tournées prévues dans cette salle','rooms.period_required'=>'Sélectionnez au moins un créneau de tournée par salle.','rooms.animal_not_allowed'=>'Cet animal ne correspond pas aux critères d’accueil de la salle choisie.',
|
||||
];
|
||||
|
|
|
|||
|
|
@ -106,11 +106,27 @@ try {
|
|||
}
|
||||
$assert($rooms['silver']['name'] === 'Salle collective 1');
|
||||
$assert($rooms['twix']['name'] === 'Salle collective 2');
|
||||
$assert(ShelterRoomService::periods($rooms['silver']) === ['morning', 'evening']);
|
||||
$assert(ShelterRoomService::accepts($rooms['silver'], ['species' => 'chien', 'sex' => 'F', 'fiv' => 0]));
|
||||
$assert(
|
||||
count($rooms['care']['boxes']) === 5 && count($rooms['quarantine']['boxes']) === 5,
|
||||
'Les cinq box initiaux ne sont pas disponibles',
|
||||
);
|
||||
});
|
||||
$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]);
|
||||
}
|
||||
$assert((int) $db->query("SELECT COUNT(*) FROM care_rounds WHERE round_date='2099-01-01'")->fetchColumn() === 4);
|
||||
$db->exec("DELETE FROM care_rounds WHERE round_date='2099-01-01'");
|
||||
});
|
||||
$test('Les critères d’accueil d’une salle sont appliqués', function () use ($assert): void {
|
||||
$room = ['allowed_species' => 'chat', 'allowed_sex' => 'M', 'required_fiv' => 'positive'];
|
||||
$assert(ShelterRoomService::accepts($room, ['species' => 'chat', 'sex' => 'M', 'fiv' => 1]));
|
||||
$assert(!ShelterRoomService::accepts($room, ['species' => 'chien', 'sex' => 'M', 'fiv' => 1]));
|
||||
$assert(!ShelterRoomService::accepts($room, ['species' => 'chat', 'sex' => 'F', 'fiv' => 1]));
|
||||
$assert(!ShelterRoomService::accepts($room, ['species' => 'chat', 'sex' => 'M', 'fiv' => 0]));
|
||||
});
|
||||
$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');
|
||||
|
|
|
|||
Loading…
Reference in a new issue