- 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
299 lines
8.5 KiB
Vue
299 lines
8.5 KiB
Vue
<script setup lang="ts">
|
|
import type { Category, CreateReportRequest } from '~/types/api'
|
|
|
|
interface Props {
|
|
isLoading?: boolean
|
|
categories?: Category[]
|
|
}
|
|
|
|
interface Emits {
|
|
(e: 'submit', report: CreateReportRequest, media: File[]): void
|
|
}
|
|
|
|
const props = withDefaults(defineProps<Props>(), {
|
|
isLoading: false,
|
|
categories: () => []
|
|
})
|
|
|
|
const emit = defineEmits<Emits>()
|
|
|
|
const form = ref({
|
|
category_id: null as number | null,
|
|
latitude: null as number | null,
|
|
longitude: null as number | null,
|
|
address: '',
|
|
description: '',
|
|
})
|
|
|
|
const media = ref<File[]>([])
|
|
const errors = ref<Record<string, string>>({})
|
|
const successMessage = ref(false)
|
|
const isGettingLocation = ref(false)
|
|
|
|
const validateForm = (): boolean => {
|
|
errors.value = {}
|
|
|
|
if (!form.value.category_id) {
|
|
errors.value.category_id = 'Seleziona una categoria'
|
|
}
|
|
if (!form.value.latitude || !form.value.longitude) {
|
|
errors.value.location = 'Imposta la posizione'
|
|
}
|
|
if (!form.value.address?.trim()) {
|
|
errors.value.address = 'Inserisci l\'indirizzo'
|
|
}
|
|
if (!form.value.description?.trim() || form.value.description.length < 10) {
|
|
errors.value.description = 'Descrivi il problema (minimo 10 caratteri)'
|
|
}
|
|
|
|
return Object.keys(errors.value).length === 0
|
|
}
|
|
|
|
const getGeolocation = async () => {
|
|
if (!navigator.geolocation) {
|
|
errors.value.location = 'Geolocalizzazione non disponibile nel tuo browser'
|
|
return
|
|
}
|
|
|
|
isGettingLocation.value = true
|
|
errors.value.location = ''
|
|
|
|
try {
|
|
const position = await new Promise<GeolocationPosition>((resolve, reject) => {
|
|
navigator.geolocation.getCurrentPosition(resolve, reject)
|
|
})
|
|
|
|
form.value.latitude = Number(position.coords.latitude.toFixed(6))
|
|
form.value.longitude = Number(position.coords.longitude.toFixed(6))
|
|
|
|
// Richiedi indirizzo da coordinate (reverse geocoding)
|
|
// Se il backend non lo fa, userà le coord così come sono
|
|
} catch (error: any) {
|
|
errors.value.location = 'Impossibile ottenere la posizione. Inserisci manualmente.'
|
|
} finally {
|
|
isGettingLocation.value = false
|
|
}
|
|
}
|
|
|
|
const handleSubmit = async () => {
|
|
if (!validateForm()) return
|
|
|
|
const reportData: CreateReportRequest = {
|
|
category_id: form.value.category_id!,
|
|
latitude: form.value.latitude!,
|
|
longitude: form.value.longitude!,
|
|
address: form.value.address,
|
|
description: form.value.description,
|
|
}
|
|
|
|
emit('submit', reportData, media.value)
|
|
}
|
|
|
|
const resetForm = () => {
|
|
form.value = {
|
|
category_id: null,
|
|
latitude: null,
|
|
longitude: null,
|
|
address: '',
|
|
description: '',
|
|
}
|
|
media.value = []
|
|
errors.value = {}
|
|
}
|
|
|
|
// Osserva cambio categoria
|
|
watch(() => form.value.category_id, () => {
|
|
if (errors.value.category_id) {
|
|
errors.value.category_id = ''
|
|
}
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<div class="report-form">
|
|
<!-- Successo -->
|
|
<div v-if="successMessage" class="alert alert-success alert-dismissible fade show" role="alert">
|
|
Segnalazione creata con successo! Reindirizzamento...
|
|
<button type="button" class="btn-close" @click="successMessage = false"></button>
|
|
</div>
|
|
|
|
<form @submit.prevent="handleSubmit">
|
|
<!-- CATEGORIA -->
|
|
<div class="mb-4">
|
|
<label for="category" class="form-label fw-bold">
|
|
<BIIcon name="it-list" size="sm" color="primary" class="me-2" />
|
|
Categoria del problema *
|
|
</label>
|
|
<select
|
|
id="category"
|
|
v-model.number="form.category_id"
|
|
class="form-select"
|
|
:class="{ 'is-invalid': errors.category_id }"
|
|
required
|
|
>
|
|
<option :value="null">-- Seleziona una categoria --</option>
|
|
<option v-for="cat in categories" :key="cat.id" :value="cat.id">
|
|
{{ cat.name }}
|
|
</option>
|
|
</select>
|
|
<div v-if="errors.category_id" class="invalid-feedback">
|
|
{{ errors.category_id }}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- GEOLOCALIZZAZIONE -->
|
|
<div class="mb-4">
|
|
<label class="form-label fw-bold">
|
|
<BIIcon name="it-map-marker-circle" size="sm" color="primary" class="me-2" />
|
|
Posizione *
|
|
</label>
|
|
|
|
<div v-if="form.latitude && form.longitude" class="alert alert-info mb-3">
|
|
<strong>Coordinate:</strong> {{ form.latitude }}, {{ form.longitude }}
|
|
<button type="button" class="btn btn-sm btn-link float-end" @click="form.latitude = null; form.longitude = null">
|
|
Cancella
|
|
</button>
|
|
</div>
|
|
|
|
<button
|
|
type="button"
|
|
class="btn btn-outline-primary w-100 mb-3"
|
|
@click="getGeolocation"
|
|
:disabled="isGettingLocation"
|
|
>
|
|
<span v-if="isGettingLocation" class="spinner-border spinner-border-sm me-2"></span>
|
|
<BIIcon v-else name="it-location" size="sm" class="me-2" />
|
|
{{ isGettingLocation ? 'Localizzazione in corso...' : 'Usa posizione attuale' }}
|
|
</button>
|
|
|
|
<div class="alert alert-warning mb-3 small" role="alert">
|
|
Se la geolocalizzazione non funziona, inserisci manualmente coordinate o indirizzo approssimativo.
|
|
</div>
|
|
|
|
<div class="row">
|
|
<div class="col-md-6 mb-3">
|
|
<input
|
|
v-model.number="form.latitude"
|
|
type="number"
|
|
step="0.000001"
|
|
class="form-control"
|
|
:class="{ 'is-invalid': errors.location }"
|
|
placeholder="Latitudine (es. 45.5)"
|
|
required
|
|
/>
|
|
</div>
|
|
<div class="col-md-6 mb-3">
|
|
<input
|
|
v-model.number="form.longitude"
|
|
type="number"
|
|
step="0.000001"
|
|
class="form-control"
|
|
:class="{ 'is-invalid': errors.location }"
|
|
placeholder="Longitudine (es. 10.2)"
|
|
required
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div v-if="errors.location" class="invalid-feedback d-block">
|
|
{{ errors.location }}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- INDIRIZZO -->
|
|
<div class="mb-4">
|
|
<label for="address" class="form-label fw-bold">
|
|
<BIIcon name="it-home" size="sm" color="primary" class="me-2" />
|
|
Indirizzo *
|
|
</label>
|
|
<input
|
|
id="address"
|
|
v-model="form.address"
|
|
type="text"
|
|
class="form-control"
|
|
:class="{ 'is-invalid': errors.address }"
|
|
placeholder="Es. Via Roma 123, Milano"
|
|
required
|
|
/>
|
|
<div v-if="errors.address" class="invalid-feedback">
|
|
{{ errors.address }}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- DESCRIZIONE -->
|
|
<div class="mb-4">
|
|
<label for="description" class="form-label fw-bold">
|
|
<BIIcon name="it-write" size="sm" color="primary" class="me-2" />
|
|
Descrizione del problema *
|
|
</label>
|
|
<textarea
|
|
id="description"
|
|
v-model="form.description"
|
|
class="form-control"
|
|
:class="{ 'is-invalid': errors.description }"
|
|
rows="5"
|
|
placeholder="Descrivi dettagliatamente il problema (tipo buca stradale, pericolo, ecc.)"
|
|
required
|
|
/>
|
|
<small class="form-text text-muted d-block mt-1">
|
|
{{ form.description.length }} caratteri (minimo 10)
|
|
</small>
|
|
<div v-if="errors.description" class="invalid-feedback d-block">
|
|
{{ errors.description }}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- MEDIA -->
|
|
<div class="mb-4">
|
|
<label class="form-label fw-bold">
|
|
<BIIcon name="it-camera" size="sm" color="primary" class="me-2" />
|
|
Allega foto/video (opzionale)
|
|
</label>
|
|
<MediaUploader v-model="media" :max-files="5" :max-file-size="50" />
|
|
</div>
|
|
|
|
<!-- BOTTONI -->
|
|
<div class="d-grid gap-2 d-md-flex justify-content-md-between">
|
|
<button
|
|
type="button"
|
|
class="btn btn-outline-secondary"
|
|
@click="resetForm"
|
|
:disabled="isLoading"
|
|
>
|
|
Annulla
|
|
</button>
|
|
<button
|
|
type="submit"
|
|
class="btn btn-primary btn-lg"
|
|
:disabled="isLoading"
|
|
>
|
|
<span v-if="isLoading" class="spinner-border spinner-border-sm me-2"></span>
|
|
{{ isLoading ? 'Invio segnalazione...' : 'Invia segnalazione' }}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.report-form {
|
|
background-color: #fff;
|
|
border-radius: 0.5rem;
|
|
padding: 2rem;
|
|
}
|
|
|
|
textarea {
|
|
font-family: inherit;
|
|
resize: vertical;
|
|
}
|
|
|
|
.form-label {
|
|
margin-bottom: 0.75rem;
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
</style>
|
|
|
|
|
|
|
|
|