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 9393081..58ebd9b 100644 --- a/app/composables/useAdmin.ts +++ b/app/composables/useAdmin.ts @@ -30,6 +30,9 @@ export const useAdmin = () => { 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' }) @@ -39,6 +42,9 @@ export const useAdmin = () => { 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' }) @@ -47,6 +53,9 @@ export const useAdmin = () => { 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 }) @@ -62,12 +71,15 @@ export const useAdmin = () => { 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 cb44580..ba3cd4a 100644 --- a/app/pages/admin/areas.vue +++ b/app/pages/admin/areas.vue @@ -9,7 +9,7 @@ definePageMeta({ useHead({ title: 'Aree - Admin' }) -const { getAreas, createArea, updateArea } = useAdmin() +const { getAreas, createArea, updateArea, deleteArea } = useAdmin() const { data: areasRes, pending, refresh } = useAsyncData('admin-areas', () => getAreas()) const isCreating = ref(false) @@ -20,6 +20,9 @@ const formData = reactive({ geojson: '' }) const jsonError = ref('') +const actionError = ref('') +const deletingAreaId = ref(null) +const areaPendingDelete = ref(null) // Map variables let map: any = null @@ -29,8 +32,10 @@ const mapPoints = ref<[number, number][]>([]) const modalTitle = computed(() => editingAreaId.value ? 'Modifica Area' : 'Nuova Area') const submitLabel = computed(() => editingAreaId.value ? 'Salva Modifiche' : 'Crea Area') const isEditing = computed(() => editingAreaId.value !== null) +const areas = computed(() => areasRes.value?.areas || []) const openCreateModal = () => { + actionError.value = '' editingAreaId.value = null formData.name = '' formData.description = '' @@ -98,6 +103,7 @@ const loadPolygonFromGeoJson = (boundaryGeojson: any) => { } const openEditModal = (area: any) => { + actionError.value = '' editingAreaId.value = area.id formData.name = area.name || '' formData.description = area.description || '' @@ -218,22 +224,45 @@ const handleCreate = async () => { if (res.ok) { await refresh() closeCreateModal() + actionError.value = '' } } catch (err) { console.error('Failed to save area', err) - alert('Errore durante il salvataggio dell\'area.') + actionError.value = 'Errore durante il salvataggio dell\'area.' } } -const handleToggleAreaActive = async (area: any) => { +const askDeleteArea = (area: any) => { + areaPendingDelete.value = area +} + +const closeDeleteAreaModal = () => { + if (deletingAreaId.value) return + areaPendingDelete.value = null +} + +const handleDeleteArea = async () => { + if (!areaPendingDelete.value) return + + const area = areaPendingDelete.value + + deletingAreaId.value = area.id + actionError.value = '' + try { - const res = await updateArea(area.id, { active: !area.active }) + const res = await deleteArea(area.id) if (res.ok) { + if (editingAreaId.value === area.id) { + closeCreateModal() + } await refresh() + areaPendingDelete.value = null } - } catch (err) { - console.error('Failed to update area status', err) - alert('Errore durante l\'aggiornamento dello stato area.') + } catch (err: any) { + console.error('Failed to delete area', err) + actionError.value = err?.data?.message || 'Errore durante eliminazione area.' + } finally { + deletingAreaId.value = null } } @@ -255,10 +284,14 @@ const handleToggleAreaActive = async (area: any) => {
-
+
Nessuna area configurata.
+
+ {{ actionError }} +
+
@@ -266,20 +299,15 @@ const handleToggleAreaActive = async (area: any) => { - - + - @@ -360,6 +389,17 @@ const handleToggleAreaActive = async (area: any) => { + + diff --git a/app/pages/admin/catalog.vue b/app/pages/admin/catalog.vue index 03cfbf4..1cd8874 100644 --- a/app/pages/admin/catalog.vue +++ b/app/pages/admin/catalog.vue @@ -8,7 +8,7 @@ definePageMeta({ useHead({ title: 'Categorie e Team - Admin' }) -const { createCategory, updateCategory, getTeams, createTeam, updateTeam, getAreas } = useAdmin() +const { createCategory, updateCategory, deleteCategory, getTeams, createTeam, updateTeam, deleteTeam, getAreas } = useAdmin() const { getCategories } = useReports() const { data: categoriesRes, refresh: refreshCategories } = await useAsyncData('report-categories-admin-view', () => getCategories()) @@ -35,6 +35,10 @@ const isSavingCategory = ref(false) const isSavingTeam = ref(false) const categoryError = ref('') const teamError = ref('') +const deletingCategoryId = ref(null) +const deletingTeamId = ref(null) +const categoryPendingDelete = ref(null) +const teamPendingDelete = ref(null) const categories = computed(() => categoriesRes.value?.categories || []) const teams = computed(() => teamsRes.value?.teams || []) @@ -127,6 +131,37 @@ const toggleCategoryActive = async (category: Category) => { } } +const askDeleteCategory = (category: Category) => { + categoryPendingDelete.value = category +} + +const closeDeleteCategoryModal = () => { + if (deletingCategoryId.value) return + categoryPendingDelete.value = null +} + +const handleDeleteCategory = async () => { + if (!categoryPendingDelete.value) return + + const category = categoryPendingDelete.value + + deletingCategoryId.value = category.id + categoryError.value = '' + + try { + await deleteCategory(category.id) + if (editingCategoryId.value === category.id) { + resetCategoryForm() + } + await refreshCategories() + categoryPendingDelete.value = null + } catch (error: any) { + categoryError.value = error?.data?.message || 'Eliminazione categoria non riuscita.' + } finally { + deletingCategoryId.value = null + } +} + const toggleTeamActive = async (team: Team) => { try { await updateTeam(team.id, { active: team.active === false }) @@ -135,6 +170,37 @@ const toggleTeamActive = async (team: Team) => { teamError.value = error?.data?.message || 'Aggiornamento team non riuscito.' } } + +const askDeleteTeam = (team: Team) => { + teamPendingDelete.value = team +} + +const closeDeleteTeamModal = () => { + if (deletingTeamId.value) return + teamPendingDelete.value = null +} + +const handleDeleteTeam = async () => { + if (!teamPendingDelete.value) return + + const team = teamPendingDelete.value + + deletingTeamId.value = team.id + teamError.value = '' + + try { + await deleteTeam(team.id) + if (editingTeamId.value === team.id) { + resetTeamForm() + } + await refreshTeams() + teamPendingDelete.value = null + } catch (error: any) { + teamError.value = error?.data?.message || 'Eliminazione team non riuscita.' + } finally { + deletingTeamId.value = null + } +} 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 +116,14 @@ const saveRoles = async () => { Nessun utente trovato. +
+ {{ banError }} +
+ +
+ {{ roleError }} +
+
ID Nome DescrizioneStato GeoJSON Azioni
{{ area.id }} {{ area.name }} {{ area.description || '-' }} - Attiva - Inattiva - Configurato Mancante @@ -289,11 +317,12 @@ const handleToggleAreaActive = async (area: any) => { Modifica
@@ -138,7 +167,7 @@ const saveRoles = async () => { @@ -177,5 +206,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) : '' +)