style(reports): simplify timeline ui and update composer integration

This commit is contained in:
2026-06-15 23:12:41 +02:00 Unverified
parent 7138123df5
commit c9f628391a
+73 -61
View File
@@ -17,12 +17,12 @@ if (!reportId) {
}
// Fetch dati
const { data: reportData, refresh: refreshReport } = await useAsyncData(`report-${reportId}`, () =>
getReportDetail(reportId)
const { data: reportData, refresh: refreshReport } = await useAsyncData(`report-${reportId}`, () =>
getReportDetail(reportId)
)
const { data: messagesData, refresh: refreshMessages } = await useAsyncData(`messages-${reportId}`, () =>
getMessages(reportId)
getMessages(reportId)
)
const report = computed(() => (reportData.value as any)?.report)
@@ -31,7 +31,10 @@ const statusHistory = computed(() => (reportData.value as any)?.history || [])
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 isFeedbackOpen = ref(false)
const feedbackRating = ref(5)
const feedbackComment = ref('')
@@ -39,14 +42,28 @@ const isSubmittingFeedback = ref(false)
const selectedMediaIndex = ref(0)
const isGalleryOpen = ref(false)
// Gestione selezione file
const handleFileSelect = (event: Event) => {
const target = event.target as HTMLInputElement
if (target.files) {
selectedFiles.value = Array.from(target.files)
}
}
// Invia messaggio
const handleSendMessage = async (msgText: string, files: File[]) => {
if (!msgText.trim() && files.length === 0) return
const handleSendMessage = async () => {
if (!newMessage.value.trim() && selectedFiles.value.length === 0) return
isSubmittingMessage.value = true
try {
// Gestione mock di file, nella realtà si usa FormData o simle
await addMessage(reportId, { body: msgText })
// Gestione mock di file, nella realtà si usa FormData o simile
await addMessage(reportId, { body: newMessage.value })
// Pulisci i campi
newMessage.value = ''
selectedFiles.value = []
if (fileInput.value) fileInput.value.value = ''
await refreshMessages()
} catch (error) {
console.error('Error sending message:', error)
@@ -75,7 +92,10 @@ const handleSubmitFeedback = async () => {
}
const isOwner = computed(() => report.value?.user_id === user.value?.id)
const canFeedback = computed(() => report.value?.status === 'RESOLVED' || report.value?.status === 'CLOSED')
// Valutazione solo se lo stato è specificamente "RESOLVED"
const canFeedback = computed(() => report.value?.status === 'RESOLVED')
const statusMeta: Record<string, { label: string; tone: string }> = {
OPEN: { label: 'Aperta', tone: 'info' },
UNDER_REVIEW: { label: 'In revisione', tone: 'warning' },
@@ -180,30 +200,21 @@ const formatDate = (dateString: string): string => {
</div>
</div>
<div v-if="statusHistory.length > 0" 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="report-section-head mb-4">
<BIIcon name="it-history" size="sm" color="primary" class="me-2" />
<h2 class="h6 mb-0">Cronologia</h2>
</div>
<div class="timeline">
<div v-for="item in statusHistory" :key="item.id" class="timeline-item mb-3">
<div class="d-flex gap-3">
<div class="flex-shrink-0">
<div class="timeline-marker"></div>
</div>
<div class="flex-grow-1">
<small class="text-muted">{{ formatDate(item.created_at) }}</small>
<div class="small font-weight-semibold d-flex align-items-center gap-2 mt-1">
<StatusBadge :status="item.old_status" />
<BIIcon name="it-arrow-right" size="xs" />
<StatusBadge :status="item.new_status" />
</div>
<small v-if="item.note" class="text-muted d-block">{{ item.note }}</small>
</div>
<h2 class="h6 mb-3">Cronologia eventi</h2>
<div v-if="statusHistory.length === 0" class="small text-muted">Nessun evento registrato.</div>
<ul class="list-unstyled mb-0 ms-2">
<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>
<div class="d-flex align-items-center gap-2 mt-1">
<StatusBadge :status="item.old_status" />
<BIIcon name="it-arrow-right" size="xs" />
<StatusBadge :status="item.new_status" />
</div>
</div>
</div>
<div v-if="item.note" class="small mt-1 bg-light p-1 rounded">{{ item.note }}</div>
</li>
</ul>
</div>
</div>
@@ -219,8 +230,10 @@ const formatDate = (dateString: string): string => {
</div>
<span class="badge bg-light text-dark border">{{ messages.length }} messaggi</span>
</div>
<MessageList :messages="messages" />
<MessageComposer @send="handleSendMessage" />
<MessageComposer @send=handleSendMessage />
</div>
</div>
</div>
@@ -252,10 +265,10 @@ const formatDate = (dateString: string): string => {
<h2 class="h6 mb-3">Valuta intervento</h2>
<p class="text-muted small mb-3">Lascia valutazione finale quando intervento è concluso.</p>
<button
v-if="!isFeedbackOpen"
type="button"
class="btn btn-primary w-100"
@click="isFeedbackOpen = true"
v-if="!isFeedbackOpen"
type="button"
class="btn btn-primary w-100"
@click="isFeedbackOpen = true"
>
Valuta intervento
</button>
@@ -267,9 +280,9 @@ const formatDate = (dateString: string): string => {
<button v-for="n in 5" :key="n" type="button" class="btn btn-link p-0"
@click="feedbackRating = n" :aria-label="`${n} stelle`">
<BIIcon
:name="n <= feedbackRating ? 'it-star-full' : 'it-star-outline'"
size="lg"
:color="n <= feedbackRating ? 'warning' : 'primary'"
:name="n <= feedbackRating ? 'it-star-full' : 'it-star-outline'"
size="lg"
:color="n <= feedbackRating ? 'warning' : 'primary'"
/>
</button>
</div>
@@ -277,10 +290,10 @@ const formatDate = (dateString: string): string => {
<div class="mb-3">
<textarea
v-model="feedbackComment"
class="form-control"
placeholder="Commento opzionale"
rows="3"
v-model="feedbackComment"
class="form-control"
placeholder="Commento opzionale"
rows="3"
/>
</div>
@@ -408,23 +421,25 @@ const formatDate = (dateString: string): string => {
justify-content: space-between;
}
.timeline-marker {
width: 12px;
height: 12px;
background-color: #006f9d;
border-radius: 50%;
border: 3px solid #f0f0f0;
/* Stili specifici per l'input della Chat per gestire le tue richieste visive */
.chat-textarea {
resize: none !important;
height: 48px;
min-height: 48px;
overflow-y: auto;
padding-top: 12px;
padding-bottom: 12px;
border-radius: 6px;
}
.timeline-item:not(:last-child) .timeline-marker::after {
content: '';
position: absolute;
left: 50%;
top: 20px;
width: 2px;
height: calc(100% + 20px);
background-color: #006f9d;
transform: translateX(-50%);
.btn-square {
width: 48px;
height: 48px;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
border-radius: 6px;
}
.ratio {
@@ -432,7 +447,7 @@ const formatDate = (dateString: string): string => {
display: block;
width: 100%;
aspect-ratio: 1;
img {
position: absolute;
top: 0;
@@ -457,6 +472,3 @@ const formatDate = (dateString: string): string => {
}
}
</style>