feat: suivre la température pendant les tournées
This commit is contained in:
parent
68590eddbb
commit
d35434c522
15 changed files with 129 additions and 23 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.21] — 2026-09-15
|
||||
|
||||
### Suivi de la température
|
||||
|
||||
- ajout de la température à chaque contrôle d’animal pendant une tournée, préremplie à 38,5 °C et modifiable ;
|
||||
- validation des températures enregistrées entre 30 et 45 °C ;
|
||||
- regroupement du poids et de la température dans un graphique médical commun, avec une échelle dédiée à chaque unité ;
|
||||
- reprise dans la courbe des températures saisies dans les notes médicales et pendant les tournées.
|
||||
|
||||
## [1.0.0-rc.20] — 2026-09-13
|
||||
|
||||
### Localisation des animaux
|
||||
|
|
|
|||
|
|
@ -89,3 +89,7 @@ Ajout d’une date et d’une heure modifiables aux contrôles médicaux, avec r
|
|||
## `1.0.0-rc.20` — Localisations cohérentes
|
||||
|
||||
Simplification des lieux affichés dans la fiche, conservation de l’adresse actuelle et réparation des coordonnées manquantes après un import ou un placement.
|
||||
|
||||
## `1.0.0-rc.21` — Température pendant les tournées
|
||||
|
||||
Ajout d’une température préremplie à 38,5 °C lors du contrôle de chaque animal et affichage conjoint des courbes de poids et de température dans son dossier médical.
|
||||
|
|
|
|||
|
|
@ -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.20`. 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.21`. 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.20
|
||||
1.0.0-rc.21
|
||||
|
|
|
|||
|
|
@ -704,6 +704,21 @@ trait AnimalBrowseActions
|
|||
$weightsStmt->execute([':id' => $id]);
|
||||
$weights = $weightsStmt->fetchAll();
|
||||
|
||||
$temperaturesStmt = $db->prepare("
|
||||
SELECT measured_at, value FROM (
|
||||
SELECT noted_at AS measured_at, temperature_c AS value
|
||||
FROM medical_notes
|
||||
WHERE animal_id = :medical_id AND temperature_c IS NOT NULL
|
||||
UNION ALL
|
||||
SELECT observation.checked_at AS measured_at, observation.temperature_c AS value
|
||||
FROM care_round_observations observation
|
||||
WHERE observation.animal_id = :round_id AND observation.temperature_c IS NOT NULL
|
||||
)
|
||||
ORDER BY datetime(measured_at) ASC
|
||||
");
|
||||
$temperaturesStmt->execute([':medical_id' => $id, ':round_id' => $id]);
|
||||
$temperatures = $temperaturesStmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$stmt = $db->prepare("
|
||||
SELECT value
|
||||
FROM measurements
|
||||
|
|
@ -952,6 +967,7 @@ trait AnimalBrowseActions
|
|||
'adopters' => $adopters,
|
||||
'death' => $death,
|
||||
'weights' => $weights,
|
||||
'temperatures' => $temperatures,
|
||||
'lastWeight' => $lastWeight,
|
||||
'prevWeight' => $prevWeight,
|
||||
'photos' => $photos,
|
||||
|
|
|
|||
|
|
@ -139,6 +139,18 @@ final class CareRoundsController
|
|||
$value = (string) ($_POST[$field] ?? $opts[0]);
|
||||
$values[$field] = in_array($value, $opts, true) ? $value : $opts[0];
|
||||
}
|
||||
$temperatureInput = str_replace(',', '.', trim((string) ($_POST['temperature_c'] ?? '')));
|
||||
if (!is_numeric($temperatureInput)) {
|
||||
http_response_code(400);
|
||||
echo h(t('care.invalid_temperature'));
|
||||
return;
|
||||
}
|
||||
$temperature = (float) $temperatureInput;
|
||||
if ($temperature < 30 || $temperature > 45) {
|
||||
http_response_code(400);
|
||||
echo h(t('care.invalid_temperature'));
|
||||
return;
|
||||
}
|
||||
$db->prepare(
|
||||
'UPDATE care_rounds SET performed_by_user_id=COALESCE(performed_by_user_id,:user) WHERE id=:id',
|
||||
)->execute([':user' => Auth::id(), ':id' => $roundId]);
|
||||
|
|
@ -220,7 +232,7 @@ final class CareRoundsController
|
|||
$db->beginTransaction();
|
||||
try {
|
||||
$stmt = $db->prepare(
|
||||
"INSERT INTO care_round_observations(round_id,animal_id,food,water,urine,stool,general_state,comment,alert_signature) VALUES(:round,:animal,:food,:water,:urine,:stool,:general,:comment,:signature) ON CONFLICT(round_id,animal_id) DO UPDATE SET food=excluded.food,water=excluded.water,urine=excluded.urine,stool=excluded.stool,general_state=excluded.general_state,comment=excluded.comment,alert_signature=excluded.alert_signature,checked_at=datetime('now')",
|
||||
"INSERT INTO care_round_observations(round_id,animal_id,food,water,urine,stool,general_state,temperature_c,comment,alert_signature) VALUES(:round,:animal,:food,:water,:urine,:stool,:general,:temperature,:comment,:signature) ON CONFLICT(round_id,animal_id) DO UPDATE SET food=excluded.food,water=excluded.water,urine=excluded.urine,stool=excluded.stool,general_state=excluded.general_state,temperature_c=excluded.temperature_c,comment=excluded.comment,alert_signature=excluded.alert_signature,checked_at=datetime('now')",
|
||||
);
|
||||
$stmt->execute([
|
||||
':round' => $roundId,
|
||||
|
|
@ -230,6 +242,7 @@ final class CareRoundsController
|
|||
':urine' => $values['urine'],
|
||||
':stool' => $values['stool'],
|
||||
':general' => $values['general_state'],
|
||||
':temperature' => $temperature,
|
||||
':comment' => $comment ?: null,
|
||||
':signature' => $signature,
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -235,14 +235,14 @@
|
|||
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title m-0"><?= h(t('animal.weight_evolution')) ?></h3>
|
||||
<h3 class="card-title m-0"><?= h(t('animal.measurements_evolution')) ?></h3>
|
||||
</div>
|
||||
|
||||
<?php if (!$weights): ?>
|
||||
<div class="card-body text-muted"><?= h(t('animal.no_weight')) ?></div>
|
||||
<?php if (!$weights && !$temperatures): ?>
|
||||
<div class="card-body text-muted"><?= h(t('animal.no_measurement')) ?></div>
|
||||
<?php else: ?>
|
||||
<div style="height:260px">
|
||||
<canvas id="weightChart"></canvas>
|
||||
<canvas id="healthMeasurementsChart"></canvas>
|
||||
</div>
|
||||
|
||||
<?php endif; ?>
|
||||
|
|
|
|||
|
|
@ -944,25 +944,47 @@ $currentTime = date('H:i');
|
|||
</script>
|
||||
<script>
|
||||
(function() {
|
||||
const el = document.getElementById('weightChart');
|
||||
const el = document.getElementById('healthMeasurementsChart');
|
||||
if (!el) return;
|
||||
|
||||
const rows = <?= json_encode($weights ?? [], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) ?> ;
|
||||
const weights = <?= json_encode($weights ?? [], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) ?>;
|
||||
const temperatures = <?= json_encode($temperatures ?? [], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) ?>;
|
||||
const normalizeDate = value => String(value || '').replace(' ', 'T');
|
||||
const labels = [...new Set([
|
||||
...weights.map(row => normalizeDate(row.measured_at)),
|
||||
...temperatures.map(row => normalizeDate(row.measured_at))
|
||||
])].filter(Boolean).sort();
|
||||
const valuesByDate = rows => new Map(rows.map(row => [normalizeDate(row.measured_at), Number(row.value)]));
|
||||
const weightValues = valuesByDate(weights);
|
||||
const temperatureValues = valuesByDate(temperatures);
|
||||
|
||||
// labels = dates, data = poids
|
||||
const labels = rows.map(r => (r.measured_at || '').slice(0, 10)); // YYYY-MM-DD
|
||||
const data = rows.map(r => Number(r.value));
|
||||
|
||||
// Chart.js default color (on ne fixe rien volontairement)
|
||||
new Chart(el, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels,
|
||||
datasets: [{
|
||||
labels: labels.map(value => new Date(value).toLocaleString(undefined, {
|
||||
dateStyle: 'short',
|
||||
timeStyle: 'short'
|
||||
})),
|
||||
datasets: [
|
||||
{
|
||||
label: <?= json_encode(t('chart.weight'), JSON_UNESCAPED_UNICODE) ?> ,
|
||||
data,
|
||||
data: labels.map(label => weightValues.get(label) ?? null),
|
||||
borderColor: '#206bc4',
|
||||
backgroundColor: '#206bc4',
|
||||
tension: 0.25,
|
||||
pointRadius: 2
|
||||
pointRadius: 3,
|
||||
spanGaps: true,
|
||||
yAxisID: 'weight'
|
||||
},
|
||||
{
|
||||
label: <?= json_encode(t('chart.temperature'), JSON_UNESCAPED_UNICODE) ?>,
|
||||
data: labels.map(label => temperatureValues.get(label) ?? null),
|
||||
borderColor: '#d63939',
|
||||
backgroundColor: '#d63939',
|
||||
tension: 0.25,
|
||||
pointRadius: 3,
|
||||
spanGaps: true,
|
||||
yAxisID: 'temperature'
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
|
|
@ -974,12 +996,25 @@ $currentTime = date('H:i');
|
|||
}
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
weight: {
|
||||
type: 'linear',
|
||||
position: 'left',
|
||||
title: {
|
||||
display: true,
|
||||
text: 'kg'
|
||||
}
|
||||
},
|
||||
temperature: {
|
||||
type: 'linear',
|
||||
position: 'right',
|
||||
grid: {
|
||||
drawOnChartArea: false
|
||||
},
|
||||
title: {
|
||||
display: true,
|
||||
text: '°C'
|
||||
}
|
||||
},
|
||||
x: {
|
||||
title: {
|
||||
display: true,
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
/** @var array $treatments */
|
||||
/** @var array $vaccinations */
|
||||
/** @var array $weights */
|
||||
/** @var array $temperatures */
|
||||
/** @var array adoption */
|
||||
|
||||
require_once __DIR__ . '/../Services/IcadService.php';
|
||||
|
|
|
|||
|
|
@ -347,6 +347,16 @@ $fieldOptions = [
|
|||
type="radio" name="<?= h($field) ?>" value="<?= h($value) ?>"
|
||||
<?= (($obs[$field] ?? array_key_first($options)) === $value) ? 'checked' : '' ?>><span><?= h($label) ?></span></label><?php endforeach; ?>
|
||||
</div><?php endforeach; ?>
|
||||
<div class="mb-2">
|
||||
<label class="form-label fw-bold" for="temperature-<?= (int)$a['id'] ?>">🌡️ <?= h(t('care.temperature')) ?></label>
|
||||
<div class="input-group">
|
||||
<input class="form-control" id="temperature-<?= (int)$a['id'] ?>"
|
||||
name="temperature_c" type="number" min="30" max="45" step="0.1"
|
||||
inputmode="decimal" required
|
||||
value="<?= h(number_format((float)($obs['temperature_c'] ?? 38.5), 1, '.', '')) ?>">
|
||||
<span class="input-group-text">°C</span>
|
||||
</div>
|
||||
</div>
|
||||
<?php foreach ($treatmentsByAnimal[(int)$a['id']] ?? [] as $t): ?><div
|
||||
class="card card-sm bg-purple-lt mb-2">
|
||||
<div class="card-body py-2"><strong>💊 <?= h($t['medication_name']) ?></strong>
|
||||
|
|
|
|||
|
|
@ -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.20</span>
|
||||
<h3>Localisations cohérentes</h3>
|
||||
<p class="text-muted">Lieu de récupération et lieu actuel clairement distingués, avec réparation
|
||||
des coordonnées manquantes après un import ou un placement.</p>
|
||||
<span class="badge bg-blue-lt mb-2">1.0.0-rc.21</span>
|
||||
<h3>Température pendant les tournées</h3>
|
||||
<p class="text-muted">Température proposée à 38,5 °C pendant chaque contrôle et graphique médical
|
||||
commun pour suivre son évolution avec celle du poids.</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
|
|
|||
1
migrations/095_care_round_temperature.sql
Normal file
1
migrations/095_care_round_temperature.sql
Normal file
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE care_round_observations ADD COLUMN temperature_c REAL;
|
||||
|
|
@ -199,4 +199,9 @@ return [
|
|||
'updates.download_log'=>'Download technical log',
|
||||
'updates.log_missing'=>'No update log is available.',
|
||||
'error.invalid_medical_datetime'=>'Enter a valid check-up date and time.',
|
||||
'animal.measurements_evolution'=>'Weight and temperature trends',
|
||||
'animal.no_measurement'=>'No weight or temperature measurement recorded.',
|
||||
'care.temperature'=>'Temperature',
|
||||
'care.invalid_temperature'=>'Temperature must be between 30 and 45 °C.',
|
||||
'chart.temperature'=>'Temperature (°C)',
|
||||
];
|
||||
|
|
|
|||
|
|
@ -199,4 +199,9 @@ return [
|
|||
'updates.download_log'=>'Télécharger le journal technique',
|
||||
'updates.log_missing'=>'Aucun journal de mise à jour n’est disponible.',
|
||||
'error.invalid_medical_datetime'=>'Indiquez une date et une heure de contrôle valides.',
|
||||
'animal.measurements_evolution'=>'Évolution du poids et de la température',
|
||||
'animal.no_measurement'=>'Aucune mesure de poids ou de température enregistrée.',
|
||||
'care.temperature'=>'Température',
|
||||
'care.invalid_temperature'=>'La température doit être comprise entre 30 et 45 °C.',
|
||||
'chart.temperature'=>'Température (°C)',
|
||||
];
|
||||
|
|
|
|||
|
|
@ -68,6 +68,13 @@ try {
|
|||
$actual = (int) $db->query('SELECT COUNT(*) FROM _migrations')->fetchColumn();
|
||||
$assert($actual === $expected, "$actual/$expected migrations");
|
||||
});
|
||||
$test('Les tournées peuvent enregistrer une température', function () use ($db, $assert): void {
|
||||
$columns = array_column(
|
||||
$db->query('PRAGMA table_info(care_round_observations)')->fetchAll(PDO::FETCH_ASSOC),
|
||||
'name',
|
||||
);
|
||||
$assert(in_array('temperature_c', $columns, true), 'Colonne temperature_c absente');
|
||||
});
|
||||
$test("Les voies d’administration des médicaments sont initialisées", function () use ($db, $assert): void {
|
||||
$routes = $db
|
||||
->query('SELECT accro FROM ref_med_administration ORDER BY id')
|
||||
|
|
|
|||
Loading…
Reference in a new issue