feat(auth): integrate codice-fiscale-js for fiscal code validation in registration
This commit is contained in:
+272
-140
@@ -96,6 +96,15 @@ const handleFlag = async () => {
|
||||
|
||||
const isOwner = computed(() => report.value?.user_id === user.value?.id)
|
||||
const canFeedback = computed(() => report.value?.status === 'RESOLVED' || report.value?.status === 'CLOSED')
|
||||
const statusMeta: Record<string, { label: string; tone: string }> = {
|
||||
OPEN: { label: 'Aperta', tone: 'info' },
|
||||
UNDER_REVIEW: { label: 'In revisione', tone: 'warning' },
|
||||
ASSIGNED: { label: 'Assegnata', tone: 'info' },
|
||||
IN_PROGRESS: { label: 'In lavorazione', tone: 'primary' },
|
||||
RESOLVED: { label: 'Risolta', tone: 'success' },
|
||||
REJECTED: { label: 'Respinta', tone: 'danger' },
|
||||
CLOSED: { label: 'Chiusa', tone: 'secondary' },
|
||||
}
|
||||
|
||||
const formatDate = (dateString: string): string => {
|
||||
return new Date(dateString).toLocaleDateString('it-IT', {
|
||||
@@ -110,74 +119,80 @@ const formatDate = (dateString: string): string => {
|
||||
|
||||
<template>
|
||||
<div v-if="report">
|
||||
<!-- HEADER -->
|
||||
<div class="bg-light border-bottom py-4">
|
||||
<div class="report-hero py-4 py-lg-5">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-8">
|
||||
<h1 class="h3 font-weight-bold mb-2">{{ report.category_name }}</h1>
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<div class="row align-items-end g-3">
|
||||
<div class="col-12 col-lg-8">
|
||||
<div class="report-hero-meta mb-2">
|
||||
<span class="report-id">Segnalazione #{{ report.id }}</span>
|
||||
<StatusBadge :status="report.status" />
|
||||
<small class="text-muted">
|
||||
<BIIcon name="it-clock" size="xs" class="me-1" />
|
||||
Creata il {{ formatDate(report.created_at) }}
|
||||
</small>
|
||||
</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]?.label || report.status }}</strong>
|
||||
</div>
|
||||
<div class="hero-stat">
|
||||
<span class="hero-stat-label">Creata</span>
|
||||
<strong>{{ formatDate(report.created_at) }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CONTENUTO PRINCIPALE -->
|
||||
<div class="container my-5">
|
||||
<div class="row">
|
||||
|
||||
<!-- SINISTRA: Dettagli segnalazione -->
|
||||
<div class="col-12 col-lg-8 mb-4">
|
||||
|
||||
<!-- INDIRIZZO E COORDINATE -->
|
||||
<div class="card card-bg mb-4">
|
||||
<div class="row g-4">
|
||||
<div class="col-12 col-xl-8">
|
||||
<div class="card card-bg border-0 shadow-sm mb-4">
|
||||
<div class="card-body">
|
||||
<h6 class="card-title font-weight-bold mb-3">
|
||||
<div class="report-section-head mb-3">
|
||||
<BIIcon name="it-map-marker-circle" size="sm" color="primary" class="me-2" />
|
||||
Posizione
|
||||
</h6>
|
||||
<div class="mb-2">
|
||||
<strong>{{ report.address }}</strong>
|
||||
<h2 class="h6 mb-0">Posizione e dettagli</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 v-if="report.area_name">
|
||||
<dt>Area</dt>
|
||||
<dd>{{ report.area_name }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Ultimo aggiornamento</dt>
|
||||
<dd>{{ formatDate(report.updated_at || report.created_at) }}</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>
|
||||
<small class="text-muted d-block">
|
||||
Coordinate: {{ report.latitude }}, {{ report.longitude }}
|
||||
</small>
|
||||
<small v-if="report.area_name" class="text-muted d-block">
|
||||
Area: {{ report.area_name }}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- DESCRIZIONE -->
|
||||
<div class="card card-bg mb-4">
|
||||
<div v-if="media.length > 0" class="card card-bg border-0 shadow-sm mb-4">
|
||||
<div class="card-body">
|
||||
<h6 class="card-title font-weight-bold mb-3">
|
||||
<BIIcon name="it-write" size="sm" color="primary" class="me-2" />
|
||||
Descrizione
|
||||
</h6>
|
||||
<p class="card-text text-break">{{ report.description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- GALLERY MEDIA -->
|
||||
<div v-if="media.length > 0" class="card card-bg mb-4">
|
||||
<div class="card-body">
|
||||
<h6 class="card-title font-weight-bold mb-3">
|
||||
<div class="report-section-head mb-3">
|
||||
<BIIcon name="it-camera" size="sm" color="primary" class="me-2" />
|
||||
Allegati ({{ media.length }})
|
||||
</h6>
|
||||
<div class="row">
|
||||
<h2 class="h6 mb-0">Allegati ({{ media.length }})</h2>
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
<div v-for="(m, idx) in media" :key="m.id" class="col-6 col-md-4 mb-3">
|
||||
<div v-if="m.media_kind === 'IMAGE'" class="ratio ratio-1x1 bg-light rounded overflow-hidden">
|
||||
<div v-if="m.media_kind === 'IMAGE'" class="ratio ratio-1x1 bg-light rounded overflow-hidden report-media-frame">
|
||||
<img :src="runtimeConfig.public.api + m.url" :alt="`Allegato ${Number(idx) + 1}`" class="object-fit-cover" />
|
||||
</div>
|
||||
<div v-else class="ratio ratio-1x1 bg-dark rounded d-flex align-items-center justify-content-center">
|
||||
<div v-else class="ratio ratio-1x1 bg-dark rounded d-flex align-items-center justify-content-center report-media-frame">
|
||||
<BIIcon name="it-video" size="xl" color="white" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -185,13 +200,12 @@ const formatDate = (dateString: string): string => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CRONOLOGIA STATI -->
|
||||
<div v-if="statusHistory.length > 0" class="card card-bg mb-4">
|
||||
<div v-if="statusHistory.length > 0" class="card card-bg border-0 shadow-sm mb-4">
|
||||
<div class="card-body">
|
||||
<h6 class="card-title font-weight-bold mb-4">
|
||||
<div class="report-section-head mb-4">
|
||||
<BIIcon name="it-history" size="sm" color="primary" class="me-2" />
|
||||
Cronologia
|
||||
</h6>
|
||||
<h2 class="h6 mb-0">Cronologia</h2>
|
||||
</div>
|
||||
<div class="timeline">
|
||||
<div v-for="item in statusHistory" :key="item.id" class="timeline-item mb-3">
|
||||
<div class="d-flex gap-3">
|
||||
@@ -213,108 +227,122 @@ const formatDate = (dateString: string): string => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- MESSAGGI -->
|
||||
<div class="card card-bg">
|
||||
<div class="card card-bg border-0 shadow-sm">
|
||||
<div class="card-body">
|
||||
<h6 class="card-title font-weight-bold mb-3">
|
||||
<BIIcon name="it-comment" size="sm" color="primary" class="me-2" />
|
||||
Chat ({{ messages.length }})
|
||||
</h6>
|
||||
<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">Chat segnalazione</h2>
|
||||
</div>
|
||||
<p class="text-muted small mb-0">Usa chat per chiarimenti, aggiornamenti operativi e richieste rapide.</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: Sidebar azioni -->
|
||||
<div class="col-12 col-lg-4">
|
||||
<div class="col-12 col-xl-4">
|
||||
<div class="report-sidebar">
|
||||
<div class="card card-bg border-0 shadow-sm mb-4">
|
||||
<div class="card-body">
|
||||
<h2 class="h6 mb-3">Riepilogo pratica</h2>
|
||||
<ul class="report-summary-list list-unstyled mb-0">
|
||||
<li>
|
||||
<span>Cittadino proprietario</span>
|
||||
<strong>{{ isOwner ? 'Sì' : 'No' }}</strong>
|
||||
</li>
|
||||
<li>
|
||||
<span>Stato corrente</span>
|
||||
<strong>{{ statusMeta[report.status]?.label || report.status }}</strong>
|
||||
</li>
|
||||
<li>
|
||||
<span>Canale</span>
|
||||
<strong>Portale Visione</strong>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- FEEDBACK (se risolta) -->
|
||||
<div v-if="canFeedback && isOwner" class="card card-bg card-highlight mb-4">
|
||||
<div class="card-body">
|
||||
<h6 class="card-title font-weight-bold mb-3">
|
||||
<BIIcon name="it-star-outline" size="sm" color="primary" class="me-2" />
|
||||
Lascia un feedback
|
||||
</h6>
|
||||
<button
|
||||
v-if="!isFeedbackOpen"
|
||||
type="button"
|
||||
class="btn btn-primary w-100"
|
||||
@click="isFeedbackOpen = true"
|
||||
>
|
||||
Valuta l'intervento
|
||||
</button>
|
||||
<div v-if="canFeedback && isOwner" class="card card-bg border-0 shadow-sm mb-4">
|
||||
<div class="card-body">
|
||||
<h2 class="h6 mb-3">Valuta intervento</h2>
|
||||
<p class="text-muted small mb-3">Lascia valutazione finale quando intervento è concluso.</p>
|
||||
<button
|
||||
v-if="!isFeedbackOpen"
|
||||
type="button"
|
||||
class="btn btn-primary w-100"
|
||||
@click="isFeedbackOpen = true"
|
||||
>
|
||||
Valuta intervento
|
||||
</button>
|
||||
|
||||
<template v-else>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Valutazione</label>
|
||||
<div class="d-flex gap-2">
|
||||
<button v-for="n in 5" :key="n" type="button" class="btn btn-link p-0"
|
||||
@click="feedbackRating = n" :aria-label="`${n} stelle`">
|
||||
<BIIcon
|
||||
:name="n <= feedbackRating ? 'it-star' : 'it-star-outline'"
|
||||
size="lg"
|
||||
:color="n <= feedbackRating ? 'warning' : 'primary'"
|
||||
/>
|
||||
<template v-else>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Valutazione</label>
|
||||
<div class="d-flex gap-2">
|
||||
<button v-for="n in 5" :key="n" type="button" class="btn btn-link p-0"
|
||||
@click="feedbackRating = n" :aria-label="`${n} stelle`">
|
||||
<BIIcon
|
||||
:name="n <= feedbackRating ? 'it-star-full' : 'it-star-outline'"
|
||||
size="lg"
|
||||
:color="n <= feedbackRating ? 'warning' : 'primary'"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<textarea
|
||||
v-model="feedbackComment"
|
||||
class="form-control"
|
||||
placeholder="Commento opzionale"
|
||||
rows="3"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="d-grid gap-2">
|
||||
<button type="button" class="btn btn-primary" @click="handleSubmitFeedback" :disabled="isSubmittingFeedback">
|
||||
<span v-if="isSubmittingFeedback" class="spinner-border spinner-border-sm me-2"></span>
|
||||
Invia
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" @click="isFeedbackOpen = false">
|
||||
Annulla
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<textarea
|
||||
v-model="feedbackComment"
|
||||
class="form-control"
|
||||
placeholder="Commento (opzionale)"
|
||||
rows="3"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="d-grid gap-2">
|
||||
<button type="button" class="btn btn-primary" @click="handleSubmitFeedback" :disabled="isSubmittingFeedback">
|
||||
<span v-if="isSubmittingFeedback" class="spinner-border spinner-border-sm me-2"></span>
|
||||
Invia
|
||||
</button>
|
||||
<button type="button" class="btn btn-light" @click="isFeedbackOpen = false">
|
||||
Cancella
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<div class="card card-bg border-0 shadow-sm">
|
||||
<div class="card-body">
|
||||
<h2 class="h6 mb-3">Segnala abuso</h2>
|
||||
<p class="text-muted small mb-3">Usa questo canale solo per contenuti impropri o uso scorretto del servizio.</p>
|
||||
<textarea
|
||||
v-model="flagMessage"
|
||||
class="form-control mb-3"
|
||||
placeholder="Specifica motivo"
|
||||
rows="3"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline-danger w-100"
|
||||
@click="handleFlag"
|
||||
:disabled="isFlagging || !flagMessage.trim()"
|
||||
>
|
||||
<BIIcon name="it-flag" size="xs" class="me-1" />
|
||||
Invia segnalazione
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SEGNALA ABUSO -->
|
||||
<div class="card card-bg">
|
||||
<div class="card-body">
|
||||
<h6 class="card-title font-weight-bold mb-3">
|
||||
<BIIcon name="it-flag" size="sm" color="danger" class="me-2" />
|
||||
Segnala abuso
|
||||
</h6>
|
||||
<textarea
|
||||
v-model="flagMessage"
|
||||
class="form-control form-control-sm mb-2"
|
||||
placeholder="Specifica il motivo dell'abuso..."
|
||||
rows="2"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-outline-danger w-100"
|
||||
@click="handleFlag"
|
||||
:disabled="isFlagging || !flagMessage.trim()"
|
||||
>
|
||||
<BIIcon name="it-flag" size="xs" class="me-1" />
|
||||
Invia segnalazione
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- LOADING STATE -->
|
||||
<div v-else class="container my-5 text-center">
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="visually-hidden">Caricamento...</span>
|
||||
@@ -323,8 +351,102 @@ const formatDate = (dateString: string): string => {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.card-highlight {
|
||||
border-color: #006f9d;
|
||||
.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-media-frame {
|
||||
border: 1px solid #d9e3ee;
|
||||
}
|
||||
|
||||
.report-chat-header {
|
||||
align-items: flex-start;
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.report-sidebar {
|
||||
position: sticky;
|
||||
top: 1.5rem;
|
||||
}
|
||||
|
||||
.report-summary-list {
|
||||
display: grid;
|
||||
gap: 0.875rem;
|
||||
}
|
||||
|
||||
.report-summary-list li {
|
||||
align-items: baseline;
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.timeline-marker {
|
||||
@@ -364,9 +486,19 @@ const formatDate = (dateString: string): string => {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@media (max-width: 991.98px) {
|
||||
.report-detail-grid,
|
||||
.report-hero-stats {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.report-sidebar {
|
||||
position: static;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user