feat: FASE 5b/6.1+6.2 - SPC Backend + Dashboard Metrologist (Plotly.js)
Aggiunge servizio SPC con calcoli Cp/Cpk/Pp/Ppk, carta di controllo (UCL/LCL), istogramma con curva normale. Router FastAPI con 5 endpoint statistics, blueprint Flask con proxy AJAX, dashboard interattiva Alpine.js + Plotly.js con filtri per ricetta/subtask/date, riepilogo pass/fail, gauge Cpk e i18n IT/EN completo. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -259,6 +259,7 @@ def save_measurement():
|
||||
"lot_number": data.get("lot_number", session.get("lot_number", "")),
|
||||
"serial_number": data.get("serial_number", session.get("serial_number", "")),
|
||||
"measured_by": session.get("user_id"),
|
||||
"input_method": data.get("input_method", "manual"),
|
||||
}
|
||||
|
||||
resp = api_client.post("/api/measurements", data=payload)
|
||||
|
||||
@@ -1,44 +1,107 @@
|
||||
"""Metrologist blueprint - SPC statistics and dashboards."""
|
||||
from flask import Blueprint, render_template, session, redirect, url_for
|
||||
"""Metrologist blueprint - SPC statistics dashboard and API proxies."""
|
||||
from flask import Blueprint, jsonify, render_template, request
|
||||
from flask_babel import gettext as _
|
||||
|
||||
from blueprints.auth import login_required, role_required
|
||||
from services.api_client import api_client
|
||||
|
||||
statistics_bp = Blueprint("statistics", __name__)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# PAGINE
|
||||
# ============================================================================
|
||||
|
||||
@statistics_bp.route("/dashboard")
|
||||
@login_required
|
||||
@role_required("Metrologist")
|
||||
def dashboard():
|
||||
"""SPC dashboard overview."""
|
||||
if "user" not in session:
|
||||
return redirect(url_for("auth.login"))
|
||||
return render_template("statistics/dashboard.html")
|
||||
"""SPC dashboard overview — loads recipes for filter dropdown."""
|
||||
resp = api_client.get("/api/recipes", params={"per_page": 100})
|
||||
if isinstance(resp, dict) and resp.get("error"):
|
||||
recipes = []
|
||||
else:
|
||||
recipes = resp.get("items", []) if isinstance(resp, dict) else []
|
||||
|
||||
return render_template("statistics/dashboard.html", recipes=recipes)
|
||||
|
||||
|
||||
@statistics_bp.route("/control-chart")
|
||||
def control_chart():
|
||||
"""X-bar / R control chart."""
|
||||
if "user" not in session:
|
||||
return redirect(url_for("auth.login"))
|
||||
return render_template("statistics/control_chart.html")
|
||||
# ============================================================================
|
||||
# PROXY API AJAX → FastAPI
|
||||
# ============================================================================
|
||||
|
||||
def _proxy_statistics(endpoint: str):
|
||||
"""Forward query params to a FastAPI statistics endpoint."""
|
||||
params = {}
|
||||
for key in [
|
||||
"recipe_id", "version_id", "subtask_id",
|
||||
"date_from", "date_to", "operator_id",
|
||||
"lot_number", "serial_number", "n_bins",
|
||||
]:
|
||||
val = request.args.get(key)
|
||||
if val is not None and val != "":
|
||||
params[key] = val
|
||||
|
||||
resp = api_client.get(f"/api/statistics/{endpoint}", params=params)
|
||||
|
||||
if isinstance(resp, dict) and resp.get("error"):
|
||||
return jsonify({"error": True, "detail": resp.get("detail", "")}), resp.get("status_code", 500)
|
||||
|
||||
return jsonify(resp)
|
||||
|
||||
|
||||
@statistics_bp.route("/histogram")
|
||||
def histogram():
|
||||
"""Histogram with normal curve."""
|
||||
if "user" not in session:
|
||||
return redirect(url_for("auth.login"))
|
||||
return render_template("statistics/histogram.html")
|
||||
@statistics_bp.route("/api/summary")
|
||||
@login_required
|
||||
@role_required("Metrologist")
|
||||
def api_summary():
|
||||
"""Proxy: summary pass/fail/warning."""
|
||||
return _proxy_statistics("summary")
|
||||
|
||||
|
||||
@statistics_bp.route("/capability")
|
||||
def capability():
|
||||
"""Cp/Cpk/Pp/Ppk capability gauge."""
|
||||
if "user" not in session:
|
||||
return redirect(url_for("auth.login"))
|
||||
return render_template("statistics/capability.html")
|
||||
@statistics_bp.route("/api/capability")
|
||||
@login_required
|
||||
@role_required("Metrologist")
|
||||
def api_capability():
|
||||
"""Proxy: capability indices."""
|
||||
return _proxy_statistics("capability")
|
||||
|
||||
|
||||
@statistics_bp.route("/trend")
|
||||
def trend():
|
||||
"""Temporal trends and period comparison."""
|
||||
if "user" not in session:
|
||||
return redirect(url_for("auth.login"))
|
||||
return render_template("statistics/trend.html")
|
||||
@statistics_bp.route("/api/control-chart")
|
||||
@login_required
|
||||
@role_required("Metrologist")
|
||||
def api_control_chart():
|
||||
"""Proxy: control chart data."""
|
||||
return _proxy_statistics("control-chart")
|
||||
|
||||
|
||||
@statistics_bp.route("/api/histogram")
|
||||
@login_required
|
||||
@role_required("Metrologist")
|
||||
def api_histogram():
|
||||
"""Proxy: histogram data."""
|
||||
return _proxy_statistics("histogram")
|
||||
|
||||
|
||||
@statistics_bp.route("/api/subtasks")
|
||||
@login_required
|
||||
@role_required("Metrologist")
|
||||
def api_subtasks():
|
||||
"""Proxy: get subtasks for recipe filter."""
|
||||
params = {}
|
||||
recipe_id = request.args.get("recipe_id")
|
||||
if recipe_id:
|
||||
params["recipe_id"] = recipe_id
|
||||
version_id = request.args.get("version_id")
|
||||
if version_id:
|
||||
params["version_id"] = version_id
|
||||
|
||||
resp = api_client.get("/api/statistics/subtasks", params=params)
|
||||
|
||||
if isinstance(resp, dict) and resp.get("error"):
|
||||
return jsonify({"error": True, "detail": resp.get("detail", "")}), resp.get("status_code", 500)
|
||||
|
||||
# Response is a list
|
||||
if isinstance(resp, list):
|
||||
return jsonify(resp)
|
||||
|
||||
return jsonify(resp)
|
||||
|
||||
Reference in New Issue
Block a user