feat: implement citizen report creation, detail view, and chat system

- Create report creation page with step-by-step media uploads and categories loading
- Create report details view page with history events log and messaging section
- Implement MessageComposer and MessageList components for chat workflow
- Implement MediaUploader with drag-and-drop support, file type, and size validation
- Implement Pagination component with dynamic buttons and ellipsis support
- Implement user profile settings summary page and notifications listing page
This commit is contained in:
2026-06-12 19:04:19 +02:00 Unverified
parent 4c6367b328
commit 47327d3a69
9 changed files with 1800 additions and 0 deletions
+259
View File
@@ -0,0 +1,259 @@
<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;
}
.display-3 {
font-size: 3.5rem;
line-height: 1;
}
.table-hover tbody tr:hover {
background-color: rgba(0, 111, 157, 0.05);
}
</style>
+227
View File
@@ -0,0 +1,227 @@
<script setup lang="ts">
definePageMeta({
middleware: 'auth',
})
useHead({ title: 'Notifiche - Visione' })
const { fetchNotifications, markNotificationRead } = useAuth()
const router = useRouter()
const page = ref(1)
const perPage = ref(15)
const { data: notificationsData, refresh: refreshNotifications } = await useAsyncData(
'notifications',
() => fetchNotifications({ page: page.value, per_page: perPage.value })
)
const notifications = computed(() => notificationsData.value?.notifications || [])
const totalNotifications = computed(() => notificationsData.value?.notifications?.length || 0)
const unreadCount = computed(() => notifications.value.filter((n: any) => !n.read_at).length)
const handlePageChange = (newPage: number) => {
page.value = newPage
}
const handleMarkAsRead = async (notificationId: number) => {
try {
await markNotificationRead(notificationId)
await refreshNotifications()
} catch (error) {
console.error('Error marking notification as read:', error)
}
}
const handleNotificationClick = async (notification: any) => {
if (!notification.read_at) {
await handleMarkAsRead(notification.id)
}
if (notification.report_id) {
router.push(`/reports/${notification.report_id}`)
}
}
const formatDate = (dateString: string): string => {
const date = new Date(dateString)
const now = new Date()
const diffMs = now.getTime() - date.getTime()
const diffMins = Math.floor(diffMs / 60000)
const diffHours = Math.floor(diffMins / 60)
const diffDays = Math.floor(diffHours / 24)
if (diffMins < 1) return 'Proprio ora'
if (diffMins < 60) return `${diffMins}m fa`
if (diffHours < 24) return `${diffHours}h fa`
if (diffDays < 7) return `${diffDays}d fa`
return date.toLocaleDateString('it-IT', {
day: '2-digit',
month: '2-digit',
year: '2-digit'
})
}
const getNotificationIcon = (type: string): string => {
const icons: Record<string, string> = {
'report_created': 'it-pencil',
'report_assigned': 'it-check-circle',
'report_updated': 'it-refresh',
'report_resolved': 'it-check-circle',
'message_received': 'it-comment',
'feedback_requested': 'it-star',
'reward_earned': 'it-star',
}
return icons[type] || 'it-bell'
}
const getNotificationColor = (type: string): string => {
const colors: Record<string, string> = {
'report_created': 'primary',
'report_assigned': 'info',
'report_updated': 'info',
'report_resolved': 'success',
'message_received': 'primary',
'feedback_requested': 'warning',
'reward_earned': 'warning',
}
return colors[type] || 'secondary'
}
</script>
<template>
<div>
<!-- HERO -->
<div class="bg-light border-bottom py-4 mb-4">
<div class="container">
<div class="row">
<div class="col-12">
<h1 class="h3 font-weight-bold mb-2">
<BIIcon name="it-bell" size="sm" color="primary" class="me-2" />
Centro Notifiche
</h1>
<p class="text-muted mb-0">
{{ unreadCount > 0 ? `Hai ${unreadCount} notifiche non lette` : 'Tutte le notifiche lette' }}
</p>
</div>
</div>
</div>
</div>
<!-- CONTENUTO -->
<div class="container mb-5">
<div class="row">
<div class="col-12 col-lg-10 offset-lg-1">
<!-- FILTRI (Opzionale) -->
<div class="d-flex gap-2 mb-4">
<button class="btn btn-sm btn-outline-primary active">Tutti</button>
<button class="btn btn-sm btn-outline-secondary">Solo non letti</button>
</div>
<!-- LISTA NOTIFICHE -->
<div v-if="notifications.length === 0" class="text-center py-5">
<BIIcon name="it-bell" size="xl" color="info" class="mb-3 d-block" />
<h6 class="text-muted">Nessuna notifica</h6>
<p class="small text-muted">Quando riceverai nuovi aggiornamenti sulle tue segnalazioni, appariranno qui.</p>
</div>
<div v-else class="list-group">
<button
v-for="notification in notifications"
:key="notification.id"
type="button"
class="list-group-item list-group-item-action border-0 p-3 mb-3 rounded-2"
:class="{
'bg-light': !notification.read_at,
'bg-white': notification.read_at,
}"
@click="handleNotificationClick(notification)"
>
<div class="d-flex align-items-start gap-3">
<!-- ICONA -->
<div class="flex-shrink-0 mt-1">
<div class="rounded-circle p-2" :class="`bg-${getNotificationColor(notification.type)}`" style="width: 40px; height: 40px; display: flex; align-items: center; justify-content: center;">
<BIIcon :name="getNotificationIcon(notification.type)" size="sm" color="white" />
</div>
</div>
<!-- CONTENUTO -->
<div class="flex-grow-1">
<div class="d-flex align-items-start justify-content-between">
<div>
<h6 class="mb-1 font-weight-bold" :class="{ 'text-dark': !notification.read_at }">
{{ notification.title }}
</h6>
<p class="mb-1 small" :class="{ 'text-muted': notification.read_at }">
{{ notification.body }}
</p>
<small class="text-muted">
{{ formatDate(notification.created_at) }}
</small>
</div>
<!-- INDICATORE NON LETTO -->
<div v-if="!notification.read_at" class="flex-shrink-0">
<span class="badge bg-primary rounded-pill">Nuovo</span>
</div>
</div>
<!-- AZIONI RAPIDE -->
<div v-if="notification.report_id" class="mt-2 d-flex gap-2">
<NuxtLink :to="`/reports/${notification.report_id}`" class="btn btn-sm btn-outline-primary">
<BIIcon name="it-arrow-right" size="xs" class="me-1" />
Vedi segnalazione
</NuxtLink>
<button
v-if="!notification.read_at"
type="button"
class="btn btn-sm btn-outline-secondary"
@click.stop="handleMarkAsRead(notification.id)"
>
Segna come letta
</button>
</div>
</div>
</div>
</button>
</div>
<!-- PAGINATION -->
<Pagination
v-if="notifications.length > 0"
:current-page="page"
:per-page="perPage"
:total-items="totalNotifications"
@page-change="handlePageChange"
/>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.list-group-item {
border: 1px solid #e9ecef;
transition: all 0.2s ease;
cursor: pointer;
&:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transform: translateY(-1px);
}
&.bg-light {
border-left: 4px solid #006f9d;
}
}
.rounded-2 {
border-radius: 0.5rem;
}
</style>
+400
View File
@@ -0,0 +1,400 @@
<script setup lang="ts">
definePageMeta({
middleware: 'auth',
})
useHead({ title: 'Dettaglio Segnalazione - Visione' })
const runtimeConfig = useRuntimeConfig()
const route = useRoute()
const { getReportDetail, getMessages, addMessage, addFeedback, flagReport } = useReports()
const { user } = useAuth()
const reportId = Number(route.params.id)
if (!reportId) {
navigateTo('/')
}
// Fetch dati
const { data: reportData, refresh: refreshReport } = await useAsyncData(`report-${reportId}`, () =>
getReportDetail(reportId)
)
const { data: messagesData, refresh: refreshMessages } = await useAsyncData(`messages-${reportId}`, () =>
getMessages(reportId)
)
const report = computed(() => (reportData.value as any)?.report)
const media = computed(() => (reportData.value as any)?.media || [])
const statusHistory = computed(() => (reportData.value as any)?.history || [])
const messages = computed(() => (messagesData.value as any)?.messages || [])
const newMessage = ref('')
const isSubmittingMessage = ref(false)
const isFeedbackOpen = ref(false)
const feedbackRating = ref(5)
const feedbackComment = ref('')
const isSubmittingFeedback = ref(false)
const flagMessage = ref('')
const isFlagging = ref(false)
const selectedMediaIndex = ref(0)
const isGalleryOpen = ref(false)
// Status badge color
const getStatusColor = (status: string): string => {
const colors: Record<string, string> = {
OPEN: 'primary',
UNDER_REVIEW: 'info',
ASSIGNED: 'warning',
IN_PROGRESS: 'warning',
RESOLVED: 'success',
REJECTED: 'danger',
CLOSED: 'secondary',
}
return colors[status] || 'secondary'
}
// Status label italiano
const getStatusLabel = (status: string): string => {
const labels: Record<string, string> = {
OPEN: 'Aperta',
UNDER_REVIEW: 'In revisione',
ASSIGNED: 'Assegnata',
IN_PROGRESS: 'In corso',
RESOLVED: 'Risolta',
REJECTED: 'Rifiutata',
CLOSED: 'Chiusa',
}
return labels[status] || status
}
// Invia messaggio
const handleSendMessage = async () => {
if (!newMessage.value.trim()) return
isSubmittingMessage.value = true
try {
await addMessage(reportId, { body: newMessage.value })
newMessage.value = ''
await refreshMessages()
} catch (error) {
console.error('Error sending message:', error)
} finally {
isSubmittingMessage.value = false
}
}
// Invia feedback (solo se risolta)
const handleSubmitFeedback = async () => {
isSubmittingFeedback.value = true
try {
await addFeedback(reportId, {
rating: feedbackRating.value,
comment: feedbackComment.value || undefined,
})
isFeedbackOpen.value = false
feedbackRating.value = 5
feedbackComment.value = ''
await refreshReport()
} catch (error) {
console.error('Error submitting feedback:', error)
} finally {
isSubmittingFeedback.value = false
}
}
// Segnala abuso
const handleFlag = async () => {
if (!flagMessage.value.trim()) return
isFlagging.value = true
try {
await flagReport(reportId, {
reason: flagMessage.value,
})
flagMessage.value = ''
// Mostra messaggio di successo
} catch (error) {
console.error('Error flagging report:', error)
} finally {
isFlagging.value = false
}
}
const isOwner = computed(() => report.value?.user_id === user.value?.id)
const canFeedback = computed(() => report.value?.status === 'RESOLVED' || report.value?.status === 'CLOSED')
const formatDate = (dateString: string): string => {
return new Date(dateString).toLocaleDateString('it-IT', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
})
}
</script>
<template>
<div v-if="report">
<!-- HEADER -->
<div class="bg-light border-bottom py-4">
<div class="container">
<div class="row">
<div class="col-12 col-md-8">
<h1 class="h3 font-weight-bold mb-2">{{ report.category_name }}</h1>
<div class="d-flex align-items-center gap-3">
<span class="badge" :class="`bg-${getStatusColor(report.status)}`">
{{ getStatusLabel(report.status) }}
</span>
<small class="text-muted">
<BIIcon name="it-clock" size="xs" class="me-1" />
Creata il {{ formatDate(report.created_at) }}
</small>
</div>
</div>
</div>
</div>
</div>
<!-- CONTENUTO PRINCIPALE -->
<div class="container my-5">
<div class="row">
<!-- SINISTRA: Dettagli segnalazione -->
<div class="col-12 col-lg-8 mb-4">
<!-- INDIRIZZO E COORDINATE -->
<div class="card card-bg mb-4">
<div class="card-body">
<h6 class="card-title font-weight-bold mb-3">
<BIIcon name="it-map-marker-circle" size="sm" color="primary" class="me-2" />
Posizione
</h6>
<div class="mb-2">
<strong>{{ report.address }}</strong>
</div>
<small class="text-muted d-block">
Coordinate: {{ report.latitude }}, {{ report.longitude }}
</small>
<small v-if="report.area_name" class="text-muted d-block">
Area: {{ report.area_name }}
</small>
</div>
</div>
<!-- DESCRIZIONE -->
<div class="card card-bg mb-4">
<div class="card-body">
<h6 class="card-title font-weight-bold mb-3">
<BIIcon name="it-write" size="sm" color="primary" class="me-2" />
Descrizione
</h6>
<p class="card-text text-break">{{ report.description }}</p>
</div>
</div>
<!-- GALLERY MEDIA -->
<div v-if="media.length > 0" class="card card-bg mb-4">
<div class="card-body">
<h6 class="card-title font-weight-bold mb-3">
<BIIcon name="it-camera" size="sm" color="primary" class="me-2" />
Allegati ({{ media.length }})
</h6>
<div class="row">
<div v-for="(m, idx) in media" :key="m.id" class="col-6 col-md-4 mb-3">
<div v-if="m.media_kind === 'IMAGE'" class="ratio ratio-1x1 bg-light rounded overflow-hidden">
<img :src="runtimeConfig.public.api + m.url" :alt="`Allegato ${idx + 1}`" class="object-fit-cover" />
</div>
<div v-else class="ratio ratio-1x1 bg-dark rounded d-flex align-items-center justify-content-center">
<BIIcon name="it-video" size="xl" color="white" />
</div>
</div>
</div>
</div>
</div>
<!-- CRONOLOGIA STATI -->
<div v-if="statusHistory.length > 0" class="card card-bg mb-4">
<div class="card-body">
<h6 class="card-title font-weight-bold mb-4">
<BIIcon name="it-history" size="sm" color="primary" class="me-2" />
Cronologia
</h6>
<div class="timeline">
<div v-for="item in statusHistory" :key="item.id" class="timeline-item mb-3">
<div class="d-flex gap-3">
<div class="flex-shrink-0">
<div class="timeline-marker"></div>
</div>
<div class="flex-grow-1">
<small class="text-muted">{{ formatDate(item.created_at) }}</small>
<div class="small font-weight-semibold">
{{ getStatusLabel(item.old_status) }} → {{ getStatusLabel(item.new_status) }}
</div>
<small v-if="item.note" class="text-muted d-block">{{ item.note }}</small>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- MESSAGGI -->
<div class="card card-bg">
<div class="card-body">
<h6 class="card-title font-weight-bold mb-3">
<BIIcon name="it-comment" size="sm" color="primary" class="me-2" />
Chat ({{ messages.length }})
</h6>
<MessageList :messages="messages" />
<MessageComposer @send="(msg) => { newMessage = msg; handleSendMessage() }" />
</div>
</div>
</div>
<!-- DESTRA: Sidebar azioni -->
<div class="col-12 col-lg-4">
<!-- FEEDBACK (se risolta) -->
<div v-if="canFeedback && isOwner" class="card card-bg card-highlight mb-4">
<div class="card-body">
<h6 class="card-title font-weight-bold mb-3">
<BIIcon name="it-star-outline" size="sm" color="primary" class="me-2" />
Lascia un feedback
</h6>
<button
v-if="!isFeedbackOpen"
type="button"
class="btn btn-primary w-100"
@click="isFeedbackOpen = true"
>
Valuta l'intervento
</button>
<template v-else>
<div class="mb-3">
<label class="form-label">Valutazione</label>
<div class="d-flex gap-2">
<button v-for="n in 5" :key="n" type="button" class="btn btn-link p-0"
@click="feedbackRating = n" :aria-label="`${n} stelle`">
<BIIcon
:name="n <= feedbackRating ? 'it-star' : 'it-star-outline'"
size="lg"
:color="n <= feedbackRating ? 'warning' : 'primary'"
/>
</button>
</div>
</div>
<div class="mb-3">
<textarea
v-model="feedbackComment"
class="form-control"
placeholder="Commento (opzionale)"
rows="3"
/>
</div>
<div class="d-grid gap-2">
<button type="button" class="btn btn-primary" @click="handleSubmitFeedback" :disabled="isSubmittingFeedback">
<span v-if="isSubmittingFeedback" class="spinner-border spinner-border-sm me-2"></span>
Invia
</button>
<button type="button" class="btn btn-light" @click="isFeedbackOpen = false">
Cancella
</button>
</div>
</template>
</div>
</div>
<!-- SEGNALA ABUSO -->
<div class="card card-bg">
<div class="card-body">
<h6 class="card-title font-weight-bold mb-3">
<BIIcon name="it-flag" size="sm" color="danger" class="me-2" />
Segnala abuso
</h6>
<textarea
v-model="flagMessage"
class="form-control form-control-sm mb-2"
placeholder="Specifica il motivo dell'abuso..."
rows="2"
/>
<button
type="button"
class="btn btn-sm btn-outline-danger w-100"
@click="handleFlag"
:disabled="isFlagging || !flagMessage.trim()"
>
<BIIcon name="it-flag" size="xs" class="me-1" />
Invia segnalazione
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- LOADING STATE -->
<div v-else class="container my-5 text-center">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Caricamento...</span>
</div>
</div>
</template>
<style scoped>
.card-highlight {
border-color: #006f9d;
}
.timeline-marker {
width: 12px;
height: 12px;
background-color: #006f9d;
border-radius: 50%;
border: 3px solid #f0f0f0;
}
.timeline-item:not(:last-child) .timeline-marker::after {
content: '';
position: absolute;
left: 50%;
top: 20px;
width: 2px;
height: calc(100% + 20px);
background-color: #006f9d;
transform: translateX(-50%);
}
.ratio {
position: relative;
display: block;
width: 100%;
aspect-ratio: 1;
img {
position: absolute;
top: 0;
left: 0;
}
}
.object-fit-cover {
object-fit: cover !important;
width: 100%;
height: 100%;
}
</style>
+149
View File
@@ -0,0 +1,149 @@
<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 { user } = useAuth()
// Fetch categories
const { data: categoriesData } = await useAsyncData('categories', () =>
$fetch('/api/v1/admin/categories')
)
const categories = computed(() => (categoriesData.value as any)?.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">
<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>
<style scoped>
.it-primary-bg {
background: linear-gradient(135deg, #006f9d 0%, #003d5c 100%);
}
</style>