statistiche: con Tutti i punti tabella capability per punto, carta e istogramma solo con punto scelto, il select ricarica da solo (Refs #100)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-25 15:51:38 +01:00
parent 27d7c1e832
commit 842ceca007
3 changed files with 71 additions and 6 deletions
@@ -51,7 +51,9 @@
<!-- Subtask --> <!-- Subtask -->
<div> <div>
<label class="block text-xs font-medium text-steel mb-1">{{ _("Punto di misura") }}</label> <label class="block text-xs font-medium text-steel mb-1">{{ _("Punto di misura") }}</label>
<!-- #100: il cambio di punto ricarica subito, senza «Applica filtri» -->
<select x-model="selectedSubtaskId" <select x-model="selectedSubtaskId"
@change="loadData()"
:disabled="subtasks.length === 0" :disabled="subtasks.length === 0"
class="w-full rounded-lg border border-[var(--border-color)] bg-[var(--bg-primary)] text-[var(--text-primary)] px-3 py-2 text-sm focus:ring-2 focus:ring-primary focus:border-primary disabled:opacity-50"> class="w-full rounded-lg border border-[var(--border-color)] bg-[var(--bg-primary)] text-[var(--text-primary)] px-3 py-2 text-sm focus:ring-2 focus:ring-primary focus:border-primary disabled:opacity-50">
<option value="">{{ _("Tutti i punti") }}</option> <option value="">{{ _("Tutti i punti") }}</option>
@@ -287,20 +289,52 @@
</div> </div>
</div> </div>
</template> </template>
<template x-if="!capability || capability.cp == null"> <!-- #100: con «Tutti i punti» tabella con una riga per punto di misura -->
<template x-if="!loadedSubtaskId && capabilityAll.length > 0">
<div>
<table class="w-full text-sm">
<thead>
<tr class="text-xs text-steel text-left">
<th class="py-1 font-medium">{{ _("Punto di misura") }}</th>
<th class="py-1 font-medium text-right">n</th>
<th class="py-1 font-medium text-right">Cp</th>
<th class="py-1 font-medium text-right">Cpk</th>
</tr>
</thead>
<tbody>
<template x-for="row in capabilityAll" :key="row.subtask.id">
<tr class="cursor-pointer hover:bg-[var(--bg-primary)] border-t border-[var(--border-color)]"
@click="selectedSubtaskId = String(row.subtask.id); loadData()">
<td class="py-1.5 pr-2" x-text="'#' + row.subtask.marker_number + ' — ' + row.subtask.description"></td>
<td class="py-1.5 text-right font-mono" x-text="row.data && row.data.n != null ? row.data.n : '—'"></td>
<td class="py-1.5 text-right font-mono font-bold"
:class="row.data && row.data.cp != null ? capabilityColor(row.data.cp) : 'text-steel'"
x-text="row.data && row.data.cp != null ? row.data.cp : '—'"></td>
<td class="py-1.5 text-right font-mono font-bold"
:class="row.data && row.data.cpk != null ? capabilityColor(row.data.cpk) : 'text-steel'"
x-text="row.data && row.data.cpk != null ? row.data.cpk : '—'"></td>
</tr>
</template>
</tbody>
</table>
<p class="text-xs text-steel mt-3">{{ _("Scegli un punto per carta di controllo e istogramma") }}</p>
</div>
</template>
<template x-if="(!capability || capability.cp == null) && (loadedSubtaskId || capabilityAll.length === 0)">
<p class="text-center text-steel py-8">{{ _("Seleziona un punto di misura per calcolare gli indici") }}</p> <p class="text-center text-steel py-8">{{ _("Seleziona un punto di misura per calcolare gli indici") }}</p>
</template> </template>
</div> </div>
</div> </div>
<!-- Row 2: Carta di Controllo --> <!-- Row 2: Carta di Controllo -->
<div class="bg-[var(--bg-secondary)] rounded-xl border border-[var(--border-color)] p-5 shadow-sm mb-6"> <!-- #100: x-show (non x-if) perche' renderCharts() cerca gli id nel DOM; nascosta senza punto -->
<div x-show="loadedSubtaskId" class="bg-[var(--bg-secondary)] rounded-xl border border-[var(--border-color)] p-5 shadow-sm mb-6">
<h2 class="text-sm font-semibold text-steel uppercase tracking-wider mb-4">{{ _("Carta di Controllo") }}</h2> <h2 class="text-sm font-semibold text-steel uppercase tracking-wider mb-4">{{ _("Carta di Controllo") }}</h2>
<div id="control-chart" style="min-height: 350px;"></div> <div id="control-chart" style="min-height: 350px;"></div>
</div> </div>
<!-- Row 3: Istogramma --> <!-- Row 3: Istogramma -->
<div class="bg-[var(--bg-secondary)] rounded-xl border border-[var(--border-color)] p-5 shadow-sm mb-6"> <div x-show="loadedSubtaskId" class="bg-[var(--bg-secondary)] rounded-xl border border-[var(--border-color)] p-5 shadow-sm mb-6">
<h2 class="text-sm font-semibold text-steel uppercase tracking-wider mb-4">{{ _("Istogramma") }}</h2> <h2 class="text-sm font-semibold text-steel uppercase tracking-wider mb-4">{{ _("Istogramma") }}</h2>
<div id="histogram-chart" style="min-height: 350px;"></div> <div id="histogram-chart" style="min-height: 350px;"></div>
</div> </div>
@@ -346,6 +380,8 @@ function spcDashboard() {
subtasks: [], subtasks: [],
// Data state // Data state
loadedSubtaskId: '', // #100 punto con cui sono stati caricati i dati (i pannelli dipendono da questo)
capabilityAll: [], // #100 con «Tutti i punti»: [{subtask, data}] una riga per punto
summary: null, summary: null,
capability: null, capability: null,
controlChart: null, controlChart: null,
@@ -408,6 +444,8 @@ function spcDashboard() {
async onRecipeChange() { async onRecipeChange() {
this.selectedSubtaskId = ''; this.selectedSubtaskId = '';
this.loadedSubtaskId = ''; // #100
this.capabilityAll = []; // #100
this.selectedRunId = ''; // #58 this.selectedRunId = ''; // #58
this.runs = []; this.runs = [];
this.subtasks = []; this.subtasks = [];
@@ -459,10 +497,11 @@ function spcDashboard() {
if (this.selectedRunId) params.set('production_run_id', this.selectedRunId); // #58 if (this.selectedRunId) params.set('production_run_id', this.selectedRunId); // #58
if (this.dateFrom) params.set('date_from', new Date(this.dateFrom + 'T00:00:00').toISOString()); if (this.dateFrom) params.set('date_from', new Date(this.dateFrom + 'T00:00:00').toISOString());
if (this.dateTo) params.set('date_to', new Date(this.dateTo + 'T23:59:59.999').toISOString()); if (this.dateTo) params.set('date_to', new Date(this.dateTo + 'T23:59:59.999').toISOString());
const requestedSubtaskId = this.selectedSubtaskId; // #100 punto richiesto da questa chiamata
try { try {
// Parallel fetch of all endpoints // Parallel fetch of all endpoints
const [summaryResp, capabilityResp, controlResp, histogramResp] = await Promise.allSettled([ const [summaryResp, capabilityResp, controlResp, histogramResp, allResp] = await Promise.allSettled([
fetch('/statistics/api/summary?' + params.toString()).then(r => r.json()), fetch('/statistics/api/summary?' + params.toString()).then(r => r.json()),
this.selectedSubtaskId this.selectedSubtaskId
? fetch('/statistics/api/capability?' + params.toString()).then(r => r.json()) ? fetch('/statistics/api/capability?' + params.toString()).then(r => r.json())
@@ -473,6 +512,17 @@ function spcDashboard() {
this.selectedSubtaskId this.selectedSubtaskId
? fetch('/statistics/api/histogram?' + params.toString()).then(r => r.json()) ? fetch('/statistics/api/histogram?' + params.toString()).then(r => r.json())
: Promise.resolve(null), : Promise.resolve(null),
// #100: senza punto scelto, capability di ogni punto della ricetta
requestedSubtaskId
? Promise.resolve([])
: Promise.all(this.subtasks.map(st => {
const p = new URLSearchParams(params.toString());
p.set('subtask_id', st.id);
return fetch('/statistics/api/capability?' + p.toString())
.then(r => r.json())
.then(d => ({ subtask: st, data: (d && !d.error) ? d : null }))
.catch(() => ({ subtask: st, data: null }));
})),
]); ]);
// Summary (always available) // Summary (always available)
@@ -505,6 +555,9 @@ function spcDashboard() {
this.histogram = null; this.histogram = null;
} }
// #100: lista per-punto solo con «Tutti i punti»; con un punto resta il flusso di prima
this.capabilityAll = (allResp.status === 'fulfilled' && Array.isArray(allResp.value)) ? allResp.value : [];
this.loadedSubtaskId = requestedSubtaskId;
this.hasData = true; this.hasData = true;
// Render charts after DOM update // Render charts after DOM update
@@ -522,7 +575,7 @@ function spcDashboard() {
renderCharts() { renderCharts() {
// Control chart // Control chart
if (this.controlChart && this.selectedSubtaskId) { if (this.controlChart && this.loadedSubtaskId) { // #100
renderControlChart('control-chart', this.controlChart); renderControlChart('control-chart', this.controlChart);
} else { } else {
const el = document.getElementById('control-chart'); const el = document.getElementById('control-chart');
@@ -530,7 +583,7 @@ function spcDashboard() {
} }
// Histogram // Histogram
if (this.histogram && this.selectedSubtaskId) { if (this.histogram && this.loadedSubtaskId) { // #100
const tol = this.capability ? { const tol = this.capability ? {
utl: this.capability.utl, utl: this.capability.utl,
ltl: this.capability.ltl, ltl: this.capability.ltl,
@@ -167,3 +167,11 @@ class TestRunTaskResultsProxy:
assert 'id="run-task-results"' in html assert 'id="run-task-results"' in html
assert "loadTaskResults" in html assert "loadTaskResults" in html
assert "/statistics/api/runs/" in html assert "/statistics/api/runs/" in html
def test_dashboard_all_points_overview(self, logged_in_client, mock_api_client):
# #100: con «Tutti i punti» la pagina ha tabella per-punto e select che ricarica da solo
mock_api_client.get.return_value = {"items": [], "total": 0, "pages": 1}
html = logged_in_client.get("/statistics/dashboard").get_data(as_text=True)
assert "capabilityAll" in html
assert "loadedSubtaskId" in html
assert '@change="loadData()"' in html
@@ -3485,3 +3485,7 @@ msgstr "Invalid camera address"
#: templates/admin/stations.html blueprints/admin.py (#88) #: templates/admin/stations.html blueprints/admin.py (#88)
msgid "Risposta non valida dal servizio camera" msgid "Risposta non valida dal servizio camera"
msgstr "Invalid response from the camera service" msgstr "Invalid response from the camera service"
#: templates/statistics/dashboard.html (#100)
msgid "Scegli un punto per carta di controllo e istogramma"
msgstr "Choose a point for the control chart and histogram"