feat: update authentication flow and user navigation

This commit is contained in:
2026-06-13 17:15:59 +02:00 Unverified
parent ab6455345e
commit a66339a7fc
18 changed files with 662 additions and 759 deletions
+71
View File
@@ -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;
}
} }
+10 -4
View File
@@ -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">
+90
View File
@@ -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>
+58 -200
View File
@@ -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>
+31 -33
View File
@@ -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>
+2 -4
View File
@@ -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,
} }
} }
+2 -2
View File
@@ -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')
} }
}) })
+4 -8
View File
@@ -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>
+4 -7
View File
@@ -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">
-253
View File
@@ -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>
+5 -33
View File
@@ -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"
+7 -35
View File
@@ -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"
+5 -33
View File
@@ -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"
+156
View File
@@ -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>
-144
View File
@@ -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>
+115
View File
@@ -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>
+102
View File
@@ -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>
-3
View File
@@ -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)
}) })