nis2-agile/docs/supplier-portal/UX_MINI_SPEC.md
DevEnv nis2-agile aa2db4c6c2 [DOCS] Design modulo questionari fornitori + portale OTP + AI consulente normativo
Pacchetto di design completo (nessun codice applicato, nessuna migrazione eseguita):
- DESIGN aggiornato con 5 review agenti + 3 decisioni utente + pilastro AI consulente (sez. 12-14)
- docs/supplier-portal/template-nis2-base.questions.json: 26 domande GV.SC (Allegato 2 ACN) con nis2_ref/vuln_flag e fonti certe verbatim
- docs/supplier-portal/AI_CONSULENTE_NORMATIVO.md: corpus normativo aggiornato + persona consulente (modello TRPG)
- docs/supplier-portal/UX_MINI_SPEC.md: mini-spec portale fornitore (stati/copy/autosave/mobile/a11y/editor no-code)
- docs/sql/032-035: migrazioni idempotenti proposte (modulo, suppliers, portale auth, migrazione 027->campaigns)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 09:53:07 +02:00

69 lines
9.5 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Mini-spec UX — Portale fornitore + Editor lato azienda (§6)
> Deliverable agente design 2026-05-31. Chiude i finding UX F1F25 di [[DESIGN_MODULO_QUESTIONARI_FORNITORI]] §12. Copy + comportamento + acceptance criteria.
> Utente di riferimento del portale: referente PMI fornitrice, NON tecnico, da smartphone, 1 volta ogni 6-12 mesi, senza supporto IT. Ogni scelta privilegia chiarezza, target ≥44px, recupero da errore sempre possibile.
> Convenzioni riusate (verificate): `--primary:#06B6D4`; progress bar di `assessment.html` (`updateProgressBar`, id `progress-pct/-bar-fill/-answered/-total`, i18n `assessment.progress`); `debounce`, `showNotification`, `_loadFirmBranding()` (GET `/api/branding/current`), `initIdleTimeout()` overlay countdown — in `common.js`; badge `badge-success|warning|danger|neutral`; `sendSupplierQuestionnaire`+prompt rigenera-link in `supply-chain.html`; baseline + footer GDPR + `noindex,nofollow` in `supplier-assessment.html`.
> Endpoint (forma router §12): `POST /api/supplier-portal/request-otp`, `verify-otp`, `GET /me`, `GET /{id}`, `POST /{id}/submit`, `PATCH /{id}/answers`.
> i18n: namespace `sp.*` (+ `sp.editor.*`) in `i18n.js`; ogni stringa IT ha controparte EN.
## 1. Portale fornitore — supplier-portal.html
Principi: una colonna max 560px, branding del **committente** in header (NIS2 Agile solo footer), zero gergo ("codice"/"link"/"questionario", mai "token/OTP/JWT/campaign"), selettore lingua IT/EN in alto a destra, `noindex,nofollow`, nessun link all'app interna.
Stati:
1. **Loading**: spinner; se `GET /me`/branding >8s → stato errore rete.
2. **Richiesta accesso**: input email (`type=email inputmode=email autocomplete=email autofocus`, label visibile), CTA "Invia link di accesso", micro-copy GDPR con link informativa. **Risposta sempre opaca** (anti-enumerazione) → va sempre allo stato "email inviata".
3. **Email inviata**: "Controlla la tua email" + avviso spam + mittente; campo OTP fallback; **reinvio con countdown 60s** (raddoppia dal 3° invio); via d'uscita "Usa un'altra email".
4. **Verifica OTP — errori specifici** (F2/F3): messaggi umani con via d'uscita per: codice errato ("…hai ancora {n} tentativi", contatore reale da `supplier_otp.attempts`), scaduto ("→ Invia un nuovo codice"), lockout ("bloccato per {minuti} min", persistente lato DB), link già usato ("→ Invia un nuovo link"), errore rete ("Riprova"). Mai codici HTTP/"token".
5. **Gerarchia magic-link/OTP** (F1/F4): nel portale e nell'email il **pulsante magic-link è primario**, OTP fallback. **No doppio-consumo**: l'apertura del link (prefetch mail mobile) NON consuma il token; consumo solo al click esplicito "Entra nel portale". AC: aprire 2× e poi cliccare deve funzionare.
6. **Dashboard**: se **1 solo questionario aperto** → vai diretto alla compilazione. Se multipli → card con badge stato + scadenza in chiaro + CTA "Compila/Riprendi/Vedi ricevuta"; ordinamento da-compilare prima per scadenza.
7. **Compilazione**: header con badge scadenza sticky + progress bar + domande single-page (vedi §4).
8. **Vuoto**: "Nessun questionario in attesa" + suggerimento contattare committente.
9. **Completato/Ricevuta**: "Questionario inviato. Grazie!" + "Scarica PDF" + (Caso B) "Torna ai questionari". **Score mostrato solo se `show_score_to_supplier=true`** (default OFF, F23) → rimuovere/condizionare lo score sempre-mostrato dell'attuale supplier-assessment.html.
10. **Errore rete**: non distruttivo, risposte salvate localmente, "Riprova".
11. **Sessione in scadenza (4h)**: overlay countdown (riuso `initIdleTimeout`), "Continua" estende; alla scadenza "Le tue risposte sono salvate. Richiedi un nuovo link per riprendere". Ripresa dall'ultima domanda dopo nuovo OTP.
## 2. Autosave (F11 — priorità assoluta)
- Stato `in_progress` alla prima risposta. Salvataggio per-domanda via `PATCH /{id}/answers` con `debounce(fn,800)`.
- **Doppio livello**: localStorage immediato (resilienza offline) + PATCH server (cross-device). Server = source of truth; rinvia PATCH falliti.
- Indicatore discreto: "Salvataggio…" / "Bozza salvata {ora}" / "Salvataggio in sospeso — riproveremo".
- **Ripresa**: banner "Bentornato. Hai risposto a {x} di {y} domande" + "Riprendi" → scroll alla prima domanda senza risposta.
- **"Salva bozza"** (ghost, ridondante ma rassicurante) vs **"Invia definitivo"** (CTA primaria, conferma in modale "dopo l'invio non potrai modificare", disabilitata finché le obbligatorie non sono complete; se mancano → scroll alla prima + "{k} risposte obbligatorie mancanti").
- AC: chiudere/riaprire (anche altro device dopo nuovo OTP) → risposte presenti, cursore sull'ultima non risposta; submit blocca PATCH successivi (409 "già inviato").
## 3. Email invito/OTP (F6)
Inviata via template `/api/emails/send` (NON `send-raw` → OTP fuori da `email_log`). Branding committente, **una sola CTA**.
Contenuto minimo: (1) oggetto con nome committente; (2) motivo 1 frase ("Come fornitore di {Committente}…richiesto dalla normativa NIS2"); (3) tempo stimato (~{n} min); (4) scadenza in chiaro (solo se `due_at`); (5) CTA singola magic-link "Compila il questionario"; (6) OTP fallback "Oppure inserisci questo codice: 123 456 (15 min)"; (7) validità "15 minuti e una sola volta"; (8) footer GDPR Art.13/14 (titolare=committente, NIS2 Agile=responsabile Art.28, link informativa, "se non eri tu, ignora").
AC: OTP mai nei log; 1 sola CTA; nome committente in oggetto+corpo; scadenza solo se valorizzata.
## 4. Compilazione — ergonomia per tipo
Chrome: progress bar (riuso `updateProgressBar`) + "{answered} di {total}"; badge scadenza sticky (neutral >7gg / warning ≤7gg / danger scaduto).
Per tipo:
- `yes_no_partial`: 3 bottoni grandi (radio nativi sotto) "Sì/Parzialmente/No".
- `scale_1_5`: 5 bottoni con **etichette agli estremi sempre visibili** (da options min/max). Mai scala nuda.
- `single_choice`: radio nativi verticali. `multi_choice`: checkbox + hint "Puoi selezionare più opzioni".
- `text`: textarea + esempio placeholder. `number`: `inputmode=numeric`.
- `file`: formati/dimensioni dichiarati prima del tap; `accept` + `capture` (foto documenti); feedback upload (%/check/rimuovi); errori formato/dimensione con suggerimento; async, sopravvive ad autosave (`file_ref`).
- `help_text`: **sempre visibile** sotto la domanda (mai tooltip/hover — non funziona su touch).
- **"Non applicabile" / "Non so"** dove sensato: conta come risposta per la progress bar, distinto dal "No" nello scoring.
## 5. Ricevuta, multi-referente, score
- **Ricevuta (F22)**: email conferma al referente + **PDF risposte** scaricabile (da email e da §1.9). PDF = intestazione committente+fornitore, data, domande+risposte+allegati, riferimento normativo. Niente score se OFF. Snapshot immutabile → rigenerabile identico.
- **Multi-referente/delega (F24)**: "Inoltra a un collega" → nuovo accesso OTP per quell'email, stessa campagna/`supplier_id`, autosave condiviso, traccia `answered_by`. Delegato vede solo quella campagna (no IDOR), stessi limiti OTP.
- **Score (F23)**: `show_score_to_supplier` per template/campagna, **default false**. OFF = solo conferma invio. Toggle nell'editor "Mostra il punteggio al fornitore".
## 6. Editor lato azienda (F16) — no-code in supply-chain.html
Tab "Categorie" / "Template questionari" / "Campagne-scadenze". Mappatura tecnico→no-code:
- `slug`/`code`: auto-generati, nascosti. `order_index`: drag-and-drop (+ frecce ↑/↓ accessibili).
- `options` JSON: editor a righe ("+ Aggiungi opzione"); scale = 2 campi "Etichetta minimo/massimo".
- `reminder_offsets`: chip "14/7/1 giorni prima". `recurrence_months`: dropdown "Una tantum/Ogni 6/12/24 mesi/Personalizzato".
- `weight`: dropdown "Bassa/Media/Alta". `type`: etichette umane. `required`: toggle. `nis2_ref`: chip informativo non editabile. `show_score_to_supplier`: toggle (default OFF).
- **Anteprima "come lo vede il fornitore"**: stesso componente di rendering della compilazione (single source).
- **Versioning (F17)**: salvare un template con campagne attive → modale "crea nuova versione v{x+1}, si applica solo alle campagne future; le {n} in corso restano su v{x}". Snapshot in `questionnaire_template_versions`.
- **Cruscotto campagne**: semaforo (badge), colonne Fornitore/Questionario/Stato/Scadenza/Avanzamento/Azioni. Azioni rapide: Sollecita, Copia link (fallback manuale), Vedi risposte. **Bulk**: sollecita/avvia/esporta selezionati, con conferma e conteggio. Rispetta rate-limit reminder.
## 7. Accessibilità (F21) + mobile-first (F10)
Elementi nativi (`button`, `input radio/checkbox` con `label`); OTP `inputmode=numeric autocomplete=one-time-code pattern=[0-9]* maxlength=6`; email `inputmode=email autocomplete=email`. ARIA: `aria-live=polite` per autosave/progress, `role=alert` per errori; focus order logico, focus torna sul campo dopo errore OTP; modali con focus trap + Esc + `aria-modal`. Drag-drop con alternativa tastiera. Contrasto ≥WCAG AA, stato non solo da colore. Target ≥44×44px, single-column, no hover-only. AC: flusso completo navigabile da sola tastiera e da screen reader; OTP autofill; nessun target <44px a 360px.
## 8. i18n (F20)
Namespace `sp.*`. Lingua portale: (1) impostata dal cliente su campagna (`language it|en|auto`), (2) selezionabile dal fornitore (persistita in localStorage), (3) fallback `navigator.language` IT. Email nella lingua della campagna. Chrome (header/footer/badge/errori/pulsanti) bilingue; i **contenuti domande** restano nella lingua in cui l'azienda li ha scritti (il selettore non auto-traduce i contenuti). AC: cambio lingua immediato senza reload; `campaign.language=en` portale ed email in EN; nessuna stringa IT hardcoded in modalità EN.