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;
|
||||
}
|
||||
|
||||
.it-hero-wrapper.hero-compact .container {
|
||||
min-height: 132px;
|
||||
}
|
||||
|
||||
.it-hero-wrapper .container > .row {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
@@ -229,6 +233,69 @@ html body .navbar .navbar-collapsable .navbar-nav li.nav-item a.nav-link[data-fo
|
||||
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 .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-right: 1rem !important;
|
||||
}
|
||||
|
||||
.report-address {
|
||||
max-width: 14rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ watch(() => route.path, () => {
|
||||
|
||||
const navLinks = [
|
||||
{ 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: 'Come funziona', path: '/info' }
|
||||
]
|
||||
@@ -43,9 +43,16 @@ const { isAuthenticated, user, logout } = useAuth()
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<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">
|
||||
<span class="rounded-icon" aria-hidden="true">
|
||||
<BIIcon name="it-user" color="black" />
|
||||
@@ -62,7 +69,6 @@ const { isAuthenticated, user, logout } = useAuth()
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Utente NON Autenticato: Bottone Login -->
|
||||
<NuxtLink v-else class="btn btn-primary btn-icon btn-full" to="/auth/login"
|
||||
data-element="personal-area-login" data-focus-mouse="false">
|
||||
<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 { getReports } = useReports()
|
||||
|
||||
const page = ref(1)
|
||||
const perPage = ref(10)
|
||||
const filterStatus = ref<string>('') // Stringa vuota = "Tutte"
|
||||
const { data: reportsRes, pending } = useAsyncData('citizen-dashboard-reports', () => getReports({}))
|
||||
|
||||
// Carichiamo TUTTE le segnalazioni per avere statistiche globali e filtri veloci
|
||||
const { data: reportsRes, pending } = useAsyncData('citizen-reports-all', () =>
|
||||
getReports({}) // Non passiamo filtri alla chiamata API
|
||||
)
|
||||
const reports = computed<Report[]>(() => reportsRes.value?.reports || [])
|
||||
const recentReports = computed(() => reports.value.slice(0, 4))
|
||||
|
||||
// Dati grezzi dal server
|
||||
const rawReports = computed<Report[]>(() => reportsRes.value?.reports || [])
|
||||
|
||||
// 1. STATISTICHE GLOBALI (Sempre basate su tutti i dati, ignorano i filtri)
|
||||
const statsGlobal = computed(() => {
|
||||
const stats = { total: 0, OPEN: 0, IN_PROGRESS: 0, RESOLVED: 0 }
|
||||
rawReports.value.forEach((report: Report) => {
|
||||
stats.total++
|
||||
if (report.status === 'OPEN') stats.OPEN++
|
||||
if (report.status === 'IN_PROGRESS' || report.status === 'ASSIGNED') stats.IN_PROGRESS++
|
||||
if (report.status === 'RESOLVED') stats.RESOLVED++
|
||||
})
|
||||
return stats
|
||||
const stats = computed(() => {
|
||||
return reports.value.reduce(
|
||||
(acc, report) => {
|
||||
acc.total++
|
||||
if (report.status === 'OPEN') acc.open++
|
||||
if (report.status === 'IN_PROGRESS' || report.status === 'ASSIGNED') acc.inProgress++
|
||||
if (report.status === 'RESOLVED' || report.status === 'CLOSED') acc.resolved++
|
||||
return acc
|
||||
},
|
||||
{ total: 0, open: 0, inProgress: 0, resolved: 0 }
|
||||
)
|
||||
})
|
||||
|
||||
// 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>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<!-- Callout Gamification -->
|
||||
<div class="callout callout-highlight callout-success mb-4">
|
||||
<div class="callout-title d-flex align-items-center">
|
||||
<BIIcon name="it-star-full" size="sm" color="success" class="me-2" />
|
||||
I tuoi Punti Cittadino
|
||||
</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 class="row g-3 mb-4">
|
||||
<div class="col-6 col-lg-3">
|
||||
<div class="card card-bg dashboard-stat h-100">
|
||||
<div class="card-body py-3">
|
||||
<div class="stat-value">{{ stats.total }}</div>
|
||||
<div class="small text-muted">Segnalazioni</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<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-warning font-weight-bold">{{ statsGlobal.OPEN }}</div>
|
||||
<small class="text-muted">Aperte</small>
|
||||
<div class="col-6 col-lg-3">
|
||||
<div class="card card-bg dashboard-stat h-100">
|
||||
<div class="card-body py-3">
|
||||
<div class="stat-value">{{ stats.open }}</div>
|
||||
<div class="small text-muted">Aperte</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<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-info font-weight-bold">{{ statsGlobal.IN_PROGRESS }}</div>
|
||||
<small class="text-muted">In corso</small>
|
||||
<div class="col-6 col-lg-3">
|
||||
<div class="card card-bg dashboard-stat h-100">
|
||||
<div class="card-body py-3">
|
||||
<div class="stat-value">{{ stats.inProgress }}</div>
|
||||
<div class="small text-muted">In corso</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<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-success font-weight-bold">{{ statsGlobal.RESOLVED }}</div>
|
||||
<small class="text-muted">Risolte</small>
|
||||
<div class="col-6 col-lg-3">
|
||||
<div class="card card-bg dashboard-stat h-100">
|
||||
<div class="card-body py-3">
|
||||
<div class="stat-value">{{ user?.points || 0 }}</div>
|
||||
<div class="small text-muted">Punti cittadino</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- AZIONI RAPIDE E FILTRI -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="d-flex gap-2 flex-wrap align-items-center">
|
||||
<NuxtLink to="/reports/create" class="btn btn-primary me-md-3">
|
||||
<BIIcon name="it-plus-circle" size="sm" color="white" class="me-2" />
|
||||
Nuova segnalazione
|
||||
</NuxtLink>
|
||||
<div class="d-flex flex-wrap gap-2 mb-4">
|
||||
<NuxtLink to="/reports/new" class="btn btn-primary">
|
||||
<BIIcon name="it-plus-circle" size="sm" color="white" class="me-2" />
|
||||
Nuova segnalazione
|
||||
</NuxtLink>
|
||||
<NuxtLink to="/reports" class="btn btn-outline-primary">
|
||||
<BIIcon name="it-list" size="sm" class="me-2" />
|
||||
Le mie segnalazioni
|
||||
</NuxtLink>
|
||||
</div>
|
||||
|
||||
<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 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">Aggiornamenti recenti</h2>
|
||||
<NuxtLink to="/reports" class="btn btn-link btn-sm pe-0">Vedi tutte</NuxtLink>
|
||||
</div>
|
||||
|
||||
<div v-if="pending" class="text-center py-4">
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="visually-hidden">Caricamento...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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 v-else-if="recentReports.length === 0" class="alert alert-info mb-0" role="alert">
|
||||
Non hai ancora creato segnalazioni.
|
||||
</div>
|
||||
|
||||
<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"
|
||||
/>
|
||||
<ReportTable v-else :reports="recentReports" action-label="Apri segnalazione" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.table-hover tbody tr:hover { background-color: rgba(0, 111, 157, 0.03); }
|
||||
.btn-group .btn { min-width: 80px; }
|
||||
</style>
|
||||
|
||||
@@ -14,46 +14,44 @@ const handleTakeReport = async (id: number) => {
|
||||
|
||||
<template>
|
||||
<div class="row">
|
||||
<!-- Colonna Sinistra: Segnalazioni da prendere in carico -->
|
||||
<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="list-group">
|
||||
<div v-if="openReportsRes?.ok && openReportsRes.reports.length === 0" class="text-muted">
|
||||
Nessuna nuova segnalazione in ingresso.
|
||||
</div>
|
||||
<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 class="card card-bg h-100">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h3 class="h5 mb-0">Da prendere in carico</h3>
|
||||
<NuxtLink to="/operator" class="btn btn-link btn-sm pe-0">Apri coda</NuxtLink>
|
||||
</div>
|
||||
<button @click="handleTakeReport(report.id)" class="btn btn-outline-primary btn-sm">
|
||||
Prendi in carico
|
||||
</button>
|
||||
<div v-if="openReportsRes?.ok && openReportsRes.reports.length === 0" class="alert alert-info mb-0">
|
||||
Nessuna nuova segnalazione in ingresso.
|
||||
</div>
|
||||
<ReportTable
|
||||
v-else
|
||||
:reports="openReportsRes?.reports || []"
|
||||
action-variant="manage"
|
||||
detail-base-path="/operator/reports"
|
||||
@take="handleTakeReport"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Colonna Destra: Segnalazioni in gestione dell'operatore -->
|
||||
<div class="col-12 col-lg-6 mb-4">
|
||||
<h3 class="mb-3 border-bottom pb-2">Le mie assegnazioni</h3>
|
||||
<div class="list-group">
|
||||
<div v-if="assignedReportsRes?.ok && assignedReportsRes.reports.length === 0" class="text-muted">
|
||||
Non hai segnalazioni in carico.
|
||||
</div>
|
||||
<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 class="card card-bg h-100">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h3 class="h5 mb-0">Le mie assegnazioni</h3>
|
||||
<NuxtLink to="/operator/assigned" class="btn btn-link btn-sm pe-0">Vedi tutte</NuxtLink>
|
||||
</div>
|
||||
<BIIcon name="it-chevron-right" size="sm" color="primary" />
|
||||
</NuxtLink>
|
||||
<div v-if="assignedReportsRes?.ok && assignedReportsRes.reports.length === 0" class="alert alert-info mb-0">
|
||||
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>
|
||||
|
||||
@@ -37,14 +37,12 @@ export const useAuth = () => {
|
||||
try {
|
||||
const res = await fetchCurrentUser()
|
||||
if (res.ok) {
|
||||
console.log('[Auth] User synced successfully')
|
||||
user.value = res.user
|
||||
|
||||
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'))
|
||||
} else if (redirectOnSuccess && route.path.startsWith('/auth')) {
|
||||
await nuxtApp.runWithContext(() => navigateTo('/me'))
|
||||
await nuxtApp.runWithContext(() => navigateTo('/dashboard'))
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
@@ -10,6 +10,6 @@ export default defineNuxtRouteMiddleware(async () => {
|
||||
}
|
||||
|
||||
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 formData = reactive({
|
||||
current_password: '', // FIX: rinominato da old_password
|
||||
current_password: '',
|
||||
new_password: '',
|
||||
confirm_password: ''
|
||||
})
|
||||
@@ -17,7 +17,6 @@ const formData = reactive({
|
||||
const isLoading = ref(false)
|
||||
const errorMessage = ref('')
|
||||
|
||||
// FIX: Stati separati per la visibilità di ogni campo
|
||||
const showCurrent = ref(false)
|
||||
const showNew = ref(false)
|
||||
const showConfirm = ref(false)
|
||||
@@ -37,15 +36,14 @@ const handleSubmit = async () => {
|
||||
errorMessage.value = ''
|
||||
|
||||
try {
|
||||
// FIX: Payload con current_password come richiesto dall'API
|
||||
const res = await changePassword({
|
||||
current_password: formData.current_password,
|
||||
new_password: formData.new_password
|
||||
} as any) // Cast temporaneo se l'interfaccia types/api non è ancora aggiornata
|
||||
})
|
||||
|
||||
if (res.ok) {
|
||||
await syncUser()
|
||||
await router.push('/me')
|
||||
await router.push('/profile')
|
||||
}
|
||||
} catch (error: any) {
|
||||
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>
|
||||
|
||||
<form @submit.prevent="handleSubmit">
|
||||
|
||||
<!-- 1. PASSWORD ATTUALE -->
|
||||
<div class="form-group mb-3">
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
|
||||
@@ -9,7 +9,6 @@ import CitizenDashboard from '~/components/dashboard/CitizenDashboard.vue'
|
||||
import OperatorDashboard from '~/components/dashboard/OperatorDashboard.vue'
|
||||
import AdminDashboard from '~/components/dashboard/AdminDashboard.vue'
|
||||
|
||||
// Normalizziamo in uppercase nel caso il backend risponda con 'citizen' o 'CITIZEN'
|
||||
const currentDashboard = computed(() => {
|
||||
const role = user.value?.role?.toUpperCase()
|
||||
if (role === 'CITIZEN') return CitizenDashboard
|
||||
@@ -18,25 +17,24 @@ const currentDashboard = computed(() => {
|
||||
return null
|
||||
})
|
||||
|
||||
// Testi dinamici per l'Hero (la fascia blu in alto) in base al ruolo
|
||||
const heroTitle = computed(() => {
|
||||
const role = user.value?.role?.toUpperCase()
|
||||
if (role === 'ADMIN') return 'Pannello di Amministrazione'
|
||||
if (role === 'OPERATOR') return 'Gestione Operativa'
|
||||
return 'Le Mie Segnalazioni' // Cittadino
|
||||
return 'Dashboard'
|
||||
})
|
||||
|
||||
const heroSubtitle = computed(() => {
|
||||
const role = user.value?.role?.toUpperCase()
|
||||
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'
|
||||
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>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="it-hero-wrapper it-primary">
|
||||
<div class="it-hero-wrapper it-primary hero-compact">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12 text-center">
|
||||
@@ -53,7 +51,7 @@ const heroSubtitle = computed(() => {
|
||||
|
||||
<div class="container my-5">
|
||||
|
||||
<div class="row mb-4">
|
||||
<div class="row mb-3">
|
||||
<div class="col-12">
|
||||
<h4 class="mb-2 text-muted">
|
||||
Benvenuto, <strong>{{ user?.first_name }} {{ user?.last_name }}</strong>.
|
||||
@@ -61,7 +59,6 @@ const heroSubtitle = computed(() => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Qui viene montato CitizenDashboard.vue, OperatorDashboard.vue o AdminDashboard.vue -->
|
||||
<component :is="currentDashboard" v-if="currentDashboard" />
|
||||
|
||||
<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">
|
||||
import type { Report } from '~/types/api'
|
||||
|
||||
definePageMeta({
|
||||
middleware: 'auth',
|
||||
auth: { role: 'OPERATOR' }
|
||||
@@ -19,7 +17,6 @@ const { data: reportsRes, pending } = useAsyncData(
|
||||
{ watch: [page] }
|
||||
)
|
||||
|
||||
const formatDate = (dateString: string) => new Date(dateString).toLocaleDateString('it-IT')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -41,36 +38,11 @@ const formatDate = (dateString: string) => new Date(dateString).toLocaleDateStri
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<div 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">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>
|
||||
<ReportTable
|
||||
:reports="reportsRes.reports"
|
||||
detail-base-path="/operator/reports"
|
||||
action-label="Apri segnalazione"
|
||||
/>
|
||||
|
||||
<Pagination
|
||||
v-if="reportsRes.total > perPage"
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
<script setup lang="ts">
|
||||
import type { Report } from '~/types/api'
|
||||
|
||||
definePageMeta({
|
||||
middleware: 'auth',
|
||||
auth: { role: 'OPERATOR' }
|
||||
@@ -23,15 +21,13 @@ const handleTakeReport = async (id: number) => {
|
||||
try {
|
||||
const res = await takeReport(id)
|
||||
if (res.ok) {
|
||||
// Optimistic update or refresh
|
||||
await refresh()
|
||||
await refresh()
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to take report', err)
|
||||
}
|
||||
}
|
||||
|
||||
const formatDate = (dateString: string) => new Date(dateString).toLocaleDateString('it-IT')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -53,36 +49,12 @@ const formatDate = (dateString: string) => new Date(dateString).toLocaleDateStri
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<div 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">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>
|
||||
<ReportTable
|
||||
:reports="reportsRes.reports"
|
||||
action-variant="manage"
|
||||
detail-base-path="/operator/reports"
|
||||
@take="handleTakeReport"
|
||||
/>
|
||||
|
||||
<Pagination
|
||||
v-if="reportsRes.total > perPage"
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
<script setup lang="ts">
|
||||
import type { Report } from '~/types/api'
|
||||
|
||||
definePageMeta({
|
||||
middleware: 'auth',
|
||||
auth: { role: 'OPERATOR' }
|
||||
@@ -19,7 +17,6 @@ const { data: reportsRes, pending } = useAsyncData(
|
||||
{ watch: [page] }
|
||||
)
|
||||
|
||||
const formatDate = (dateString: string) => new Date(dateString).toLocaleDateString('it-IT')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -41,36 +38,11 @@ const formatDate = (dateString: string) => new Date(dateString).toLocaleDateStri
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<div 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">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>
|
||||
<ReportTable
|
||||
:reports="reportsRes.reports"
|
||||
detail-base-path="/operator/reports"
|
||||
action-label="Apri segnalazione"
|
||||
/>
|
||||
|
||||
<Pagination
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[Auth Plugin] Initialized - User:', auth.user.value?.id || 'None', 'Authenticated:', auth.isAuthenticated.value)
|
||||
})
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user