fix: chat in reports/[id]

This commit is contained in:
2026-06-17 23:31:10 +02:00 Unverified
parent edf7b4b329
commit b72a2ef365
3 changed files with 46 additions and 77 deletions
+14 -10
View File
@@ -50,7 +50,7 @@ const senderInitials = (msg: Message): string =>
</div> </div>
<div v-else-if="messages.length === 0" class="message-empty-state" role="alert"> <div v-else-if="messages.length === 0" class="message-empty-state" role="alert">
<BIIcon name="it-comment" size="lg" color="primary" class="mb-2" /> <BIIcon class="mb-2" color="primary" name="it-comment" size="lg"/>
<strong class="d-block mb-1">Nessun messaggio</strong> <strong class="d-block mb-1">Nessun messaggio</strong>
<span>Usa chat per aggiornamenti rapidi tra cittadino e operatori.</span> <span>Usa chat per aggiornamenti rapidi tra cittadino e operatori.</span>
</div> </div>
@@ -59,18 +59,18 @@ const senderInitials = (msg: Message): string =>
<div <div
v-for="msg in messages" v-for="msg in messages"
:key="msg.id" :key="msg.id"
class="message-row"
:class="{ 'is-current-user': isCurrentUser(msg.user_id) }" :class="{ 'is-current-user': isCurrentUser(msg.user_id) }"
class="message-row"
> >
<div v-if="!isCurrentUser(msg.user_id)" class="message-avatar" aria-hidden="true"> <div v-if="!isCurrentUser(msg.user_id)" class="message-avatar" aria-hidden="true">
{{ senderInitials(msg) }} {{ senderInitials(msg) }}
</div> </div>
<div <div
class="message-bubble-content"
:class="{ :class="{
'message-bubble-outgoing': isCurrentUser(msg.user_id), 'message-bubble-outgoing': isCurrentUser(msg.user_id),
'message-bubble-incoming': !isCurrentUser(msg.user_id) 'message-bubble-incoming': !isCurrentUser(msg.user_id)
}" }"
class="message-bubble-content"
> >
<div class="message-meta"> <div class="message-meta">
<span v-if="!isCurrentUser(msg.user_id)" class="message-author"> <span v-if="!isCurrentUser(msg.user_id)" class="message-author">
@@ -90,15 +90,16 @@ const senderInitials = (msg: Message): string =>
{{ msg.body }} {{ msg.body }}
</div> </div>
<div v-if="canFlagMessage(msg)" class="message-actions"> <div v-if="canFlagMessage?.(msg)" class="message-actions">
<button <button
type="button"
class="btn btn-link btn-xs px-0"
:disabled="isFlaggingMessageId === msg.id" :disabled="isFlaggingMessageId === msg.id"
class="btn btn-link btn-xs px-0"
type="button"
@click="emit('flag', msg)" @click="emit('flag', msg)"
> >
<span v-if="isFlaggingMessageId === msg.id" class="spinner-border spinner-border-sm me-1" aria-hidden="true"></span> <span v-if="isFlaggingMessageId === msg.id" aria-hidden="true"
<BIIcon v-else name="it-flag" size="xs" class="me-1" /> class="spinner-border spinner-border-sm me-1"></span>
<BIIcon v-else class="me-1" name="it-flag" size="xs"/>
Segnala messaggio Segnala messaggio
</button> </button>
</div> </div>
@@ -112,9 +113,12 @@ const senderInitials = (msg: Message): string =>
.message-list { .message-list {
border: 1px solid #d9e3ee; border: 1px solid #d9e3ee;
border-radius: 4px; border-radius: 4px;
max-height: 34rem; height: 34rem;
overflow-y: auto; overflow-y: auto;
padding: 1rem; padding: 1rem;
display: flex;
flex-direction: column;
} }
.messages-container { .messages-container {
@@ -129,7 +133,7 @@ const senderInitials = (msg: Message): string =>
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
min-height: 14rem; flex: 1;
padding: 2rem 1rem; padding: 2rem 1rem;
text-align: center; text-align: center;
} }
+2 -2
View File
@@ -45,10 +45,10 @@ const { data: reportsRes, pending } = useAsyncData(
/> />
<Pagination <Pagination
v-if="reportsRes.total > perPage" v-if="(reportsRes.total || 0) > perPage"
:current-page="page" :current-page="page"
:per-page="perPage" :per-page="perPage"
:total-items="reportsRes.total" :total-items="reportsRes.total!"
@page-change="page = $event" @page-change="page = $event"
/> />
</div> </div>
+17 -52
View File
@@ -6,7 +6,6 @@ definePageMeta({
useHead({ title: 'Dettaglio Segnalazione - Visione' }) useHead({ title: 'Dettaglio Segnalazione - Visione' })
const runtimeConfig = useRuntimeConfig() const runtimeConfig = useRuntimeConfig()
const route = useRoute() const route = useRoute()
const { getReportDetail, getMessages, addMessage, addFeedback } = useReports() const { getReportDetail, getMessages, addMessage, addFeedback } = useReports()
const { user } = useAuth() const { user } = useAuth()
@@ -29,35 +28,17 @@ 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(() => (messagesData.value as any)?.messages || [])
const newMessage = ref('')
const fileInput = ref<HTMLInputElement | null>(null)
const selectedFiles = ref<File[]>([])
const isSubmittingMessage = ref(false) const isSubmittingMessage = ref(false)
const isFeedbackOpen = ref(false) 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 selectedMediaIndex = ref(0)
const isGalleryOpen = ref(false)
const handleFileSelect = (event: Event) => {
const target = event.target as HTMLInputElement
if (target.files) {
selectedFiles.value = Array.from(target.files)
}
}
const handleSendMessage = async () => {
if (!newMessage.value.trim() && selectedFiles.value.length === 0) return
const handleSendMessage = async (messageText: string, files: File[]) => {
isSubmittingMessage.value = true isSubmittingMessage.value = true
try { try {
await addMessage(reportId, { body: newMessage.value }) await addMessage(reportId, { body: messageText })
newMessage.value = ''
selectedFiles.value = []
if (fileInput.value) fileInput.value.value = ''
await refreshMessages() await refreshMessages()
} catch (error) { } catch (error) {
@@ -86,7 +67,6 @@ const handleSubmitFeedback = async () => {
} }
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') const canFeedback = computed(() => report.value?.status === 'RESOLVED')
const statusMeta: Record<string, { label: string; tone: string }> = { const statusMeta: Record<string, { label: string; tone: string }> = {
@@ -145,7 +125,7 @@ const formatDate = (dateString: string): string => {
<div class="card card-bg border-0 shadow-sm mb-4"> <div class="card card-bg border-0 shadow-sm mb-4">
<div class="card-body"> <div class="card-body">
<div class="report-section-head mb-3"> <div class="report-section-head mb-3">
<BIIcon name="it-map-marker-circle" size="sm" color="primary" class="me-2" /> <BIIcon class="me-2" color="primary" name="it-map-marker-circle" size="sm"/>
<h2 class="h6 mb-0">Posizione e dettagli</h2> <h2 class="h6 mb-0">Posizione e dettagli</h2>
</div> </div>
<dl class="report-detail-grid mb-0"> <dl class="report-detail-grid mb-0">
@@ -177,16 +157,19 @@ const formatDate = (dateString: string): string => {
<div v-if="media.length > 0" class="card card-bg border-0 shadow-sm mb-4"> <div v-if="media.length > 0" class="card card-bg border-0 shadow-sm mb-4">
<div class="card-body"> <div class="card-body">
<div class="report-section-head mb-3"> <div class="report-section-head mb-3">
<BIIcon name="it-camera" size="sm" color="primary" class="me-2" /> <BIIcon class="me-2" color="primary" name="it-camera" size="sm"/>
<h2 class="h6 mb-0">Allegati ({{ media.length }})</h2> <h2 class="h6 mb-0">Allegati ({{ media.length }})</h2>
</div> </div>
<div class="row g-3"> <div class="row g-3">
<div v-for="(m, idx) in media" :key="m.id" class="col-6 col-md-4 mb-3"> <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 report-media-frame"> <div v-if="m.media_kind === 'IMAGE'"
<img :src="runtimeConfig.public.api + m.url" :alt="`Allegato ${Number(idx) + 1}`" class="object-fit-cover" /> class="ratio ratio-1x1 bg-light rounded overflow-hidden report-media-frame">
<img :alt="`Allegato ${Number(idx) + 1}`" :src="runtimeConfig.public.api + m.url"
class="object-fit-cover"/>
</div> </div>
<div v-else class="ratio ratio-1x1 bg-dark rounded d-flex align-items-center justify-content-center report-media-frame"> <div v-else
<BIIcon name="it-video" size="xl" color="white" /> class="ratio ratio-1x1 bg-dark rounded d-flex align-items-center justify-content-center report-media-frame">
<BIIcon color="white" name="it-video" size="xl"/>
</div> </div>
</div> </div>
</div> </div>
@@ -216,17 +199,18 @@ const formatDate = (dateString: string): string => {
<div class="report-chat-header mb-3"> <div class="report-chat-header mb-3">
<div> <div>
<div class="report-section-head mb-1"> <div class="report-section-head mb-1">
<BIIcon name="it-comment" size="sm" color="primary" class="me-2" /> <BIIcon class="me-2" color="primary" name="it-comment" size="sm"/>
<h2 class="h6 mb-0">Chat segnalazione</h2> <h2 class="h6 mb-0">Chat segnalazione</h2>
</div> </div>
<p class="text-muted small mb-0">Usa chat per chiarimenti, aggiornamenti operativi e richieste rapide.</p> <p class="text-muted small mb-0">Usa chat per chiarimenti, aggiornamenti operativi e richieste
rapide.</p>
</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"/>
<MessageComposer @send=handleSendMessage /> <MessageComposer @send="handleSendMessage"/>
</div> </div>
</div> </div>
</div> </div>
@@ -291,7 +275,8 @@ const formatDate = (dateString: string): string => {
</div> </div>
<div class="d-grid gap-2"> <div class="d-grid gap-2">
<button type="button" class="btn btn-primary" @click="handleSubmitFeedback" :disabled="isSubmittingFeedback"> <button :disabled="isSubmittingFeedback" class="btn btn-primary" type="button"
@click="handleSubmitFeedback">
<span v-if="isSubmittingFeedback" class="spinner-border spinner-border-sm me-2"></span> <span v-if="isSubmittingFeedback" class="spinner-border spinner-border-sm me-2"></span>
Invia Invia
</button> </button>
@@ -414,26 +399,6 @@ const formatDate = (dateString: string): string => {
justify-content: space-between; justify-content: space-between;
} }
.chat-textarea {
resize: none !important;
height: 48px;
min-height: 48px;
overflow-y: auto;
padding-top: 12px;
padding-bottom: 12px;
border-radius: 6px;
}
.btn-square {
width: 48px;
height: 48px;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
border-radius: 6px;
}
.ratio { .ratio {
position: relative; position: relative;
display: block; display: block;