feat: enhance message handling and UI improvements across components
This commit is contained in:
@@ -64,7 +64,7 @@ const removeFile = (idx: number) => {
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-outline-secondary btn-icon composer-attach"
|
class="btn btn-outline-primary btn-icon composer-attach"
|
||||||
title="Allega file"
|
title="Allega file"
|
||||||
@click="fileInputRef?.click()"
|
@click="fileInputRef?.click()"
|
||||||
:disabled="isSubmitting || files.length >= 5"
|
:disabled="isSubmitting || files.length >= 5"
|
||||||
@@ -113,9 +113,9 @@ const removeFile = (idx: number) => {
|
|||||||
|
|
||||||
.message-file-chip {
|
.message-file-chip {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background: #f2f7fc;
|
background: #fff;
|
||||||
border: 1px solid #d9e3ee;
|
border: 1px solid #d9e3ee;
|
||||||
border-radius: 999px;
|
border-radius: 4px;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
@@ -130,7 +130,7 @@ const removeFile = (idx: number) => {
|
|||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border: 1px solid #c5d0db;
|
border: 1px solid #c5d0db;
|
||||||
border-radius: 8px;
|
border-radius: 4px;
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
grid-template-columns: auto minmax(0, 1fr) auto;
|
grid-template-columns: auto minmax(0, 1fr) auto;
|
||||||
@@ -149,7 +149,7 @@ const removeFile = (idx: number) => {
|
|||||||
.composer-textarea {
|
.composer-textarea {
|
||||||
border: 0;
|
border: 0;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
min-height: 3rem;
|
min-height: 4rem;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
resize: vertical;
|
resize: vertical;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,12 +4,20 @@ import type { Message } from '~/types/api'
|
|||||||
interface Props {
|
interface Props {
|
||||||
messages: Message[]
|
messages: Message[]
|
||||||
isLoading?: boolean
|
isLoading?: boolean
|
||||||
|
canFlagMessage?: (message: Message) => boolean
|
||||||
|
isFlaggingMessageId?: number | null
|
||||||
}
|
}
|
||||||
|
|
||||||
withDefaults(defineProps<Props>(), {
|
withDefaults(defineProps<Props>(), {
|
||||||
isLoading: false
|
isLoading: false,
|
||||||
|
canFlagMessage: () => false,
|
||||||
|
isFlaggingMessageId: null,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'flag', message: Message): void
|
||||||
|
}>()
|
||||||
|
|
||||||
const formatDate = (dateString: string): string => {
|
const formatDate = (dateString: string): string => {
|
||||||
const date = new Date(dateString)
|
const date = new Date(dateString)
|
||||||
const today = new Date()
|
const today = new Date()
|
||||||
@@ -81,6 +89,19 @@ const senderInitials = (msg: Message): string =>
|
|||||||
<div class="message-body">
|
<div class="message-body">
|
||||||
{{ msg.body }}
|
{{ msg.body }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="canFlagMessage(msg)" class="message-actions">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-link btn-xs px-0"
|
||||||
|
:disabled="isFlaggingMessageId === msg.id"
|
||||||
|
@click="emit('flag', msg)"
|
||||||
|
>
|
||||||
|
<span v-if="isFlaggingMessageId === msg.id" class="spinner-border spinner-border-sm me-1" aria-hidden="true"></span>
|
||||||
|
<BIIcon v-else name="it-flag" size="xs" class="me-1" />
|
||||||
|
Segnala messaggio
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -89,9 +110,8 @@ const senderInitials = (msg: Message): string =>
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.message-list {
|
.message-list {
|
||||||
background: linear-gradient(180deg, #f7fafc 0%, #ffffff 100%);
|
|
||||||
border: 1px solid #d9e3ee;
|
border: 1px solid #d9e3ee;
|
||||||
border-radius: 8px;
|
border-radius: 4px;
|
||||||
max-height: 34rem;
|
max-height: 34rem;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
@@ -127,7 +147,7 @@ const senderInitials = (msg: Message): string =>
|
|||||||
|
|
||||||
.message-avatar {
|
.message-avatar {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background: #dceeff;
|
background: #e6f0fa;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
color: #17324d;
|
color: #17324d;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
@@ -139,14 +159,14 @@ const senderInitials = (msg: Message): string =>
|
|||||||
}
|
}
|
||||||
|
|
||||||
.message-bubble-content {
|
.message-bubble-content {
|
||||||
border-radius: 8px;
|
border-radius: 4px;
|
||||||
max-width: min(100%, 40rem);
|
max-width: min(100%, 40rem);
|
||||||
padding: 0.875rem 1rem;
|
padding: 0.875rem 1rem;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-bubble-incoming {
|
.message-bubble-incoming {
|
||||||
background: #fff;
|
background: #f5f7f9;
|
||||||
border: 1px solid #d9e3ee;
|
border: 1px solid #d9e3ee;
|
||||||
color: #17324d;
|
color: #17324d;
|
||||||
}
|
}
|
||||||
@@ -179,6 +199,15 @@ const senderInitials = (msg: Message): string =>
|
|||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.message-actions {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-actions .btn {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 767.98px) {
|
@media (max-width: 767.98px) {
|
||||||
.message-bubble-content {
|
.message-bubble-content {
|
||||||
max-width: calc(100% - 3rem);
|
max-width: calc(100% - 3rem);
|
||||||
|
|||||||
@@ -54,116 +54,107 @@ const handleSubmit = async () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="password-reset-page py-5">
|
<div class="py-5">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row justify-content-center">
|
<div class="row justify-content-center">
|
||||||
<div class="col-12 col-xl-10">
|
<div class="col-12 col-lg-8 col-xl-6">
|
||||||
<div class="row g-4 align-items-stretch">
|
<div v-if="user?.must_change_password" class="callout callout-highlight callout-warning mb-4">
|
||||||
<div class="col-12 col-lg-5">
|
<div class="callout-title d-flex align-items-center">
|
||||||
<section class="password-reset-intro h-100">
|
<BIIcon name="it-warning-circle" size="sm" color="warning" class="me-2" />
|
||||||
<span class="password-reset-eyebrow">Accesso protetto</span>
|
Azione richiesta
|
||||||
<h1 class="h2 mb-3">Imposta nuova password</h1>
|
|
||||||
<p class="mb-4">
|
|
||||||
Aggiorna credenziali prima di continuare. Password nuova deve essere diversa da quella attuale.
|
|
||||||
</p>
|
|
||||||
<div v-if="user?.must_change_password" class="callout callout-highlight callout-warning mb-0">
|
|
||||||
<div class="callout-title d-flex align-items-center">
|
|
||||||
<BIIcon name="it-warning-circle" size="sm" color="warning" class="me-2" />
|
|
||||||
Azione richiesta
|
|
||||||
</div>
|
|
||||||
<p class="mb-0">Per motivi di sicurezza, imposta una nuova password per il tuo account.</p>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 col-lg-7">
|
<p class="mb-0">Per motivi di sicurezza, imposta una nuova password per il tuo account.</p>
|
||||||
<div class="card card-bg shadow-sm border-0 h-100">
|
</div>
|
||||||
<div class="card-body p-4 p-lg-5">
|
|
||||||
<h2 class="card-title h4 mb-4">Cambia password</h2>
|
|
||||||
|
|
||||||
<form @submit.prevent="handleSubmit" class="d-grid gap-4">
|
<div class="card card-bg border-0 shadow-sm">
|
||||||
<div class="password-field">
|
<div class="card-body p-4 p-lg-5">
|
||||||
<label for="current_password" class="form-label">Password attuale</label>
|
<h1 class="h3 mb-3">Cambia password</h1>
|
||||||
<div class="password-field-control">
|
<p class="text-muted mb-4">
|
||||||
<BIIcon name="it-lock" size="sm" class="password-field-icon" />
|
Aggiorna credenziali prima di continuare. Password nuova deve essere diversa da quella attuale.
|
||||||
<input
|
</p>
|
||||||
id="current_password"
|
|
||||||
v-model="formData.current_password"
|
|
||||||
:type="showCurrent ? 'text' : 'password'"
|
|
||||||
class="form-control"
|
|
||||||
autocomplete="current-password"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn btn-password-toggle"
|
|
||||||
:aria-label="showCurrent ? 'Nascondi password attuale' : 'Mostra password attuale'"
|
|
||||||
@click="showCurrent = !showCurrent"
|
|
||||||
>
|
|
||||||
<BIIcon :name="showCurrent ? 'it-password-visible' : 'it-password-invisible'" size="sm" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="password-field">
|
<form @submit.prevent="handleSubmit" class="d-grid gap-4">
|
||||||
<label for="new_password" class="form-label">Nuova password</label>
|
<div class="password-field">
|
||||||
<div class="password-field-control">
|
<label for="current_password" class="form-label">Password attuale</label>
|
||||||
<BIIcon name="it-lock" size="sm" class="password-field-icon" />
|
<div class="password-field-control">
|
||||||
<input
|
<BIIcon name="it-lock" size="sm" class="password-field-icon" />
|
||||||
id="new_password"
|
<input
|
||||||
v-model="formData.new_password"
|
id="current_password"
|
||||||
:type="showNew ? 'text' : 'password'"
|
v-model="formData.current_password"
|
||||||
class="form-control"
|
:type="showCurrent ? 'text' : 'password'"
|
||||||
autocomplete="new-password"
|
class="form-control"
|
||||||
>
|
autocomplete="current-password"
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn btn-password-toggle"
|
|
||||||
:aria-label="showNew ? 'Nascondi nuova password' : 'Mostra nuova password'"
|
|
||||||
@click="showNew = !showNew"
|
|
||||||
>
|
|
||||||
<BIIcon :name="showNew ? 'it-password-visible' : 'it-password-invisible'" size="sm" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="form-text">Minimo 8 caratteri.</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="password-field">
|
|
||||||
<label for="confirm_password" class="form-label">Conferma nuova password</label>
|
|
||||||
<div class="password-field-control">
|
|
||||||
<BIIcon name="it-lock" size="sm" class="password-field-icon" />
|
|
||||||
<input
|
|
||||||
id="confirm_password"
|
|
||||||
v-model="formData.confirm_password"
|
|
||||||
:type="showConfirm ? 'text' : 'password'"
|
|
||||||
:class="{ 'is-invalid': formData.confirm_password && formData.confirm_password !== formData.new_password }"
|
|
||||||
class="form-control"
|
|
||||||
autocomplete="new-password"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn btn-password-toggle"
|
|
||||||
:aria-label="showConfirm ? 'Nascondi conferma password' : 'Mostra conferma password'"
|
|
||||||
@click="showConfirm = !showConfirm"
|
|
||||||
>
|
|
||||||
<BIIcon :name="showConfirm ? 'it-password-visible' : 'it-password-invisible'" size="sm" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="errorMessage" class="alert alert-danger mb-0" role="alert">
|
|
||||||
{{ errorMessage }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
class="btn btn-primary w-100 py-2"
|
|
||||||
:disabled="!canSubmit || isLoading"
|
|
||||||
>
|
>
|
||||||
<span v-if="isLoading" class="spinner-border spinner-border-sm me-2"></span>
|
<button
|
||||||
{{ isLoading ? 'Aggiornamento in corso' : 'Aggiorna password' }}
|
type="button"
|
||||||
|
class="btn btn-password-toggle"
|
||||||
|
:aria-label="showCurrent ? 'Nascondi password attuale' : 'Mostra password attuale'"
|
||||||
|
@click="showCurrent = !showCurrent"
|
||||||
|
>
|
||||||
|
<BIIcon :name="showCurrent ? 'it-password-visible' : 'it-password-invisible'" size="sm" />
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
<div class="password-field">
|
||||||
|
<label for="new_password" class="form-label">Nuova password</label>
|
||||||
|
<div class="password-field-control">
|
||||||
|
<BIIcon name="it-lock" size="sm" class="password-field-icon" />
|
||||||
|
<input
|
||||||
|
id="new_password"
|
||||||
|
v-model="formData.new_password"
|
||||||
|
:type="showNew ? 'text' : 'password'"
|
||||||
|
class="form-control"
|
||||||
|
autocomplete="new-password"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-password-toggle"
|
||||||
|
:aria-label="showNew ? 'Nascondi nuova password' : 'Mostra nuova password'"
|
||||||
|
@click="showNew = !showNew"
|
||||||
|
>
|
||||||
|
<BIIcon :name="showNew ? 'it-password-visible' : 'it-password-invisible'" size="sm" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="form-text">Minimo 8 caratteri.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="password-field">
|
||||||
|
<label for="confirm_password" class="form-label">Conferma nuova password</label>
|
||||||
|
<div class="password-field-control">
|
||||||
|
<BIIcon name="it-lock" size="sm" class="password-field-icon" />
|
||||||
|
<input
|
||||||
|
id="confirm_password"
|
||||||
|
v-model="formData.confirm_password"
|
||||||
|
:type="showConfirm ? 'text' : 'password'"
|
||||||
|
:class="{ 'is-invalid': formData.confirm_password && formData.confirm_password !== formData.new_password }"
|
||||||
|
class="form-control"
|
||||||
|
autocomplete="new-password"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-password-toggle"
|
||||||
|
:aria-label="showConfirm ? 'Nascondi conferma password' : 'Mostra conferma password'"
|
||||||
|
@click="showConfirm = !showConfirm"
|
||||||
|
>
|
||||||
|
<BIIcon :name="showConfirm ? 'it-password-visible' : 'it-password-invisible'" size="sm" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="errorMessage" class="alert alert-danger mb-0" role="alert">
|
||||||
|
{{ errorMessage }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="btn btn-primary w-100 py-2"
|
||||||
|
:disabled="!canSubmit || isLoading"
|
||||||
|
>
|
||||||
|
<span v-if="isLoading" class="spinner-border spinner-border-sm me-2"></span>
|
||||||
|
{{ isLoading ? 'Aggiornamento in corso' : 'Aggiorna password' }}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -173,32 +164,6 @@ const handleSubmit = async () => {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.password-reset-page {
|
|
||||||
background: linear-gradient(180deg, #f0f6fc 0%, #ffffff 65%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.password-reset-intro {
|
|
||||||
background: #17324d;
|
|
||||||
border-radius: 8px;
|
|
||||||
color: #fff;
|
|
||||||
padding: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.password-reset-intro :deep(h1),
|
|
||||||
.password-reset-intro :deep(p),
|
|
||||||
.password-reset-intro :deep(span) {
|
|
||||||
color: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
.password-reset-eyebrow {
|
|
||||||
display: inline-block;
|
|
||||||
font-size: 0.8125rem;
|
|
||||||
font-weight: 700;
|
|
||||||
letter-spacing: 0.04em;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
|
|
||||||
.password-field {
|
.password-field {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
@@ -249,10 +214,4 @@ const handleSubmit = async () => {
|
|||||||
background: rgba(0, 102, 204, 0.08);
|
background: rgba(0, 102, 204, 0.08);
|
||||||
color: #06c;
|
color: #06c;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 991.98px) {
|
|
||||||
.password-reset-intro {
|
|
||||||
padding: 1.5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { ReportStatus } from '~/types/api'
|
import type { Message, ReportStatus } from '~/types/api'
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
middleware: 'auth',
|
middleware: 'auth',
|
||||||
@@ -16,8 +16,9 @@ if (!reportId) {
|
|||||||
navigateTo('/operator')
|
navigateTo('/operator')
|
||||||
}
|
}
|
||||||
|
|
||||||
const { getReportDetail, getMessages, addMessage } = useReports()
|
const { getReportDetail, getMessages, addMessage, flagMessage } = useReports()
|
||||||
const { getInterventionSummary, updateReportStatus, assignTeam } = useOperator()
|
const { getInterventionSummary, updateReportStatus, assignTeam } = useOperator()
|
||||||
|
const { user } = useAuth()
|
||||||
|
|
||||||
// Dati principali
|
// Dati principali
|
||||||
const { data: reportData, refresh: refreshReport } = await useAsyncData(`op-report-${reportId}`, () => getReportDetail(reportId))
|
const { data: reportData, refresh: refreshReport } = await useAsyncData(`op-report-${reportId}`, () => getReportDetail(reportId))
|
||||||
@@ -53,6 +54,7 @@ const isAssigning = ref(false)
|
|||||||
|
|
||||||
// Messaggi
|
// Messaggi
|
||||||
const isSubmittingMessage = ref(false)
|
const isSubmittingMessage = ref(false)
|
||||||
|
const isFlaggingMessageId = ref<number | null>(null)
|
||||||
|
|
||||||
const availableStatuses: { value: ReportStatus, label: string }[] = [
|
const availableStatuses: { value: ReportStatus, label: string }[] = [
|
||||||
{ value: 'OPEN', label: 'Aperta' },
|
{ value: 'OPEN', label: 'Aperta' },
|
||||||
@@ -123,6 +125,26 @@ const handleSendMessage = async (msgText: string, files: File[]) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const canFlagMessage = (message: Message) => {
|
||||||
|
if (!user.value) return false
|
||||||
|
return message.user_id !== user.value.id && message.user_id === report.value?.user_id
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFlagMessage = async (message: Message) => {
|
||||||
|
if (!canFlagMessage(message)) return
|
||||||
|
|
||||||
|
isFlaggingMessageId.value = message.id
|
||||||
|
try {
|
||||||
|
await flagMessage(reportId, message.id, {
|
||||||
|
reason: 'Messaggio segnalato da operatore responsabile per verifica moderazione',
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to flag message', e)
|
||||||
|
} finally {
|
||||||
|
isFlaggingMessageId.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const formatDate = (dateString: string) => {
|
const formatDate = (dateString: string) => {
|
||||||
if (!dateString) return '-'
|
if (!dateString) return '-'
|
||||||
return new Date(dateString).toLocaleDateString('it-IT', {
|
return new Date(dateString).toLocaleDateString('it-IT', {
|
||||||
@@ -220,7 +242,12 @@ const formatDate = (dateString: string) => {
|
|||||||
</div>
|
</div>
|
||||||
<span class="badge bg-light text-dark border">{{ messages.length }} messaggi</span>
|
<span class="badge bg-light text-dark border">{{ messages.length }} messaggi</span>
|
||||||
</div>
|
</div>
|
||||||
<MessageList :messages="messages" />
|
<MessageList
|
||||||
|
:messages="messages"
|
||||||
|
:can-flag-message="canFlagMessage"
|
||||||
|
:is-flagging-message-id="isFlaggingMessageId"
|
||||||
|
@flag="handleFlagMessage"
|
||||||
|
/>
|
||||||
<MessageComposer @send="handleSendMessage" />
|
<MessageComposer @send="handleSendMessage" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -347,7 +374,7 @@ const formatDate = (dateString: string) => {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.report-hero {
|
.report-hero {
|
||||||
background: linear-gradient(180deg, #f0f6fc 0%, #ffffff 100%);
|
background: #f5f7f9;
|
||||||
border-bottom: 1px solid #d9e3ee;
|
border-bottom: 1px solid #d9e3ee;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -376,7 +403,7 @@ const formatDate = (dateString: string) => {
|
|||||||
.report-hero-stats {
|
.report-hero-stats {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border: 1px solid #d9e3ee;
|
border: 1px solid #d9e3ee;
|
||||||
border-radius: 8px;
|
border-radius: 4px;
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
@@ -410,9 +437,9 @@ const formatDate = (dateString: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.report-description {
|
.report-description {
|
||||||
background: #f7fafc;
|
background: #f5f7f9;
|
||||||
border: 1px solid #e4ebf1;
|
border: 1px solid #e4ebf1;
|
||||||
border-radius: 8px;
|
border-radius: 4px;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ useHead({ title: 'Dettaglio Segnalazione - Visione' })
|
|||||||
const runtimeConfig = useRuntimeConfig()
|
const runtimeConfig = useRuntimeConfig()
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const { getReportDetail, getMessages, addMessage, addFeedback, flagReport } = useReports()
|
const { getReportDetail, getMessages, addMessage, addFeedback } = useReports()
|
||||||
const { user } = useAuth()
|
const { user } = useAuth()
|
||||||
|
|
||||||
const reportId = Number(route.params.id)
|
const reportId = Number(route.params.id)
|
||||||
@@ -36,8 +36,6 @@ const isFeedbackOpen = ref(false)
|
|||||||
const feedbackRating = ref(5)
|
const feedbackRating = ref(5)
|
||||||
const feedbackComment = ref('')
|
const feedbackComment = ref('')
|
||||||
const isSubmittingFeedback = ref(false)
|
const isSubmittingFeedback = ref(false)
|
||||||
const flagMessage = ref('')
|
|
||||||
const isFlagging = ref(false)
|
|
||||||
const selectedMediaIndex = ref(0)
|
const selectedMediaIndex = ref(0)
|
||||||
const isGalleryOpen = ref(false)
|
const isGalleryOpen = ref(false)
|
||||||
|
|
||||||
@@ -76,24 +74,6 @@ const handleSubmitFeedback = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 isOwner = computed(() => report.value?.user_id === user.value?.id)
|
||||||
const canFeedback = computed(() => report.value?.status === 'RESOLVED' || report.value?.status === 'CLOSED')
|
const canFeedback = computed(() => report.value?.status === 'RESOLVED' || report.value?.status === 'CLOSED')
|
||||||
const statusMeta: Record<string, { label: string; tone: string }> = {
|
const statusMeta: Record<string, { label: string; tone: string }> = {
|
||||||
@@ -317,27 +297,6 @@ const formatDate = (dateString: string): string => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card card-bg border-0 shadow-sm">
|
|
||||||
<div class="card-body">
|
|
||||||
<h2 class="h6 mb-3">Segnala abuso</h2>
|
|
||||||
<p class="text-muted small mb-3">Usa questo canale solo per contenuti impropri o uso scorretto del servizio.</p>
|
|
||||||
<textarea
|
|
||||||
v-model="flagMessage"
|
|
||||||
class="form-control mb-3"
|
|
||||||
placeholder="Specifica motivo"
|
|
||||||
rows="3"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn 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>
|
||||||
</div>
|
</div>
|
||||||
@@ -352,7 +311,7 @@ const formatDate = (dateString: string): string => {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.report-hero {
|
.report-hero {
|
||||||
background: linear-gradient(180deg, #f0f6fc 0%, #ffffff 100%);
|
background: #f5f7f9;
|
||||||
border-bottom: 1px solid #d9e3ee;
|
border-bottom: 1px solid #d9e3ee;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -381,7 +340,7 @@ const formatDate = (dateString: string): string => {
|
|||||||
.report-hero-stats {
|
.report-hero-stats {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border: 1px solid #d9e3ee;
|
border: 1px solid #d9e3ee;
|
||||||
border-radius: 8px;
|
border-radius: 4px;
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
@@ -415,9 +374,9 @@ const formatDate = (dateString: string): string => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.report-description {
|
.report-description {
|
||||||
background: #f7fafc;
|
background: #f5f7f9;
|
||||||
border: 1px solid #e4ebf1;
|
border: 1px solid #e4ebf1;
|
||||||
border-radius: 8px;
|
border-radius: 4px;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -501,4 +460,3 @@ const formatDate = (dateString: string): string => {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -10,8 +10,6 @@ useHead({ title: 'Nuova segnalazione - Visione' })
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { createReport, uploadMedia } = useReports()
|
const { createReport, uploadMedia } = useReports()
|
||||||
const { getCategories } = useAdmin()
|
const { getCategories } = useAdmin()
|
||||||
const { user } = useAuth()
|
|
||||||
|
|
||||||
const { data: categoriesData } = await useAsyncData('categories', () => getCategories())
|
const { data: categoriesData } = await useAsyncData('categories', () => getCategories())
|
||||||
const categories = computed(() => categoriesData.value?.categories || [])
|
const categories = computed(() => categoriesData.value?.categories || [])
|
||||||
|
|
||||||
@@ -67,35 +65,15 @@ const handleReportSubmit = async (reportData: CreateReportRequest, mediaFiles: F
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="container my-5">
|
<div class="container my-5">
|
||||||
<div class="row g-4">
|
<div class="row justify-content-center">
|
||||||
<div class="col-12 col-lg-8">
|
<div class="col-12 col-xl-10">
|
||||||
<div v-if="errorMessage" class="alert alert-danger alert-dismissible fade show" role="alert">
|
<div v-if="errorMessage" class="alert alert-danger alert-dismissible fade show mb-4" role="alert">
|
||||||
{{ errorMessage }}
|
{{ errorMessage }}
|
||||||
<button type="button" class="btn-close" aria-label="Chiudi" @click="errorMessage = ''"></button>
|
<button type="button" class="btn-close" aria-label="Chiudi" @click="errorMessage = ''"></button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ReportForm :categories="categories" :is-loading="isLoading" @submit="handleReportSubmit" />
|
<ReportForm :categories="categories" :is-loading="isLoading" @submit="handleReportSubmit" />
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -20,29 +20,6 @@ export default defineNuxtConfig({
|
|||||||
url: 'https://visione.studentindipendenti.it',
|
url: 'https://visione.studentindipendenti.it',
|
||||||
name: 'Visione - Un\'occhio per la sicurezza del cittadino.',
|
name: 'Visione - Un\'occhio per la sicurezza del cittadino.',
|
||||||
},
|
},
|
||||||
|
|
||||||
nitro: {
|
|
||||||
devProxy: {
|
|
||||||
'/api/v1': {
|
|
||||||
target: 'https://bk.visione.studentindipendenti.it/api/v1',
|
|
||||||
changeOrigin: true,
|
|
||||||
headers: {
|
|
||||||
host: 'bk.visione.studentindipendenti.it',
|
|
||||||
origin: 'https://visione.studentindipendenti.it',
|
|
||||||
referer: 'https://visione.studentindipendenti.it/'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
'/uploads': {
|
|
||||||
target: 'https://bk.visione.studentindipendenti.it/uploads',
|
|
||||||
changeOrigin: true,
|
|
||||||
headers: {
|
|
||||||
host: 'bk.visione.studentindipendenti.it',
|
|
||||||
origin: 'https://visione.studentindipendenti.it',
|
|
||||||
referer: 'https://visione.studentindipendenti.it/'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
css: [
|
css: [
|
||||||
'leaflet/dist/leaflet.css',
|
'leaflet/dist/leaflet.css',
|
||||||
|
|||||||
Reference in New Issue
Block a user