Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 351153e78a | |||
| 842ceca007 | |||
| b78f2422d8 | |||
| 27d7c1e832 |
@@ -21,6 +21,7 @@ function dxfRecipeWizard(taskId, csrf, msgs) {
|
|||||||
imageName: '',
|
imageName: '',
|
||||||
imageSrc: '',
|
imageSrc: '',
|
||||||
dxfFile: null,
|
dxfFile: null,
|
||||||
|
mmPx: '', // #98: mm/px a mano (immagine da file), stringa con virgola o punto
|
||||||
bandMm: 0.1,
|
bandMm: 0.1,
|
||||||
minPointsPct: 95,
|
minPointsPct: 95,
|
||||||
dark: true, // spuntato = pezzo scuro su fondo chiaro (controluce)
|
dark: true, // spuntato = pezzo scuro su fondo chiaro (controluce)
|
||||||
@@ -59,8 +60,19 @@ function dxfRecipeWizard(taskId, csrf, msgs) {
|
|||||||
},
|
},
|
||||||
|
|
||||||
get calibrated() { return this.calState === 'calibrated'; },
|
get calibrated() { return this.calState === 'calibrated'; },
|
||||||
|
// #98: mm/px inserito a mano -> px/mm; null se vuoto o fuori da 0.00001..100 (limiti di /camera/calibrazioni/manuale, #95)
|
||||||
|
get manualPxMm() {
|
||||||
|
const raw = String(this.mmPx || '').trim().replace(',', '.');
|
||||||
|
if (!raw) return null;
|
||||||
|
const v = Number(raw);
|
||||||
|
if (!Number.isFinite(v) || v < 0.00001 || v > 100) return null;
|
||||||
|
return 1 / v;
|
||||||
|
},
|
||||||
|
// #98: compilato ma non valido -> errore in pagina
|
||||||
|
get mmPxInvalid() { return String(this.mmPx || '').trim() !== '' && this.manualPxMm === null; },
|
||||||
get canRun() {
|
get canRun() {
|
||||||
return !!(this.stationId && this.calibrated && this.imageFile && this.dxfFile) && !this.busy;
|
// #98: la scala a mano sostituisce la calibrazione della stazione
|
||||||
|
return !!(this.stationId && (this.calibrated || this.manualPxMm !== null) && this.imageFile && this.dxfFile) && !this.busy;
|
||||||
},
|
},
|
||||||
|
|
||||||
_setImage(file) {
|
_setImage(file) {
|
||||||
@@ -120,6 +132,8 @@ function dxfRecipeWizard(taskId, csrf, msgs) {
|
|||||||
fd.append('image', this.imageFile, this.imageFile.name || 'capture.png');
|
fd.append('image', this.imageFile, this.imageFile.name || 'capture.png');
|
||||||
fd.append('dxf', this.dxfFile, this.dxfFile.name);
|
fd.append('dxf', this.dxfFile, this.dxfFile.name);
|
||||||
fd.append('station_id', this.stationId);
|
fd.append('station_id', this.stationId);
|
||||||
|
// #98: scala a mano; il backend la preferisce alla calibrazione della stazione
|
||||||
|
if (this.manualPxMm !== null) fd.append('px_per_mm', String(this.manualPxMm));
|
||||||
fd.append('band_mm', String(this.bandMm));
|
fd.append('band_mm', String(this.bandMm));
|
||||||
fd.append('min_points_pct', String(this.minPointsPct));
|
fd.append('min_points_pct', String(this.minPointsPct));
|
||||||
fd.append('polarity', this.dark ? 'dark' : 'light');
|
fd.append('polarity', this.dark ? 'dark' : 'light');
|
||||||
|
|||||||
@@ -680,13 +680,23 @@
|
|||||||
<p x-show="stationId && calibrated" class="mt-1 text-xs text-emerald-600">
|
<p x-show="stationId && calibrated" class="mt-1 text-xs text-emerald-600">
|
||||||
{{ _('Calibrazione') }}: <span x-text="calLabel"></span>
|
{{ _('Calibrazione') }}: <span x-text="calLabel"></span>
|
||||||
</p>
|
</p>
|
||||||
<p x-show="stationId && calState && !calibrated" class="mt-1 text-xs text-amber-600">
|
<p x-show="stationId && calState && !calibrated && manualPxMm === null" class="mt-1 text-xs text-amber-600">{# #98: nascosto con mm/px valido #}
|
||||||
<span x-text="calState === 'uncalibrated' ? {{ _('Stazione non calibrata: calibrala prima di creare la ricetta.')|tojson_attr }} : {{ _('Acquisitore non raggiungibile: impossibile leggere la calibrazione.')|tojson_attr }}"></span>
|
<span x-text="calState === 'uncalibrated' ? {{ _('Stazione non calibrata: calibrala prima di creare la ricetta.')|tojson_attr }} : {{ _('Acquisitore non raggiungibile: impossibile leggere la calibrazione.')|tojson_attr }}"></span>
|
||||||
<a :href="calUrl" target="_blank" rel="noopener" x-show="calUrl"
|
<a :href="calUrl" target="_blank" rel="noopener" x-show="calUrl"
|
||||||
class="font-medium text-primary hover:underline">{{ _('Calibra') }}</a>
|
class="font-medium text-primary hover:underline">{{ _('Calibra') }}</a>
|
||||||
<span class="text-[var(--text-muted)]">({{ _('oppure vedi la pagina Stazioni') }})</span>
|
<span class="text-[var(--text-muted)]">({{ _('oppure vedi la pagina Stazioni') }})</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- #98: mm/px a mano per immagini da file, sostituisce la calibrazione della stazione -->
|
||||||
|
<div>
|
||||||
|
<label class="tmf-label text-xs">{{ _('mm/px (immagine da file)') }}</label>
|
||||||
|
<input type="text" inputmode="decimal" class="tmf-input text-xs" x-model="mmPx" placeholder="0,01406">
|
||||||
|
<p x-show="manualPxMm !== null" class="mt-1 text-xs text-emerald-600">
|
||||||
|
= <span x-text="fmt(manualPxMm)"></span> px/mm
|
||||||
|
</p>
|
||||||
|
<p x-show="mmPxInvalid" class="mt-1 text-xs text-amber-600">{{ _('mm/px non valido') }}</p>
|
||||||
|
<p class="mt-1 text-xs text-[var(--text-muted)]">{{ _('Se compilato sostituisce la calibrazione della stazione; la scala resta salvata nella ricetta.') }}</p>
|
||||||
|
</div>
|
||||||
<!-- Immagine: camera o file -->
|
<!-- Immagine: camera o file -->
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -178,6 +178,12 @@ class TestDxfCompareProxy:
|
|||||||
assert set(kwargs["files"]) == {"image", "dxf"}
|
assert set(kwargs["files"]) == {"image", "dxf"}
|
||||||
assert kwargs["files"]["image"][1] == PNG
|
assert kwargs["files"]["image"][1] == PNG
|
||||||
|
|
||||||
|
def test_manual_px_per_mm_forwarded(self, logged_in_client, mock_api_client):
|
||||||
|
# #98: scala a mano dal wizard (immagine da file): inoltrata al backend
|
||||||
|
mock_api_client.post.return_value = self.OK
|
||||||
|
self._post(logged_in_client, station_id="4", px_per_mm="71.1237")
|
||||||
|
assert mock_api_client.post.call_args.kwargs["data"]["px_per_mm"] == "71.1237"
|
||||||
|
|
||||||
def test_save_true_forwarded(self, logged_in_client, mock_api_client):
|
def test_save_true_forwarded(self, logged_in_client, mock_api_client):
|
||||||
mock_api_client.post.return_value = {**self.OK, "saved": True, "recipe_dir": "vision/recipes/7/x"}
|
mock_api_client.post.return_value = {**self.OK, "saved": True, "recipe_dir": "vision/recipes/7/x"}
|
||||||
resp = self._post(logged_in_client, save="true")
|
resp = self._post(logged_in_client, save="true")
|
||||||
@@ -229,4 +235,5 @@ class TestPages:
|
|||||||
assert "js/dxf_recipe_wizard.js" in html
|
assert "js/dxf_recipe_wizard.js" in html
|
||||||
assert "dxfRecipeWizard(" in html
|
assert "dxfRecipeWizard(" in html
|
||||||
assert "@dxf-recipe-saved" in html
|
assert "@dxf-recipe-saved" in html
|
||||||
|
assert 'x-model="mmPx"' in html # #98: campo mm/px a mano
|
||||||
assert "Pezzo scuro su fondo chiaro" in html or "Dark part on light" in html
|
assert "Pezzo scuro su fondo chiaro" in html or "Dark part on light" in html
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user