diff --git a/app/pages/operator/reports/[id].vue b/app/pages/operator/reports/[id].vue index 7552666..d74b4dd 100644 --- a/app/pages/operator/reports/[id].vue +++ b/app/pages/operator/reports/[id].vue @@ -33,6 +33,7 @@ const messages = computed(() => (messagesData.value as any)?.messages || []) const summary = computed(() => (summaryData.value as any)?.summary) const summaryMedia = computed(() => (summaryData.value as any)?.media || []) const teams = computed(() => teamsData.value?.teams || []) + const statusMeta: Record = { OPEN: 'Aperta', UNDER_REVIEW: 'In revisione', @@ -43,6 +44,43 @@ const statusMeta: Record = { CLOSED: 'Chiusa' } +// Mappa delle transizioni (Equivalente al backend C++) +const statusTransitions: Record = { + OPEN: ['UNDER_REVIEW', 'REJECTED'], + UNDER_REVIEW: ['ASSIGNED', 'REJECTED'], + ASSIGNED: ['IN_PROGRESS', 'REJECTED'], + IN_PROGRESS: ['RESOLVED', 'REJECTED'], + RESOLVED: ['CLOSED'], + REJECTED: ['CLOSED'], + CLOSED: [] +} + +const availableStatuses: { value: ReportStatus, label: string }[] = [ + { value: 'OPEN', label: 'Aperta' }, + { value: 'UNDER_REVIEW', label: 'In Revisione' }, + { value: 'ASSIGNED', label: 'Assegnata' }, + { value: 'IN_PROGRESS', label: 'In Lavorazione' }, + { value: 'RESOLVED', label: 'Risolta' }, + { value: 'REJECTED', label: 'Rifiutata' }, + { value: 'CLOSED', label: 'Chiusa' } +] + +// Computed per ottenere solo gli stati di destinazione validi +const selectableStatuses = computed(() => { + if (!report.value?.status) return [] + const allowed = statusTransitions[report.value.status] || [] + return availableStatuses.filter(s => allowed.includes(s.value)) +}) + +// Logica per consentire il link di intervento +// Corrisponde alla query: SELECT id FROM assignments WHERE report_id = ? AND ended_at IS NULL +const canCreateInterventionLink = computed(() => { + if (summary.value?.assignments && Array.isArray(summary.value.assignments)) { + return summary.value.assignments.some((a: any) => !a.ended_at) + } + return ['ASSIGNED', 'IN_PROGRESS'].includes(report.value?.status) +}) + // Form Cambia Stato const newStatus = ref('IN_PROGRESS') const statusNote = ref('') @@ -62,21 +100,20 @@ const isRevokeInterventionModalOpen = ref(false) // Messaggi const isSubmittingMessage = ref(false) -const isFlaggingMessageId = ref(null) +const flaggedMessageIds = ref([]) // Array locale per evitare lo spam di segnalazioni +const isFlagMessageModalOpen = ref(false) +const messageToFlag = ref(null) +const isFlaggingMessage = ref(false) +const isFlaggingMessageId = computed(() => isFlaggingMessage.value ? messageToFlag.value?.id : null) -const availableStatuses: { value: ReportStatus, label: string }[] = [ - { value: 'OPEN', label: 'Aperta' }, - { value: 'UNDER_REVIEW', label: 'In Revisione' }, - { value: 'ASSIGNED', label: 'Assegnata' }, - { value: 'IN_PROGRESS', label: 'In Lavorazione' }, - { value: 'RESOLVED', label: 'Risolta' }, - { value: 'REJECTED', label: 'Rifiutata' }, - { value: 'CLOSED', label: 'Chiusa' } -] - -// Inizializza lo stato nel form +// Inizializza lo stato nel form (seleziona automaticamente la prima transizione disponibile) watch(() => report.value?.status, (val) => { - if (val) newStatus.value = val as ReportStatus + if (val) { + const allowed = statusTransitions[val] || [] + if (allowed.length > 0) { + newStatus.value = allowed[0] as ReportStatus + } + } }, { immediate: true }) const handleStatusUpdate = async () => { @@ -130,6 +167,7 @@ const copyInterventionLink = async () => { } const handleCreateInterventionLink = async () => { + if (!canCreateInterventionLink.value) return isGeneratingInterventionLink.value = true interventionLinkError.value = '' interventionLinkSuccess.value = '' @@ -197,21 +235,37 @@ 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 isCitizenMessage = message.user_id !== user.value.id && message.user_id === report.value?.user_id + // Non permettere di segnalare se l'id del messaggio è già nell'array (anti-spam) + const isAlreadyFlagged = flaggedMessageIds.value.includes(message.id) + return isCitizenMessage && !isAlreadyFlagged } -const handleFlagMessage = async (message: Message) => { +const handleFlagMessage = (message: Message) => { if (!canFlagMessage(message)) return - isFlaggingMessageId.value = message.id + // Apri il modale per chiedere conferma + messageToFlag.value = message + isFlagMessageModalOpen.value = true +} + +const executeFlagMessage = async () => { + if (!messageToFlag.value) return + + isFlaggingMessage.value = true try { - await flagMessage(reportId, message.id, { + await flagMessage(reportId, messageToFlag.value.id, { reason: 'Messaggio segnalato da operatore responsabile per verifica moderazione', }) + // Aggiungi l'id all'array anti-spam locale così l'utente non può più segnalarlo in questa sessione + flaggedMessageIds.value.push(messageToFlag.value.id) + + isFlagMessageModalOpen.value = false + messageToFlag.value = null } catch (e) { console.error('Failed to flag message', e) } finally { - isFlaggingMessageId.value = null + isFlaggingMessage.value = false } } @@ -228,12 +282,12 @@ const latestCitizenMessage = computed(() => { if (!reportUserId) return null return [...messages.value] - .reverse() - .find((message) => message.user_id === reportUserId) || null + .reverse() + .find((message) => message.user_id === reportUserId) || null }) const formattedInterventionLinkExpiry = computed(() => - interventionLinkExpiresAt.value ? formatDate(interventionLinkExpiresAt.value) : '' + interventionLinkExpiresAt.value ? formatDate(interventionLinkExpiresAt.value) : '' ) @@ -326,10 +380,10 @@ const formattedInterventionLinkExpiry = computed(() => {{ messages.length }} messaggi @@ -338,126 +392,139 @@ const formattedInterventionLinkExpiry = computed(() =>
-
-
-

Quadro operativo

-
    -
  • - Operatore - {{ summary.assigned_operator_id ? `#${summary.assigned_operator_id}` : 'Nessuno' }} -
  • -
  • - Team disponibili - {{ teams.length }} -
  • -
  • - Allegati - {{ summaryMedia.length || media.length }} -
  • -
-
-

Ultimo messaggio cittadino

-

- {{ latestCitizenMessage?.body || 'Nessun messaggio del cittadino disponibile.' }} -

+
+
+

Quadro operativo

+
    +
  • + Operatore + {{ summary.assigned_operator_id ? `#${summary.assigned_operator_id}` : 'Nessuno' }} +
  • +
  • + Team disponibili + {{ teams.length }} +
  • +
  • + Allegati + {{ summaryMedia.length || media.length }} +
  • +
+
+

Ultimo messaggio cittadino

+

+ {{ latestCitizenMessage?.body || 'Nessun messaggio del cittadino disponibile.' }} +

+
-
-
- -
-
- -
-
-

Link intervento pubblico

-

Genera link temporaneo per team esterno. Nuova generazione ruota link esistente.

- - - - - - - -
-
- -
-
-

Aggiorna stato

-
-
- - -
-
- - -
- -
+
-
-
-
-

Cronologia eventi

-
Nessun evento registrato.
-
    -
  • - {{ formatDate(item.created_at) }} -
    - - - +
    +
    +

    Link intervento pubblico

    +

    Genera link temporaneo per team esterno. Nuova generazione ruota link esistente.

    + + + + + + + + +
    + È necessaria un'assegnazione in corso per generare o rigenerare questo link. +
    +
    +
    + +
    +
    +

    Aggiorna stato

    + + + + +
    +
    + +
    +
    +

    Cronologia eventi

    +
    Nessun evento registrato.
    +
      +
    • + {{ formatDate(item.created_at) }} +
      + + + +
      +
      {{ item.note }}
      +
    • +
    +
    -
@@ -501,14 +568,26 @@ const formattedInterventionLinkExpiry = computed(() =>
+ + +
@@ -629,4 +708,4 @@ const formattedInterventionLinkExpiry = computed(() => position: static; } } - + \ No newline at end of file