feat: update authentication flow and user navigation
This commit is contained in:
@@ -152,6 +152,10 @@ html body .navbar .navbar-collapsable .navbar-nav li.nav-item a.nav-link[data-fo
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.it-hero-wrapper.hero-compact .container {
|
||||||
|
min-height: 132px;
|
||||||
|
}
|
||||||
|
|
||||||
.it-hero-wrapper .container > .row {
|
.it-hero-wrapper .container > .row {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -229,6 +233,69 @@ html body .navbar .navbar-collapsable .navbar-nav li.nav-item a.nav-link[data-fo
|
|||||||
fill: #fff;
|
fill: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-xs {
|
||||||
|
--bs-btn-padding-y: 0.25rem;
|
||||||
|
--bs-btn-padding-x: 0.55rem;
|
||||||
|
--bs-btn-font-size: 0.875rem;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-icon.btn-xs {
|
||||||
|
align-items: center;
|
||||||
|
display: inline-flex;
|
||||||
|
height: 2rem;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0;
|
||||||
|
width: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-table-wrapper {
|
||||||
|
border: 1px solid #dfe4ea;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-table thead th {
|
||||||
|
background: #f7f9fa;
|
||||||
|
color: var(--visione-text-soft);
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
font-weight: 700;
|
||||||
|
padding: 0.65rem 0.75rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-table tbody td {
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
padding: 0.75rem;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-table-id {
|
||||||
|
color: var(--visione-primary-dark);
|
||||||
|
font-weight: 700;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-address {
|
||||||
|
color: var(--visione-text-soft);
|
||||||
|
display: inline-block;
|
||||||
|
max-width: min(36vw, 26rem);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
vertical-align: bottom;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-stat {
|
||||||
|
border-left: 4px solid var(--visione-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-stat .stat-value {
|
||||||
|
color: var(--visione-primary-dark);
|
||||||
|
font-size: 1.75rem;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.it-header-wrapper .header-slim-right-zone .btn .rounded-icon,
|
.it-header-wrapper .header-slim-right-zone .btn .rounded-icon,
|
||||||
.it-header-wrapper .header-slim-right-zone .btn .rounded-icon .icon {
|
.it-header-wrapper .header-slim-right-zone .btn .rounded-icon .icon {
|
||||||
|
|
||||||
@@ -257,4 +324,8 @@ html body .navbar .navbar-collapsable .navbar-nav li.nav-item a.nav-link[data-fo
|
|||||||
padding-left: 1rem !important;
|
padding-left: 1rem !important;
|
||||||
padding-right: 1rem !important;
|
padding-right: 1rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.report-address {
|
||||||
|
max-width: 14rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ watch(() => route.path, () => {
|
|||||||
|
|
||||||
const navLinks = [
|
const navLinks = [
|
||||||
{ name: 'Home', path: '/' },
|
{ name: 'Home', path: '/' },
|
||||||
{ name: 'Fai una segnalazione', path: '/reports/create' },
|
{ name: 'Fai una segnalazione', path: '/reports/new' },
|
||||||
{ name: 'Esplora i dati', path: '/open-data' },
|
{ name: 'Esplora i dati', path: '/open-data' },
|
||||||
{ name: 'Come funziona', path: '/info' }
|
{ name: 'Come funziona', path: '/info' }
|
||||||
]
|
]
|
||||||
@@ -43,9 +43,16 @@ const { isAuthenticated, user, logout } = useAuth()
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="header-slim-right-zone" role="navigation">
|
<div class="header-slim-right-zone" role="navigation">
|
||||||
<!-- Utente Autenticato: Bottone Utente + Bottone Esci -->
|
|
||||||
<div v-if="isAuthenticated" class="d-flex align-items-center gap-2">
|
<div v-if="isAuthenticated" class="d-flex align-items-center gap-2">
|
||||||
<NuxtLink class="btn btn-primary btn-icon btn-full" to="/me"
|
<NuxtLink class="btn btn-primary btn-icon btn-full" to="/dashboard"
|
||||||
|
data-element="personal-area-dashboard" data-focus-mouse="false">
|
||||||
|
<span class="rounded-icon" aria-hidden="true">
|
||||||
|
<BIIcon name="it-list" color="black" />
|
||||||
|
</span>
|
||||||
|
<span class="d-none d-lg-block">Dashboard</span>
|
||||||
|
</NuxtLink>
|
||||||
|
|
||||||
|
<NuxtLink class="btn btn-primary btn-icon btn-full" to="/profile"
|
||||||
data-element="personal-area-dashboard" data-focus-mouse="false">
|
data-element="personal-area-dashboard" data-focus-mouse="false">
|
||||||
<span class="rounded-icon" aria-hidden="true">
|
<span class="rounded-icon" aria-hidden="true">
|
||||||
<BIIcon name="it-user" color="black" />
|
<BIIcon name="it-user" color="black" />
|
||||||
@@ -62,7 +69,6 @@ const { isAuthenticated, user, logout } = useAuth()
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Utente NON Autenticato: Bottone Login -->
|
|
||||||
<NuxtLink v-else class="btn btn-primary btn-icon btn-full" to="/auth/login"
|
<NuxtLink v-else class="btn btn-primary btn-icon btn-full" to="/auth/login"
|
||||||
data-element="personal-area-login" data-focus-mouse="false">
|
data-element="personal-area-login" data-focus-mouse="false">
|
||||||
<span class="rounded-icon" aria-hidden="true">
|
<span class="rounded-icon" aria-hidden="true">
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { Report } from '~/types/api'
|
||||||
|
|
||||||
|
type ActionVariant = 'open' | 'manage'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
reports: Report[]
|
||||||
|
detailBasePath?: string
|
||||||
|
actionVariant?: ActionVariant
|
||||||
|
actionLabel?: string
|
||||||
|
showAssignee?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
detailBasePath: '/reports',
|
||||||
|
actionVariant: 'open',
|
||||||
|
actionLabel: 'Open report',
|
||||||
|
showAssignee: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'take', id: number): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const formatDate = (dateString?: string): string => {
|
||||||
|
if (!dateString) return '-'
|
||||||
|
return new Date(dateString).toLocaleDateString('it-IT', {
|
||||||
|
day: '2-digit',
|
||||||
|
month: '2-digit',
|
||||||
|
year: '2-digit',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const reportHref = (report: Report) => `${props.detailBasePath}/${report.id}`
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="table-responsive report-table-wrapper">
|
||||||
|
<table class="table table-hover report-table mb-0">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col">ID</th>
|
||||||
|
<th scope="col">Categoria</th>
|
||||||
|
<th scope="col">Indirizzo</th>
|
||||||
|
<th scope="col">Stato</th>
|
||||||
|
<th v-if="showAssignee" scope="col">Operatore</th>
|
||||||
|
<th scope="col">Creazione</th>
|
||||||
|
<th scope="col" class="text-end">Azioni</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="report in reports" :key="report.id" class="align-middle">
|
||||||
|
<td class="report-table-id">#{{ report.id }}</td>
|
||||||
|
<td class="text-nowrap">{{ report.category_name || '-' }}</td>
|
||||||
|
<td>
|
||||||
|
<span class="report-address" :title="report.address">
|
||||||
|
{{ report.address || '-' }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<StatusBadge :status="report.status" />
|
||||||
|
</td>
|
||||||
|
<td v-if="showAssignee" class="text-muted">
|
||||||
|
{{ report.assigned_operator_id ? `#${report.assigned_operator_id}` : '-' }}
|
||||||
|
</td>
|
||||||
|
<td class="text-muted text-nowrap">{{ formatDate(report.created_at) }}</td>
|
||||||
|
<td class="text-end">
|
||||||
|
<button
|
||||||
|
v-if="actionVariant === 'manage'"
|
||||||
|
type="button"
|
||||||
|
class="btn btn-primary btn-xs"
|
||||||
|
@click="emit('take', report.id)"
|
||||||
|
>
|
||||||
|
Prendi in carico
|
||||||
|
</button>
|
||||||
|
<NuxtLink
|
||||||
|
v-else
|
||||||
|
:to="reportHref(report)"
|
||||||
|
class="btn btn-outline-primary btn-xs btn-icon"
|
||||||
|
:aria-label="actionLabel"
|
||||||
|
:title="actionLabel"
|
||||||
|
>
|
||||||
|
<BIIcon name="it-arrow-right" size="xs" />
|
||||||
|
</NuxtLink>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -4,232 +4,90 @@ import type { Report } from '~/types/api'
|
|||||||
const { user } = useAuth()
|
const { user } = useAuth()
|
||||||
const { getReports } = useReports()
|
const { getReports } = useReports()
|
||||||
|
|
||||||
const page = ref(1)
|
const { data: reportsRes, pending } = useAsyncData('citizen-dashboard-reports', () => getReports({}))
|
||||||
const perPage = ref(10)
|
|
||||||
const filterStatus = ref<string>('') // Stringa vuota = "Tutte"
|
|
||||||
|
|
||||||
// Carichiamo TUTTE le segnalazioni per avere statistiche globali e filtri veloci
|
const reports = computed<Report[]>(() => reportsRes.value?.reports || [])
|
||||||
const { data: reportsRes, pending } = useAsyncData('citizen-reports-all', () =>
|
const recentReports = computed(() => reports.value.slice(0, 4))
|
||||||
getReports({}) // Non passiamo filtri alla chiamata API
|
|
||||||
)
|
|
||||||
|
|
||||||
// Dati grezzi dal server
|
const stats = computed(() => {
|
||||||
const rawReports = computed<Report[]>(() => reportsRes.value?.reports || [])
|
return reports.value.reduce(
|
||||||
|
(acc, report) => {
|
||||||
// 1. STATISTICHE GLOBALI (Sempre basate su tutti i dati, ignorano i filtri)
|
acc.total++
|
||||||
const statsGlobal = computed(() => {
|
if (report.status === 'OPEN') acc.open++
|
||||||
const stats = { total: 0, OPEN: 0, IN_PROGRESS: 0, RESOLVED: 0 }
|
if (report.status === 'IN_PROGRESS' || report.status === 'ASSIGNED') acc.inProgress++
|
||||||
rawReports.value.forEach((report: Report) => {
|
if (report.status === 'RESOLVED' || report.status === 'CLOSED') acc.resolved++
|
||||||
stats.total++
|
return acc
|
||||||
if (report.status === 'OPEN') stats.OPEN++
|
},
|
||||||
if (report.status === 'IN_PROGRESS' || report.status === 'ASSIGNED') stats.IN_PROGRESS++
|
{ total: 0, open: 0, inProgress: 0, resolved: 0 }
|
||||||
if (report.status === 'RESOLVED') stats.RESOLVED++
|
)
|
||||||
})
|
|
||||||
return stats
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// 2. FILTRO LATO CLIENT (Rende il cambio filtro istantaneo)
|
|
||||||
const filteredReports = computed(() => {
|
|
||||||
if (!filterStatus.value) return rawReports.value
|
|
||||||
|
|
||||||
return rawReports.value.filter((report: Report) => {
|
|
||||||
// Raggruppiamo IN_PROGRESS e ASSIGNED sotto la stessa voce per l'utente
|
|
||||||
if (filterStatus.value === 'IN_PROGRESS') {
|
|
||||||
return report.status === 'IN_PROGRESS' || report.status === 'ASSIGNED'
|
|
||||||
}
|
|
||||||
return report.status === filterStatus.value
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// 3. PAGINAZIONE LATO CLIENT
|
|
||||||
const paginatedReports = computed(() => {
|
|
||||||
const start = (page.value - 1) * perPage.value
|
|
||||||
const end = start + perPage.value
|
|
||||||
return filteredReports.value.slice(start, end)
|
|
||||||
})
|
|
||||||
|
|
||||||
const totalFiltered = computed(() => filteredReports.value.length)
|
|
||||||
|
|
||||||
// GESTIONE FILTRI (Evita reload se clicchi il filtro attivo)
|
|
||||||
const handleStatusFilter = (status: string) => {
|
|
||||||
const targetStatus = status === 'all' ? '' : status
|
|
||||||
|
|
||||||
// Se clicco sullo stesso filtro, non fare nulla
|
|
||||||
if (filterStatus.value === targetStatus) return
|
|
||||||
|
|
||||||
filterStatus.value = targetStatus
|
|
||||||
page.value = 1 // Reset alla prima pagina
|
|
||||||
}
|
|
||||||
|
|
||||||
const handlePageChange = (newPage: number) => {
|
|
||||||
page.value = newPage
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helpers grafici
|
|
||||||
const formatDate = (dateString?: string): string => {
|
|
||||||
if (!dateString) return ''
|
|
||||||
return new Date(dateString).toLocaleDateString('it-IT', {
|
|
||||||
day: '2-digit', month: '2-digit', year: '2-digit',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<!-- Callout Gamification -->
|
<div class="row g-3 mb-4">
|
||||||
<div class="callout callout-highlight callout-success mb-4">
|
<div class="col-6 col-lg-3">
|
||||||
<div class="callout-title d-flex align-items-center">
|
<div class="card card-bg dashboard-stat h-100">
|
||||||
<BIIcon name="it-star-full" size="sm" color="success" class="me-2" />
|
<div class="card-body py-3">
|
||||||
I tuoi Punti Cittadino
|
<div class="stat-value">{{ stats.total }}</div>
|
||||||
</div>
|
<div class="small text-muted">Segnalazioni</div>
|
||||||
<p>
|
|
||||||
Hai accumulato <strong>{{ user?.points || 0 }} punti</strong>! Continua ad aiutare la comunità segnalando i problemi urbani.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- STATISTICHE RAPIDE (Basate su statsGlobal, non cambiano filtrando) -->
|
|
||||||
<div class="row mb-5">
|
|
||||||
<div class="col-6 col-md-3 mb-3">
|
|
||||||
<div class="card card-bg text-center">
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="display-6 text-primary font-weight-bold">{{ statsGlobal.total }}</div>
|
|
||||||
<small class="text-muted">Totali</small>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-6 col-md-3 mb-3">
|
<div class="col-6 col-lg-3">
|
||||||
<div class="card card-bg text-center">
|
<div class="card card-bg dashboard-stat h-100">
|
||||||
<div class="card-body">
|
<div class="card-body py-3">
|
||||||
<div class="display-6 text-warning font-weight-bold">{{ statsGlobal.OPEN }}</div>
|
<div class="stat-value">{{ stats.open }}</div>
|
||||||
<small class="text-muted">Aperte</small>
|
<div class="small text-muted">Aperte</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-6 col-md-3 mb-3">
|
<div class="col-6 col-lg-3">
|
||||||
<div class="card card-bg text-center">
|
<div class="card card-bg dashboard-stat h-100">
|
||||||
<div class="card-body">
|
<div class="card-body py-3">
|
||||||
<div class="display-6 text-info font-weight-bold">{{ statsGlobal.IN_PROGRESS }}</div>
|
<div class="stat-value">{{ stats.inProgress }}</div>
|
||||||
<small class="text-muted">In corso</small>
|
<div class="small text-muted">In corso</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-6 col-md-3 mb-3">
|
<div class="col-6 col-lg-3">
|
||||||
<div class="card card-bg text-center">
|
<div class="card card-bg dashboard-stat h-100">
|
||||||
<div class="card-body">
|
<div class="card-body py-3">
|
||||||
<div class="display-6 text-success font-weight-bold">{{ statsGlobal.RESOLVED }}</div>
|
<div class="stat-value">{{ user?.points || 0 }}</div>
|
||||||
<small class="text-muted">Risolte</small>
|
<div class="small text-muted">Punti cittadino</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- AZIONI RAPIDE E FILTRI -->
|
<div class="d-flex flex-wrap gap-2 mb-4">
|
||||||
<div class="row mb-4">
|
<NuxtLink to="/reports/new" class="btn btn-primary">
|
||||||
<div class="col-12">
|
<BIIcon name="it-plus-circle" size="sm" color="white" class="me-2" />
|
||||||
<div class="d-flex gap-2 flex-wrap align-items-center">
|
Nuova segnalazione
|
||||||
<NuxtLink to="/reports/create" class="btn btn-primary me-md-3">
|
</NuxtLink>
|
||||||
<BIIcon name="it-plus-circle" size="sm" color="white" class="me-2" />
|
<NuxtLink to="/reports" class="btn btn-outline-primary">
|
||||||
Nuova segnalazione
|
<BIIcon name="it-list" size="sm" class="me-2" />
|
||||||
</NuxtLink>
|
Le mie segnalazioni
|
||||||
|
</NuxtLink>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="btn-group" role="group" aria-label="Filtri segnalazioni">
|
<div class="card card-bg">
|
||||||
<button
|
<div class="card-body">
|
||||||
type="button"
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
class="btn btn-sm"
|
<h2 class="h5 card-title mb-0">Aggiornamenti recenti</h2>
|
||||||
:class="filterStatus === '' ? 'btn-primary' : 'btn-outline-primary'"
|
<NuxtLink to="/reports" class="btn btn-link btn-sm pe-0">Vedi tutte</NuxtLink>
|
||||||
@click="handleStatusFilter('all')"
|
</div>
|
||||||
>
|
|
||||||
Tutte
|
<div v-if="pending" class="text-center py-4">
|
||||||
</button>
|
<div class="spinner-border text-primary" role="status">
|
||||||
<button
|
<span class="visually-hidden">Caricamento...</span>
|
||||||
type="button"
|
|
||||||
class="btn btn-sm"
|
|
||||||
:class="filterStatus === 'OPEN' ? 'btn-primary' : 'btn-outline-primary'"
|
|
||||||
@click="handleStatusFilter('OPEN')"
|
|
||||||
>
|
|
||||||
Aperte
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn btn-sm"
|
|
||||||
:class="filterStatus === 'IN_PROGRESS' ? 'btn-primary' : 'btn-outline-primary'"
|
|
||||||
@click="handleStatusFilter('IN_PROGRESS')"
|
|
||||||
>
|
|
||||||
In corso
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn btn-sm"
|
|
||||||
:class="filterStatus === 'RESOLVED' ? 'btn-primary' : 'btn-outline-primary'"
|
|
||||||
@click="handleStatusFilter('RESOLVED')"
|
|
||||||
>
|
|
||||||
Risolte
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div v-else-if="recentReports.length === 0" class="alert alert-info mb-0" role="alert">
|
||||||
</div>
|
Non hai ancora creato segnalazioni.
|
||||||
|
|
||||||
<div v-if="pending" class="progress-spinner progress-spinner-active my-5 mx-auto">
|
|
||||||
<span class="visually-hidden">Caricamento...</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- TABELLA SEGNALAZIONI -->
|
|
||||||
<div class="row" v-else>
|
|
||||||
<div class="col-12">
|
|
||||||
<div v-if="filteredReports.length === 0" class="alert alert-info text-center" role="alert">
|
|
||||||
<h6>Nessuna segnalazione trovata</h6>
|
|
||||||
<p class="small text-muted mb-0">Non ci sono segnalazioni con lo stato "{{ filterStatus || 'Tutte' }}".</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
<ReportTable v-else :reports="recentReports" action-label="Apri segnalazione" />
|
||||||
<div v-else class="table-responsive">
|
|
||||||
<table class="table table-hover border-bottom">
|
|
||||||
<thead class="table-light">
|
|
||||||
<tr>
|
|
||||||
<th scope="col" class="small">#ID</th>
|
|
||||||
<th scope="col" class="small">Categoria</th>
|
|
||||||
<th scope="col" class="small">Indirizzo</th>
|
|
||||||
<th scope="col" class="small">Stato</th>
|
|
||||||
<th scope="col" class="small">Creazione</th>
|
|
||||||
<th scope="col" class="text-center small">Azioni</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr v-for="report in paginatedReports" :key="report.id" class="align-middle">
|
|
||||||
<td class="small font-weight-semibold">#{{ report.id }}</td>
|
|
||||||
<td class="small">{{ report.category_name }}</td>
|
|
||||||
<td class="small">
|
|
||||||
<span class="text-muted text-truncate" style="max-width: 200px; display: inline-block;">
|
|
||||||
{{ report.address }}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td class="small">
|
|
||||||
<StatusBadge :status="report.status" />
|
|
||||||
</td>
|
|
||||||
<td class="small text-muted">{{ formatDate(report.created_at) }}</td>
|
|
||||||
<td class="text-center">
|
|
||||||
<NuxtLink :to="`/reports/${report.id}`" class="btn btn-sm btn-outline-primary p-1">
|
|
||||||
<BIIcon name="it-arrow-right" size="xs" />
|
|
||||||
</NuxtLink>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Pagination
|
|
||||||
v-if="totalFiltered > perPage"
|
|
||||||
:current-page="page"
|
|
||||||
:per-page="perPage"
|
|
||||||
:total-items="totalFiltered"
|
|
||||||
@page-change="handlePageChange"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.table-hover tbody tr:hover { background-color: rgba(0, 111, 157, 0.03); }
|
|
||||||
.btn-group .btn { min-width: 80px; }
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -14,47 +14,45 @@ const handleTakeReport = async (id: number) => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<!-- Colonna Sinistra: Segnalazioni da prendere in carico -->
|
|
||||||
<div class="col-12 col-lg-6 mb-4">
|
<div class="col-12 col-lg-6 mb-4">
|
||||||
<h3 class="mb-3 border-bottom pb-2">Da prendere in carico (OPEN)</h3>
|
<div class="card card-bg h-100">
|
||||||
<div class="list-group">
|
<div class="card-body">
|
||||||
<div v-if="openReportsRes?.ok && openReportsRes.reports.length === 0" class="text-muted">
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
Nessuna nuova segnalazione in ingresso.
|
<h3 class="h5 mb-0">Da prendere in carico</h3>
|
||||||
</div>
|
<NuxtLink to="/operator" class="btn btn-link btn-sm pe-0">Apri coda</NuxtLink>
|
||||||
<div
|
|
||||||
v-for="report in openReportsRes?.reports" :key="report.id"
|
|
||||||
class="list-group-item d-flex justify-content-between align-items-center"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<h6 class="mb-1">{{ report.address }}</h6>
|
|
||||||
<small class="text-muted">{{ report.created_at }}</small>
|
|
||||||
</div>
|
</div>
|
||||||
<button @click="handleTakeReport(report.id)" class="btn btn-outline-primary btn-sm">
|
<div v-if="openReportsRes?.ok && openReportsRes.reports.length === 0" class="alert alert-info mb-0">
|
||||||
Prendi in carico
|
Nessuna nuova segnalazione in ingresso.
|
||||||
</button>
|
</div>
|
||||||
|
<ReportTable
|
||||||
|
v-else
|
||||||
|
:reports="openReportsRes?.reports || []"
|
||||||
|
action-variant="manage"
|
||||||
|
detail-base-path="/operator/reports"
|
||||||
|
@take="handleTakeReport"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Colonna Destra: Segnalazioni in gestione dell'operatore -->
|
|
||||||
<div class="col-12 col-lg-6 mb-4">
|
<div class="col-12 col-lg-6 mb-4">
|
||||||
<h3 class="mb-3 border-bottom pb-2">Le mie assegnazioni</h3>
|
<div class="card card-bg h-100">
|
||||||
<div class="list-group">
|
<div class="card-body">
|
||||||
<div v-if="assignedReportsRes?.ok && assignedReportsRes.reports.length === 0" class="text-muted">
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
Non hai segnalazioni in carico.
|
<h3 class="h5 mb-0">Le mie assegnazioni</h3>
|
||||||
</div>
|
<NuxtLink to="/operator/assigned" class="btn btn-link btn-sm pe-0">Vedi tutte</NuxtLink>
|
||||||
<NuxtLink
|
|
||||||
v-for="report in assignedReportsRes?.reports" :key="report.id"
|
|
||||||
:to="`/operator/reports/${report.id}`"
|
|
||||||
class="list-group-item list-group-item-action d-flex justify-content-between align-items-center"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<h6 class="mb-1">{{ report.address }}</h6>
|
|
||||||
<StatusBadge :status="report.status" />
|
|
||||||
</div>
|
</div>
|
||||||
<BIIcon name="it-chevron-right" size="sm" color="primary" />
|
<div v-if="assignedReportsRes?.ok && assignedReportsRes.reports.length === 0" class="alert alert-info mb-0">
|
||||||
</NuxtLink>
|
Non hai segnalazioni in carico.
|
||||||
|
</div>
|
||||||
|
<ReportTable
|
||||||
|
v-else
|
||||||
|
:reports="assignedReportsRes?.reports || []"
|
||||||
|
detail-base-path="/operator/reports"
|
||||||
|
action-label="Apri segnalazione"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -37,14 +37,12 @@ export const useAuth = () => {
|
|||||||
try {
|
try {
|
||||||
const res = await fetchCurrentUser()
|
const res = await fetchCurrentUser()
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
console.log('[Auth] User synced successfully')
|
|
||||||
user.value = res.user
|
user.value = res.user
|
||||||
|
|
||||||
if (res.user.must_change_password && route.path !== '/auth/change-password') {
|
if (res.user.must_change_password && route.path !== '/auth/change-password') {
|
||||||
console.log('[Auth] User must change password, redirecting...')
|
|
||||||
await nuxtApp.runWithContext(() => navigateTo('/auth/change-password'))
|
await nuxtApp.runWithContext(() => navigateTo('/auth/change-password'))
|
||||||
} else if (redirectOnSuccess && route.path.startsWith('/auth')) {
|
} else if (redirectOnSuccess && route.path.startsWith('/auth')) {
|
||||||
await nuxtApp.runWithContext(() => navigateTo('/me'))
|
await nuxtApp.runWithContext(() => navigateTo('/dashboard'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -115,4 +113,4 @@ export const useAuth = () => {
|
|||||||
fetchNotifications,
|
fetchNotifications,
|
||||||
markNotificationRead,
|
markNotificationRead,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,6 @@ export default defineNuxtRouteMiddleware(async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (auth.isAuthenticated.value && auth.user.value) {
|
if (auth.isAuthenticated.value && auth.user.value) {
|
||||||
return navigateTo('/me')
|
return navigateTo('/dashboard')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ const { changePassword, user, syncUser } = useAuth()
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const formData = reactive({
|
const formData = reactive({
|
||||||
current_password: '', // FIX: rinominato da old_password
|
current_password: '',
|
||||||
new_password: '',
|
new_password: '',
|
||||||
confirm_password: ''
|
confirm_password: ''
|
||||||
})
|
})
|
||||||
@@ -17,7 +17,6 @@ const formData = reactive({
|
|||||||
const isLoading = ref(false)
|
const isLoading = ref(false)
|
||||||
const errorMessage = ref('')
|
const errorMessage = ref('')
|
||||||
|
|
||||||
// FIX: Stati separati per la visibilità di ogni campo
|
|
||||||
const showCurrent = ref(false)
|
const showCurrent = ref(false)
|
||||||
const showNew = ref(false)
|
const showNew = ref(false)
|
||||||
const showConfirm = ref(false)
|
const showConfirm = ref(false)
|
||||||
@@ -37,15 +36,14 @@ const handleSubmit = async () => {
|
|||||||
errorMessage.value = ''
|
errorMessage.value = ''
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// FIX: Payload con current_password come richiesto dall'API
|
|
||||||
const res = await changePassword({
|
const res = await changePassword({
|
||||||
current_password: formData.current_password,
|
current_password: formData.current_password,
|
||||||
new_password: formData.new_password
|
new_password: formData.new_password
|
||||||
} as any) // Cast temporaneo se l'interfaccia types/api non è ancora aggiornata
|
})
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
await syncUser()
|
await syncUser()
|
||||||
await router.push('/me')
|
await router.push('/profile')
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
errorMessage.value = error.data?.message || 'Errore. Verifica la password attuale.'
|
errorMessage.value = error.data?.message || 'Errore. Verifica la password attuale.'
|
||||||
@@ -73,8 +71,6 @@ const handleSubmit = async () => {
|
|||||||
<h3 class="card-title h4 mb-4">Cambia la tua password</h3>
|
<h3 class="card-title h4 mb-4">Cambia la tua password</h3>
|
||||||
|
|
||||||
<form @submit.prevent="handleSubmit">
|
<form @submit.prevent="handleSubmit">
|
||||||
|
|
||||||
<!-- 1. PASSWORD ATTUALE -->
|
|
||||||
<div class="form-group mb-3">
|
<div class="form-group mb-3">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<div class="input-group-prepend">
|
<div class="input-group-prepend">
|
||||||
@@ -185,4 +181,4 @@ const handleSubmit = async () => {
|
|||||||
.form-control:focus + label {
|
.form-control:focus + label {
|
||||||
color: #006f9d;
|
color: #006f9d;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import CitizenDashboard from '~/components/dashboard/CitizenDashboard.vue'
|
|||||||
import OperatorDashboard from '~/components/dashboard/OperatorDashboard.vue'
|
import OperatorDashboard from '~/components/dashboard/OperatorDashboard.vue'
|
||||||
import AdminDashboard from '~/components/dashboard/AdminDashboard.vue'
|
import AdminDashboard from '~/components/dashboard/AdminDashboard.vue'
|
||||||
|
|
||||||
// Normalizziamo in uppercase nel caso il backend risponda con 'citizen' o 'CITIZEN'
|
|
||||||
const currentDashboard = computed(() => {
|
const currentDashboard = computed(() => {
|
||||||
const role = user.value?.role?.toUpperCase()
|
const role = user.value?.role?.toUpperCase()
|
||||||
if (role === 'CITIZEN') return CitizenDashboard
|
if (role === 'CITIZEN') return CitizenDashboard
|
||||||
@@ -18,25 +17,24 @@ const currentDashboard = computed(() => {
|
|||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
|
|
||||||
// Testi dinamici per l'Hero (la fascia blu in alto) in base al ruolo
|
|
||||||
const heroTitle = computed(() => {
|
const heroTitle = computed(() => {
|
||||||
const role = user.value?.role?.toUpperCase()
|
const role = user.value?.role?.toUpperCase()
|
||||||
if (role === 'ADMIN') return 'Pannello di Amministrazione'
|
if (role === 'ADMIN') return 'Pannello di Amministrazione'
|
||||||
if (role === 'OPERATOR') return 'Gestione Operativa'
|
if (role === 'OPERATOR') return 'Gestione Operativa'
|
||||||
return 'Le Mie Segnalazioni' // Cittadino
|
return 'Dashboard'
|
||||||
})
|
})
|
||||||
|
|
||||||
const heroSubtitle = computed(() => {
|
const heroSubtitle = computed(() => {
|
||||||
const role = user.value?.role?.toUpperCase()
|
const role = user.value?.role?.toUpperCase()
|
||||||
if (role === 'ADMIN') return 'Gestione del sistema, delle aree e moderazione utenti'
|
if (role === 'ADMIN') return 'Gestione del sistema, delle aree e moderazione utenti'
|
||||||
if (role === 'OPERATOR') return 'Prendi in carico e gestisci le segnalazioni in corso'
|
if (role === 'OPERATOR') return 'Prendi in carico e gestisci le segnalazioni in corso'
|
||||||
return 'Traccia lo stato di tutte le tue segnalazioni e comunica con gli operatori' // Cittadino
|
return 'Riepilogo rapido delle tue segnalazioni e dei punti cittadino'
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div class="it-hero-wrapper it-primary">
|
<div class="it-hero-wrapper it-primary hero-compact">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 text-center">
|
<div class="col-12 text-center">
|
||||||
@@ -53,7 +51,7 @@ const heroSubtitle = computed(() => {
|
|||||||
|
|
||||||
<div class="container my-5">
|
<div class="container my-5">
|
||||||
|
|
||||||
<div class="row mb-4">
|
<div class="row mb-3">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<h4 class="mb-2 text-muted">
|
<h4 class="mb-2 text-muted">
|
||||||
Benvenuto, <strong>{{ user?.first_name }} {{ user?.last_name }}</strong>.
|
Benvenuto, <strong>{{ user?.first_name }} {{ user?.last_name }}</strong>.
|
||||||
@@ -61,7 +59,6 @@ const heroSubtitle = computed(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Qui viene montato CitizenDashboard.vue, OperatorDashboard.vue o AdminDashboard.vue -->
|
|
||||||
<component :is="currentDashboard" v-if="currentDashboard" />
|
<component :is="currentDashboard" v-if="currentDashboard" />
|
||||||
|
|
||||||
<div v-else class="alert alert-warning" role="alert">
|
<div v-else class="alert alert-warning" role="alert">
|
||||||
|
|||||||
@@ -1,253 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
definePageMeta({
|
|
||||||
middleware: 'auth',
|
|
||||||
})
|
|
||||||
|
|
||||||
useHead({ title: 'Il Mio Profilo - Visione' })
|
|
||||||
|
|
||||||
const { user, fetchRewards } = useAuth()
|
|
||||||
|
|
||||||
// Fetch rewards/punti
|
|
||||||
const page = ref(1)
|
|
||||||
const perPage = ref(10)
|
|
||||||
|
|
||||||
const { data: rewardsData } = await useAsyncData(() =>
|
|
||||||
fetchRewards({ page: page.value, per_page: perPage.value })
|
|
||||||
)
|
|
||||||
|
|
||||||
const rewards = computed(() => (rewardsData.value as any)?.reward_events || [])
|
|
||||||
const totalPoints = computed(() => (rewardsData.value as any)?.points || 0)
|
|
||||||
const totalRewards = computed(() => rewards.value.length)
|
|
||||||
|
|
||||||
const handlePageChange = (newPage: number) => {
|
|
||||||
page.value = newPage
|
|
||||||
}
|
|
||||||
|
|
||||||
const formatDate = (dateString: string): string => {
|
|
||||||
return new Date(dateString).toLocaleDateString('it-IT', {
|
|
||||||
day: '2-digit',
|
|
||||||
month: '2-digit',
|
|
||||||
year: 'numeric',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleLogout = () => {
|
|
||||||
useAuth().logout()
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<!-- HERO -->
|
|
||||||
<div class="bg-light border-bottom py-4 mb-5">
|
|
||||||
<div class="container">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-12">
|
|
||||||
<h1 class="h3 font-weight-bold mb-3">Il Mio Profilo</h1>
|
|
||||||
<p class="text-muted">Gestisci i tuoi dati, visualizza i tuoi punti e la cronologia delle segnalazioni</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- CONTENUTO -->
|
|
||||||
<div class="container mb-5">
|
|
||||||
<div class="row">
|
|
||||||
|
|
||||||
<!-- SINISTRA: Profilo -->
|
|
||||||
<div class="col-12 col-lg-8">
|
|
||||||
|
|
||||||
<!-- DATI PERSONALI -->
|
|
||||||
<div class="card card-bg mb-4">
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="d-flex align-items-center justify-content-between mb-4">
|
|
||||||
<h5 class="card-title font-weight-bold mb-0">
|
|
||||||
<BIIcon name="it-user" size="sm" color="primary" class="me-2" />
|
|
||||||
Dati Personali
|
|
||||||
</h5>
|
|
||||||
<NuxtLink to="/settings/profile" class="btn btn-sm btn-outline-primary">
|
|
||||||
<BIIcon name="it-pencil" size="xs" class="me-1" />
|
|
||||||
Modifica
|
|
||||||
</NuxtLink>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-6 mb-3">
|
|
||||||
<small class="text-muted d-block">Nome</small>
|
|
||||||
<strong>{{ user?.first_name }}</strong>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6 mb-3">
|
|
||||||
<small class="text-muted d-block">Cognome</small>
|
|
||||||
<strong>{{ user?.last_name }}</strong>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6 mb-3">
|
|
||||||
<small class="text-muted d-block">Email</small>
|
|
||||||
<strong>{{ user?.email }}</strong>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6 mb-3">
|
|
||||||
<small class="text-muted d-block">Codice Fiscale</small>
|
|
||||||
<strong>{{ user?.fiscal_code }}</strong>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6 mb-3">
|
|
||||||
<small class="text-muted d-block">Telefono</small>
|
|
||||||
<strong>{{ user?.phone || 'Non fornito' }}</strong>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6 mb-3">
|
|
||||||
<small class="text-muted d-block">Ruolo</small>
|
|
||||||
<span class="badge bg-info">{{ user?.role }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- PUNTI CITTADINO -->
|
|
||||||
<div class="card card-bg card-highlight mb-4">
|
|
||||||
<div class="card-body">
|
|
||||||
<h5 class="card-title font-weight-bold mb-4">
|
|
||||||
<BIIcon name="it-star" size="sm" color="warning" class="me-2" />
|
|
||||||
I Tuoi Punti Cittadino
|
|
||||||
</h5>
|
|
||||||
|
|
||||||
<div class="text-center p-4 bg-light rounded mb-4">
|
|
||||||
<div class="display-3 font-weight-bold text-primary">{{ totalPoints }}</div>
|
|
||||||
<small class="text-muted">Segnalazioni risolte completate con successo</small>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="alert alert-info" role="alert">
|
|
||||||
<small>
|
|
||||||
<strong>Come guadagni punti?</strong><br>
|
|
||||||
Ricevi 1 punto per ogni segnalazione che risolviamo. Quando raggiungi determinati traguardi, potrai sbloccare riconoscimenti speciali!
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- CRONOLOGIA PUNTI -->
|
|
||||||
<div class="card card-bg">
|
|
||||||
<div class="card-body">
|
|
||||||
<h5 class="card-title font-weight-bold mb-4">
|
|
||||||
<BIIcon name="it-history" size="sm" color="primary" class="me-2" />
|
|
||||||
Cronologia Punti (ultimi {{ rewards.length }})
|
|
||||||
</h5>
|
|
||||||
|
|
||||||
<div v-if="rewards.length === 0" class="alert alert-info" role="alert">
|
|
||||||
Non hai ancora accumulato punti. Crea la tua prima segnalazione!
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else class="table-responsive">
|
|
||||||
<table class="table table-hover mb-0">
|
|
||||||
<thead class="table-light">
|
|
||||||
<tr>
|
|
||||||
<th scope="col" class="small">Data</th>
|
|
||||||
<th scope="col" class="small">Segnalazione</th>
|
|
||||||
<th scope="col" class="text-center small">Punti</th>
|
|
||||||
<th scope="col" class="small">Motivo</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr v-for="reward in rewards" :key="reward.id">
|
|
||||||
<td class="small">
|
|
||||||
<small class="text-muted">{{ formatDate(reward.created_at) }}</small>
|
|
||||||
</td>
|
|
||||||
<td class="small">
|
|
||||||
<NuxtLink :to="`/reports/${reward.report_id}`" class="text-decoration-none">
|
|
||||||
#{{ reward.report_id }}
|
|
||||||
</NuxtLink>
|
|
||||||
</td>
|
|
||||||
<td class="text-center">
|
|
||||||
<span class="badge bg-success">+{{ reward.points }}</span>
|
|
||||||
</td>
|
|
||||||
<td class="small">{{ reward.reason }}</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- PAGINATION -->
|
|
||||||
<Pagination
|
|
||||||
v-if="rewards.length > 0"
|
|
||||||
:current-page="page"
|
|
||||||
:per-page="perPage"
|
|
||||||
:total-items="totalRewards"
|
|
||||||
@page-change="handlePageChange"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- DESTRA: Azioni rapide -->
|
|
||||||
<div class="col-12 col-lg-4">
|
|
||||||
|
|
||||||
<!-- NUOVA SEGNALAZIONE -->
|
|
||||||
<div class="card card-bg card-highlight mb-4">
|
|
||||||
<div class="card-body text-center">
|
|
||||||
<BIIcon name="it-plus-circle" size="xl" color="primary" class="mb-3" />
|
|
||||||
<h6 class="font-weight-bold mb-3">Crea una nuova segnalazione</h6>
|
|
||||||
<p class="small text-muted mb-3">Aiutaci a migliorare il tuo quartiere</p>
|
|
||||||
<NuxtLink to="/reports/create" class="btn btn-primary w-100">
|
|
||||||
Segnala ora
|
|
||||||
</NuxtLink>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- LE MIE SEGNALAZIONI -->
|
|
||||||
<div class="card card-bg mb-4">
|
|
||||||
<div class="card-body">
|
|
||||||
<h6 class="card-title font-weight-bold mb-3">
|
|
||||||
<BIIcon name="it-list" size="sm" color="primary" class="me-2" />
|
|
||||||
Le Mie Segnalazioni
|
|
||||||
</h6>
|
|
||||||
<NuxtLink to="/dashboard" class="btn btn-outline-primary w-100 btn-sm">
|
|
||||||
Vai alla Dashboard
|
|
||||||
</NuxtLink>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- NOTIFICHE -->
|
|
||||||
<div class="card card-bg mb-4">
|
|
||||||
<div class="card-body">
|
|
||||||
<h6 class="card-title font-weight-bold mb-3">
|
|
||||||
<BIIcon name="it-bell" size="sm" color="primary" class="me-2" />
|
|
||||||
Centro Notifiche
|
|
||||||
</h6>
|
|
||||||
<NuxtLink to="/notifications" class="btn btn-outline-primary w-100 btn-sm">
|
|
||||||
Visualizza notifiche
|
|
||||||
</NuxtLink>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- IMPOSTAZIONI & LOGOUT -->
|
|
||||||
<div class="card card-bg">
|
|
||||||
<div class="card-body">
|
|
||||||
<h6 class="card-title font-weight-bold mb-3">
|
|
||||||
<BIIcon name="it-settings" size="sm" color="primary" class="me-2" />
|
|
||||||
Account
|
|
||||||
</h6>
|
|
||||||
<NuxtLink to="/settings/password" class="btn btn-outline-secondary w-100 btn-sm mb-2">
|
|
||||||
Cambia Password
|
|
||||||
</NuxtLink>
|
|
||||||
<button type="button" class="btn btn-outline-danger w-100 btn-sm" @click="handleLogout">
|
|
||||||
Logout
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.card-highlight {
|
|
||||||
border-color: #ffc107;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-hover tbody tr:hover {
|
|
||||||
background-color: rgba(0, 111, 157, 0.05);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
@@ -1,6 +1,4 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Report } from '~/types/api'
|
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
middleware: 'auth',
|
middleware: 'auth',
|
||||||
auth: { role: 'OPERATOR' }
|
auth: { role: 'OPERATOR' }
|
||||||
@@ -19,7 +17,6 @@ const { data: reportsRes, pending } = useAsyncData(
|
|||||||
{ watch: [page] }
|
{ watch: [page] }
|
||||||
)
|
)
|
||||||
|
|
||||||
const formatDate = (dateString: string) => new Date(dateString).toLocaleDateString('it-IT')
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -41,36 +38,11 @@ const formatDate = (dateString: string) => new Date(dateString).toLocaleDateStri
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<div class="table-responsive">
|
<ReportTable
|
||||||
<table class="table table-hover border-bottom">
|
:reports="reportsRes.reports"
|
||||||
<thead class="table-light">
|
detail-base-path="/operator/reports"
|
||||||
<tr>
|
action-label="Apri segnalazione"
|
||||||
<th scope="col" class="small">#ID</th>
|
/>
|
||||||
<th scope="col" class="small">Categoria</th>
|
|
||||||
<th scope="col" class="small">Indirizzo</th>
|
|
||||||
<th scope="col" class="small">Stato</th>
|
|
||||||
<th scope="col" class="small">Creata il</th>
|
|
||||||
<th scope="col" class="text-center small">Azioni</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr v-for="report in reportsRes.reports" :key="report.id" class="align-middle">
|
|
||||||
<td class="small font-weight-semibold">#{{ report.id }}</td>
|
|
||||||
<td class="small">{{ report.category_name }}</td>
|
|
||||||
<td class="small">{{ report.address }}</td>
|
|
||||||
<td class="small">
|
|
||||||
<StatusBadge :status="report.status" />
|
|
||||||
</td>
|
|
||||||
<td class="small text-muted">{{ formatDate(report.created_at) }}</td>
|
|
||||||
<td class="text-center">
|
|
||||||
<NuxtLink :to="`/operator/reports/${report.id}`" class="btn btn-sm btn-outline-primary">
|
|
||||||
Gestisci
|
|
||||||
</NuxtLink>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Pagination
|
<Pagination
|
||||||
v-if="reportsRes.total > perPage"
|
v-if="reportsRes.total > perPage"
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Report } from '~/types/api'
|
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
middleware: 'auth',
|
middleware: 'auth',
|
||||||
auth: { role: 'OPERATOR' }
|
auth: { role: 'OPERATOR' }
|
||||||
@@ -23,15 +21,13 @@ const handleTakeReport = async (id: number) => {
|
|||||||
try {
|
try {
|
||||||
const res = await takeReport(id)
|
const res = await takeReport(id)
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
// Optimistic update or refresh
|
await refresh()
|
||||||
await refresh()
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to take report', err)
|
console.error('Failed to take report', err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const formatDate = (dateString: string) => new Date(dateString).toLocaleDateString('it-IT')
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -53,36 +49,12 @@ const formatDate = (dateString: string) => new Date(dateString).toLocaleDateStri
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<div class="table-responsive">
|
<ReportTable
|
||||||
<table class="table table-hover border-bottom">
|
:reports="reportsRes.reports"
|
||||||
<thead class="table-light">
|
action-variant="manage"
|
||||||
<tr>
|
detail-base-path="/operator/reports"
|
||||||
<th scope="col" class="small">#ID</th>
|
@take="handleTakeReport"
|
||||||
<th scope="col" class="small">Categoria</th>
|
/>
|
||||||
<th scope="col" class="small">Indirizzo</th>
|
|
||||||
<th scope="col" class="small">Stato</th>
|
|
||||||
<th scope="col" class="small">Creata il</th>
|
|
||||||
<th scope="col" class="text-center small">Azioni</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr v-for="report in reportsRes.reports" :key="report.id" class="align-middle">
|
|
||||||
<td class="small font-weight-semibold">#{{ report.id }}</td>
|
|
||||||
<td class="small">{{ report.category_name }}</td>
|
|
||||||
<td class="small">{{ report.address }}</td>
|
|
||||||
<td class="small">
|
|
||||||
<StatusBadge :status="report.status" />
|
|
||||||
</td>
|
|
||||||
<td class="small text-muted">{{ formatDate(report.created_at) }}</td>
|
|
||||||
<td class="text-center">
|
|
||||||
<button class="btn btn-primary btn-sm" @click="handleTakeReport(report.id)">
|
|
||||||
Prendi in carico
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Pagination
|
<Pagination
|
||||||
v-if="reportsRes.total > perPage"
|
v-if="reportsRes.total > perPage"
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Report } from '~/types/api'
|
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
middleware: 'auth',
|
middleware: 'auth',
|
||||||
auth: { role: 'OPERATOR' }
|
auth: { role: 'OPERATOR' }
|
||||||
@@ -19,7 +17,6 @@ const { data: reportsRes, pending } = useAsyncData(
|
|||||||
{ watch: [page] }
|
{ watch: [page] }
|
||||||
)
|
)
|
||||||
|
|
||||||
const formatDate = (dateString: string) => new Date(dateString).toLocaleDateString('it-IT')
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -41,36 +38,11 @@ const formatDate = (dateString: string) => new Date(dateString).toLocaleDateStri
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<div class="table-responsive">
|
<ReportTable
|
||||||
<table class="table table-hover border-bottom">
|
:reports="reportsRes.reports"
|
||||||
<thead class="table-light">
|
detail-base-path="/operator/reports"
|
||||||
<tr>
|
action-label="Apri segnalazione"
|
||||||
<th scope="col" class="small">#ID</th>
|
/>
|
||||||
<th scope="col" class="small">Categoria</th>
|
|
||||||
<th scope="col" class="small">Indirizzo</th>
|
|
||||||
<th scope="col" class="small">Stato</th>
|
|
||||||
<th scope="col" class="small">Creata il</th>
|
|
||||||
<th scope="col" class="text-center small">Dettagli</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr v-for="report in reportsRes.reports" :key="report.id" class="align-middle">
|
|
||||||
<td class="small font-weight-semibold">#{{ report.id }}</td>
|
|
||||||
<td class="small">{{ report.category_name }}</td>
|
|
||||||
<td class="small">{{ report.address }}</td>
|
|
||||||
<td class="small">
|
|
||||||
<StatusBadge :status="report.status" />
|
|
||||||
</td>
|
|
||||||
<td class="small text-muted">{{ formatDate(report.created_at) }}</td>
|
|
||||||
<td class="text-center">
|
|
||||||
<NuxtLink :to="`/operator/reports/${report.id}`" class="btn btn-sm btn-outline-secondary">
|
|
||||||
Vedi
|
|
||||||
</NuxtLink>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Pagination
|
<Pagination
|
||||||
v-if="reportsRes.total > perPage"
|
v-if="reportsRes.total > perPage"
|
||||||
|
|||||||
@@ -0,0 +1,156 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
definePageMeta({
|
||||||
|
middleware: 'auth',
|
||||||
|
})
|
||||||
|
|
||||||
|
useHead({ title: 'Profilo - Visione' })
|
||||||
|
|
||||||
|
const { user, fetchRewards, logout } = useAuth()
|
||||||
|
|
||||||
|
const page = ref(1)
|
||||||
|
const perPage = ref(10)
|
||||||
|
|
||||||
|
const { data: rewardsData } = await useAsyncData(
|
||||||
|
'profile-rewards',
|
||||||
|
() => fetchRewards({ page: page.value, per_page: perPage.value }),
|
||||||
|
{ watch: [page] }
|
||||||
|
)
|
||||||
|
|
||||||
|
const rewards = computed(() => rewardsData.value?.reward_events || [])
|
||||||
|
const totalPoints = computed(() => rewardsData.value?.points || 0)
|
||||||
|
const totalRewards = computed(() => rewards.value.length)
|
||||||
|
|
||||||
|
const formatDate = (dateString: string) =>
|
||||||
|
new Date(dateString).toLocaleDateString('it-IT', {
|
||||||
|
day: '2-digit',
|
||||||
|
month: '2-digit',
|
||||||
|
year: 'numeric',
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="bg-light border-bottom py-4 mb-4">
|
||||||
|
<div class="container">
|
||||||
|
<h1 class="h3 font-weight-bold mb-1">Profilo</h1>
|
||||||
|
<p class="text-muted mb-0">Dati account, punti cittadino e impostazioni personali.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container mb-5">
|
||||||
|
<div class="row g-4">
|
||||||
|
<div class="col-12 col-lg-8">
|
||||||
|
<div class="card card-bg mb-4">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
|
<h2 class="h5 card-title mb-0">Dati personali</h2>
|
||||||
|
<NuxtLink to="/settings/profile" class="btn btn-outline-primary btn-sm">
|
||||||
|
Modifica
|
||||||
|
</NuxtLink>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<small class="text-muted d-block">Nome</small>
|
||||||
|
<strong>{{ user?.first_name }}</strong>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<small class="text-muted d-block">Cognome</small>
|
||||||
|
<strong>{{ user?.last_name }}</strong>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<small class="text-muted d-block">Email</small>
|
||||||
|
<strong>{{ user?.email }}</strong>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<small class="text-muted d-block">Telefono</small>
|
||||||
|
<strong>{{ user?.phone || 'Non fornito' }}</strong>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-0">
|
||||||
|
<small class="text-muted d-block">Codice fiscale</small>
|
||||||
|
<strong>{{ user?.fiscal_code }}</strong>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-0">
|
||||||
|
<small class="text-muted d-block">Ruolo</small>
|
||||||
|
<span class="badge bg-info">{{ user?.role }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card card-bg">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
|
<h2 class="h5 card-title mb-0">Cronologia punti</h2>
|
||||||
|
<span class="badge bg-primary">{{ totalPoints }} punti</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="rewards.length === 0" class="alert alert-info mb-0" role="alert">
|
||||||
|
Nessun punto registrato.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover mb-0">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th scope="col">Data</th>
|
||||||
|
<th scope="col">Segnalazione</th>
|
||||||
|
<th scope="col">Motivo</th>
|
||||||
|
<th scope="col" class="text-end">Punti</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="reward in rewards" :key="reward.id">
|
||||||
|
<td class="text-muted">{{ formatDate(reward.created_at) }}</td>
|
||||||
|
<td>
|
||||||
|
<NuxtLink :to="`/reports/${reward.report_id}`">#{{ reward.report_id }}</NuxtLink>
|
||||||
|
</td>
|
||||||
|
<td>{{ reward.reason }}</td>
|
||||||
|
<td class="text-end">
|
||||||
|
<span class="badge bg-success">+{{ reward.points }}</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<Pagination
|
||||||
|
v-if="totalRewards > perPage"
|
||||||
|
:current-page="page"
|
||||||
|
:per-page="perPage"
|
||||||
|
:total-items="totalRewards"
|
||||||
|
@page-change="page = $event"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 col-lg-4">
|
||||||
|
<div class="card card-bg mb-4">
|
||||||
|
<div class="card-body">
|
||||||
|
<h2 class="h6 card-title mb-3">Azioni account</h2>
|
||||||
|
<div class="d-grid gap-2">
|
||||||
|
<NuxtLink to="/reports/new" class="btn btn-primary">Nuova segnalazione</NuxtLink>
|
||||||
|
<NuxtLink to="/notifications" class="btn btn-outline-primary">Notifiche</NuxtLink>
|
||||||
|
<NuxtLink to="/settings/password" class="btn btn-outline-secondary">Cambia password</NuxtLink>
|
||||||
|
<button type="button" class="btn btn-outline-danger" @click="logout">
|
||||||
|
Logout
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card card-bg">
|
||||||
|
<div class="card-body">
|
||||||
|
<h2 class="h6 card-title mb-2">Punti cittadino</h2>
|
||||||
|
<div class="display-6 text-primary font-weight-bold mb-2">{{ totalPoints }}</div>
|
||||||
|
<p class="small text-muted mb-0">
|
||||||
|
I punti vengono accreditati sulle segnalazioni concluse con esito positivo.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -1,144 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import type { CreateReportRequest } from '~/types/api'
|
|
||||||
|
|
||||||
definePageMeta({
|
|
||||||
middleware: 'auth',
|
|
||||||
})
|
|
||||||
|
|
||||||
useHead({ title: 'Crea Segnalazione - Visione' })
|
|
||||||
|
|
||||||
const router = useRouter()
|
|
||||||
const { createReport, uploadMedia } = useReports()
|
|
||||||
const { getCategories } = useAdmin()
|
|
||||||
const { user } = useAuth()
|
|
||||||
|
|
||||||
// Fetch categories
|
|
||||||
const { data: categoriesData } = await useAsyncData('categories', () =>
|
|
||||||
getCategories()
|
|
||||||
)
|
|
||||||
const categories = computed(() => categoriesData.value?.categories || [])
|
|
||||||
|
|
||||||
// 👇 Manteniamo una sola dichiarazione per queste variabili
|
|
||||||
const isLoading = ref(false)
|
|
||||||
const errorMessage = ref('')
|
|
||||||
|
|
||||||
// 👇 Manteniamo una sola funzione handleReportSubmit
|
|
||||||
const handleReportSubmit = async (reportData: CreateReportRequest, mediaFiles: File[]) => {
|
|
||||||
isLoading.value = true
|
|
||||||
errorMessage.value = ''
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Step 1: Carica i file media se presenti
|
|
||||||
let uploadedMediaUrls: string[] = []
|
|
||||||
|
|
||||||
if (mediaFiles.length > 0) {
|
|
||||||
console.log('📸 Media files to upload:', mediaFiles)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 2: Crea la segnalazione
|
|
||||||
const response = await createReport({
|
|
||||||
...reportData,
|
|
||||||
media: uploadedMediaUrls, // Se l'API richiede stringhe pre-uploadate, va bene. Altrimenti passa array vuoto.
|
|
||||||
})
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
// Step 3: Se ha media, carica dopo la creazione usando l'ID
|
|
||||||
if (mediaFiles.length > 0 && response.report_id) {
|
|
||||||
try {
|
|
||||||
const formData = new FormData()
|
|
||||||
mediaFiles.forEach((file) => {
|
|
||||||
formData.append('files', file)
|
|
||||||
})
|
|
||||||
|
|
||||||
await uploadMedia(response.report_id, formData)
|
|
||||||
console.log('✅ Media uploaded successfully')
|
|
||||||
} catch (err) {
|
|
||||||
console.error('⚠️ Media upload failed (non-critical):', err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('✅ Segnalazione creata:', response.report_id)
|
|
||||||
|
|
||||||
// Redirect al dettaglio della segnalazione
|
|
||||||
await router.push(`/reports/${response.report_id}`)
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
errorMessage.value = error.data?.message || 'Errore durante la creazione della segnalazione. Riprova.'
|
|
||||||
console.error('❌ Error creating report:', error)
|
|
||||||
} finally {
|
|
||||||
isLoading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<!-- HERO -->
|
|
||||||
<div class="it-hero-wrapper it-primary-bg">
|
|
||||||
<div class="container">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-12 text-center">
|
|
||||||
<div class="it-hero-text-wrapper p-4">
|
|
||||||
<h1 class="text-white font-weight-bold">Segnala un problema</h1>
|
|
||||||
<p class="text-white mt-2 lead">
|
|
||||||
Aiutaci a migliorare la città condividendo i disservizi che noti
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- CONTENUTO -->
|
|
||||||
<div class="container my-5">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-12 col-lg-8 offset-lg-2">
|
|
||||||
|
|
||||||
<!-- CALLOUT INFO -->
|
|
||||||
<div class="callout callout-highlight callout-primary mb-4">
|
|
||||||
<div class="callout-title">
|
|
||||||
<BIIcon name="it-info-circle" />
|
|
||||||
La tua segnalazione è anonima
|
|
||||||
</div>
|
|
||||||
<p>
|
|
||||||
I tuoi dati personali non saranno visibili pubblicamente. La segnalazione verrà elaborata dagli operatori comunali e potrai tracciare lo stato dell'intervento in tempo reale.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- ERRORE GLOBALE -->
|
|
||||||
<div v-if="errorMessage" class="alert alert-danger alert-dismissible fade show" role="alert">
|
|
||||||
{{ errorMessage }}
|
|
||||||
<button type="button" class="btn-close" @click="errorMessage = ''"></button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- INFO UTENTE -->
|
|
||||||
<div class="card card-bg mb-4">
|
|
||||||
<div class="card-body">
|
|
||||||
<h6 class="card-title font-weight-bold mb-3">
|
|
||||||
<BIIcon name="it-user" size="sm" color="primary" class="me-2" />
|
|
||||||
Stai segnalando come:
|
|
||||||
</h6>
|
|
||||||
<div class="small">
|
|
||||||
<strong>{{ user?.first_name }} {{ user?.last_name }}</strong><br>
|
|
||||||
<span class="text-muted">{{ user?.email }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- FORM PRINCIPALE -->
|
|
||||||
<ReportForm :categories="categories" :is-loading="isLoading" @submit="handleReportSubmit" />
|
|
||||||
|
|
||||||
<!-- NOTE LEGALI -->
|
|
||||||
<div class="alert alert-info mt-5" role="alert">
|
|
||||||
<small class="d-block">
|
|
||||||
Sottoponendo questa segnalazione accetti i nostri <NuxtLink to="/privacy" target="_blank">Termini di Servizio</NuxtLink>
|
|
||||||
e l'<NuxtLink to="/privacy" target="_blank">Informativa sulla Privacy</NuxtLink>.
|
|
||||||
Le segnalazioni con contenuti abusivi o offensivi verranno rimosse e l'utente potrebbe essere sanzionato.
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { Report } from '~/types/api'
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
middleware: 'auth',
|
||||||
|
})
|
||||||
|
|
||||||
|
useHead({ title: 'Le mie segnalazioni - Visione' })
|
||||||
|
|
||||||
|
const { getReports } = useReports()
|
||||||
|
|
||||||
|
const page = ref(1)
|
||||||
|
const perPage = ref(10)
|
||||||
|
const filterStatus = ref('')
|
||||||
|
|
||||||
|
const { data: reportsRes, pending } = useAsyncData('citizen-reports-list', () => getReports({}))
|
||||||
|
|
||||||
|
const reports = computed<Report[]>(() => reportsRes.value?.reports || [])
|
||||||
|
const filteredReports = computed(() => {
|
||||||
|
if (!filterStatus.value) return reports.value
|
||||||
|
if (filterStatus.value === 'IN_PROGRESS') {
|
||||||
|
return reports.value.filter((report) => report.status === 'IN_PROGRESS' || report.status === 'ASSIGNED')
|
||||||
|
}
|
||||||
|
return reports.value.filter((report) => report.status === filterStatus.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
const paginatedReports = computed(() => {
|
||||||
|
const start = (page.value - 1) * perPage.value
|
||||||
|
return filteredReports.value.slice(start, start + perPage.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleStatusFilter = (status: string) => {
|
||||||
|
filterStatus.value = status === 'all' ? '' : status
|
||||||
|
page.value = 1
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="bg-light border-bottom py-4 mb-4">
|
||||||
|
<div class="container">
|
||||||
|
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-3">
|
||||||
|
<div>
|
||||||
|
<h1 class="h3 font-weight-bold mb-1">Le mie segnalazioni</h1>
|
||||||
|
<p class="text-muted mb-0">Consulta stato, cronologia e conversazioni con gli operatori.</p>
|
||||||
|
</div>
|
||||||
|
<NuxtLink to="/reports/new" class="btn btn-primary">
|
||||||
|
<BIIcon name="it-plus-circle" size="sm" color="white" class="me-2" />
|
||||||
|
Nuova segnalazione
|
||||||
|
</NuxtLink>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container mb-5">
|
||||||
|
<div class="d-flex flex-wrap gap-2 align-items-center mb-3">
|
||||||
|
<div class="btn-group" role="group" aria-label="Filtri segnalazioni">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm"
|
||||||
|
:class="filterStatus === '' ? 'btn-primary' : 'btn-outline-primary'"
|
||||||
|
@click="handleStatusFilter('all')"
|
||||||
|
>
|
||||||
|
Tutte
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm"
|
||||||
|
:class="filterStatus === 'OPEN' ? 'btn-primary' : 'btn-outline-primary'"
|
||||||
|
@click="handleStatusFilter('OPEN')"
|
||||||
|
>
|
||||||
|
Aperte
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm"
|
||||||
|
:class="filterStatus === 'IN_PROGRESS' ? 'btn-primary' : 'btn-outline-primary'"
|
||||||
|
@click="handleStatusFilter('IN_PROGRESS')"
|
||||||
|
>
|
||||||
|
In corso
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm"
|
||||||
|
:class="filterStatus === 'RESOLVED' ? 'btn-primary' : 'btn-outline-primary'"
|
||||||
|
@click="handleStatusFilter('RESOLVED')"
|
||||||
|
>
|
||||||
|
Risolte
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="pending" class="text-center py-5">
|
||||||
|
<div class="spinner-border text-primary" role="status">
|
||||||
|
<span class="visually-hidden">Caricamento...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="filteredReports.length === 0" class="alert alert-info" role="alert">
|
||||||
|
Nessuna segnalazione trovata.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<ReportTable :reports="paginatedReports" action-label="Apri segnalazione" />
|
||||||
|
<Pagination
|
||||||
|
v-if="filteredReports.length > perPage"
|
||||||
|
:current-page="page"
|
||||||
|
:per-page="perPage"
|
||||||
|
:total-items="filteredReports.length"
|
||||||
|
@page-change="page = $event"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { CreateReportRequest } from '~/types/api'
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
middleware: 'auth',
|
||||||
|
})
|
||||||
|
|
||||||
|
useHead({ title: 'Nuova segnalazione - Visione' })
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const { createReport, uploadMedia } = useReports()
|
||||||
|
const { getCategories } = useAdmin()
|
||||||
|
const { user } = useAuth()
|
||||||
|
|
||||||
|
const { data: categoriesData } = await useAsyncData('categories', () => getCategories())
|
||||||
|
const categories = computed(() => categoriesData.value?.categories || [])
|
||||||
|
|
||||||
|
const isLoading = ref(false)
|
||||||
|
const errorMessage = ref('')
|
||||||
|
|
||||||
|
const handleReportSubmit = async (reportData: CreateReportRequest, mediaFiles: File[]) => {
|
||||||
|
isLoading.value = true
|
||||||
|
errorMessage.value = ''
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await createReport({
|
||||||
|
...reportData,
|
||||||
|
media: [],
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.ok && response.report_id) {
|
||||||
|
if (mediaFiles.length > 0) {
|
||||||
|
try {
|
||||||
|
const formData = new FormData()
|
||||||
|
mediaFiles.forEach((file) => formData.append('files', file))
|
||||||
|
await uploadMedia(response.report_id, formData)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Media upload failed:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await router.push(`/reports/${response.report_id}`)
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
errorMessage.value = error.data?.message || 'Errore durante la creazione della segnalazione. Riprova.'
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="it-hero-wrapper it-primary-bg hero-compact">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="it-hero-text-wrapper p-4">
|
||||||
|
<h1 class="text-white font-weight-bold">Nuova segnalazione</h1>
|
||||||
|
<p class="text-white mt-2 lead">
|
||||||
|
Descrivi il problema urbano, indica la posizione e allega eventuali media.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container my-5">
|
||||||
|
<div class="row g-4">
|
||||||
|
<div class="col-12 col-lg-8">
|
||||||
|
<div v-if="errorMessage" class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||||
|
{{ errorMessage }}
|
||||||
|
<button type="button" class="btn-close" aria-label="Chiudi" @click="errorMessage = ''"></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ReportForm :categories="categories" :is-loading="isLoading" @submit="handleReportSubmit" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<aside class="col-12 col-lg-4">
|
||||||
|
<div class="card card-bg mb-4">
|
||||||
|
<div class="card-body">
|
||||||
|
<h2 class="h6 card-title mb-3">Account segnalante</h2>
|
||||||
|
<p class="mb-1"><strong>{{ user?.first_name }} {{ user?.last_name }}</strong></p>
|
||||||
|
<p class="small text-muted mb-0">{{ user?.email }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="callout callout-highlight callout-primary">
|
||||||
|
<div class="callout-title d-flex align-items-center">
|
||||||
|
<BIIcon name="it-info-circle" size="sm" color="primary" class="me-2" />
|
||||||
|
Privacy
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
I tuoi dati personali non saranno pubblicati. Gli operatori li useranno solo per gestire la segnalazione.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -9,8 +9,5 @@ export default defineNuxtPlugin(async () => {
|
|||||||
auth.logout()
|
auth.logout()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[Auth Plugin] Initialized - User:', auth.user.value?.id || 'None', 'Authenticated:', auth.isAuthenticated.value)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user