feat(auth): integrate codice-fiscale-js for fiscal code validation in registration

This commit is contained in:
2026-06-14 19:54:18 +02:00 Unverified
parent 762a42196c
commit 038a399e93
7 changed files with 895 additions and 386 deletions
+215 -46
View File
@@ -29,6 +29,15 @@ const media = computed(() => (reportData.value as any)?.media || [])
const statusHistory = computed(() => (reportData.value as any)?.history || [])
const messages = computed(() => (messagesData.value as any)?.messages || [])
const summary = computed(() => (summaryData.value as any)?.summary)
const statusMeta: Record<string, string> = {
OPEN: 'Aperta',
UNDER_REVIEW: 'In revisione',
ASSIGNED: 'Assegnata',
IN_PROGRESS: 'In lavorazione',
RESOLVED: 'Risolta',
REJECTED: 'Respinta',
CLOSED: 'Chiusa'
}
// Form Cambia Stato
const newStatus = ref<ReportStatus>('IN_PROGRESS')
@@ -125,80 +134,132 @@ const formatDate = (dateString: string) => {
<template>
<div v-if="report">
<div class="bg-light border-bottom py-3">
<div class="container d-flex justify-content-between align-items-center">
<div>
<NuxtLink to="/operator/assigned" class="btn btn-sm btn-link text-decoration-none ps-0">
&larr; Torna alle assegnazioni
</NuxtLink>
<h1 class="h4 font-weight-bold mb-1">Intervento #{{ reportId }}</h1>
<div class="d-flex align-items-center gap-2">
<StatusBadge :status="report.status" />
<small class="text-muted">{{ report.category_name }}</small>
<div class="report-hero py-4">
<div class="container">
<NuxtLink to="/operator/assigned" class="btn btn-sm btn-link text-decoration-none ps-0 mb-2">
&larr; Torna alle assegnazioni
</NuxtLink>
<div class="row g-3 align-items-end">
<div class="col-12 col-lg-8">
<div class="report-hero-meta mb-2">
<span class="report-id">Intervento #{{ reportId }}</span>
<StatusBadge :status="report.status" />
</div>
<h1 class="h3 font-weight-bold mb-2">{{ report.category_name }}</h1>
<p class="mb-0 report-hero-address">{{ report.address }}</p>
</div>
<div class="col-12 col-lg-4">
<div class="report-hero-stats">
<div class="hero-stat">
<span class="hero-stat-label">Stato</span>
<strong>{{ statusMeta[report.status] || report.status }}</strong>
</div>
<div class="hero-stat">
<span class="hero-stat-label">Aperta</span>
<strong>{{ formatDate(report.created_at) }}</strong>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="container my-5">
<div class="row">
<!-- Sinistra: Dettagli -->
<div class="col-12 col-lg-7 mb-4">
<div class="card card-bg mb-4">
<div class="row g-4">
<div class="col-12 col-xl-7">
<div class="card card-bg border-0 shadow-sm mb-4">
<div class="card-body">
<h5 class="card-title font-weight-bold mb-3">Dettagli Cittadino</h5>
<p class="mb-1"><strong>Indirizzo:</strong> {{ report.address }}</p>
<p class="mb-3"><strong>Posizione:</strong> {{ report.latitude }}, {{ report.longitude }}</p>
<h6 class="font-weight-bold">Descrizione</h6>
<p class="text-break bg-white p-3 border rounded">{{ report.description }}</p>
<div class="report-section-head mb-3">
<BIIcon name="it-map-marker-circle" size="sm" color="primary" class="me-2" />
<h2 class="h6 mb-0">Dettagli segnalazione</h2>
</div>
<dl class="report-detail-grid mb-0">
<div>
<dt>Indirizzo</dt>
<dd>{{ report.address }}</dd>
</div>
<div>
<dt>Coordinate</dt>
<dd>{{ report.latitude }}, {{ report.longitude }}</dd>
</div>
<div>
<dt>Area</dt>
<dd>{{ report.area_name || 'Non specificata' }}</dd>
</div>
<div>
<dt>Operatore assegnato</dt>
<dd>{{ report.assigned_operator_id ? `#${report.assigned_operator_id}` : 'Non assegnato' }}</dd>
</div>
</dl>
<div class="report-description mt-4">
<h3 class="h6 mb-2">Descrizione</h3>
<p class="mb-0 text-break">{{ report.description }}</p>
</div>
<div v-if="media.length > 0">
<h6 class="font-weight-bold mt-4 mb-3">Allegati del cittadino</h6>
<h3 class="h6 mt-4 mb-3">Allegati del cittadino</h3>
<div class="row g-2">
<div v-for="m in media" :key="m.id" class="col-4 col-md-3">
<img v-if="m.media_kind === 'IMAGE'" :src="runtimeConfig.public.api + m.url" class="img-fluid rounded object-fit-cover shadow-sm" style="aspect-ratio: 1;" />
<img v-if="m.media_kind === 'IMAGE'" :src="runtimeConfig.public.api + m.url" class="img-fluid rounded object-fit-cover shadow-sm report-media-frame" style="aspect-ratio: 1;" />
</div>
</div>
</div>
</div>
</div>
<div class="card card-bg mb-4">
<div class="card card-bg border-0 shadow-sm mb-4">
<div class="card-body">
<h5 class="card-title font-weight-bold mb-3">Comunicazioni con il Cittadino</h5>
<div class="report-chat-header mb-3">
<div>
<div class="report-section-head mb-1">
<BIIcon name="it-comment" size="sm" color="primary" class="me-2" />
<h2 class="h6 mb-0">Comunicazioni con cittadino</h2>
</div>
<p class="text-muted small mb-0">Chat unica pratica. Mantieni aggiornamenti chiari e brevi.</p>
</div>
<span class="badge bg-light text-dark border">{{ messages.length }} messaggi</span>
</div>
<MessageList :messages="messages" />
<MessageComposer @send="handleSendMessage" />
</div>
</div>
</div>
<!-- Destra: Strumenti Operatore -->
<div class="col-12 col-lg-5">
<!-- Summary Box -->
<div class="card border-primary mb-4 shadow-sm" v-if="summary">
<div class="card-header bg-primary text-white">
<h5 class="mb-0">Riepilogo Intervento</h5>
</div>
<div class="col-12 col-xl-5">
<div class="report-sidebar">
<div class="card card-bg border-0 shadow-sm mb-4" v-if="summary">
<div class="card-body">
<p class="mb-2"><strong>Operatore Assegnato:</strong> {{ summary.operator_id ? `#${summary.operator_id}` : 'Nessuno' }}</p>
<p class="mb-2"><strong>Squadra Assegnata:</strong> <span class="badge bg-secondary">{{ summary.assigned_to_team || 'Nessuna' }}</span></p>
<p class="mb-2"><strong>Data Prevista:</strong> {{ summary.estimated_resolution_date ? formatDate(summary.estimated_resolution_date) : 'Non impostata' }}</p>
<p class="mb-0"><strong>Note Interne:</strong></p>
<p class="small text-muted bg-light p-2 rounded border mt-1 mb-0">{{ summary.internal_notes || 'Nessuna nota interna' }}</p>
<h2 class="h6 mb-3">Riepilogo intervento</h2>
<ul class="report-summary-list list-unstyled mb-4">
<li>
<span>Operatore</span>
<strong>{{ summary.operator_id ? `#${summary.operator_id}` : 'Nessuno' }}</strong>
</li>
<li>
<span>Squadra</span>
<strong>{{ summary.assigned_to_team || 'Nessuna' }}</strong>
</li>
<li>
<span>Data prevista</span>
<strong>{{ summary.estimated_resolution_date ? formatDate(summary.estimated_resolution_date) : 'Non impostata' }}</strong>
</li>
</ul>
<div class="report-description">
<h3 class="h6 mb-2">Note interne</h3>
<p class="small mb-0">{{ summary.internal_notes || 'Nessuna nota interna' }}</p>
</div>
</div>
<div class="card-footer bg-white border-top-0 pt-0">
<div class="card-body pt-0">
<button class="btn btn-outline-primary w-100" @click="isAssignModalOpen = true">
Gestisci Squadra e Note
</button>
</div>
</div>
<!-- Modifica Stato -->
<div class="card mb-4">
<div class="card card-bg border-0 shadow-sm mb-4">
<div class="card-body">
<h5 class="card-title font-weight-bold mb-3">Aggiorna Stato</h5>
<h2 class="h6 mb-3">Aggiorna stato</h2>
<form @submit.prevent="handleStatusUpdate">
<div class="mb-3">
<label class="form-label">Nuovo Stato</label>
@@ -220,10 +281,9 @@ const formatDate = (dateString: string) => {
</div>
</div>
<!-- Cronologia -->
<div class="card card-bg">
<div class="card card-bg border-0 shadow-sm">
<div class="card-body">
<h5 class="card-title font-weight-bold mb-3">Cronologia Eventi</h5>
<h2 class="h6 mb-3">Cronologia eventi</h2>
<div v-if="statusHistory.length === 0" class="small text-muted">Nessun evento registrato.</div>
<ul class="list-unstyled mb-0 ms-2">
<li v-for="item in statusHistory" :key="item.id" class="mb-3 border-start border-primary border-3 ps-3">
@@ -238,7 +298,7 @@ const formatDate = (dateString: string) => {
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
@@ -286,7 +346,116 @@ const formatDate = (dateString: string) => {
</template>
<style scoped>
.report-hero {
background: linear-gradient(180deg, #f0f6fc 0%, #ffffff 100%);
border-bottom: 1px solid #d9e3ee;
}
.report-hero-meta,
.report-section-head {
align-items: center;
display: flex;
}
.report-hero-meta {
flex-wrap: wrap;
gap: 0.75rem;
}
.report-id {
color: #5c6f82;
font-size: 0.875rem;
font-weight: 700;
}
.report-hero-address {
color: #455b71;
font-size: 1.05rem;
}
.report-hero-stats {
background: #fff;
border: 1px solid #d9e3ee;
border-radius: 8px;
display: grid;
gap: 1rem;
grid-template-columns: repeat(2, minmax(0, 1fr));
padding: 1rem;
}
.hero-stat {
display: grid;
gap: 0.25rem;
}
.hero-stat-label,
.report-detail-grid dt {
color: #5c6f82;
font-size: 0.8125rem;
font-weight: 700;
margin: 0 0 0.25rem;
text-transform: uppercase;
}
.report-detail-grid {
display: grid;
gap: 1rem;
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.report-detail-grid dd {
color: #17324d;
font-weight: 600;
margin: 0;
}
.report-description {
background: #f7fafc;
border: 1px solid #e4ebf1;
border-radius: 8px;
padding: 1rem;
}
.report-chat-header {
align-items: flex-start;
display: flex;
gap: 1rem;
justify-content: space-between;
}
.report-summary-list {
display: grid;
gap: 0.875rem;
}
.report-summary-list li {
align-items: baseline;
display: flex;
gap: 1rem;
justify-content: space-between;
}
.report-sidebar {
position: sticky;
top: 1.5rem;
}
.report-media-frame {
border: 1px solid #d9e3ee;
}
.object-fit-cover {
object-fit: cover;
}
</style>
@media (max-width: 991.98px) {
.report-detail-grid,
.report-hero-stats {
grid-template-columns: 1fr;
}
.report-sidebar {
position: static;
}
}
</style>