feat: update authentication flow and user navigation
This commit is contained in:
@@ -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,47 +14,45 @@ 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>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user