feat: Quickstart, Benutzerverwaltung und Orion-Ergebnisbox

This commit is contained in:
Schubert Ferenc 2026-07-12 12:19:54 +02:00
parent 4f669a3426
commit e9e46f2cb4
404 changed files with 1032 additions and 600 deletions

View file

@ -90,6 +90,26 @@ def default_report_checklists(session: Session = Depends(get_session)) -> list[d
@router.get("/dashboard")
def dashboard(session: Session = Depends(get_session)) -> dict[str, int]:
today = func.current_date()
result_groups = {
"validation_result_open": ["OFFEN", "offen", None, ""],
"validation_result_passed": ["BESTANDEN", "bestanden"],
"validation_result_conditional": ["BESTANDEN_MIT_AUFLAGEN", "bestanden_mit_auflagen", "mit_auflagen", "bestanden mit Auflagen"],
"validation_result_failed": ["NICHT_BESTANDEN", "nicht_bestanden", "nicht bestanden"],
}
result_counts = {
key: session.scalar(select(func.count()).select_from(Validation).where(Validation.result.in_(values)))
or 0
for key, values in result_groups.items()
if None not in values
}
result_counts["validation_result_open"] = (
session.scalar(
select(func.count())
.select_from(Validation)
.where((Validation.result.in_(["OFFEN", "offen", ""])) | (Validation.result.is_(None)))
)
or 0
)
return {
"customers": session.scalar(select(func.count()).select_from(Customer)) or 0,
"locations": session.scalar(select(func.count()).select_from(Location)) or 0,
@ -137,6 +157,7 @@ def dashboard(session: Session = Depends(get_session)) -> dict[str, int]:
.where(Validation.next_validation_on < today)
)
or 0,
**result_counts,
}

View file

@ -8,6 +8,7 @@ from app.modules.orion.assets import schubamed_logo_uri
from app.modules.orion.components.base import ReportComponent
from app.modules.orion.context import ReportContext
from app.modules.orion.html import definition_list, paragraph, section, table, text, yes_no
from app.modules.orion.result import validation_result_box, validation_result_label
def render_template_text(content: str, context: ReportContext) -> str:
@ -63,7 +64,6 @@ class CoverComponent(ReportComponent):
("Pruefdatum", validation.performed_on),
("Pruefungsort", validation.test_location),
("Pruefer", validation.examiner_name),
("Gesamtergebnis", validation.result),
("Status", validation.status),
(
"Ansprechpartner",
@ -83,6 +83,7 @@ class CoverComponent(ReportComponent):
'<p class="cover-subtitle">Funktions- und Leistungsqualifikation Klein-Sterilisator</p>'
f'<p class="cover-subtitle">{text(context.customer.name)}</p>'
f"{rows}"
f"{validation_result_box(validation.result)}"
'<div class="signature-grid"><div>Unterschrift technische Validierung</div><div>Unterschrift Auftraggeber</div></div>'
"</section>"
)
@ -121,7 +122,8 @@ class SummaryComponent(ReportComponent):
def render(self, context: ReportContext) -> str:
validation = context.validation
block = context.text_blocks.get("summary")
body = f"<p>{paragraph(render_template_text(block.content, context) if block else 'nicht erfasst')}</p>"
body = validation_result_box(validation.result, compact=True)
body += f"<p>{paragraph(render_template_text(block.content, context) if block else 'nicht erfasst')}</p>"
body += definition_list(
[
("Kunde", context.customer.name),
@ -135,7 +137,7 @@ class SummaryComponent(ReportComponent):
),
),
("Pruefmittel", len(context.equipment)),
("Ergebnis", validation.result),
("Ergebnis", validation_result_label(validation.result)),
("Mitwirkende Personen", validation.participants),
(
"Hinweis auf naechste Leistungsbeurteilung",
@ -166,6 +168,10 @@ class StaticTextComponent(ReportComponent):
return f"{self.number} {self.title}" if self.number else self.title
def render(self, context: ReportContext) -> str:
if self.anchor == "results":
body = validation_result_box(context.validation.result, compact=True)
body += f"<p>{paragraph(self.body)}</p>"
return section(self.anchor, self.title, body, self.bookmark_label)
if self.block_key and self.block_key in context.text_blocks:
content = render_template_text(context.text_blocks[self.block_key].content, context)
return section(self.anchor, self.title, f"<p>{paragraph(content)}</p>", self.bookmark_label)

View file

@ -0,0 +1,66 @@
from __future__ import annotations
from dataclasses import dataclass
from app.modules.orion.html import text
@dataclass(frozen=True)
class ResultPresentation:
key: str
label: str
css_class: str
color: str
RESULT_PRESENTATIONS: dict[str, ResultPresentation] = {
"BESTANDEN": ResultPresentation("BESTANDEN", "Bestanden", "result-box--passed", "#245C36"),
"BESTANDEN_MIT_AUFLAGEN": ResultPresentation(
"BESTANDEN_MIT_AUFLAGEN",
"Bestanden mit Auflagen",
"result-box--conditional",
"#7A5A00",
),
"NICHT_BESTANDEN": ResultPresentation(
"NICHT_BESTANDEN",
"Nicht bestanden",
"result-box--failed",
"#7A1F26",
),
"OFFEN": ResultPresentation("OFFEN", "Noch nicht bewertet", "result-box--open", "#2E3B40"),
}
RESULT_ALIASES = {
"": "OFFEN",
"OFFEN": "OFFEN",
"BESTANDEN": "BESTANDEN",
"BESTANDEN_MIT_AUFLAGEN": "BESTANDEN_MIT_AUFLAGEN",
"NICHT_BESTANDEN": "NICHT_BESTANDEN",
"offen": "OFFEN",
"bestanden": "BESTANDEN",
"bestanden_mit_auflagen": "BESTANDEN_MIT_AUFLAGEN",
"mit_auflagen": "BESTANDEN_MIT_AUFLAGEN",
"bestanden mit Auflagen": "BESTANDEN_MIT_AUFLAGEN",
"nicht_bestanden": "NICHT_BESTANDEN",
"nicht bestanden": "NICHT_BESTANDEN",
}
def validation_result_presentation(value: str | None) -> ResultPresentation:
key = RESULT_ALIASES.get(str(value or "").strip(), "OFFEN")
return RESULT_PRESENTATIONS[key]
def validation_result_label(value: str | None) -> str:
return validation_result_presentation(value).label
def validation_result_box(value: str | None, *, compact: bool = False) -> str:
presentation = validation_result_presentation(value)
size_class = "result-box--compact" if compact else "result-box--cover"
return (
f'<div class="result-box {size_class} {presentation.css_class}">'
'<div class="result-box__label">VALIDIERUNGSERGEBNIS</div>'
f'<div class="result-box__value">{text(presentation.label.upper() if not compact else presentation.label)}</div>'
"</div>"
)

View file

@ -151,7 +151,7 @@ body {
display: grid;
gap: 18mm;
grid-template-columns: 1fr 55mm;
margin-bottom: 32mm;
margin-bottom: 18mm;
}
.cover-logo {
@ -178,22 +178,94 @@ body {
h1 {
color: #2E3B40;
font-size: 34pt;
font-size: 30pt;
line-height: 1.05;
margin: 0 0 8mm 0;
margin: 0 0 5mm 0;
}
.cover-subtitle {
color: #4F6A74;
font-size: 14pt;
margin: 0 0 9mm 0;
}
.cover-page .definition-row {
grid-template-columns: 36mm 1fr;
padding: 1.4mm 0;
}
.result-box {
border: 1.2mm solid #8A9498;
border-radius: 2mm;
break-inside: avoid;
margin: 8mm 0;
padding: 7mm 8mm;
page-break-inside: avoid;
text-align: center;
width: 100%;
}
.result-box__label {
color: #4F5E63;
font-size: 9.5pt;
font-weight: 700;
letter-spacing: .06em;
margin-bottom: 4mm;
}
.result-box__value {
font-weight: 800;
line-height: 1.15;
}
.result-box--cover {
margin-top: 7mm;
padding: 6mm 8mm;
}
.result-box--cover .result-box__value {
font-size: 24pt;
text-transform: uppercase;
}
.result-box--compact {
margin: 0 0 6mm 0;
padding: 5mm 6mm;
}
.result-box--compact .result-box__value {
font-size: 16pt;
margin: 0 0 18mm 0;
}
.result-box--passed {
background: #EAF5EE;
border-color: #6FA37D;
color: #245C36;
}
.result-box--conditional {
background: #FFF7DD;
border-color: #C9A64A;
color: #7A5A00;
}
.result-box--failed {
background: #FCEBEC;
border-color: #C96A70;
color: #7A1F26;
}
.result-box--open {
background: #F1F3F3;
border-color: #9AA6AA;
color: #2E3B40;
}
.signature-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16mm;
margin-top: 18mm;
margin-top: 11mm;
}
.signature-grid div {

View file

@ -206,9 +206,21 @@ class ValidationWorkflowService:
Validation.result.ilike(term),
)
)
for key in ["status", "customer_id", "device_id", "validation_type", "result"]:
for key in ["status", "customer_id", "device_id", "validation_type"]:
if filters.get(key):
conditions.append(getattr(Validation, key) == filters[key])
if filters.get("result"):
result_aliases = {
"OFFEN": ["OFFEN", "offen", ""],
"BESTANDEN": ["BESTANDEN", "bestanden"],
"BESTANDEN_MIT_AUFLAGEN": ["BESTANDEN_MIT_AUFLAGEN", "bestanden_mit_auflagen", "mit_auflagen", "bestanden mit Auflagen"],
"NICHT_BESTANDEN": ["NICHT_BESTANDEN", "nicht_bestanden", "nicht bestanden"],
}
result_values = result_aliases.get(filters["result"], [filters["result"]])
if filters["result"] == "OFFEN":
conditions.append(or_(Validation.result.in_(result_values), Validation.result.is_(None)))
else:
conditions.append(Validation.result.in_(result_values))
if filters.get("date_from"):
conditions.append(Validation.performed_on >= filters["date_from"])
if filters.get("date_to"):