Compare commits
2
Commits
@@ -38,7 +38,7 @@ const auth = useAuth()
|
|||||||
const isCurrentUser = (userId: number): boolean => auth.user.value?.id === userId
|
const isCurrentUser = (userId: number): boolean => auth.user.value?.id === userId
|
||||||
|
|
||||||
const senderInitials = (msg: Message): string =>
|
const senderInitials = (msg: Message): string =>
|
||||||
`${msg.first_name?.[0] || ''}${msg.last_name?.[0] || ''}`.toUpperCase() || '?'
|
`${msg.first_name?.[0] || ''}${msg.last_name?.[0] || ''}`.toUpperCase() || '?'
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -50,27 +50,27 @@ 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>
|
||||||
|
|
||||||
<div v-else class="messages-container">
|
<div v-else class="messages-container">
|
||||||
<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"
|
:disabled="isFlaggingMessageId === msg.id"
|
||||||
class="btn btn-link btn-xs px-0"
|
class="btn btn-link btn-xs px-0"
|
||||||
:disabled="isFlaggingMessageId === msg.id"
|
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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ export interface Member {
|
|||||||
|
|
||||||
export function useMembers() {
|
export function useMembers() {
|
||||||
const members: Member[] = [
|
const members: Member[] = [
|
||||||
{ name: "Renato Loiacono", image: "https://placehold.co/50x50" },
|
{ name: "Renato Loiacono", image: "/renatoloiacono.webp" },
|
||||||
{ name: "Michelangelo Parisi", image: "https://placehold.co/50x50" },
|
{ name: "Michelangelo Parisi", image: "/michelangeloparisi.webp" },
|
||||||
{ name: "Gabriele Schiraldi", image: "https://placehold.co/50x50" },
|
{ name: "Gabriele Schiraldi", image: "/gabrielepaoloschiraldi.webp" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const sortedMembers = computed(() => {
|
const sortedMembers = computed(() => {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
+22
-57
@@ -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 }> = {
|
||||||
@@ -118,7 +98,7 @@ const formatDate = (dateString: string): string => {
|
|||||||
<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">Segnalazione #{{ report.id }}</span>
|
<span class="report-id">Segnalazione #{{ report.id }}</span>
|
||||||
<StatusBadge :status="report.status" />
|
<StatusBadge :status="report.status"/>
|
||||||
</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>
|
||||||
@@ -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>
|
||||||
@@ -201,9 +184,9 @@ const formatDate = (dateString: string): string => {
|
|||||||
<li v-for="item in statusHistory" :key="item.id" class="mb-3 border-start border-primary border-3 ps-3">
|
<li v-for="item in statusHistory" :key="item.id" class="mb-3 border-start border-primary border-3 ps-3">
|
||||||
<small class="text-muted d-block">{{ formatDate(item.created_at) }}</small>
|
<small class="text-muted d-block">{{ formatDate(item.created_at) }}</small>
|
||||||
<div class="d-flex align-items-center gap-2 mt-1">
|
<div class="d-flex align-items-center gap-2 mt-1">
|
||||||
<StatusBadge :status="item.old_status" />
|
<StatusBadge :status="item.old_status"/>
|
||||||
<BIIcon name="it-arrow-right" size="xs" />
|
<BIIcon name="it-arrow-right" size="xs"/>
|
||||||
<StatusBadge :status="item.new_status" />
|
<StatusBadge :status="item.new_status"/>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="item.note" class="small mt-1 bg-light p-1 rounded">{{ item.note }}</div>
|
<div v-if="item.note" class="small mt-1 bg-light p-1 rounded">{{ item.note }}</div>
|
||||||
</li>
|
</li>
|
||||||
@@ -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;
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 76 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
Reference in New Issue
Block a user