feat: refactor operator dashboard and report handling, add public intervention page
This commit is contained in:
@@ -80,11 +80,11 @@ const reportHref = (report: Report) => `${props.detailBasePath}/${report.id}`
|
|||||||
<NuxtLink
|
<NuxtLink
|
||||||
v-else
|
v-else
|
||||||
:to="reportHref(report)"
|
:to="reportHref(report)"
|
||||||
class="btn btn-outline-primary btn-xs btn-icon d-inline-flex align-items-center justify-content-center"
|
class="btn btn-outline-primary btn-xs"
|
||||||
:aria-label="actionLabel"
|
:aria-label="actionLabel"
|
||||||
:title="actionLabel"
|
:title="actionLabel"
|
||||||
>
|
>
|
||||||
<BIIcon name="it-arrow-right" size="xs" />
|
{{ actionLabel }}
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -1,22 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const { getOpenReports, getAssignedReports, takeReport } = useOperator()
|
const { getOpenReports, getAssignedReports } = useOperator()
|
||||||
|
|
||||||
const { data: openReportsRes, refresh: refreshOpen } = useAsyncData('open-reports', () => getOpenReports({ per_page: 5 }))
|
const { data: openReportsRes } = useAsyncData('open-reports', () => getOpenReports({ per_page: 5 }))
|
||||||
const { data: assignedReportsRes, refresh: refreshAssigned } = useAsyncData('assigned-reports', () => getAssignedReports({ per_page: 5 }))
|
const { data: assignedReportsRes } = useAsyncData('assigned-reports', () => getAssignedReports({ per_page: 5 }))
|
||||||
|
|
||||||
const processingIds = ref<number[]>([])
|
|
||||||
|
|
||||||
const handleTakeReport = async (id: number) => {
|
|
||||||
processingIds.value.push(id)
|
|
||||||
try {
|
|
||||||
const res = await takeReport(id)
|
|
||||||
if (res.ok) {
|
|
||||||
await Promise.all([refreshOpen(), refreshAssigned()])
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
processingIds.value = processingIds.value.filter(pid => pid !== id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -25,8 +11,8 @@ const handleTakeReport = async (id: number) => {
|
|||||||
<div class="card card-bg h-100">
|
<div class="card card-bg h-100">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
<h3 class="h5 mb-0">Da prendere in carico</h3>
|
<h3 class="h5 mb-0">Segnalazioni disponibili</h3>
|
||||||
<NuxtLink to="/operator" class="btn btn-link btn-sm pe-0">Apri coda</NuxtLink>
|
<NuxtLink to="/operator" class="btn btn-link btn-sm pe-0">Apri elenco</NuxtLink>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="openReportsRes?.ok && openReportsRes.reports.length === 0" class="alert alert-info mb-0">
|
<div v-if="openReportsRes?.ok && openReportsRes.reports.length === 0" class="alert alert-info mb-0">
|
||||||
Nessuna nuova segnalazione in ingresso.
|
Nessuna nuova segnalazione in ingresso.
|
||||||
@@ -34,9 +20,8 @@ const handleTakeReport = async (id: number) => {
|
|||||||
<ReportTable
|
<ReportTable
|
||||||
v-else
|
v-else
|
||||||
:reports="openReportsRes?.reports || []"
|
:reports="openReportsRes?.reports || []"
|
||||||
action-variant="manage"
|
|
||||||
detail-base-path="/operator/reports"
|
detail-base-path="/operator/reports"
|
||||||
@take="handleTakeReport"
|
action-label="Visualizza"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -39,6 +39,9 @@ export const useOperator = () => {
|
|||||||
const revokeInterventionLink = (id: number) =>
|
const revokeInterventionLink = (id: number) =>
|
||||||
useApiFetch<OkResponse>(`/api/v1/reports/${id}/intervention-link`, { method: 'DELETE' })
|
useApiFetch<OkResponse>(`/api/v1/reports/${id}/intervention-link`, { method: 'DELETE' })
|
||||||
|
|
||||||
|
const getPublicIntervention = (token: string) =>
|
||||||
|
useApiFetch<any>(`/api/v1/interventions/${token}`, { method: 'GET' })
|
||||||
|
|
||||||
return {
|
return {
|
||||||
getOpenReports,
|
getOpenReports,
|
||||||
getAssignedReports,
|
getAssignedReports,
|
||||||
@@ -50,5 +53,6 @@ export const useOperator = () => {
|
|||||||
getInterventionSummary,
|
getInterventionSummary,
|
||||||
createInterventionLink,
|
createInterventionLink,
|
||||||
revokeInterventionLink,
|
revokeInterventionLink,
|
||||||
|
getPublicIntervention,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -106,6 +106,19 @@ const formatDate = (dateStr: string) => {
|
|||||||
hour: '2-digit', minute: '2-digit'
|
hour: '2-digit', minute: '2-digit'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const formatFlagStatus = (status: string) => {
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
OPEN: 'Aperto',
|
||||||
|
RESOLVED: 'Risolto',
|
||||||
|
DISMISSED: 'Ignorato'
|
||||||
|
}
|
||||||
|
|
||||||
|
return map[status] || status
|
||||||
|
}
|
||||||
|
|
||||||
|
const contentTypeLabel = (type: string) => type === 'REPORT' ? 'Segnalazione' : 'Messaggio'
|
||||||
|
const canRejectReport = (flag: any) => flag.content_type === 'REPORT'
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -153,7 +166,7 @@ const formatDate = (dateStr: string) => {
|
|||||||
'bg-warning': flag.status === 'OPEN',
|
'bg-warning': flag.status === 'OPEN',
|
||||||
'bg-success': flag.status === 'RESOLVED',
|
'bg-success': flag.status === 'RESOLVED',
|
||||||
'bg-secondary': flag.status === 'DISMISSED'
|
'bg-secondary': flag.status === 'DISMISSED'
|
||||||
}">{{ flag.status }}</span>
|
}">{{ formatFlagStatus(flag.status) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<small class="text-muted">{{ formatDate(flag.created_at) }}</small>
|
<small class="text-muted">{{ formatDate(flag.created_at) }}</small>
|
||||||
</div>
|
</div>
|
||||||
@@ -166,7 +179,7 @@ const formatDate = (dateStr: string) => {
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="mt-info">
|
<div class="mt-info">
|
||||||
<span class="badge bg-info me-2">{{ flag.content_type }}</span>
|
<span class="badge bg-info me-2">{{ contentTypeLabel(flag.content_type) }}</span>
|
||||||
<span v-if="flag.report_id">
|
<span v-if="flag.report_id">
|
||||||
Rif:
|
Rif:
|
||||||
<NuxtLink :to="`/operator/reports/${flag.report_id}`" class="text-decoration-none">
|
<NuxtLink :to="`/operator/reports/${flag.report_id}`" class="text-decoration-none">
|
||||||
@@ -190,11 +203,11 @@ const formatDate = (dateStr: string) => {
|
|||||||
|
|
||||||
<div class="col-md-4 text-end" v-if="flag.status === 'OPEN'">
|
<div class="col-md-4 text-end" v-if="flag.status === 'OPEN'">
|
||||||
<div class="d-flex flex-column gap-2 mt-3 mt-md-0">
|
<div class="d-flex flex-column gap-2 mt-3 mt-md-0">
|
||||||
<button class="btn btn-outline-danger btn-sm" @click="openResolveModal(flag.id, true)">
|
<button class="btn btn-outline-danger btn-sm" @click="openResolveModal(flag.id, canRejectReport(flag))">
|
||||||
<BIIcon name="it-ban" size="xs" /> Risolvi & Rigetta Segn.
|
<BIIcon name="it-ban" size="xs" /> {{ canRejectReport(flag) ? 'Risolvi & Rigetta Segn.' : 'Risolvi flag' }}
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-outline-warning btn-sm" @click="openResolveModal(flag.id, false)">
|
<button class="btn btn-outline-warning btn-sm" @click="openResolveModal(flag.id, false)">
|
||||||
<BIIcon name="it-check" size="xs" /> Risolvi (Solo Avviso)
|
<BIIcon name="it-check" size="xs" /> {{ canRejectReport(flag) ? 'Risolvi (Solo Avviso)' : 'Risolvi senza rigetto' }}
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-outline-secondary btn-sm" @click="openDismissModal(flag.id)">
|
<button class="btn btn-outline-secondary btn-sm" @click="openDismissModal(flag.id)">
|
||||||
<BIIcon name="it-close" size="xs" /> Ignora Flag
|
<BIIcon name="it-close" size="xs" /> Ignora Flag
|
||||||
|
|||||||
@@ -0,0 +1,117 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
definePageMeta({
|
||||||
|
layout: 'default'
|
||||||
|
})
|
||||||
|
|
||||||
|
useHead({ title: 'Intervento pubblico - Visione' })
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const runtimeConfig = useRuntimeConfig()
|
||||||
|
const token = computed(() => String(route.params.token || ''))
|
||||||
|
const { getPublicIntervention } = useOperator()
|
||||||
|
|
||||||
|
const { data, pending, error } = await useAsyncData(
|
||||||
|
`public-intervention-${token.value}`,
|
||||||
|
() => getPublicIntervention(token.value),
|
||||||
|
)
|
||||||
|
|
||||||
|
const payload = computed<any>(() => data.value || null)
|
||||||
|
const report = computed<any>(() => payload.value?.report || payload.value?.summary || payload.value?.intervention || payload.value)
|
||||||
|
const media = computed<any[]>(() => payload.value?.media || [])
|
||||||
|
const messages = computed<any[]>(() => payload.value?.latest_messages || payload.value?.messages || [])
|
||||||
|
|
||||||
|
const formatDate = (dateString?: string) => {
|
||||||
|
if (!dateString) return '-'
|
||||||
|
return new Date(dateString).toLocaleString('it-IT', {
|
||||||
|
day: '2-digit',
|
||||||
|
month: '2-digit',
|
||||||
|
year: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="container my-5">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-12 col-lg-8">
|
||||||
|
<div v-if="pending" class="text-center py-5">
|
||||||
|
<div class="spinner-border text-primary" role="status"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="error || !report" class="card card-bg border-0 shadow-sm">
|
||||||
|
<div class="card-body py-5 text-center">
|
||||||
|
<BIIcon name="it-close-circle" size="lg" color="danger" class="mb-3" />
|
||||||
|
<h1 class="h4 mb-2">Link non disponibile</h1>
|
||||||
|
<p class="text-muted mb-0">Questo link di intervento non è valido, è scaduto oppure è stato revocato.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<div class="card card-bg border-0 shadow-sm mb-4">
|
||||||
|
<div class="card-body">
|
||||||
|
<h1 class="h4 mb-2">Intervento pubblico</h1>
|
||||||
|
<p class="text-muted mb-4">Scheda condivisa per team esterno.</p>
|
||||||
|
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-12 col-md-6">
|
||||||
|
<small class="text-muted d-block">Segnalazione</small>
|
||||||
|
<strong>#{{ report.id || '-' }}</strong>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-6">
|
||||||
|
<small class="text-muted d-block">Categoria</small>
|
||||||
|
<strong>{{ report.category_name || '-' }}</strong>
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<small class="text-muted d-block">Indirizzo</small>
|
||||||
|
<strong>{{ report.address || '-' }}</strong>
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<small class="text-muted d-block">Descrizione</small>
|
||||||
|
<p class="mb-0">{{ report.description || '-' }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-6">
|
||||||
|
<small class="text-muted d-block">Stato</small>
|
||||||
|
<strong>{{ report.status || '-' }}</strong>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-6">
|
||||||
|
<small class="text-muted d-block">Aperta</small>
|
||||||
|
<strong>{{ formatDate(report.created_at) }}</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="media.length > 0" class="card card-bg border-0 shadow-sm mb-4">
|
||||||
|
<div class="card-body">
|
||||||
|
<h2 class="h6 mb-3">Allegati</h2>
|
||||||
|
<div class="row g-2">
|
||||||
|
<div v-for="item in media" :key="item.id" class="col-6 col-md-4">
|
||||||
|
<img
|
||||||
|
v-if="item.media_kind === 'IMAGE'"
|
||||||
|
:src="runtimeConfig.public.api + item.url"
|
||||||
|
class="img-fluid rounded border"
|
||||||
|
:alt="item.original_filename || 'Allegato'"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="messages.length > 0" class="card card-bg border-0 shadow-sm">
|
||||||
|
<div class="card-body">
|
||||||
|
<h2 class="h6 mb-3">Ultimi aggiornamenti</h2>
|
||||||
|
<ul class="list-unstyled mb-0">
|
||||||
|
<li v-for="message in messages" :key="message.id" class="border-bottom py-2">
|
||||||
|
<div class="small text-muted mb-1">{{ formatDate(message.created_at) }}</div>
|
||||||
|
<div>{{ message.body }}</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -25,7 +25,7 @@ const { data: reportsRes, pending } = useAsyncData(
|
|||||||
<h1 class="h3 font-weight-bold mb-0">Le Mie Assegnazioni</h1>
|
<h1 class="h3 font-weight-bold mb-0">Le Mie Assegnazioni</h1>
|
||||||
<div>
|
<div>
|
||||||
<NuxtLink to="/dashboard" class="btn btn-outline-secondary btn-sm me-2">Dashboard</NuxtLink>
|
<NuxtLink to="/dashboard" class="btn btn-outline-secondary btn-sm me-2">Dashboard</NuxtLink>
|
||||||
<NuxtLink to="/operator" class="btn btn-outline-primary btn-sm">Coda Open</NuxtLink>
|
<NuxtLink to="/operator" class="btn btn-outline-primary btn-sm">Segnalazioni disponibili</NuxtLink>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -6,34 +6,22 @@ definePageMeta({
|
|||||||
|
|
||||||
useHead({ title: 'Segnalazioni Aperte - Operatore' })
|
useHead({ title: 'Segnalazioni Aperte - Operatore' })
|
||||||
|
|
||||||
const { getOpenReports, takeReport } = useOperator()
|
const { getOpenReports } = useOperator()
|
||||||
|
|
||||||
const page = ref(1)
|
const page = ref(1)
|
||||||
const perPage = ref(10)
|
const perPage = ref(10)
|
||||||
|
|
||||||
const { data: reportsRes, pending, refresh } = useAsyncData(
|
const { data: reportsRes, pending } = useAsyncData(
|
||||||
'operator-open-reports',
|
'operator-open-reports',
|
||||||
() => getOpenReports({ page: page.value, per_page: perPage.value }),
|
() => getOpenReports({ page: page.value, per_page: perPage.value }),
|
||||||
{ watch: [page] }
|
{ watch: [page] }
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleTakeReport = async (id: number) => {
|
|
||||||
try {
|
|
||||||
const res = await takeReport(id)
|
|
||||||
if (res.ok) {
|
|
||||||
await refresh()
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Failed to take report', err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="container my-5">
|
<div class="container my-5">
|
||||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
<h1 class="h3 font-weight-bold mb-0">Segnalazioni da prendere in carico (OPEN)</h1>
|
<h1 class="h3 font-weight-bold mb-0">Segnalazioni da valutare</h1>
|
||||||
<div>
|
<div>
|
||||||
<NuxtLink to="/dashboard" class="btn btn-outline-secondary btn-sm me-2">Dashboard</NuxtLink>
|
<NuxtLink to="/dashboard" class="btn btn-outline-secondary btn-sm me-2">Dashboard</NuxtLink>
|
||||||
<NuxtLink to="/operator/assigned" class="btn btn-outline-primary btn-sm">Le mie assegnazioni</NuxtLink>
|
<NuxtLink to="/operator/assigned" class="btn btn-outline-primary btn-sm">Le mie assegnazioni</NuxtLink>
|
||||||
@@ -45,15 +33,14 @@ const handleTakeReport = async (id: number) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else-if="!reportsRes?.reports || reportsRes.reports.length === 0" class="alert alert-info">
|
<div v-else-if="!reportsRes?.reports || reportsRes.reports.length === 0" class="alert alert-info">
|
||||||
Non ci sono segnalazioni aperte in questo momento.
|
Non ci sono segnalazioni disponibili in questo momento.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<ReportTable
|
<ReportTable
|
||||||
:reports="reportsRes.reports"
|
:reports="reportsRes.reports"
|
||||||
action-variant="manage"
|
|
||||||
detail-base-path="/operator/reports"
|
detail-base-path="/operator/reports"
|
||||||
@take="handleTakeReport"
|
action-label="Visualizza"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Pagination
|
<Pagination
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ const { data: reportsRes, pending } = useAsyncData(
|
|||||||
<h1 class="h3 font-weight-bold mb-0">Gestite da altri Operatori</h1>
|
<h1 class="h3 font-weight-bold mb-0">Gestite da altri Operatori</h1>
|
||||||
<div>
|
<div>
|
||||||
<NuxtLink to="/dashboard" class="btn btn-outline-secondary btn-sm me-2">Dashboard</NuxtLink>
|
<NuxtLink to="/dashboard" class="btn btn-outline-secondary btn-sm me-2">Dashboard</NuxtLink>
|
||||||
<NuxtLink to="/operator" class="btn btn-outline-primary btn-sm">Coda Open</NuxtLink>
|
<NuxtLink to="/operator" class="btn btn-outline-primary btn-sm">Segnalazioni disponibili</NuxtLink>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Message, ReportStatus } from '~/types/api'
|
import type { FlagRequest, Message, Report, ReportStatus } from '~/types/api'
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
middleware: 'auth',
|
middleware: 'auth',
|
||||||
@@ -16,8 +16,16 @@ if (!reportId) {
|
|||||||
navigateTo('/operator')
|
navigateTo('/operator')
|
||||||
}
|
}
|
||||||
|
|
||||||
const { getReportDetail, getMessages, addMessage, flagMessage } = useReports()
|
const { getReportDetail, getMessages, addMessage, flagMessage, flagReport } = useReports()
|
||||||
const { getInterventionSummary, updateReportStatus, assignTeam, getTeams, createInterventionLink, revokeInterventionLink } = useOperator()
|
const {
|
||||||
|
takeReport,
|
||||||
|
getInterventionSummary,
|
||||||
|
updateReportStatus,
|
||||||
|
assignTeam,
|
||||||
|
getTeams,
|
||||||
|
createInterventionLink,
|
||||||
|
revokeInterventionLink
|
||||||
|
} = useOperator()
|
||||||
const { user } = useAuth()
|
const { user } = useAuth()
|
||||||
|
|
||||||
const { data: reportData, refresh: refreshReport } = await useAsyncData(`op-report-${reportId}`, () => getReportDetail(reportId))
|
const { data: reportData, refresh: refreshReport } = await useAsyncData(`op-report-${reportId}`, () => getReportDetail(reportId))
|
||||||
@@ -25,14 +33,20 @@ const { data: messagesData, refresh: refreshMessages } = await useAsyncData(`op-
|
|||||||
const { data: summaryData, refresh: refreshSummary } = await useAsyncData(`op-summary-${reportId}`, () => getInterventionSummary(reportId))
|
const { data: summaryData, refresh: refreshSummary } = await useAsyncData(`op-summary-${reportId}`, () => getInterventionSummary(reportId))
|
||||||
const { data: teamsData } = await useAsyncData('operator-teams', () => getTeams())
|
const { data: teamsData } = await useAsyncData('operator-teams', () => getTeams())
|
||||||
|
|
||||||
const report = computed(() => (reportData.value as any)?.report)
|
const report = computed<Report | undefined>(() => (reportData.value as any)?.report)
|
||||||
const media = computed(() => (reportData.value as any)?.media || [])
|
const media = computed(() => (reportData.value as any)?.media || [])
|
||||||
const statusHistory = computed(() => (reportData.value as any)?.history || [])
|
const statusHistory = computed(() => (reportData.value as any)?.history || [])
|
||||||
const messages = computed(() => (messagesData.value as any)?.messages || [])
|
const messages = computed<Message[]>(() => (messagesData.value as any)?.messages || [])
|
||||||
const summary = computed(() => (summaryData.value as any)?.summary)
|
const summary = computed(() => (summaryData.value as any)?.summary)
|
||||||
const summaryMedia = computed(() => (summaryData.value as any)?.media || [])
|
const summaryMedia = computed(() => (summaryData.value as any)?.media || [])
|
||||||
const teams = computed(() => teamsData.value?.teams || [])
|
const teams = computed(() => teamsData.value?.teams || [])
|
||||||
|
|
||||||
|
const currentUserId = computed(() => user.value?.id ?? null)
|
||||||
|
const isTakenByCurrentOperator = computed(() => report.value?.assigned_operator_id === currentUserId.value)
|
||||||
|
const isOpenAndUnassigned = computed(() => report.value?.status === 'OPEN' && !report.value?.assigned_operator_id)
|
||||||
|
const isReadOnlyPreview = computed(() => !isTakenByCurrentOperator.value)
|
||||||
|
const canTakeReport = computed(() => isOpenAndUnassigned.value && !isTakenByCurrentOperator.value)
|
||||||
|
|
||||||
const statusMeta: Record<string, string> = {
|
const statusMeta: Record<string, string> = {
|
||||||
OPEN: 'Aperta',
|
OPEN: 'Aperta',
|
||||||
UNDER_REVIEW: 'In revisione',
|
UNDER_REVIEW: 'In revisione',
|
||||||
@@ -70,26 +84,30 @@ const selectableStatuses = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const canCreateInterventionLink = computed(() => {
|
const canCreateInterventionLink = computed(() => {
|
||||||
|
if (isReadOnlyPreview.value) return false
|
||||||
if (summary.value?.assignments && Array.isArray(summary.value.assignments)) {
|
if (summary.value?.assignments && Array.isArray(summary.value.assignments)) {
|
||||||
return summary.value.assignments.some((a: any) => !a.ended_at)
|
return summary.value.assignments.some((a: any) => !a.ended_at)
|
||||||
}
|
}
|
||||||
return ['ASSIGNED', 'IN_PROGRESS'].includes(report.value?.status)
|
return ['ASSIGNED', 'IN_PROGRESS'].includes(report.value?.status || '')
|
||||||
})
|
})
|
||||||
|
|
||||||
const newStatus = ref<ReportStatus>('IN_PROGRESS')
|
const newStatus = ref<ReportStatus>('IN_PROGRESS')
|
||||||
const statusNote = ref('')
|
const statusNote = ref('')
|
||||||
const isUpdatingStatus = ref(false)
|
const isUpdatingStatus = ref(false)
|
||||||
|
const isTakingReport = ref(false)
|
||||||
|
|
||||||
const isAssignModalOpen = ref(false)
|
const isAssignModalOpen = ref(false)
|
||||||
const selectedTeamId = ref<number | null>(null)
|
const selectedTeamId = ref<number | null>(null)
|
||||||
const isAssigning = ref(false)
|
const isAssigning = ref(false)
|
||||||
const interventionLink = ref('')
|
const interventionLink = ref('')
|
||||||
const interventionLinkExpiresAt = ref('')
|
const interventionLinkExpiresAt = ref('')
|
||||||
|
const interventionPublicLink = ref('')
|
||||||
const isGeneratingInterventionLink = ref(false)
|
const isGeneratingInterventionLink = ref(false)
|
||||||
const isRevokingInterventionLink = ref(false)
|
const isRevokingInterventionLink = ref(false)
|
||||||
const interventionLinkError = ref('')
|
const interventionLinkError = ref('')
|
||||||
const interventionLinkSuccess = ref('')
|
const interventionLinkSuccess = ref('')
|
||||||
const isRevokeInterventionModalOpen = ref(false)
|
const isRevokeInterventionModalOpen = ref(false)
|
||||||
|
const copyFeedback = ref('')
|
||||||
|
|
||||||
const isSubmittingMessage = ref(false)
|
const isSubmittingMessage = ref(false)
|
||||||
const flaggedMessageIds = ref<number[]>([])
|
const flaggedMessageIds = ref<number[]>([])
|
||||||
@@ -98,6 +116,15 @@ const messageToFlag = ref<Message | null>(null)
|
|||||||
const isFlaggingMessage = ref(false)
|
const isFlaggingMessage = ref(false)
|
||||||
const isFlaggingMessageId = computed(() => isFlaggingMessage.value ? messageToFlag.value?.id : null)
|
const isFlaggingMessageId = computed(() => isFlaggingMessage.value ? messageToFlag.value?.id : null)
|
||||||
|
|
||||||
|
const isFlagReportModalOpen = ref(false)
|
||||||
|
const isFlaggingReport = ref(false)
|
||||||
|
const reportFlagError = ref('')
|
||||||
|
const reportFlagSuccess = ref('')
|
||||||
|
const reportFlagForm = reactive<FlagRequest>({
|
||||||
|
reason: '',
|
||||||
|
details: ''
|
||||||
|
})
|
||||||
|
|
||||||
watch(() => report.value?.status, (val) => {
|
watch(() => report.value?.status, (val) => {
|
||||||
if (val) {
|
if (val) {
|
||||||
const allowed = statusTransitions[val] || []
|
const allowed = statusTransitions[val] || []
|
||||||
@@ -107,7 +134,44 @@ watch(() => report.value?.status, (val) => {
|
|||||||
}
|
}
|
||||||
}, { immediate: true })
|
}, { immediate: true })
|
||||||
|
|
||||||
|
const resetReportFlagForm = () => {
|
||||||
|
reportFlagForm.reason = ''
|
||||||
|
reportFlagForm.details = ''
|
||||||
|
reportFlagError.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const setPublicInterventionLink = (url?: string) => {
|
||||||
|
if (!url) {
|
||||||
|
interventionPublicLink.value = ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const rawUrl = new URL(url, runtimeConfig.public.api)
|
||||||
|
interventionPublicLink.value = `${runtimeConfig.public.url}${rawUrl.pathname.replace(/^\/api\/v1/, '')}`
|
||||||
|
} catch {
|
||||||
|
interventionPublicLink.value = url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleTakeReport = async () => {
|
||||||
|
if (!canTakeReport.value || !report.value) return
|
||||||
|
|
||||||
|
isTakingReport.value = true
|
||||||
|
try {
|
||||||
|
const res = await takeReport(report.value.id)
|
||||||
|
if (res.ok) {
|
||||||
|
await Promise.all([refreshReport(), refreshSummary()])
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to take report', e)
|
||||||
|
} finally {
|
||||||
|
isTakingReport.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleStatusUpdate = async () => {
|
const handleStatusUpdate = async () => {
|
||||||
|
if (isReadOnlyPreview.value) return
|
||||||
isUpdatingStatus.value = true
|
isUpdatingStatus.value = true
|
||||||
try {
|
try {
|
||||||
const res = await updateReportStatus(reportId, {
|
const res = await updateReportStatus(reportId, {
|
||||||
@@ -126,7 +190,7 @@ const handleStatusUpdate = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleAssignTeam = async () => {
|
const handleAssignTeam = async () => {
|
||||||
if (isAdmin(user.value?.role)) return
|
if (isReadOnlyPreview.value) return
|
||||||
if (!selectedTeamId.value) return
|
if (!selectedTeamId.value) return
|
||||||
|
|
||||||
isAssigning.value = true
|
isAssigning.value = true
|
||||||
@@ -147,15 +211,20 @@ const handleAssignTeam = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const copyInterventionLink = async () => {
|
const copyInterventionLink = async () => {
|
||||||
if (!interventionLink.value || !navigator?.clipboard) return
|
if (!interventionPublicLink.value || !navigator?.clipboard) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(interventionLink.value)
|
await navigator.clipboard.writeText(interventionPublicLink.value)
|
||||||
|
copyFeedback.value = 'Copiato'
|
||||||
interventionLinkSuccess.value = 'Link copiato negli appunti.'
|
interventionLinkSuccess.value = 'Link copiato negli appunti.'
|
||||||
interventionLinkError.value = ''
|
interventionLinkError.value = ''
|
||||||
|
setTimeout(() => {
|
||||||
|
copyFeedback.value = ''
|
||||||
|
}, 2000)
|
||||||
} catch {
|
} catch {
|
||||||
interventionLinkError.value = 'Copia automatica non riuscita.'
|
interventionLinkError.value = 'Copia automatica non riuscita.'
|
||||||
interventionLinkSuccess.value = ''
|
interventionLinkSuccess.value = ''
|
||||||
|
copyFeedback.value = ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,6 +239,7 @@ const handleCreateInterventionLink = async () => {
|
|||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
interventionLink.value = res.url
|
interventionLink.value = res.url
|
||||||
interventionLinkExpiresAt.value = res.expires_at
|
interventionLinkExpiresAt.value = res.expires_at
|
||||||
|
setPublicInterventionLink(res.url)
|
||||||
interventionLinkSuccess.value = 'Link intervento generato.'
|
interventionLinkSuccess.value = 'Link intervento generato.'
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
@@ -202,6 +272,7 @@ const handleRevokeInterventionLink = async () => {
|
|||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
interventionLink.value = ''
|
interventionLink.value = ''
|
||||||
interventionLinkExpiresAt.value = ''
|
interventionLinkExpiresAt.value = ''
|
||||||
|
interventionPublicLink.value = ''
|
||||||
interventionLinkSuccess.value = 'Link intervento revocato.'
|
interventionLinkSuccess.value = 'Link intervento revocato.'
|
||||||
isRevokeInterventionModalOpen.value = false
|
isRevokeInterventionModalOpen.value = false
|
||||||
}
|
}
|
||||||
@@ -214,6 +285,7 @@ const handleRevokeInterventionLink = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleSendMessage = async (msgText: string, files: File[]) => {
|
const handleSendMessage = async (msgText: string, files: File[]) => {
|
||||||
|
if (isReadOnlyPreview.value) return
|
||||||
if (!msgText.trim() && files.length === 0) return
|
if (!msgText.trim() && files.length === 0) return
|
||||||
isSubmittingMessage.value = true
|
isSubmittingMessage.value = true
|
||||||
try {
|
try {
|
||||||
@@ -228,7 +300,6 @@ const handleSendMessage = async (msgText: string, files: File[]) => {
|
|||||||
|
|
||||||
const canFlagMessage = (message: Message) => {
|
const canFlagMessage = (message: Message) => {
|
||||||
if (!user.value) return false
|
if (!user.value) return false
|
||||||
if (isAdmin(user.value?.role)) return false
|
|
||||||
|
|
||||||
const isCitizenMessage = message.user_id !== user.value.id && message.user_id === report.value?.user_id
|
const isCitizenMessage = message.user_id !== user.value.id && message.user_id === report.value?.user_id
|
||||||
const isAlreadyFlagged = flaggedMessageIds.value.includes(message.id)
|
const isAlreadyFlagged = flaggedMessageIds.value.includes(message.id)
|
||||||
@@ -252,7 +323,6 @@ const executeFlagMessage = async () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
flaggedMessageIds.value.push(messageToFlag.value.id)
|
flaggedMessageIds.value.push(messageToFlag.value.id)
|
||||||
|
|
||||||
isFlagMessageModalOpen.value = false
|
isFlagMessageModalOpen.value = false
|
||||||
messageToFlag.value = null
|
messageToFlag.value = null
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -262,6 +332,33 @@ const executeFlagMessage = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const executeFlagReport = async () => {
|
||||||
|
if (!reportFlagForm.reason.trim()) {
|
||||||
|
reportFlagError.value = 'Inserisci un motivo.'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isFlaggingReport.value = true
|
||||||
|
reportFlagError.value = ''
|
||||||
|
reportFlagSuccess.value = ''
|
||||||
|
|
||||||
|
try {
|
||||||
|
await flagReport(reportId, {
|
||||||
|
reason: reportFlagForm.reason.trim(),
|
||||||
|
details: reportFlagForm.details.trim() || undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
reportFlagSuccess.value = 'Segnalazione inviata alla moderazione.'
|
||||||
|
isFlagReportModalOpen.value = false
|
||||||
|
resetReportFlagForm()
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error('Failed to flag report', e)
|
||||||
|
reportFlagError.value = e?.data?.message || 'Invio segnalazione non riuscito.'
|
||||||
|
} finally {
|
||||||
|
isFlaggingReport.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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', {
|
||||||
@@ -288,14 +385,15 @@ const formattedInterventionLinkExpiry = computed(() =>
|
|||||||
<div v-if="report">
|
<div v-if="report">
|
||||||
<div class="report-hero py-4">
|
<div class="report-hero py-4">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<NuxtLink to="/operator/assigned" class="btn btn-sm btn-link text-decoration-none ps-0 mb-2">
|
<NuxtLink :to="isTakenByCurrentOperator ? '/operator/assigned' : '/operator'" class="btn btn-sm btn-link text-decoration-none ps-0 mb-2">
|
||||||
← Torna alle assegnazioni
|
← Torna all'elenco
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
<div class="row g-3 align-items-end">
|
<div class="row g-3 align-items-end">
|
||||||
<div class="col-12 col-lg-8">
|
<div class="col-12 col-lg-8">
|
||||||
<div class="report-hero-meta mb-2">
|
<div class="report-hero-meta mb-2">
|
||||||
<span class="report-id">Intervento #{{ reportId }}</span>
|
<span class="report-id">Segnalazione #{{ reportId }}</span>
|
||||||
<StatusBadge :status="report.status" />
|
<StatusBadge :status="report.status" />
|
||||||
|
<span v-if="isReadOnlyPreview" class="badge bg-secondary">Sola lettura</span>
|
||||||
</div>
|
</div>
|
||||||
<h1 class="h3 font-weight-bold mb-2">{{ report.category_name }}</h1>
|
<h1 class="h3 font-weight-bold mb-2">{{ report.category_name }}</h1>
|
||||||
<p class="mb-0 report-hero-address">{{ report.address }}</p>
|
<p class="mb-0 report-hero-address">{{ report.address }}</p>
|
||||||
@@ -308,11 +406,15 @@ const formattedInterventionLinkExpiry = computed(() =>
|
|||||||
</div>
|
</div>
|
||||||
<div class="hero-stat">
|
<div class="hero-stat">
|
||||||
<span class="hero-stat-label">Aperta</span>
|
<span class="hero-stat-label">Aperta</span>
|
||||||
<strong>{{ formatDate(report.created_at) }}</strong>
|
<strong>{{ formatDate(report.created_at || '') }}</strong>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="isReadOnlyPreview" class="alert alert-info mt-3 mb-0">
|
||||||
|
Stai visualizzando segnalazione in sola lettura. Prendila in carico per aggiornare stato, assegnare team o scrivere al cittadino.
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -357,6 +459,20 @@ const formattedInterventionLinkExpiry = computed(() =>
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4 d-flex flex-wrap gap-2">
|
||||||
|
<button type="button" class="btn btn-outline-warning btn-sm" @click="isFlagReportModalOpen = true">
|
||||||
|
Segnala alla moderazione
|
||||||
|
</button>
|
||||||
|
<button v-if="canTakeReport" type="button" class="btn btn-primary btn-sm" :disabled="isTakingReport" @click="handleTakeReport">
|
||||||
|
<span v-if="isTakingReport" class="spinner-border spinner-border-sm me-2"></span>
|
||||||
|
Prendi in carico
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="reportFlagSuccess" class="alert alert-success mt-3 mb-0">
|
||||||
|
{{ reportFlagSuccess }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -378,7 +494,10 @@ const formattedInterventionLinkExpiry = computed(() =>
|
|||||||
:is-flagging-message-id="isFlaggingMessageId"
|
:is-flagging-message-id="isFlaggingMessageId"
|
||||||
@flag="handleFlagMessage"
|
@flag="handleFlagMessage"
|
||||||
/>
|
/>
|
||||||
<MessageComposer @send="handleSendMessage" />
|
<div v-if="isReadOnlyPreview" class="alert alert-secondary mt-3 mb-0 small">
|
||||||
|
Messaggistica disponibile dopo la presa in carico.
|
||||||
|
</div>
|
||||||
|
<MessageComposer v-else @send="handleSendMessage" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -409,7 +528,7 @@ const formattedInterventionLinkExpiry = computed(() =>
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!isAdmin(user?.role)" class="card-body pt-0">
|
<div class="card-body pt-0" v-if="!isReadOnlyPreview">
|
||||||
<button class="btn btn-outline-primary w-100" @click="isAssignModalOpen = true" :disabled="teams.length === 0">
|
<button class="btn btn-outline-primary w-100" @click="isAssignModalOpen = true" :disabled="teams.length === 0">
|
||||||
Assegna team
|
Assegna team
|
||||||
</button>
|
</button>
|
||||||
@@ -428,18 +547,18 @@ const formattedInterventionLinkExpiry = computed(() =>
|
|||||||
{{ interventionLinkSuccess }}
|
{{ interventionLinkSuccess }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template v-if="interventionLink">
|
<template v-if="interventionPublicLink">
|
||||||
<label class="form-label">URL attivo</label>
|
<label class="form-label">URL attivo</label>
|
||||||
<div class="input-group mb-2">
|
<div class="input-group mb-2">
|
||||||
<input type="text" class="form-control" :value="interventionLink" readonly />
|
<input type="text" class="form-control" :value="interventionPublicLink" readonly />
|
||||||
<button type="button" class="btn btn-outline-primary" @click="copyInterventionLink">
|
<button type="button" class="btn btn-outline-primary" @click="copyInterventionLink">
|
||||||
Copia
|
{{ copyFeedback || 'Copia' }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p class="small text-muted mb-3">
|
<p class="small text-muted mb-3">
|
||||||
Scade: {{ formattedInterventionLinkExpiry || '-' }}
|
Scade: {{ formattedInterventionLinkExpiry || '-' }}
|
||||||
</p>
|
</p>
|
||||||
<div class="d-grid gap-2">
|
<div class="d-grid gap-2" v-if="!isReadOnlyPreview">
|
||||||
<button type="button" class="btn btn-outline-primary" :disabled="isGeneratingInterventionLink || !canCreateInterventionLink" @click="handleCreateInterventionLink">
|
<button type="button" class="btn btn-outline-primary" :disabled="isGeneratingInterventionLink || !canCreateInterventionLink" @click="handleCreateInterventionLink">
|
||||||
<span v-if="isGeneratingInterventionLink" class="spinner-border spinner-border-sm me-2"></span>
|
<span v-if="isGeneratingInterventionLink" class="spinner-border spinner-border-sm me-2"></span>
|
||||||
Rigenera link
|
Rigenera link
|
||||||
@@ -463,7 +582,7 @@ const formattedInterventionLinkExpiry = computed(() =>
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div v-if="!canCreateInterventionLink" class="text-danger small mt-2">
|
<div v-if="!canCreateInterventionLink" class="text-danger small mt-2">
|
||||||
È necessaria un'assegnazione in corso per generare o rigenerare questo link.
|
È necessaria una presa in carico e un'assegnazione in corso per generare questo link.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -472,7 +591,13 @@ const formattedInterventionLinkExpiry = computed(() =>
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h2 class="h6 mb-3">Aggiorna stato</h2>
|
<h2 class="h6 mb-3">Aggiorna stato</h2>
|
||||||
|
|
||||||
<template v-if="selectableStatuses.length > 0">
|
<template v-if="isReadOnlyPreview">
|
||||||
|
<div class="alert alert-secondary mb-0 small">
|
||||||
|
Aggiornamento stato disponibile dopo la presa in carico.
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else-if="selectableStatuses.length > 0">
|
||||||
<form @submit.prevent="handleStatusUpdate">
|
<form @submit.prevent="handleStatusUpdate">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">Nuovo Stato</label>
|
<label class="form-label">Nuovo Stato</label>
|
||||||
@@ -559,6 +684,36 @@ const formattedInterventionLinkExpiry = computed(() =>
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="isFlagReportModalOpen" class="modal-backdrop fade show d-block" style="background: rgba(0,0,0,0.5); z-index: 1040;"></div>
|
||||||
|
<div v-if="isFlagReportModalOpen" class="modal fade show d-block" tabindex="-1" style="z-index: 1050;">
|
||||||
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">Segnala segnalazione alla moderazione</h5>
|
||||||
|
<button type="button" class="btn-close" :disabled="isFlaggingReport" @click="isFlagReportModalOpen = false; resetReportFlagForm()"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div v-if="reportFlagError" class="alert alert-danger py-2">{{ reportFlagError }}</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Motivo</label>
|
||||||
|
<input v-model="reportFlagForm.reason" type="text" class="form-control" maxlength="200" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="form-label">Dettagli</label>
|
||||||
|
<textarea v-model="reportFlagForm.details" class="form-control" rows="4" placeholder="Dettagli aggiuntivi opzionali"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-outline-secondary" :disabled="isFlaggingReport" @click="isFlagReportModalOpen = false; resetReportFlagForm()">Annulla</button>
|
||||||
|
<button type="button" class="btn btn-warning" :disabled="isFlaggingReport || !reportFlagForm.reason.trim()" @click="executeFlagReport">
|
||||||
|
<span v-if="isFlaggingReport" class="spinner-border spinner-border-sm me-2"></span>
|
||||||
|
Invia a moderazione
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<ConfirmModal
|
<ConfirmModal
|
||||||
:open="isRevokeInterventionModalOpen"
|
:open="isRevokeInterventionModalOpen"
|
||||||
title="Revoca link intervento"
|
title="Revoca link intervento"
|
||||||
|
|||||||
Reference in New Issue
Block a user