diff --git a/app/components/ConfirmModal.vue b/app/components/ConfirmModal.vue new file mode 100644 index 0000000..b6df5e7 --- /dev/null +++ b/app/components/ConfirmModal.vue @@ -0,0 +1,60 @@ + + + diff --git a/app/composables/useAdmin.ts b/app/composables/useAdmin.ts index faf12a8..58ebd9b 100644 --- a/app/composables/useAdmin.ts +++ b/app/composables/useAdmin.ts @@ -3,6 +3,7 @@ import type { ModerationListResponse, OkResponse, OverviewStatsResponse, TeamListResponse, UpdateModerationRequest, + UpdateAreaRequest, UpdateCategoryRequest, UpdateTeamRequest, UpdateUserBanRequest, UpdateUserRolesRequest, UserListResponse } from "~/types/api"; @@ -26,17 +27,35 @@ export const useAdmin = () => { const createArea = (body: CreateAreaRequest) => useApiFetch('/api/v1/admin/areas', { method: 'POST', body }) + const updateArea = (id: number, body: UpdateAreaRequest) => + useApiFetch(`/api/v1/admin/areas/${id}`, { method: 'PATCH', body }) + + const deleteArea = (id: number) => + useApiFetch(`/api/v1/admin/areas/${id}`, { method: 'DELETE' }) + const getCategories = () => useApiFetch('/api/v1/admin/categories', { method: 'GET' }) const createCategory = (body: CreateCategoryRequest) => useApiFetch('/api/v1/admin/categories', { method: 'POST', body }) + const updateCategory = (id: number, body: UpdateCategoryRequest) => + useApiFetch(`/api/v1/admin/categories/${id}`, { method: 'PATCH', body }) + + const deleteCategory = (id: number) => + useApiFetch(`/api/v1/admin/categories/${id}`, { method: 'DELETE' }) + const getTeams = () => useApiFetch('/api/v1/admin/teams', { method: 'GET' }) const createTeam = (body: CreateTeamRequest) => useApiFetch('/api/v1/admin/teams', { method: 'POST', body }) + + const updateTeam = (id: number, body: UpdateTeamRequest) => + useApiFetch(`/api/v1/admin/teams/${id}`, { method: 'PATCH', body }) + + const deleteTeam = (id: number) => + useApiFetch(`/api/v1/admin/teams/${id}`, { method: 'DELETE' }) const getModerationFlags = (query?: { status?: string; page?: number; per_page?: number }) => useApiFetch('/api/v1/admin/moderation', { method: 'GET', query }) @@ -51,10 +70,16 @@ export const useAdmin = () => { updateUserRoles, getAreas, createArea, + updateArea, + deleteArea, getCategories, createCategory, + updateCategory, + deleteCategory, getTeams, createTeam, + updateTeam, + deleteTeam, getModerationFlags, updateModerationFlag, } diff --git a/app/composables/useOperator.ts b/app/composables/useOperator.ts index c10b51f..600eae2 100644 --- a/app/composables/useOperator.ts +++ b/app/composables/useOperator.ts @@ -1,4 +1,5 @@ import type { + AssignmentLinkResponse, AssignTeamRequest, InterventionSummaryResponse, OkResponse, @@ -32,6 +33,12 @@ export const useOperator = () => { const getInterventionSummary = (id: number) => useApiFetch(`/api/v1/reports/${id}/intervention-summary`, { method: 'GET' }) + const createInterventionLink = (id: number) => + useApiFetch(`/api/v1/reports/${id}/intervention-link`, { method: 'POST' }) + + const revokeInterventionLink = (id: number) => + useApiFetch(`/api/v1/reports/${id}/intervention-link`, { method: 'DELETE' }) + return { getOpenReports, getAssignedReports, @@ -41,5 +48,7 @@ export const useOperator = () => { updateReportStatus, assignTeam, getInterventionSummary, + createInterventionLink, + revokeInterventionLink, } } diff --git a/app/pages/admin/areas.vue b/app/pages/admin/areas.vue index 6b49fcf..0ae8190 100644 --- a/app/pages/admin/areas.vue +++ b/app/pages/admin/areas.vue @@ -1,5 +1,5 @@ @@ -165,10 +287,14 @@ const handleCreate = async () => {
-
+
Nessuna area configurata.
+
+ {{ actionError }} +
+
@@ -176,23 +302,32 @@ const handleCreate = async () => { - + - + - +
ID Nome DescrizioneStato GeoJSONAzioni
{{ area.id }} {{ area.name }} {{ area.description || '-' }} - Attiva - Inattiva - Configurato Mancante + + +
@@ -204,7 +339,7 @@ const handleCreate = async () => { + +
diff --git a/app/pages/admin/catalog.vue b/app/pages/admin/catalog.vue index fedcaf3..9a00de1 100644 --- a/app/pages/admin/catalog.vue +++ b/app/pages/admin/catalog.vue @@ -1,4 +1,6 @@ diff --git a/app/pages/admin/moderation.vue b/app/pages/admin/moderation.vue index 2c85838..f7ee66e 100644 --- a/app/pages/admin/moderation.vue +++ b/app/pages/admin/moderation.vue @@ -1,5 +1,5 @@ @@ -95,6 +123,14 @@ const saveRoles = async () => { Nessun utente trovato.
+
+ {{ banError }} +
+ +
+ {{ roleError }} +
+
@@ -138,7 +174,7 @@ const saveRoles = async () => { @@ -177,5 +213,16 @@ const saveRoles = async () => { + + diff --git a/app/pages/operator/reports/[id].vue b/app/pages/operator/reports/[id].vue index af8c7c9..7552666 100644 --- a/app/pages/operator/reports/[id].vue +++ b/app/pages/operator/reports/[id].vue @@ -17,7 +17,7 @@ if (!reportId) { } const { getReportDetail, getMessages, addMessage, flagMessage } = useReports() -const { getInterventionSummary, updateReportStatus, assignTeam, getTeams } = useOperator() +const { getInterventionSummary, updateReportStatus, assignTeam, getTeams, createInterventionLink, revokeInterventionLink } = useOperator() const { user } = useAuth() // Dati principali @@ -52,6 +52,13 @@ const isUpdatingStatus = ref(false) const isAssignModalOpen = ref(false) const selectedTeamId = ref(null) const isAssigning = ref(false) +const interventionLink = ref('') +const interventionLinkExpiresAt = ref('') +const isGeneratingInterventionLink = ref(false) +const isRevokingInterventionLink = ref(false) +const interventionLinkError = ref('') +const interventionLinkSuccess = ref('') +const isRevokeInterventionModalOpen = ref(false) // Messaggi const isSubmittingMessage = ref(false) @@ -109,6 +116,72 @@ const handleAssignTeam = async () => { } } +const copyInterventionLink = async () => { + if (!interventionLink.value || !navigator?.clipboard) return + + try { + await navigator.clipboard.writeText(interventionLink.value) + interventionLinkSuccess.value = 'Link copiato negli appunti.' + interventionLinkError.value = '' + } catch { + interventionLinkError.value = 'Copia automatica non riuscita.' + interventionLinkSuccess.value = '' + } +} + +const handleCreateInterventionLink = async () => { + isGeneratingInterventionLink.value = true + interventionLinkError.value = '' + interventionLinkSuccess.value = '' + + try { + const res = await createInterventionLink(reportId) + if (res.ok) { + interventionLink.value = res.url + interventionLinkExpiresAt.value = res.expires_at + interventionLinkSuccess.value = 'Link intervento generato.' + } + } catch (e: any) { + console.error('Failed to create intervention link', e) + interventionLinkError.value = e?.data?.message || 'Generazione link non riuscita.' + } finally { + isGeneratingInterventionLink.value = false + } +} + +const openRevokeInterventionLinkModal = () => { + if (!interventionLink.value) return + isRevokeInterventionModalOpen.value = true +} + +const closeRevokeInterventionLinkModal = () => { + if (isRevokingInterventionLink.value) return + isRevokeInterventionModalOpen.value = false +} + +const handleRevokeInterventionLink = async () => { + if (!interventionLink.value) return + + isRevokingInterventionLink.value = true + interventionLinkError.value = '' + interventionLinkSuccess.value = '' + + try { + const res = await revokeInterventionLink(reportId) + if (res.ok) { + interventionLink.value = '' + interventionLinkExpiresAt.value = '' + interventionLinkSuccess.value = 'Link intervento revocato.' + isRevokeInterventionModalOpen.value = false + } + } catch (e: any) { + console.error('Failed to revoke intervention link', e) + interventionLinkError.value = e?.data?.message || 'Revoca link non riuscita.' + } finally { + isRevokingInterventionLink.value = false + } +} + const handleSendMessage = async (msgText: string, files: File[]) => { if (!msgText.trim() && files.length === 0) return isSubmittingMessage.value = true @@ -158,6 +231,10 @@ const latestCitizenMessage = computed(() => { .reverse() .find((message) => message.user_id === reportUserId) || null }) + +const formattedInterventionLinkExpiry = computed(() => + interventionLinkExpiresAt.value ? formatDate(interventionLinkExpiresAt.value) : '' +)