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>
9.5 KiB
Mini-spec UX — Portale fornitore + Editor lato azienda (§6)
Deliverable agente design 2026-05-31. Chiude i finding UX F1–F25 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 diassessment.html(updateProgressBar, idprogress-pct/-bar-fill/-answered/-total, i18nassessment.progress);debounce,showNotification,_loadFirmBranding()(GET/api/branding/current),initIdleTimeout()overlay countdown — incommon.js; badgebadge-success|warning|danger|neutral;sendSupplierQuestionnaire+prompt rigenera-link insupply-chain.html; baseline + footer GDPR +noindex,nofollowinsupplier-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: namespacesp.*(+sp.editor.*) ini18n.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:
- Loading: spinner; se
GET /me/branding >8s → stato errore rete. - 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". - 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".
- 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". - 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.
- 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.
- Compilazione: header con badge scadenza sticky + progress bar + domande single-page (vedi §4).
- Vuoto: "Nessun questionario in attesa" + suggerimento contattare committente.
- 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. - Errore rete: non distruttivo, risposte salvate localmente, "Riprova".
- 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_progressalla prima risposta. Salvataggio per-domanda viaPATCH /{id}/answerscondebounce(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, tracciaanswered_by. Delegato vede solo quella campagna (no IDOR), stessi limiti OTP. - Score (F23):
show_score_to_supplierper 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).optionsJSON: 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.