Finish v1.1.0(4)

This commit is contained in:
2026-07-14 14:25:55 +02:00
20 changed files with 1153 additions and 155 deletions
+5
View File
@@ -0,0 +1,5 @@
{
"enabledPlugins": {
"impeccable@impeccable": true
}
}
@@ -0,0 +1,98 @@
---
target: IspezionePage
total_score: 18
p0_count: 1
p1_count: 3
timestamp: 2026-07-08T13-15-08Z
slug: steup-shared-components-pages-ispezionepage-razor
---
# Critique: IspezionePage
Method: dual-agent (A: design-review · B: detector+evidence). Browser visualization SKIPPED (MAUI Blazor Hybrid / WASM requires app build + auth + runtime state; no static HTML or dev server to inject into).
## Design Health Score
| # | Heuristic | Score | Key Issue |
|---|-----------|-------|-----------|
| 1 | Visibility of System Status | 1 | SchedaCard never shows local-only vs already-sent (`ActivityId`); "offline è la verità" is invisible on its central screen |
| 2 | Match System / Real World | 3 | Clean Italian, correct `sched{a/e}` pluralization; but online "+" is icon-only, unlabelled |
| 3 | User Control and Freedom | 1 | ExportReparto and Completa push to server irreversibly, no undo; export has no confirm |
| 4 | Consistency and Standards | 1 | MudBlazor default color rainbow (Warning/Success/Info/Error) violates single-accent; MudFab is both tiny icon and wide labelled button |
| 5 | Error Prevention | 2 | Completa & Delete confirmed; Export has no confirm and no double-tap guard |
| 6 | Recognition Rather Than Recall | 2 | No per-scheda sync badge → user must remember which reparti were exported |
| 7 | Flexibility and Efficiency | 2 | Three overlapping sync paths (per-scheda / per-reparto / Completa), unclear which supersedes |
| 8 | Aesthetic and Minimalist Design | 2 | Multi-color FAB clusters + info chips add chroma noise |
| 9 | Error Recovery | 2 | OnError surfaces raw `e.Message` (+ Console.WriteLine) — technical, not field-friendly |
| 10 | Help and Documentation | 2 | No empty state — empty SchedeGrouped renders a blank body |
| **Total** | | **18/40** | **Poor — major UX work before it delivers its promise** |
## Anti-Patterns Verdict
**LLM assessment:** No gradients/glass/eyebrows/side-stripes. The tell is subtler: the screen wears MudBlazor's default demo palette. Actions form a semantic rainbow — Warning(orange)=Add, Success(green)=Export, Info(blue)=Edit, Error(red)=Delete — none overridden in MudTheme (only Primary/Secondary/Tertiary/TextPrimary are set). DESIGN.md's loudest rule (La Regola dell'Unico Accento — coral #ec4c41 as the sole action color) appears nowhere on the action surface. Reads as "component-library defaults, lightly arranged" — the enterprise-Blazor flavor of AI slop.
**Deterministic scan:** detect.mjs clean (exit 0, 0 findings) on IspezionePage.razor and on InspectionCard + SchedaCard. No false positives. The scanner targets HTML/CSS slop patterns; the real defects here are semantic (color roles, state visibility) which it structurally cannot see — so the LLM review carries this critique.
**Visual overlays:** none — browser visualization not applicable to this target.
## Overall Impression
The bones are good — reparto grouping with live counts, restrained rounded surfaces, honest completion-confirm copy — but the screen fails at the one thing the product exists for: making sync/offline state unambiguous. A rilevatore cannot glance at this screen and know what is still trapped on the phone. On top of that, the signature coral accent is absent from every action, replaced by library-default colors. Biggest single opportunity: make per-scheda sync state visible and route all actions through the single coral accent.
## What's Working
1. **Completion-confirm copy** is plain, honest, consequence-first — the "conferme che prevengono l'errore" principle done right.
2. **Reparto grouping with live count chips** + correct Italian pluralization — thoughtful, task-shaped IA.
3. **Restrained surface design** (1em panels, 20px cards, tonal #f5f5f5, flat-by-default) respects the "taccuino da campo" north star.
## Priority Issues
**[P0] Sync state is invisible on SchedaCard / the whole screen** — violates "Offline è la verità" (#1 principle)
- Why: nothing distinguishes a locally-saved scheda from one on the server (`ActivityId`); no reparto "X da inviare" signal. The core product promise breaks exactly where it matters.
- Fix: persistent per-scheda status badge (Locale / Da inviare / Inviato) — coral for needs-action, success green for inviato; reparto-level unsent counter.
- Command: /impeccable clarify
**[P1] Kill the MudBlazor color rainbow; restore the single accent** — violates Regola dell'Unico Accento
- Why: coral never appears on actions; "Add" literally looks like a warning; reads as default slop and destroys the accent's meaning.
- Fix: coral (Primary) for the single primary action per context; demote secondary actions to neutral/text; reserve red strictly for Delete.
- Command: /impeccable colorize
**[P1] Export reparto is irreversible, unconfirmed, and double-tap-able**
- Why: fires on FAB tap with no confirmation; overlay mounts a frame late, so a fast gloved double-tap can re-send the batch → duplicate server activities.
- Fix: confirm dialog matching Completa; disable/guard the FAB the instant it's tapped, not on next render.
- Command: /impeccable harden
**[P1] Primary actions sit in panel headers, not at thumb reach** — violates "Il pollice comanda"
- Why: Add/Export FABs live in TitleContent at the top of a scrolling list; Size.Small FABs ≈40px (under 44px); Delete sits ~1rem from Export.
- Fix: move primary create to bottom-right custom-mudfab; enforce ≥44px; separate Delete from Export (spacing or overflow menu).
- Command: /impeccable layout
**[P2] Opaque blocking wait + no success payoff at completion**
- Why: Completa loop shows a featureless full-screen spinner through save→upload-photos→complete with zero progress; SpinnerOverlay's built-in success-checkmark is never wired here — the emotional peak has no reassurance.
- Fix: step progress ("Invio scheda 3/7 · foto 2/4"); trigger the existing success animation on completion.
- Command: /impeccable animate
**[P3] All panels Expanded="true" + no empty state**
- Why: every reparto expands by default (scroll purgatory for many reparti); an empty inspection renders blank.
- Fix: expand only the first (or none) with counts visible; add empty state ("Nessuna scheda — tocca + per iniziare").
- Command: /impeccable distill
## Persona Red Flags
- **Casey (distracted mobile):** unlabelled orange "+" online; three different sync verbs; nothing shows what's already sent → taps Export twice or completes without exporting.
- **Sam (accessibility):** Size.Small FABs under 44px; meaning by color alone across a Warning/Info/Success/Error rainbow (color-blind users can't tell Add from Delete by hue); `info-subtitle` (gray-default) and `info-inspection-text` (text-secondary) muted gray violate the sun-contrast rule and likely fail AA.
- **Riley (stress tester):** double-tap Export → duplicate records; errors surface as raw exception strings; 40 schede across 8 reparti all expanded → scroll purgatory.
- **Marco the rilevatore (gloved, offline, sunlight):** offline, the Export FAB silently disappears with no "why"; the Add FAB stays enabled here yet the same action is Disabled in NavMenu (contradictory); in bright sun he cannot tell which schede are still trapped on the device — the exact fear the app exists to remove.
## Minor Observations
- InspectionCard runs an artificial `await Task.Delay(250)` before showing store data — a deliberate stall on a screen whose principle is "feedback immediato".
- scheda action cluster is center-justified — harder to acquire one-handed than edge-anchored.
- OnError keeps a `Console.WriteLine(e.Message)` beside the logger — dev residue.
- Photo/QR chips in SchedaCard add visual weight but aren't tappable.
## Questions to Consider
- If a rilevatore can't glance at this screen and instantly know what's still only on the phone, does the app deliver its single promise?
- Why do three separate "send" affordances exist (per-scheda, per-reparto, complete)? Could one honest sync model with clear per-item state replace all three?
- The success-checkmark animation was built but never wired here — cut or forgotten? Which is worse?
- If coral is "the color of action" but every action button is orange/green/blue, what is coral the color of on this screen?
@@ -0,0 +1,89 @@
---
target: IspezionePage
total_score: 34
p0_count: 0
p1_count: 1
timestamp: 2026-07-08T13-44-46Z
slug: steup-shared-components-pages-ispezionepage-razor
---
# Critique: IspezionePage (re-run post-fix)
Method: dual-agent (A: design-review · B: detector+evidence). Browser visualization SKIPPED (MAUI Blazor Hybrid / WASM requires app build + auth + runtime state).
## Design Health Score
| # | Heuristic | Score | Key Issue |
|---|-----------|-------|-----------|
| 1 | Visibility of System Status | 4 | Per-scheda sync badge + reparto "da inviare" counter + granular completion progress + success checkmark |
| 2 | Match System / Real World | 3 | Italian & readable, but status shown ALL-CAPS and scheda title is a raw ActivityTypeId code |
| 3 | User Control and Freedom | 3 | Confirmations everywhere; no abort during long upload (sends irreversible by nature) |
| 4 | Consistency and Standards | 3 | coral=send now consistent; undermined by off-palette amber status chip |
| 5 | Error Prevention | 4 | Export confirms + double-tap guards; completion confirmed + _isBusy guard |
| 6 | Recognition Rather Than Recall | 4 | Grouped by reparto, counters, icons, labels |
| 7 | Flexibility and Efficiency | 3 | Three send paths (scheda/reparto/concludi) powerful but diffuse |
| 8 | Aesthetic and Minimalist | 3 | Clean; one expanded panel still stacks several action types |
| 9 | Error Recovery | 4 | OnError copy reassuring, tech detail to logs |
| 10 | Help and Documentation | 3 | Good empty-state; no hint why send buttons vanish offline |
| **Total** | | **34/40** | **Strong / ship-worthy with targeted fixes** |
## Anti-Patterns Verdict
**LLM assessment:** Largely free of AI slop; reads as a genuine field tool. Single-Accent Rule now respected on the action surface — coral is consistently "the send action" (per-scheda Invia, panel Esporta reparto, "da inviare" counter), neutral gray for non-send actions, red reserved for delete. SchedaSyncHelper encodes coral=azione-richiesta / green=fatto. No gradients/glass/side-stripes/identical-grid. One genuine rainbow leak remains: StatusEnumHelper.GetColor() paints the InspectionCard status chip Color.Warning (amber) for InCorso/Verifica — amber is not in the SteUP palette and competes with coral. Status labels are ALL-CAPS (violates Regola del Peso, non del Maiuscolo).
**Deterministic scan:** detect.mjs clean (exit 0) on IspezionePage.razor, SchedaCard.razor, SpinnerOverlay.razor. CSS scan: 3 advisory design-system-radius findings, all in SpinnerOverlay.razor.css (border-radius 100px/2px) — all confirmed false positives (icon geometry of the animated success-checkmark: arc masks + rounded line caps), not surface radii.
**Visual overlays:** none — browser visualization not applicable to this target.
## Overall Impression
Big jump from the first pass (18→34). The screen now delivers its core promise: sync state is unambiguous per-scheda and per-reparto, the completion moment is reassuring (real progress + success checkmark), destructive/irreversible sends are confirmed and double-tap-guarded, and the single coral accent finally means "send." The remaining issues are a real data-integrity bug in the completion loop and a few palette/copy nits — no longer structural.
## What's Working
1. SchedaSyncHelper sync-state system — per-scheda coral "Da inviare" / green "Inviato" badges (icon+text, not color-only) + reparto CountDaInviare() counter. The Offline-è-la-verità principle made concrete.
2. Completion progress + confirmation choreography — SetProgress messages, success checkmark with 1.8s dwell, irreversibility confirm guarded by _isBusy.
3. Guarded, reassuring exports — ExportReparto confirms, guards double-tap, keeps work local on failure with human copy, logs the stack trace.
## Priority Issues (remaining)
**[P1] Silent scheda skip on completion still shows success**
- What: In HandleCompleteInspection, `if (apiResponse == null) continue;` skips a scheda that failed to save, yet the loop still calls CompleteInspection, sets Completata, and shows "Ispezione inviata".
- Why: The inspection becomes non-editable while one or more schede never reached the server — exactly the data loss PRODUCT.md exists to prevent.
- Fix: track failed schede; if any failed, do NOT complete — surface "N schede non inviate, riprova" and keep them "Da inviare". Complete only when all succeed.
- Command: /impeccable harden
**[P2] Off-palette amber status chip**
- What: StatusEnumHelper.GetColor() returns Color.Warning (amber) for InCorso/Verifica (InspectionCard status chip).
- Why: Amber isn't in the DESIGN.md palette; a second attention color dilutes coral (Regola dell'Unico Accento).
- Fix: map InCorso/Verifica to Color.Default (neutral) or navy Secondary; keep green Completata, red Annullata.
- Command: /impeccable colorize
**[P3] ALL-CAPS status labels**
- What: ConvertToHumanReadable returns "IN CORSO", "COMPLETATA".
- Why: Violates Regola del Peso, non del Maiuscolo.
- Fix: sentence case ("In corso", "Completata"); rely on the chip's 700 weight.
**[P3] Scheda title is a raw code**
- What: `<b>@Scheda.ActivityTypeId</b>` as the card headline.
- Why: forces decoding an internal identifier in the field.
- Fix: show a human descrizione, fall back to the code only if none.
## Minor Observations
- OnSchedaModified uses `if (index > 0)` — skips index 0, so modifying the FIRST scheda in SchedeList won't replace it in-list (likely a `>= 0` bug). Pre-existing; now also on the post-export refresh path.
- Sync model collapsed to two states (DaInviare/Inviato) vs the three-state brief (Locale/Da inviare/Inviato) — acceptable, but "local draft" vs "ready to send" no longer distinguished.
- InspectionCard still has an artificial `await Task.Delay(250)` skeleton delay — fights "feedback immediato".
- InspectionCard address (info-subtitle, gray-darker, weight 600) is still gray, not ink — mild sun-legibility compromise (verify AA); verify amber-on-white too.
- Offline, all send buttons disappear with no "torna online per inviare" hint — badges show "da inviare" but no next step.
## Persona Red Flags (delta)
- Casey (mobile): RESOLVED — FABs Size.Medium (≥44px), flex-wrap. Remaining: per-reparto actions sit in panel TitleContent (top of expanded panel), drifting from the thumb zone on long lists.
- Sam (accessibility): RESOLVED — aria-labels on FABs; sync state icon+text not color-only. Remaining: ALL-CAPS status; verify amber-on-white and gray-darker address hit AA.
- Marco (gloved/offline/sun): RESOLVED — sync visibility + reparto counter answer "what hasn't gone yet." Remaining: offline the send buttons vanish with no explanation; address text still gray not ink.
## Questions to Consider
- If completion continues past a failed scheda and still locks the inspection, how does Marco discover which scheda didn't make it once editing is disabled?
- Should the total "da inviare" roll up onto the InspectionCard header, visible before expanding any reparto?
- Three send paths (per-scheda, per-reparto, global concludi) — does that dilute the single primary task? Could per-reparto export be the one canonical action?
+112
View File
@@ -0,0 +1,112 @@
{
"schemaVersion": 2,
"generatedAt": "2026-07-08T00:00:00.000Z",
"title": "Design System: SteUP",
"extensions": {
"colorMeta": {
"primary": { "role": "primary", "displayName": "Arancio-Rosso Allerta", "canonical": "#ec4c41", "tonalRamp": ["#3a0f0b", "#5e1712", "#8a2019", "#b62f24", "#ec4c41", "#f27a72", "#f7a9a3", "#fcdcd9"] },
"secondary": { "role": "secondary", "displayName": "Blu Profondo", "canonical": "#002339", "tonalRamp": ["#000c14", "#001622", "#002339", "#003653", "#0a5480", "#3f7ea8", "#87b3d0", "#d5e6f1"] },
"tertiary": { "role": "tertiary", "displayName": "Azzurro Tenue", "canonical": "#dff2ff", "tonalRamp": ["#12313f", "#1c5570", "#2b7ba3", "#4aa4d1", "#86c8e8", "#b3ddf3", "#dff2ff", "#f2faff"] },
"ink": { "role": "neutral", "displayName": "Inchiostro", "canonical": "#000000", "tonalRamp": ["#000000", "#1a1a1a", "#333333", "#4d4d4d", "#737373", "#999999", "#cccccc", "#f5f5f5"] },
"paper": { "role": "neutral", "displayName": "Carta", "canonical": "#ffffff", "tonalRamp": ["#c2c2c2", "#d1d1d1", "#e0e0e0", "#ededed", "#f5f5f5", "#fafafa", "#ffffff", "#ffffff"] },
"success": { "role": "semantic", "displayName": "Verde OK", "canonical": "#26b050", "tonalRamp": ["#0a3318", "#125025", "#1a7d38", "#26b050", "#4fca72", "#84dc9d", "#b7ecc6", "#e6f9ec"] },
"error": { "role": "semantic", "displayName": "Rosso Errore", "canonical": "#e50000", "tonalRamp": ["#3a0000", "#5e0000", "#8a0000", "#b60000", "#e50000", "#f24d4d", "#f79999", "#fcdcdc"] },
"focus-ring": { "role": "semantic", "displayName": "Blu Focus", "canonical": "#258cfb", "tonalRamp": ["#08213f", "#0c3a70", "#1257b6", "#258cfb", "#5aa9fc", "#8fc4fd", "#c4e0fe", "#eaf4ff"] }
},
"typographyMeta": {
"page-title": { "displayName": "Page Title", "purpose": "Titolo di schermata; ancora l'utente al compito corrente." },
"title": { "displayName": "Title", "purpose": "Intestazioni di sezione, titoli di dialog e message-box." },
"body": { "displayName": "Body", "purpose": "Testo corrente e dati; prosa a 65-75ch." },
"label": { "displayName": "Label", "purpose": "Etichette di pulsanti e controlli di form; peso 700 per presenza al tocco." }
},
"shadows": [
{ "name": "ombra-custom", "value": "1px 2px 5px hsl(from var(--mud-palette-overlay-dark) h s 40%)", "purpose": "Stacco morbido per card ed elementi che galleggiano sul contenuto." },
{ "name": "ombra-eccezione", "value": "1px 2px 5px rgba(0,0,0,0.3)", "purpose": "Rilievo leggermente più marcato per box di errore/eccezione." }
],
"motion": [
{ "name": "connection-slide", "value": "all 0.5s ease", "purpose": "Comparsa/scomparsa della barra di stato connessione (translateY)." },
{ "name": "ripple", "value": "ripple-effect 0.6s linear", "purpose": "Feedback fisico al tocco su pulsanti e contenitori tattili." },
{ "name": "spinner", "value": "l24 1s infinite linear", "purpose": "Loader di caricamento a doppio anello." }
],
"breakpoints": [
{ "name": "mobile", "value": "0px" }
]
},
"components": [
{
"name": "Primary Button",
"kind": "button",
"refersTo": "button-primary",
"description": "Azione primaria: fondo accento, etichetta bianca in grassetto, angoli molto arrotondati.",
"html": "<button class=\"ds-btn-primary\">Salva</button>",
"css": ".ds-btn-primary { font-family: Nunito, sans-serif; background: #ec4c41; color: #fff; font-weight: 700; font-size: 0.9375rem; border: none; border-radius: 20px; padding: 0.65rem 1.5rem; min-height: 44px; cursor: pointer; transition: filter 0.15s ease, transform 0.1s ease; } .ds-btn-primary:hover { filter: brightness(1.05); } .ds-btn-primary:focus-visible { outline: none; box-shadow: 0 0 0 0.1rem #fff, 0 0 0 0.25rem #258cfb; } .ds-btn-primary:active { transform: translateY(1px); filter: brightness(0.95); }"
},
{
"name": "FAB (Floating Action)",
"kind": "button",
"refersTo": "button-primary",
"description": "Azione primaria fissa in basso a destra, a portata di pollice; rispetta la safe-area.",
"html": "<button class=\"ds-fab\" aria-label=\"Nuova scheda\"><svg viewBox=\"0 0 24 24\" width=\"24\" height=\"24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\"><path d=\"M12 5v14M5 12h14\"/></svg></button>",
"css": ".ds-fab { font-family: Nunito, sans-serif; position: fixed; bottom: 4rem; right: 16px; width: 56px; height: 56px; border-radius: 50%; border: none; background: #ec4c41; color: #fff; display: inline-flex; align-items: center; justify-content: center; box-shadow: 1px 2px 5px rgba(0,0,0,0.3); cursor: pointer; transition: filter 0.15s ease, transform 0.1s ease; } .ds-fab:hover { filter: brightness(1.05); } .ds-fab:active { transform: translateY(1px); } .ds-fab:focus-visible { outline: none; box-shadow: 0 0 0 0.1rem #fff, 0 0 0 0.25rem #258cfb; }"
},
{
"name": "Input Card",
"kind": "input",
"refersTo": "input-card",
"description": "Campo a scheda su fondo grigio tonale, senza underline; etichetta a sinistra, valore a destra.",
"html": "<label class=\"ds-input-card\"><span class=\"ds-input-label\">Quantità</span><input class=\"ds-input-field\" type=\"text\" inputmode=\"numeric\" value=\"12\" /></label>",
"css": ".ds-input-card { font-family: Nunito, sans-serif; display: flex; align-items: center; justify-content: space-between; gap: 0.5rem; width: 100%; background: #f5f5f5; border-radius: 9px; padding: 0.5rem 1rem; min-height: 44px; } .ds-input-label { font-weight: 700; color: #000; } .ds-input-field { font-family: Nunito, sans-serif; font-weight: 500; text-align: right; border: none; background: transparent; color: #000; outline: none; min-width: 0; } .ds-input-field:focus { outline: none; }"
},
{
"name": "Connection Status Bar",
"kind": "custom",
"refersTo": null,
"description": "Striscia di stato rete/servizio che scorre dall'alto: verde se OK, rosso se KO. Incarna 'Offline è la verità'.",
"html": "<div class=\"ds-connection ds-connection--ko\">Nessuna connessione — lavoro salvato in locale</div>",
"css": ".ds-connection { font-family: Nunito, sans-serif; display: flex; align-items: center; gap: 1rem; min-height: 35px; padding: 0 0.75rem; font-weight: 700; font-size: 1.05rem; color: #fff; transition: all 0.5s ease; } .ds-connection--ko { background: #e50000; } .ds-connection--ok { background: #26b050; }"
},
{
"name": "Panel / Card",
"kind": "card",
"refersTo": "panel",
"description": "Superficie di contenuto: carta bianca, angoli 1em, piatta di default, ombra solo se galleggia.",
"html": "<div class=\"ds-panel\"><h3 class=\"ds-panel-title\">Scheda articolo</h3><p class=\"ds-panel-body\">Dettagli della rilevazione.</p></div>",
"css": ".ds-panel { font-family: Nunito, sans-serif; background: #fff; color: #000; border-radius: 1em; padding: 1rem; } .ds-panel-title { font-weight: 700; font-size: 1.1rem; margin: 0 0 0.5rem; } .ds-panel-body { font-weight: 400; font-size: 0.875rem; line-height: 1.8; margin: 0; }"
}
],
"narrative": {
"northStar": "Il taccuino da campo",
"overview": "SteUP è lo strumento che il rilevatore tiene in mano mentre gira i punti vendita: deve funzionare come carta e inchiostro — sempre, anche senza rete, leggibile in pieno sole, comprensibile con un'occhiata. La chiarezza viene prima di qualsiasi ornamento. Il sistema è tattile e sicuro: bersagli ampi per il pollice, una sola mano e i guanti; feedback fisico immediato a ogni tocco e scansione. Lo stato — salvato in locale, in attesa di sync, inviato, completato — non è mai ambiguo. Rifiuta il gestionale datato e denso e l'app consumer social/giocosa: è uno strumento professionale.",
"keyCharacteristics": [
"Offline-first: lo stato di salvataggio e sincronizzazione è sempre esplicito",
"Mobile-first, uso a una mano/guanti: tocchi ≥44px, azioni a portata di pollice",
"Alto contrasto e testo generoso per la leggibilità all'aperto",
"Un solo accento (Arancio-Rosso Allerta), usato per azione e stato, mai per decorazione",
"Un'unica famiglia tipografica, angoli arrotondati coerenti (9px → 20px → 1em)"
],
"rules": [
{ "name": "La Regola dell'Unico Accento", "body": "L'Arancio-Rosso Allerta è l'unico accento del sistema e appare solo su azione o stato attivo, mai come decorazione. La sua rarità è ciò che lo rende un segnale.", "section": "colors" },
{ "name": "La Regola del Contrasto da Sole", "body": "Il testo di corpo è inchiostro (#000) su carta (#fff). Il grigio chiaro 'per eleganza' è vietato sul testo leggibile: si progetta per lo schermo colpito dal sole.", "section": "colors" },
{ "name": "La Regola del Peso, non del Maiuscolo", "body": "L'enfasi si ottiene con il peso (700/800) e la dimensione, non con il maiuscolo spaziato. Niente eyebrow maiuscoli tracciati sopra le sezioni.", "section": "typography" },
{ "name": "La Regola del Piatto-di-Default", "body": "Le superfici sono piatte a riposo. L'ombra compare solo quando un elemento deve galleggiare (overlay, dialog, card sollevata) o rispondere a uno stato.", "section": "elevation" }
],
"dos": [
"Do usare l'Arancio-Rosso Allerta (#ec4c41) solo per azione e stato attivo; mantenerlo raro.",
"Do tenere il testo di corpo in inchiostro (#000) su carta (#fff); alto contrasto per la lettura al sole.",
"Do dimensionare i bersagli di tocco ≥44px e collocare le azioni primarie a portata di pollice (FAB in basso a destra).",
"Do rendere sempre esplicito lo stato offline/sync (barra di connessione, stati di scheda/ispezione).",
"Do dare feedback fisico immediato a ogni tocco e scansione (ripple, cambio di stato).",
"Do usare un'unica famiglia (Nunito) e fare gerarchia con peso e dimensione.",
"Do rispettare le safe-area iOS/Android e prefers-reduced-motion."
],
"donts": [
"Don't costruire un gestionale datato e denso: niente tabelle fittissime, testo minuscolo, grigio ovunque.",
"Don't virare verso un'estetica consumer social/giocosa: niente colore ludico o decorazione fine a sé stessa.",
"Don't usare testo grigio chiaro sul corpo 'per eleganza': causa numero uno di illeggibilità al sole.",
"Don't usare bordo-laterale colorato (border-left/right > 1px) come accento su card, liste o alert.",
"Don't usare testo con gradiente, glassmorphism decorativo, griglie di card tutte identiche, o eyebrow maiuscoli su ogni sezione.",
"Don't introdurre animazioni di scena orchestrate al caricamento: il movimento serve allo stato, non allo spettacolo.",
"Don't reinventare affordance standard (scrollbar strane, controlli di form non convenzionali) per 'carattere'."
]
}
}
+91
View File
@@ -0,0 +1,91 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## What this is
SteUP is an Integry .NET MAUI Blazor Hybrid app for field inspections ("ispezioni") of retail stores — the operator scans product barcodes (including via Honeywell handheld scanners), fills in inspection sheets ("schede"), attaches photos, and syncs with the Integry backend. Work is stored locally (offline-first via SQLite) and pushed to the server. The same Blazor UI also runs as a standalone WebAssembly app.
> Note: `README.md` is the leftover Microsoft sample template and does NOT describe this app. Ignore it.
## Design Context
For any UI/design work, read **`PRODUCT.md`** (strategy) and **`DESIGN.md`** (visual system) at the repo root first; they are the source of truth for the impeccable design skill.
- **Register:** product (a field tool; the UI serves the task, it is not a marketing surface).
- **Users:** field inspectors ("rilevatori") in stores — one-handed / gloved, outdoors in sunlight, often offline.
- **North Star:** "Il taccuino da campo" — works like paper and ink: always, offline, readable in full sun.
- **Core principles:** offline state is never ambiguous · thumb-first (touch ≥44px) · high contrast for sunlight · immediate feedback per scan · fewer steps, fewer errors.
- **Visual system:** MudBlazor · single font **Nunito** · accent `#ec4c41` (Arancio-Rosso Allerta, action/state only), anchor `#002339` (Blu Profondo) · rounded (9px/20px/1em) · locale `it-IT`.
- **Anti-references:** no dated/dense back-office look; no playful consumer-social aesthetic.
## Build & run
`global.json` pins SDK 8.0.0 with `rollForward: latestMajor`, but all projects target **.NET 10** (MAUI targets `net10.0-android` / `net10.0-ios`). A .NET 10 SDK must be installed.
NuGet restores from a **private Integry feed only** (`NuGet.Config` clears nuget.org and uses `nuget.studioml.it`). The `IntegryApiClient.*` packages come from there.
```bash
# MAUI app (primary target) — Android
dotnet build SteUp.Maui/SteUp.Maui.csproj -t:Run -f net10.0-android
# MAUI app — iOS (requires Mac build host; codesigning wired in csproj)
dotnet build SteUp.Maui/SteUp.Maui.csproj -f net10.0-ios
# Web (Blazor WASM) app
dotnet run --project SteUp.Web/SteUp.Web.csproj
```
There is **no test project** in the solution. There is no lint config beyond `Nullable`/`ImplicitUsings` enabled per csproj.
## Projects (5)
- **SteUp.Shared** — Razor Class Library holding almost everything: all Blazor pages/components, DTOs, entities, enums, the auth state provider, API service wrappers, messaging, and business orchestration (`SteupDataService`). Both MAUI and Web reference it. Uses MudBlazor + MudExtensions for UI.
- **SteUp.Maui** — the native host. Owns platform services (DI registration in `Core/CoreModule.cs`), the `BlazorWebView` host (`MainPage.xaml.cs`), file/logging/network/scanner implementations, and app config.
- **SteUp.Web** — Blazor WebAssembly host of the same shared UI. Its `Program.cs` mirrors a subset of the MAUI DI setup.
- **SteUp.Data** — EF Core + SQLite local database (`AppDbContext`, migrations, `IspezioniService`). References Shared for the entity types.
- **Steup.HoneywellScanner** — Android-only binding library wrapping Honeywell `DataCollection.aar`. Referenced by MAUI only on the Android target.
## Architecture
### Interface / implementation split (important convention)
Interfaces live in **SteUp.Shared** (`Core/Interface/...`) so the shared UI depends only on abstractions. Concrete platform implementations live in **SteUp.Maui** (`Core/Services`, `Core/System`) and are registered per-host:
- MAUI wires everything in `SteUp.Maui/Core/CoreModule.cs` via `Register*` extension methods called from `MauiProgram.cs`.
- Web wires a smaller subset directly in `SteUp.Web/Program.cs` (only `IFormFactor` + auth; it has no local DB, scanner, or file manager).
When adding a service consumed by the shared UI: define the interface in `SteUp.Shared/Core/Interface`, implement it in the host, and register it in **both** `CoreModule.cs` and (if the Web app needs it) `Program.cs`.
### Auth
`AppAuthenticationStateProvider` (Shared) bridges Integry's `IUserSession`/`IUserAccountService` (from `IntegryApiClient`) to Blazor's `AuthenticationStateProvider`, building `ClaimsPrincipal` from the JWT. Both hosts call `builder.UseIntegry(appToken, useLoginAzienda: true)` — the `AppToken` is the same GUID in `MauiProgram.cs` and `SteUp.Web/Program.cs`.
### Backend calls
Two service wrappers over `IIntegryApiRestClient`:
- `IntegrySteupService` — the app-specific `steup/*` endpoints (retrieve punti vendita, reparti, griglia PLU, barcode lookup, save/complete/delete schede, upload attachments).
- `IntegryApiService` — generic Integry endpoints (system health, activity descriptions, email).
### Offline-first data flow
`SteupDataService` (Shared) is the orchestrator: on `Init()` it runs EF migrations (`DbInitializer.InitializeAsync``db.Database.MigrateAsync()`), loads reference data from the API, reconciles inspection status, and prunes closed inspections older than 60 days. Local reads/writes go through `IspezioniService` (SteUp.Data).
### Local DB model
`AppDbContext`: `Ispezione``Scheda``SchedaArticolo`. `Ispezione` has a **composite key** `(CodMdep, Data, Rilevatore)` which is also the FK from `Scheda`. `Scheda.ImageNames` is a `List<string>` persisted as JSON via a value converter. DB file: `steup_db.db3` in `FileSystem.AppDataDirectory` (see `DbPathProvider`).
Add a migration (from repo root; `AppDbContextFactory` provides the design-time context using a throwaway `design-time.db3`):
```bash
dotnet ef migrations add <Name> --project SteUp.Data
```
Migrations are applied automatically at app startup, not via `database update`.
### Messaging (in-app events)
Uses `CommunityToolkit.Mvvm` `WeakReferenceMessenger`. Singleton "services" in `Shared/Core/Messages` (`NewSchedaService`, `OnScannerService`, `CompleteInspectionService`) publish/relay messages that decouple the barcode scanner and page components.
### Barcode scanning
`IBarcodeReaderService` is implemented by `HoneywellScannerService` (MAUI, partial class with Android-specific parts + the Honeywell binding lib). `IBarcodeManager`/`BarcodeManager` (Shared) is the host-agnostic front the UI talks to. Scans flow to the UI through the messaging services above.
### Attached images
Photos are served into the `BlazorWebView` through a custom scheme: `MainPage.xaml.cs` intercepts `WebResourceRequested` for `https://localfiles/attached/<file>` and streams files from `CacheDirectory/attached` (with path-traversal guards).
## Error tracking
Sentry is configured in both hosts (`.UseSentry(...)` in MAUI, Sentry package in Shared). MAUI also has a `GlobalExceptionHandler` (registered in `App.xaml.cs`) and a `FileLogger`/`FileLoggerProvider` writing to `AppDataDirectory/logs`.
## Versioning
App version lives in `SteUp.Maui.csproj` (`ApplicationDisplayVersion` + `ApplicationVersion`). Commit messages track releases (e.g. "-> v1.0.2 (3)"). Bump both when releasing. `ApplicationId` is `it.integry.SteUp`.
+192
View File
@@ -0,0 +1,192 @@
---
name: SteUP
description: Strumento da campo per ispezioni nei punti vendita — solido, chiaro, offline-first.
colors:
primary: "#ec4c41"
secondary: "#002339"
tertiary: "#dff2ff"
ink: "#000000"
paper: "#ffffff"
surface-dark: "#000406"
success: "#26b050"
error: "#e50000"
focus-ring: "#258cfb"
typography:
page-title:
fontFamily: "Nunito, sans-serif"
fontSize: "1.5rem"
fontWeight: 800
lineHeight: 1.2
letterSpacing: "normal"
title:
fontFamily: "Nunito, sans-serif"
fontSize: "1.1rem"
fontWeight: 700
lineHeight: 1.4
letterSpacing: "normal"
body:
fontFamily: "Nunito, sans-serif"
fontSize: "0.875rem"
fontWeight: 400
lineHeight: 1.8
letterSpacing: "normal"
label:
fontFamily: "Nunito, sans-serif"
fontSize: "0.875rem"
fontWeight: 700
lineHeight: 1.4
letterSpacing: "normal"
rounded:
input: "9px"
panel: "1em"
container: "20px"
skeleton: "0.5em"
spacing:
page-x: "1rem"
header-h: "4rem"
touch-min: "44px"
components:
button-primary:
backgroundColor: "{colors.primary}"
textColor: "{colors.paper}"
rounded: "{rounded.container}"
typography: "{typography.label}"
padding: "0.5rem 1.25rem"
button-primary-active:
backgroundColor: "{colors.primary}"
textColor: "{colors.paper}"
rounded: "{rounded.container}"
input-card:
backgroundColor: "#f5f5f5"
textColor: "{colors.ink}"
rounded: "{rounded.input}"
padding: "0.5rem 1rem"
panel:
backgroundColor: "{colors.paper}"
textColor: "{colors.ink}"
rounded: "{rounded.panel}"
padding: "1rem"
---
# Design System: SteUP
## 1. Overview
**Creative North Star: "Il taccuino da campo"**
SteUP è lo strumento che il rilevatore tiene in mano mentre gira i punti vendita: deve funzionare come carta e inchiostro — sempre, anche senza rete, leggibile in pieno sole, comprensibile con un'occhiata. La chiarezza viene prima di qualsiasi ornamento. L'interfaccia poggia su MudBlazor con un'unica famiglia tipografica (Nunito) e una palette essenziale: un solo accento caldo per l'azione, un blu profondo come base stabile, superfici bianche e pulite con angoli generosamente arrotondati.
Il sistema è **tattile e sicuro**: bersagli ampi pensati per il pollice, una sola mano e i guanti; feedback fisico immediato a ogni tocco e a ogni scansione (effetto ripple, cambi di stato evidenti). Lo stato — salvato in locale, in attesa di sync, inviato al server, completato — non è mai ambiguo: è la funzione più importante che la UI comunica.
Questo sistema rifiuta esplicitamente due cose. Non è un **gestionale datato e denso**: niente tabelle fittissime, testo minuscolo, grigio ovunque, densità che non serve al lavoro da campo. E non è un'**app consumer social/giocosa**: niente colore ludico, niente decorazione fine a sé stessa. È uno strumento professionale.
**Key Characteristics:**
- Offline-first: lo stato di salvataggio e sincronizzazione è sempre esplicito
- Mobile-first, uso a una mano/guanti: tocchi ≥44px, azioni a portata di pollice
- Alto contrasto e testo generoso per la leggibilità all'aperto
- Un solo accento (Arancio-Rosso Allerta), usato per azione e stato, mai per decorazione
- Un'unica famiglia tipografica, angoli arrotondati coerenti (9px → 20px → 1em)
## 2. Colors
Una palette essenziale: un accento caldo che chiama l'azione, un blu profondo come ancora, il resto neutro e pulito.
### Primary
- **Arancio-Rosso Allerta** (#ec4c41): il colore dell'azione e dello stato attivo. Pulsanti primari, FAB, selezione corrente, icone d'azione, indicatori di stato che richiedono attenzione. Riservato: la sua presenza segnala "qui si agisce".
### Secondary
- **Blu Profondo** (#002339): base stabile e di struttura. Testo forte, intestazioni, elementi strutturali, superfici scure in dark mode. È l'ancora visiva contro cui l'accento risalta.
### Tertiary
- **Azzurro Tenue** (#dff2ff): tocco freddo e leggero per evidenziazioni informative e superfici secondarie, dove serve distinguere senza gridare.
### Neutral
- **Inchiostro** (#000000): corpo del testo su fondo chiaro; massimo contrasto per la lettura al sole.
- **Carta** (#ffffff): superficie di contenuto primaria (pannelli, card, dialog).
- **Superficie Scura** (#000406): sfondo della dark mode (attualmente predisposta ma disattivata).
- **Grigio Campo** (~#f5f5f5): fondo delle input-card e delle superfici tonali secondarie; distingue per tono, non per bordo.
### Semantic
- **Verde OK** (#26b050): validazione positiva, esito riuscito.
- **Rosso Errore** (#e50000): validazione fallita, messaggi d'errore, connessione/servizio KO.
- **Blu Focus** (#258cfb): anello di focus per la navigazione da tastiera.
### Named Rules
**La Regola dell'Unico Accento.** L'Arancio-Rosso Allerta è l'unico accento del sistema e appare solo su azione o stato attivo, mai come decorazione. La sua rarità è ciò che lo rende un segnale.
**La Regola del Contrasto da Sole.** Il testo di corpo è inchiostro (#000) su carta (#fff). Il grigio chiaro "per eleganza" è vietato sul testo leggibile: si progetta per lo schermo colpito dal sole, non per lo screenshot.
## 3. Typography
**Display / Body / Label Font:** Nunito (variable 2001000, con fallback `sans-serif`). Caricata da Google Fonts; usata su tutta l'interfaccia.
**Character:** un'unica famiglia sans humanist e morbida per tutto — titoli, etichette, pulsanti, corpo, dati. Gli angoli tondeggianti di Nunito si accordano con i raggi ampi dei componenti e rafforzano il tono solido ma accessibile del "taccuino da campo". Nessun accostamento display/body: la gerarchia si fa con peso e dimensione.
### Hierarchy
- **Page Title** (peso 800, ~1.5rem/x-large, line-height stretta): titolo di schermata; ancora l'utente al compito corrente.
- **Title** (peso 700, ~1.1rem): intestazioni di sezione, titoli di dialog e message-box.
- **Body** (peso 400, 0.875rem/14px, line-height 1.8): testo corrente e dati. Per la prosa mantenere 6575ch; dati e UI compatta possono essere più densi.
- **Label** (peso 700, 0.875rem): etichette dei pulsanti e dei controlli di form. Il grassetto dà presenza al tocco senza ricorrere al maiuscolo.
### Named Rules
**La Regola del Peso, non del Maiuscolo.** L'enfasi si ottiene con il peso (700/800) e la dimensione, non con il maiuscolo spaziato. Niente eyebrow maiuscoli tracciati sopra le sezioni.
## 4. Elevation
Sistema prevalentemente **piatto con ombre funzionali leggere**. La profondità è tonale (Carta su Grigio Campo) più un'ombra morbida e diffusa dove un elemento deve staccarsi davvero (card in rilievo, overlay). MudBlazor fornisce la scala di elevazione; sopra di essa il progetto definisce due ombre custom soffuse. Le superfici a riposo sono piatte; l'ombra è una risposta al ruolo (galleggiamento, overlay), non decorazione.
### Shadow Vocabulary
- **Ombra Custom** (`box-shadow: 1px 2px 5px hsl(from var(--mud-palette-overlay-dark) h s 40%)`): stacco morbido per card ed elementi che devono galleggiare sul contenuto.
- **Ombra Eccezione** (`box-shadow: 1px 2px 5px rgba(0,0,0,0.3)`): rilievo leggermente più marcato per box di errore/eccezione.
### Named Rules
**La Regola del Piatto-di-Default.** Le superfici sono piatte a riposo. L'ombra compare solo quando un elemento deve galleggiare (overlay, dialog, card sollevata) o rispondere a uno stato. Se sembra un'app del 2014, l'ombra è troppo scura e troppo stretta.
## 5. Components
Componenti **tattili e sicuri**: bersagli generosi e arrotondati, feedback fisico, presenza netta, pensati per il pollice in campo.
### Buttons
- **Shape:** angoli molto arrotondati (raggio 20px sul contenitore `container-button`; i pulsanti MudBlazor ereditano `--mud-default-borderradius: 20px`).
- **Primary:** fondo Arancio-Rosso Allerta (#ec4c41), testo Carta (#fff), etichetta in peso 700; padding compatto verticale ma area di tocco ampia.
- **Hover / Focus / Active:** effetto **ripple** al tocco (chiaro su fondo scuro, scuro su fondo chiaro); focus da tastiera con doppio anello (bianco + Blu Focus #258cfb). Nei contenitori `ripple-container` il ripple nativo Mud è disattivato in favore di quello custom.
- **Settings buttons:** riga con icona in "pill" arrotondata (raggio 6px) tinta per ruolo — grigio (neutro), primary (azione), verde (successo), rosso (distruttivo) — su fondo tenue della stessa tinta.
- **FAB (`custom-mudfab`):** azione primaria fissa in basso a destra (bottom 4rem, right 16px), a portata di pollice; rispetta la safe-area inferiore.
### Cards / Containers
- **Corner Style:** pannelli e dialog a 1em; contenitori d'azione a 20px; input-card a 9px.
- **Background:** Carta (#fff) per il contenuto; Grigio Campo (~#f5f5f5) per input-card e superfici tonali secondarie.
- **Shadow Strategy:** piatte di default; Ombra Custom solo quando devono galleggiare (vedi Elevation).
- **Border:** preferire lo stacco tonale al bordo; quando serve, bordo pieno sottile (`--card-border-color`). Mai bordo-laterale colorato come accento.
- **Internal Padding:** input-card `.5rem 1rem`; pannelli ~1rem; margine orizzontale di pagina `--m-page-x: 1rem`.
### Inputs / Fields
- **Style:** input dentro `input-card` su fondo Grigio Campo, raggio 9px; underline Mud rimossa (`:before/:after` a `none`) per un aspetto pulito a "scheda".
- **Layout:** `form-container` a due colonne (etichetta in peso 700 a sinistra, valore a destra), altezza minima 35px per un tocco comodo.
- **Valid / Error:** valido → outline verde 1px (#26b050); invalido → outline rosso 1px (#e50000) con `validation-message` in rosso.
### Navigation
- **NavMenu** in testa alla pagina; barra di stato connessione (`ConnectionState`) che scorre dall'alto: verde (SystemOk) o rosso (NetworkKo / ServicesIsDown), peso 700, testo bianco. Comunica lo stato di rete/servizio senza rubare spazio al contenuto.
### Signature: Barra di Stato Connessione
Striscia sottile animata (`.Connection`) ancorata in cima che appare/scompare con transizione morbida (translateY) per segnalare rete assente o backend giù. È l'incarnazione visiva del principio "Offline è la verità": lo stato di connettività è sempre onesto e visibile.
## 6. Do's and Don'ts
### Do:
- **Do** usare l'Arancio-Rosso Allerta (#ec4c41) solo per azione e stato attivo; mantenerlo raro (Regola dell'Unico Accento).
- **Do** tenere il testo di corpo in inchiostro (#000) su carta (#fff); alto contrasto per la lettura al sole.
- **Do** dimensionare i bersagli di tocco ≥44px e collocare le azioni primarie a portata di pollice (FAB in basso a destra), pensando all'uso a una mano/guanti.
- **Do** rendere sempre esplicito lo stato offline/sync (barra di connessione, stati di scheda/ispezione).
- **Do** dare feedback fisico immediato a ogni tocco e scansione (ripple, cambio di stato).
- **Do** usare un'unica famiglia (Nunito) e fare gerarchia con peso e dimensione.
- **Do** rispettare le safe-area iOS/Android e `prefers-reduced-motion`.
### Don't:
- **Don't** costruire un **gestionale datato e denso**: niente tabelle fittissime, testo minuscolo, form infiniti, grigio ovunque.
- **Don't** virare verso un'estetica **consumer social/giocosa**: niente colore ludico o decorazione fine a sé stessa.
- **Don't** usare testo grigio chiaro sul corpo "per eleganza": è la causa numero uno di illeggibilità al sole.
- **Don't** usare bordo-laterale colorato (`border-left/right` > 1px) come accento su card, liste o alert.
- **Don't** usare testo con gradiente (`background-clip: text`), glassmorphism decorativo, griglie di card tutte identiche, o eyebrow maiuscoli tracciati su ogni sezione.
- **Don't** introdurre animazioni di scena orchestrate al caricamento: l'app entra dritta nel compito; il movimento serve allo stato, non allo spettacolo.
- **Don't** reinventare affordance standard (scrollbar strane, controlli di form non convenzionali) per "carattere".
+38
View File
@@ -0,0 +1,38 @@
# Product
## Register
product
## Users
Utente primario: **il rilevatore in campo**. Gira i punti vendita con un telefono o uno scanner Honeywell in mano, spesso in piedi, in movimento, all'aperto con luce forte, a volte con una mano sola o con i guanti, e con connessione di rete incerta o assente. Il suo compito durante il turno: scansionare i barcode degli articoli, compilare le schede di ispezione, allegare foto e chiudere l'ispezione. Strumento a uso interno Integry (non rivolto a clienti esterni), quindi contano più l'efficienza e la fiducia operativa che l'identità di marca.
## Product Purpose
SteUP è lo strumento da campo per le ispezioni dei punti vendita. Esiste per far completare le rilevazioni in modo rapido, accurato e senza perdita di dati, anche quando manca la rete. Il lavoro viene salvato localmente (offline-first, SQLite sul dispositivo) e sincronizzato con il backend Integry quando la connessione torna. Successo = il rilevatore finisce il turno certo che ogni scheda sia stata registrata e inviata, senza mai dover ripetere il lavoro per un dato perso o un dubbio sullo stato.
## Brand Personality
Solida, chiara, senza fronzoli. È uno strumento da lavoro, non una vetrina: nitido, diretto, prevedibile. La fiducia nasce dalla robustezza percepita e dalla leggibilità dello stato, non dalla decorazione. Tono asciutto e concreto nei testi (etichette, conferme, errori). Coerente con l'identità esistente: corallo (`#ec4c41`) come colore d'azione, navy (`#002339`) come ancora, superfici pulite e arrotondate.
## Anti-references
- **Gestionale datato/denso.** Niente look da ERP: tabelle fittissime, testo minuscolo, form infiniti, grigio ovunque, densità che non serve al compito da campo.
- **Consumer social/giocoso.** Niente estetica colorata/ludica da app social. Resta uno strumento professionale.
- Valgono inoltre i divieti trasversali di impeccable: niente gradienti decorativi, niente testo con gradiente, niente glassmorphism di default, niente griglie di card tutte identiche, niente eyebrow maiuscoli su ogni sezione.
## Design Principles
- **Offline è la verità.** Lo stato di salvataggio e sincronizzazione non è mai ambiguo. Il rilevatore deve sempre poter capire, senza pensarci, cosa è salvato in locale e cosa è arrivato al server.
- **Il pollice comanda.** Progettato per l'uso a una mano, con i guanti, in movimento: target di tocco ampi (≥44px), azioni primarie a portata di pollice, poco affollamento, niente bersagli piccoli.
- **Leggibile al sole.** Contrasto alto e testo generoso come default, non come opzione. La leggibilità all'aperto viene prima dell'eleganza del grigio chiaro.
- **Feedback immediato a ogni scan.** Ogni scansione e ogni tocco producono una risposta percepibile e istantanea; l'app conferma, non fa attendere.
- **Meno passaggi, meno errori.** Il percorso più breve per completare una scheda corretta. Ogni schermata ha un compito primario evidente; le conferme prevengono l'errore invece di segnalarlo dopo.
## Accessibility & Inclusion
- **Uso all'aperto:** alto contrasto e dimensioni testo generose per la luce diretta del sole; evitare testo grigio chiaro su fondi tenui.
- **Uso a una mano / con guanti:** target di tocco ≥44px, spaziatura sufficiente tra i controlli, azioni chiave raggiungibili in basso.
- **Base WCAG AA come buona pratica:** corpo testo ≥4.5:1, focus visibili, rispetto di `prefers-reduced-motion`.
- Interfaccia in **italiano** (locale `it-IT`).
+2 -2
View File
@@ -30,8 +30,8 @@
<ApplicationId>it.integry.SteUp</ApplicationId>
<!-- Versions -->
<ApplicationDisplayVersion>1.0.2</ApplicationDisplayVersion>
<ApplicationVersion>3</ApplicationVersion>
<ApplicationDisplayVersion>1.1.0</ApplicationDisplayVersion>
<ApplicationVersion>4</ApplicationVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'ios'">15.0</SupportedOSPlatformVersion>
<!-- <SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'maccatalyst'">-->
+11 -3
View File
@@ -42,8 +42,11 @@
Size="Size.Medium" IconSize="Size.Medium">
@if (SchedaVisible)
{
<MudFabMenuItem Disabled="!NetworkService.IsNetworkAvailable()" OnClick="@NewScheda"
Label="Nuova scheda" Color="Color.Surface"/>
@if (!_isCompleted)
{
<MudFabMenuItem Disabled="!NetworkService.IsNetworkAvailable()" OnClick="@NewScheda"
Label="Nuova scheda" Color="Color.Surface"/>
}
<MudFabMenuItem Disabled="!NetworkService.IsNetworkAvailable()" OnClick="@DeleteInspection"
Label="Cancella ispezione" Color="Color.Surface"/>
@@ -73,6 +76,7 @@
private bool PlusVisible { get; set; } = true;
private bool ShowCompleteInspection { get; set; }
private bool SchedaVisible { get; set; }
private bool _isCompleted;
private ConfirmMessageBox _messageBox = null!;
@@ -138,7 +142,11 @@
var location = NavigationManager.Uri.Remove(0, NavigationManager.BaseUri.Length);
SchedaVisible = new List<string> { "ispezione" }.Contains(location);
ShowCompleteInspection = !SteupDataService.InspectionPageState.Ispezione.ActivityId.IsNullOrEmpty();
var ispezione = SteupDataService.InspectionPageState.Ispezione;
_isCompleted = ispezione.Stato == StatusEnum.Completata;
// Concludi non ha senso su un'ispezione gia completata
ShowCompleteInspection = !ispezione.ActivityId.IsNullOrEmpty() && !_isCompleted;
StateHasChanged();
}
@@ -3,22 +3,37 @@
<MudOverlay Visible="VisibleOverlay" LightBackground="true">
@if (SuccessAnimation)
{
<div class="success-checkmark">
<div class="check-icon">
<span class="icon-line line-tip"></span>
<span class="icon-line line-long"></span>
<div class="icon-circle"></div>
<div class="icon-fix"></div>
<div class="overlay-content">
<div class="success-checkmark">
<div class="check-icon">
<span class="icon-line line-tip"></span>
<span class="icon-line line-long"></span>
<div class="icon-circle"></div>
<div class="icon-fix"></div>
</div>
</div>
@if (!string.IsNullOrWhiteSpace(Message))
{
<p class="overlay-message">@Message</p>
}
</div>
}
else
{
<SpinnerLayout/>
<div class="overlay-content">
<SpinnerLayout/>
@if (!string.IsNullOrWhiteSpace(Message))
{
<p class="overlay-message">@Message</p>
}
</div>
}
</MudOverlay>
@code {
[Parameter] public required bool SuccessAnimation { get; set; }
[Parameter] public required bool VisibleOverlay { get; set; }
/// Testo opzionale mostrato sotto lo spinner/spunta (es. "Invio scheda 3 di 7…").
[Parameter] public string? Message { get; set; }
}
@@ -1,9 +1,37 @@
.overlay-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 1.5rem;
padding: 0 2rem;
text-align: center;
}
.overlay-message {
margin: 0;
max-width: 30ch;
font-weight: 700;
font-size: 1.05rem;
line-height: 1.4;
/* Inchiostro su fondo chiaro dell'overlay: leggibile anche al sole */
color: var(--mud-palette-text-primary);
}
.success-checkmark {
width: 80px;
height: 80px;
margin: 0 auto;
}
/* Reduced motion: mostra la spunta di successo statica, senza coreografia */
@media (prefers-reduced-motion: reduce) {
.success-checkmark .check-icon::after,
.icon-line.line-tip,
.icon-line.line-long {
animation: none !important;
}
}
.success-checkmark .check-icon {
width: 80px;
height: 80px;
+182 -66
View File
@@ -3,6 +3,7 @@
@using SteUp.Shared.Components.Layout
@using SteUp.Shared.Components.Layout.Overlay
@using SteUp.Shared.Components.SingleElements.Card
@using SteUp.Shared.Components.SingleElements.MessageBox
@using SteUp.Shared.Core.Dto
@using SteUp.Shared.Core.Entities
@using SteUp.Shared.Core.Enum
@@ -28,36 +29,47 @@
@if (!SchedeGrouped.IsNullOrEmpty())
{
<MudExpansionPanels MultiExpansion="true">
@{
var panelIndex = 0;
}
@foreach (var group in SchedeGrouped)
{
<MudExpansionPanel Class="expansion-panel" Expanded="true" Text="@group.Key.Descrizione">
var isFirstPanel = panelIndex == 0;
panelIndex++;
<MudExpansionPanel Class="expansion-panel" Expanded="isFirstPanel" Text="@group.Key.Descrizione">
<TitleContent>
@{
var daInviare = group.Value.CountDaInviare();
}
<div class="header-scheda-group">
<div class="title">
<MudText Typo="Typo.h6"><b>@group.Key.Descrizione</b></MudText>
<MudChip T="string" Size="Size.Small" Color="Color.Default">
@($"{group.Value.Count} sched{(group.Value.Count == 1 ? "a" : "e")}")
</MudChip>
@if (daInviare > 0)
{
<MudChip T="string" Size="Size.Small" Variant="Variant.Filled"
Color="Color.Primary" Icon="@Icons.Material.Rounded.PhonelinkOff">
@($"{daInviare} da inviare")
</MudChip>
}
</div>
@if (SteupDataService.InspectionPageState.Ispezione.Stato != StatusEnum.Completata)
{
<div class="action-scheda-group">
@if (NetworkService.IsNetworkAvailable())
{
<MudFab StartIcon="@Icons.Material.Rounded.Add"
Color="Color.Warning" Size="Size.Small"
OnClick="@(() => CreateNewScheda(group.Key))"/>
<MudFab StartIcon="@Icons.Material.Rounded.Add"
Color="Color.Default" Size="Size.Medium"
Disabled="!NetworkService.IsNetworkAvailable()"
aria-label="@($"Nuova scheda su {group.Key.Descrizione}")"
OnClick="@(() => CreateNewScheda(group.Key))"/>
<MudFab StartIcon="@Icons.Material.Rounded.CloudSync" Label="Esporta reparto"
Color="Color.Success" Size="Size.Small"
OnClick="@(() => ExportReparto(group.Key))"/>
}
else
@if (NetworkService.IsNetworkAvailable() && daInviare > 0)
{
<MudFab StartIcon="@Icons.Material.Rounded.Add"
Label="@($"Nuova scheda su {group.Key.Descrizione}")"
Color="Color.Warning" Size="Size.Medium"
OnClick="@(() => CreateNewScheda(group.Key))"/>
<MudFab StartIcon="@Icons.Material.Rounded.CloudUpload" Label="Esporta reparto"
Color="Color.Primary" Size="Size.Medium"
aria-label="@($"Esporta il reparto {group.Key.Descrizione} al server")"
OnClick="@(() => ExportReparto(group.Key))"/>
}
</div>
}
@@ -75,15 +87,35 @@
}
</MudExpansionPanels>
}
else
{
<div class="empty-state">
<MudIcon Icon="@Icons.Material.Rounded.AssignmentLate" Class="empty-state-icon" Size="Size.Large"/>
<p class="empty-state-title">Nessuna scheda ancora</p>
@if (SteupDataService.InspectionPageState.Ispezione.Stato != StatusEnum.Completata)
{
<p class="empty-state-text">
Tocca <b>+</b> in basso per creare la prima scheda di questa ispezione.
</p>
}
</div>
}
</div>
<SpinnerOverlay VisibleOverlay="VisibleOverlay"/>
<SpinnerOverlay VisibleOverlay="VisibleOverlay" SuccessAnimation="_success" Message="@_progressMessage"/>
<ConfirmMessageBox @ref="_exportRepartoBox" NoText="Annulla" YesText="Esporta" YesColor="Color.Primary"
Message="Le schede di questo reparto verranno inviate al server. Procedere?"/>
@code {
private List<Scheda> SchedeList { get; set; } = [];
private Dictionary<JtbFasiDto, List<Scheda>> SchedeGrouped { get; set; } = [];
private bool VisibleOverlay { get; set; }
private bool _isBusy;
private bool _success;
private string? _progressMessage;
private ConfirmMessageBox _exportRepartoBox = null!;
protected override void OnInitialized()
{
@@ -110,6 +142,10 @@
private async void HandleCompleteInspection()
{
// Guardia anti doppio-invio: il completamento è irreversibile.
if (_isBusy) return;
_isBusy = true;
try
{
await InvokeAsync(() =>
@@ -120,8 +156,16 @@
var ispezione = SteupDataService.InspectionPageState.Ispezione;
foreach (var scheda in ispezione.Schede.Where(x => x.ActivityId == null))
var daInviare = ispezione.Schede.Where(x => x.ActivityId == null).ToList();
var totale = daInviare.Count;
var indice = 0;
var fallite = 0;
foreach (var scheda in daInviare)
{
indice++;
await SetProgress($"Invio scheda {indice} di {totale}…");
var apiResponse = await IntegrySteupService.SaveScheda(
new SaveRequestDto
{
@@ -134,11 +178,18 @@
PersonaRif = scheda.Responsabile,
Barcodes = scheda.Articoli.ConvertAll(x => x.Barcode),
Scandeza = (ScadenzaEnum)scheda.Scadenza,
ParentActivityId = scheda.Ispezione?.ActivityId
// Aggancia all'ispezione esistente: dopo la prima scheda l'id e' noto,
// così le successive non creano nuove ispezioni.
ParentActivityId = ispezione.ActivityId
}
);
if (apiResponse == null) continue;
// Fallita: resta "Da inviare". Non completeremo l'ispezione finché tutte non sono arrivate.
if (apiResponse == null)
{
fallite++;
continue;
}
scheda.ActivityId = apiResponse.ActivityIdScheda;
await IspezioniService.UpdateSchedaAsync(scheda);
@@ -149,20 +200,49 @@
ispezione.Rilevatore,
apiResponse.ActivityIdIspezione
);
// Aggiorna l'id in memoria per l'iterazione successiva.
ispezione.ActivityId = apiResponse.ActivityIdIspezione;
if (scheda.ImageNames == null) continue;
var fileList = (await FileManager.GetInspectionFiles(ispezione, scheda.ImageNames))?
.Where(x => x.ToUpload).ToList();
if (fileList == null) continue;
var totFoto = fileList.Count;
var indiceFoto = 0;
foreach (var file in fileList)
{
indiceFoto++;
await SetProgress($"Scheda {indice}/{totale} · foto {indiceFoto}/{totFoto}…");
await IntegrySteupService.UploadFile(scheda.ActivityId!, file.FileBytes!, file.Name!);
}
}
// Integrità dati: se anche una sola scheda non è arrivata al server, NON completare.
// L'ispezione resta editabile e le schede fallite restano "Da inviare".
if (fallite > 0)
{
await InvokeAsync(() =>
{
_progressMessage = null;
StateHasChanged();
});
LoadSchede(); // rinfresca i badge: riuscite → "Inviato", fallite → restano "Da inviare"
await Dialog.ShowWarning(
fallite == 1
? "1 scheda non è stata inviata e resta salvata sul dispositivo. Controlla la connessione e riprova prima di concludere l'ispezione."
: $"{fallite} schede non sono state inviate e restano salvate sul dispositivo. Controlla la connessione e riprova prima di concludere l'ispezione."
);
return;
}
await SetProgress("Completamento ispezione…");
await IntegrySteupService.CompleteInspection(ispezione.ActivityId!);
await IspezioniService.UpdateStatoIspezioneAsync(
ispezione.CodMdep,
@@ -170,29 +250,42 @@
ispezione.Rilevatore,
StatusEnum.Completata
);
SteupDataService.InspectionPageState.Ispezione.Stato = StatusEnum.Completata;
// Riscontro al momento più importante: la spunta di successo prima di chiudere l'overlay.
await InvokeAsync(() =>
{
VisibleOverlay = false;
_progressMessage = "Ispezione inviata";
_success = true;
StateHasChanged();
});
await Task.Delay(1800);
}
catch (Exception e)
{
Console.WriteLine(e.Message);
OnError(e, e.Message);
}
finally
{
_isBusy = false;
_success = false;
_progressMessage = null;
await InvokeAsync(() =>
{
VisibleOverlay = false;
StateHasChanged();
});
OnError(e, e.Message);
}
}
private Task SetProgress(string message) =>
InvokeAsync(() =>
{
_progressMessage = message;
StateHasChanged();
});
private void GroupSchede()
{
SchedeGrouped = SchedeList
@@ -207,7 +300,7 @@
private void OnSchedaModified(Scheda obj)
{
var index = SchedeList.FindIndex(x => x.Id.Equals(obj.Id));
if (index > 0) SchedeList[index] = obj;
if (index >= 0) SchedeList[index] = obj;
GroupSchede();
StateHasChanged();
@@ -234,66 +327,89 @@
private async Task ExportReparto(JtbFasiDto jtbFasi)
{
// Guardia anti doppio-tap: un secondo tocco durante l'invio non ri-esporta il batch.
if (_isBusy) return;
var confirmed = await _exportRepartoBox.ShowAsync();
if (confirmed is not true) return;
_isBusy = true;
VisibleOverlay = true;
StateHasChanged();
var ispezione = SteupDataService.InspectionPageState.Ispezione;
var saveRequest = SchedeGrouped[jtbFasi].ConvertAll(x =>
try
{
return new SaveRequestDto
var ispezione = SteupDataService.InspectionPageState.Ispezione;
var saveRequest = SchedeGrouped[jtbFasi].ConvertAll(x =>
{
LocalIdScheda = x.Id,
ActivityTypeId = x.ActivityTypeId,
CodJfas = x.CodJfas,
CodMdep = ispezione.CodMdep,
DataCreazione = ispezione.Data,
Note = x.Note,
PersonaRif = x.Responsabile,
Barcodes = x.Articoli.ConvertAll(y => y.Barcode),
Scandeza = (ScadenzaEnum)x.Scadenza,
ParentActivityId = x.Ispezione?.ActivityId
};
});
return new SaveRequestDto
{
LocalIdScheda = x.Id,
ActivityTypeId = x.ActivityTypeId,
CodJfas = x.CodJfas,
CodMdep = ispezione.CodMdep,
DataCreazione = ispezione.Data,
Note = x.Note,
PersonaRif = x.Responsabile,
Barcodes = x.Articoli.ConvertAll(y => y.Barcode),
Scandeza = (ScadenzaEnum)x.Scadenza,
// Aggancia all'ispezione esistente sul server (evita di crearne una nuova a ogni export).
ParentActivityId = ispezione.ActivityId
};
});
var apiResponse = await IntegrySteupService.SaveMultipleSchede(saveRequest);
var apiResponse = await IntegrySteupService.SaveMultipleSchede(saveRequest);
if (apiResponse == null)
{
Snackbar.Add("Esportazione non riuscita. Le schede restano sul dispositivo, riprova.", Severity.Error);
return;
}
if (apiResponse != null)
{
SteupDataService.InspectionPageState.Ispezione.ActivityId = apiResponse.ActivityIdIspezione;
await IspezioniService.UpdateActivityIdIspezioneAsync(ispezione.CodMdep, ispezione.Data,
UserSession.User.Username, apiResponse.ActivityIdIspezione
);
if (apiResponse.ActivityIdSchedaList.IsNullOrEmpty()) return;
foreach (var scheda in SchedeGrouped[jtbFasi])
if (!apiResponse.ActivityIdSchedaList.IsNullOrEmpty())
{
scheda.ActivityId = apiResponse.ActivityIdScheda;
await IspezioniService.UpdateActivityIdSchedaAsync(scheda.Id, scheda.ActivityId);
foreach (var scheda in SchedeGrouped[jtbFasi])
{
var activityId = apiResponse.ActivityIdSchedaList!.Find(x =>
x.LocalId != null && x.LocalId == scheda.Id
)?.ActivityId;
var activityId = apiResponse.ActivityIdSchedaList!.Find(x =>
x.LocalId != null && x.LocalId == scheda.Id
)?.ActivityId;
if (activityId == null) continue;
if (activityId == null) return;
scheda.ActivityId = activityId;
await IspezioniService.UpdateActivityIdSchedaAsync(scheda.Id, scheda.ActivityId);
scheda.ActivityId = activityId;
await IspezioniService.UpdateActivityIdSchedaAsync(scheda.Id, scheda.ActivityId);
}
}
}
VisibleOverlay = false;
StateHasChanged();
Snackbar.Add("Reparto esportato", Severity.Success);
}
catch (Exception e)
{
OnError(e, e.Message);
}
finally
{
_isBusy = false;
VisibleOverlay = false;
StateHasChanged();
}
}
private void OnError(Exception? e, string? errorMessage)
private void OnError(Exception? e, string? technicalMessage)
{
if (e != null) Logger.LogError(e, errorMessage);
if (errorMessage == null) return;
// Il dettaglio tecnico va nei log, non davanti al rilevatore in campo.
if (e != null) Logger.LogError(e, technicalMessage);
_ = Dialog.ShowError(errorMessage);
_ = Dialog.ShowError(
"Invio non riuscito. Il lavoro resta salvato sul dispositivo: controlla la connessione e riprova."
);
}
void IDisposable.Dispose()
@@ -1,4 +1,9 @@
.ispezione ::deep .expansion-panel .mud-expand-panel-content {
/* Tinta di fondo tenue: le card e i pannelli bianchi (Carta) risaltano sul Grigio Campo */
.ispezione {
background-color: var(--light-card-background);
}
.ispezione ::deep .expansion-panel .mud-expand-panel-content {
display: flex;
flex-direction: column;
gap: 1.5rem;
@@ -7,5 +12,36 @@
.action-scheda-group {
display: flex;
align-items: center;
justify-content: space-around;
justify-content: flex-end;
gap: .75rem;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
gap: .75rem;
margin-top: 3rem;
padding: 0 1.5rem;
}
.empty-state .empty-state-icon {
color: var(--mud-palette-primary);
opacity: .85;
}
.empty-state .empty-state-title {
margin: 0;
font-weight: 800;
font-size: 1.25rem;
color: var(--mud-palette-text-primary);
}
.empty-state .empty-state-text {
margin: 0;
max-width: 34ch;
font-weight: 600;
line-height: 1.5;
color: var(--mud-palette-gray-darker);
}
@@ -1,8 +1,10 @@
.container-primary-info {
/*background: var(--light-card-background);*/
width: 100%;
margin-bottom: 2rem;
border-radius: 20px;
background: var(--mud-palette-surface);
border: 1px solid var(--card-border-color);
box-shadow: var(--card-box-shadow);
}
.container-primary-info.no-margin {
@@ -42,7 +44,8 @@
}
.info-subtitle {
color: var(--mud-palette-gray-default);
/* gray-darker (non gray-default) per superare il contrasto AA e restare leggibile al sole */
color: var(--mud-palette-gray-darker);
font-size: medium;
font-weight: 600;
line-height: normal;
@@ -6,17 +6,29 @@
@using SteUp.Shared.Core.Interface.IntegryApi
@using SteUp.Shared.Core.Interface.LocalDb
@using SteUp.Shared.Core.Interface.System
@using Microsoft.Extensions.Logging
@inject IIspezioniService IspezioniService
@inject IIntegrySteupService IntegrySteupService
@inject IDialogService Dialog
@inject IFileManager FileManager
@inject IFileManager FileManager
@inject ILogger<SchedaCard> Logger
<div class="scheda-card">
<div class="scheda-body-section">
<div class="title-section">
<MudText Class="scheda-title" Typo="Typo.subtitle1">
<b>@Scheda.ActivityTypeId</b>
</MudText>
<div class="scheda-title-group">
<MudText Class="scheda-title" Typo="Typo.subtitle1">
<b>@Scheda.ActivityTypeId</b>
</MudText>
@{
var syncState = Scheda.GetSyncState();
}
<MudChip T="string" Class="sync-badge" Icon="@syncState.GetIcon()"
Size="Size.Small" Variant="Variant.Filled" Color="@syncState.GetColor()">
@syncState.ToLabel()
</MudChip>
</div>
<div class="sub-info-section">
<MudChip T="string" Icon="@Icons.Material.Rounded.PhotoCamera" Size="Size.Small" Color="Color.Default">
@@ -31,29 +43,26 @@
<div class="scheda-card-action">
@if (Ispezione.Stato != StatusEnum.Completata)
{
if (NetworkService.IsNetworkAvailable())
<div class="action-secondary">
<MudFab Color="Color.Default" Size="Size.Medium" StartIcon="@Icons.Material.Rounded.Edit"
aria-label="Modifica scheda" OnClick="@UpdateScheda"/>
<MudFab Color="Color.Error" Size="Size.Medium" StartIcon="@Icons.Material.Rounded.Delete"
aria-label="Elimina scheda" OnClick="@DeleteScheda"/>
</div>
@if (NetworkService.IsNetworkAvailable() && syncState == SyncState.DaInviare)
{
<MudFab Color="Color.Info" Size="Size.Small" StartIcon="@Icons.Material.Rounded.Edit"
OnClick="@UpdateScheda"/>
<MudFab Color="Color.Error" Size="Size.Small" StartIcon="@Icons.Material.Rounded.Delete"
OnClick="@DeleteScheda"/>
<MudFab Color="Color.Success" Size="Size.Small" StartIcon="@Icons.Material.Rounded.CloudSync"
OnClick="@ExportScheda"/>
}
else
{
<MudFab Color="Color.Info" Size="Size.Small" StartIcon="@Icons.Material.Rounded.Edit"
OnClick="@UpdateScheda"/>
<MudFab Color="Color.Error" Size="Size.Small" StartIcon="@Icons.Material.Rounded.Delete"
OnClick="@DeleteScheda"/>
<MudFab Color="Color.Primary" Size="Size.Medium" StartIcon="@Icons.Material.Rounded.CloudUpload"
Label="Invia" aria-label="Invia scheda al server" OnClick="@ExportScheda"/>
}
}
else
{
<MudFab Color="Color.Info" Size="Size.Small" StartIcon="@Icons.Material.Rounded.RemoveRedEye"
OnClick="@ViewScheda"/>
<MudFab Color="Color.Success" Size="Size.Small" StartIcon="@Icons.Material.Rounded.CloudSync"
OnClick="@ExportImg"/>
<div class="action-secondary">
<MudFab Color="Color.Default" Size="Size.Medium" StartIcon="@Icons.Material.Rounded.RemoveRedEye"
aria-label="Visualizza scheda" OnClick="@ViewScheda"/>
</div>
<MudFab Color="Color.Primary" Size="Size.Medium" StartIcon="@Icons.Material.Rounded.CloudUpload"
Label="Invia foto" aria-label="Invia foto al server" OnClick="@ExportImg"/>
}
</div>
</div>
@@ -112,27 +121,37 @@
private async Task ExportScheda()
{
// Guardia anti doppio-tap: invio irreversibile al server.
if (VisibleOverlay) return;
VisibleOverlay = true;
StateHasChanged();
var apiResponse = await IntegrySteupService.SaveScheda(
new SaveRequestDto
{
LocalIdScheda = Scheda.Id,
ActivityTypeId = Scheda.ActivityTypeId,
CodJfas = Scheda.CodJfas,
CodMdep = Ispezione.CodMdep,
DataCreazione = Ispezione.Data,
Note = Scheda.Note,
PersonaRif = Scheda.Responsabile,
Barcodes = Scheda.Articoli.ConvertAll(x => x.Barcode),
Scandeza = (ScadenzaEnum)Scheda.Scadenza,
ParentActivityId = Scheda.Ispezione?.ActivityId
}
);
if (apiResponse != null)
try
{
var apiResponse = await IntegrySteupService.SaveScheda(
new SaveRequestDto
{
LocalIdScheda = Scheda.Id,
ActivityTypeId = Scheda.ActivityTypeId,
CodJfas = Scheda.CodJfas,
CodMdep = Ispezione.CodMdep,
DataCreazione = Ispezione.Data,
Note = Scheda.Note,
PersonaRif = Scheda.Responsabile,
Barcodes = Scheda.Articoli.ConvertAll(x => x.Barcode),
Scandeza = (ScadenzaEnum)Scheda.Scadenza,
// Aggancia all'ispezione esistente sul server (evita di crearne una nuova a ogni invio).
ParentActivityId = SteupDataService.InspectionPageState.Ispezione.ActivityId
}
);
if (apiResponse == null)
{
Snackbar.Add("Invio non riuscito. La scheda resta sul dispositivo, riprova.", Severity.Error);
return;
}
Scheda.ActivityId = apiResponse.ActivityIdScheda;
SteupDataService.InspectionPageState.Ispezione.ActivityId = apiResponse.ActivityIdIspezione;
@@ -140,24 +159,58 @@
UserSession.User.Username, apiResponse.ActivityIdIspezione
);
await IspezioniService.UpdateActivityIdSchedaAsync(Scheda.Id, Scheda.ActivityId);
}
VisibleOverlay = false;
StateHasChanged();
// Notifica il genitore così il contatore "da inviare" del reparto si aggiorna.
await OnSchedaModified.InvokeAsync(Scheda);
Snackbar.Add("Scheda inviata", Severity.Success);
}
catch (Exception e)
{
Snackbar.Add("Invio non riuscito. La scheda resta sul dispositivo, riprova.", Severity.Error);
Logger.LogError(e, e.Message);
}
finally
{
VisibleOverlay = false;
StateHasChanged();
}
}
private async Task ExportImg()
{
if (Scheda.ImageNames == null) return;
if (VisibleOverlay || Scheda.ImageNames == null) return;
var fileList = (await FileManager.GetInspectionFiles(Ispezione, Scheda.ImageNames))?
.Where(x => x.ToUpload).ToList();
VisibleOverlay = true;
StateHasChanged();
if (fileList == null) return;
foreach (var file in fileList)
try
{
await IntegrySteupService.UploadFile(Scheda.ActivityId!, file.FileBytes!, file.Name!);
var fileList = (await FileManager.GetInspectionFiles(Ispezione, Scheda.ImageNames))?
.Where(x => x.ToUpload).ToList();
if (fileList == null || fileList.Count == 0)
{
Snackbar.Add("Nessuna nuova foto da inviare", Severity.Info);
return;
}
foreach (var file in fileList)
{
await IntegrySteupService.UploadFile(Scheda.ActivityId!, file.FileBytes!, file.Name!);
}
Snackbar.Add(fileList.Count == 1 ? "Foto inviata" : $"{fileList.Count} foto inviate", Severity.Success);
}
catch (Exception e)
{
Snackbar.Add("Invio foto non riuscito, riprova.", Severity.Error);
Logger.LogError(e, e.Message);
}
finally
{
VisibleOverlay = false;
StateHasChanged();
}
}
}
@@ -1,23 +1,48 @@
.scheda-card{
padding: .75rem 1.25rem;
background-color: var(--light-card-background);
padding: .55rem .85rem;
background-color: var(--mud-palette-surface);
border: 1px solid var(--card-border-color);
border-radius: 1em;
box-shadow: var(--card-box-shadow);
}
.scheda-body-section{
display: flex;
flex-direction: column;
gap: 1rem;
gap: .5rem;
}
.title-section{
display: flex;
align-items: center;
justify-content: space-between;
gap: .5rem;
}
.scheda-title-group{
display: flex;
flex-direction: column;
align-items: flex-start;
gap: .15rem;
min-width: 0;
}
.sync-badge{
margin: 0;
}
.scheda-card-action{
display: flex;
gap: 1rem;
justify-content: center;
gap: .5rem;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
}
/* Azioni secondarie (modifica/elimina) raggruppate e ancorate a sinistra;
l'azione primaria "Invia" (corallo) resta separata e a portata di pollice a destra. */
.action-secondary{
display: flex;
gap: .5rem;
align-items: center;
}
@@ -18,13 +18,13 @@
@inject IIspezioniService IspezioniService
@inject IIntegrySteupService IntegrySteupService
@inject OnScannerService OnScannerService
@inject IAttachedService AttachedService
@inject ILogger<ModalFormScheda> Logger
<MudDialog Class="customDialog-form">
<DialogContent>
<MudForm ReadOnly="ReadOnly" @ref="_form">
<HeaderLayout Cancel="true" OnCancel="@Cancel" LabelSave="@LabelSave"
OnSave="Save" Title="Scheda"/>
<HeaderLayout Cancel="true" OnCancel="@Cancel" Title="Scheda"/>
<div class="content">
@@ -168,7 +168,7 @@
</CardFormModal>
<CardFormModal Title="Note">
<MudTextField T="string?" Variant="Variant.Text" Lines="3"
<MudTextField @key="_noteFieldKey" T="string?" Variant="Variant.Text" Lines="3"
@bind-Value="Scheda.Note" @bind-Value:after="OnAfterChangeValue"
DebounceInterval="500" OnDebounceIntervalElapsed="OnAfterChangeValue"/>
</CardFormModal>
@@ -183,6 +183,15 @@
Suggerisci note descrittive
</MudButton>
</div>
@if (!LabelSave.IsNullOrEmpty())
{
<div class="container-button">
<MudButton OnClick="@Save" Color="Color.Primary" Variant="Variant.Filled" FullWidth="true">
@LabelSave
</MudButton>
</div>
}
</div>
</MudForm>
@@ -215,6 +224,10 @@
private MudForm _form = null!;
private string? LabelSave { get; set; }
// Cambiare questa chiave forza Blazor a ricreare il MudTextField delle note,
// così una nota impostata da codice (suggerimenti) compare subito a video
// nonostante il DebounceInterval del campo.
private int _noteFieldKey;
private bool IsDirty { get; set; }
private Scheda _originalScheda = null!;
private List<AttachedDto>? AttachedList { get; set; }
@@ -283,7 +296,9 @@
PersonaRif = Scheda.Responsabile,
Barcodes = Scheda.Articoli.ConvertAll(x => x.Barcode),
Scandeza = (ScadenzaEnum)Scheda.Scadenza,
ParentActivityId = IsNew ? null : Scheda.Ispezione?.ActivityId
// Aggancia la scheda all'ispezione GIA esistente sul server (se sincronizzata),
// altrimenti null e il server la crea. Evita di creare una nuova ispezione a ogni salvataggio.
ParentActivityId = SteupDataService.InspectionPageState.Ispezione.ActivityId
}
);
}
@@ -340,6 +355,9 @@
await IspezioniService.UpdateActivityIdIspezioneAsync(CodMdep, Data,
UserSession.User.Username, apiResponse.ActivityIdIspezione
);
// Tiene in memoria l'ActivityId dell'ispezione: il prossimo salvataggio nella stessa
// sessione lo userà come ParentActivityId invece di creare una nuova ispezione.
SteupDataService.InspectionPageState.Ispezione.ActivityId = apiResponse.ActivityIdIspezione;
Scheda.ActivityId = apiResponse.ActivityIdScheda;
}
@@ -380,9 +398,12 @@
await IspezioniService.UpdateSchedaAsync(Scheda);
if (apiResponse != null)
{
await IspezioniService.UpdateActivityIdIspezioneAsync(CodMdep, Data,
UserSession.User.Username, apiResponse.ActivityIdIspezione
);
SteupDataService.InspectionPageState.Ispezione.ActivityId = apiResponse.ActivityIdIspezione;
}
}
private async Task UploadFile(Scheda scheda)
@@ -490,8 +511,10 @@
private async Task OpenAddAttached()
{
var result = await Dialog.OpenAddAttached();
if (result is not { Canceled: false, Data: List<AttachedDto> attachedList }) return;
var selectImageFromCamera = await AttachedService.SelectImageFromCamera();
if (selectImageFromCamera == null) return;
var attachedList = new List<AttachedDto> { selectImageFromCamera };
OnLoading = true;
await InvokeAsync(StateHasChanged);
@@ -589,7 +612,7 @@
if (modal is { Canceled: false, Data: List<ArticoliInGrigliaDto> articoliSelezionati })
{
Scheda.Articoli.AddRange(articoliSelezionati.ConvertAll(x => new SchedaArticolo
Scheda.Articoli.InsertRange(0, articoliSelezionati.ConvertAll(x => new SchedaArticolo
{
Barcode = x.Barcode,
Descrizione = x.Descrizione
@@ -615,7 +638,7 @@
#endregion
private void SuggestActivityDescription()
private async Task SuggestActivityDescription()
{
if (Scheda.ActivityTypeId == null)
{
@@ -626,18 +649,25 @@
VisibleOverlay = true;
StateHasChanged();
_ = Task.Run(async () =>
try
{
var activityDescriptions = await IntegryApiService.SuggestActivityDescription(Scheda.ActivityTypeId);
var modal = await Dialog.OpenSuggestActivityDescription(activityDescriptions);
if (modal is { Canceled: false, Data: not null })
Scheda.Note = modal.Data!.ToString();
if (modal is { Canceled: false, Data: string descrizione } && !string.IsNullOrWhiteSpace(descrizione))
{
Scheda.Note = descrizione;
_noteFieldKey++; // ricrea il campo note così la nota selezionata compare subito a video
// Aggiorna anche lo stato (dirty → Salva/Aggiorna) come farebbe l'input manuale.
OnAfterChangeValue();
}
}
finally
{
VisibleOverlay = false;
await InvokeAsync(StateHasChanged);
});
StateHasChanged();
}
}
#region Scanner
@@ -674,7 +704,7 @@
{
RecalcDirty(true);
Scheda.Articoli.Add(new SchedaArticolo
Scheda.Articoli.Insert(0, new SchedaArticolo
{
Barcode = art.Barcode,
Descrizione = art.Descrizione
@@ -0,0 +1,56 @@
using MudBlazor;
using SteUp.Shared.Core.Entities;
namespace SteUp.Shared.Core.Helpers;
/// <summary>
/// Stato di sincronizzazione di una scheda rispetto al server Integry.
/// Principio "Offline è la verità": il rilevatore deve sempre vedere cosa è
/// ancora solo sul dispositivo e cosa è già arrivato al server.
/// </summary>
public enum SyncState
{
/// Salvata solo in locale, non ancora inviata al server.
DaInviare = 0,
/// Inviata al server (ha un ActivityId).
Inviato = 1
}
public static class SchedaSyncHelper
{
public static SyncState GetSyncState(this Scheda scheda) =>
string.IsNullOrEmpty(scheda.ActivityId) ? SyncState.DaInviare : SyncState.Inviato;
public static string ToLabel(this SyncState state) =>
state switch
{
SyncState.DaInviare => "Da inviare",
SyncState.Inviato => "Inviato",
_ => string.Empty
};
/// <summary>
/// Colore del badge. Corallo (Primary) = azione richiesta (da inviare);
/// verde (Success) = fatto (inviato). Rispetta la Regola dell'Unico Accento.
/// </summary>
public static Color GetColor(this SyncState state) =>
state switch
{
SyncState.DaInviare => Color.Primary,
SyncState.Inviato => Color.Success,
_ => Color.Default
};
public static string GetIcon(this SyncState state) =>
state switch
{
SyncState.DaInviare => Icons.Material.Rounded.PhonelinkOff,
SyncState.Inviato => Icons.Material.Rounded.CloudDone,
_ => string.Empty
};
/// Numero di schede non ancora inviate in una lista.
public static int CountDaInviare(this IEnumerable<Scheda> schede) =>
schede.Count(s => s.GetSyncState() == SyncState.DaInviare);
}
@@ -7,12 +7,13 @@ public static class StatusEnumHelper
{
public static string ConvertToHumanReadable(this StatusEnum enumValue)
{
// Sentence-case: l'enfasi la dà il peso del chip, non il maiuscolo (Regola del Peso, non del Maiuscolo).
return enumValue switch
{
StatusEnum.InCorso => "IN CORSO",
StatusEnum.Completata => "COMPLETATA",
StatusEnum.Verifica => "VERIFICA",
StatusEnum.Annullata => "ANNULLATA",
StatusEnum.InCorso => "In corso",
StatusEnum.Completata => "Completata",
StatusEnum.Verifica => "Verifica",
StatusEnum.Annullata => "Annullata",
_ => throw new ArgumentOutOfRangeException(nameof(enumValue), enumValue, null)
};
}
@@ -5,6 +5,8 @@
/*Utility*/
--exception-box-shadow: 1px 2px 5px rgba(0, 0, 0, 0.3);
--custom-box-shadow: 1px 2px 5px var(--gray-for-shadow);
/* Ombra morbida a due livelli, tinta col navy del brand: stacca le card dallo sfondo senza pesare */
--card-box-shadow: 0 1px 2px rgba(0, 35, 57, 0.08), 0 6px 16px rgba(0, 35, 57, 0.10);
--mud-default-borderradius: 20px !important;
--m-page-x: 1rem;
--mh-header: 4rem;