From 8bf6f29561cda118d8edce7735da2ae15c61f22a Mon Sep 17 00:00:00 2001 From: gabdevele Date: Mon, 15 Jun 2026 12:48:18 +0200 Subject: [PATCH 1/4] feat(admin): add categories and teams management page --- .gitignore | 1 + app/assets/css/style.css | 33 +++ app/components/dashboard/AdminDashboard.vue | 17 +- app/composables/useAuth.ts | 11 +- app/composables/useOperator.ts | 7 +- app/composables/useReports.ts | 7 +- app/middleware/auth.ts | 4 +- app/middleware/guest.ts | 4 +- app/pages/admin/catalog.vue | 236 ++++++++++++++++++++ app/pages/admin/users.vue | 7 +- app/pages/operator/reports/[id].vue | 73 +++--- app/pages/reports/new.vue | 3 +- app/plugins/auth.client.ts | 3 +- 13 files changed, 354 insertions(+), 52 deletions(-) create mode 100644 app/pages/admin/catalog.vue diff --git a/.gitignore b/.gitignore index ad774cb..3f78594 100644 --- a/.gitignore +++ b/.gitignore @@ -24,4 +24,5 @@ logs !.env.example pnpm-*.yaml +.pnpm-store/ AGENTS.md \ No newline at end of file diff --git a/app/assets/css/style.css b/app/assets/css/style.css index 814b736..88bdbf1 100644 --- a/app/assets/css/style.css +++ b/app/assets/css/style.css @@ -296,6 +296,35 @@ html body .navbar .navbar-collapsable .navbar-nav li.nav-item a.nav-link[data-fo line-height: 1; } +.page-shell { + padding-block: 3rem; +} + +.page-shell-header { + align-items: flex-start; + display: flex; + flex-wrap: wrap; + gap: 1rem; + justify-content: space-between; + margin-bottom: 2rem; +} + +.page-shell-title { + margin-bottom: 0.25rem; +} + +.page-shell-subtitle { + color: var(--visione-text-soft); + margin-bottom: 0; + max-width: 48rem; +} + +.surface-card { + background: #fff; + border: 1px solid #dfe4ea; + border-radius: 4px; +} + .it-header-wrapper .header-slim-right-zone .btn .rounded-icon, .it-header-wrapper .header-slim-right-zone .btn .rounded-icon .icon { @@ -316,6 +345,10 @@ html body .navbar .navbar-collapsable .navbar-nav li.nav-item a.nav-link[data-fo } @media (max-width: 767.98px) { + .page-shell { + padding-block: 2rem; + } + .it-hero-wrapper .container { min-height: 220px; } diff --git a/app/components/dashboard/AdminDashboard.vue b/app/components/dashboard/AdminDashboard.vue index b60eacf..9598581 100644 --- a/app/components/dashboard/AdminDashboard.vue +++ b/app/components/dashboard/AdminDashboard.vue @@ -1,5 +1,4 @@ + + diff --git a/app/pages/admin/users.vue b/app/pages/admin/users.vue index 96af45d..e1d8a4a 100644 --- a/app/pages/admin/users.vue +++ b/app/pages/admin/users.vue @@ -13,6 +13,7 @@ const { getUsers, updateUserBan, updateUserRoles } = useAdmin() const { data: usersRes, pending, refresh } = useAsyncData('admin-users', () => getUsers()) const search = ref('') +const parseRoles = (roles?: string) => roles ? roles.split(',').map((r) => r.trim()).filter(Boolean) : [] const filteredUsers = computed(() => { if (!usersRes.value?.users) return [] @@ -47,7 +48,7 @@ const availableRoles = ['CITIZEN', 'OPERATOR', 'ADMIN'] const openRoleModal = (user: any) => { editingUserId.value = user.id - selectedRoles.value = user.roles ? user.roles.split(',').map((r: string) => r.trim()) : [] + selectedRoles.value = parseRoles(user.roles) } const closeRoleModal = () => { @@ -118,8 +119,8 @@ const saveRoles = async () => { {{ user.fiscal_code }} - - {{ r.trim() }} + + {{ r }} {{ user.points || 0 }} diff --git a/app/pages/operator/reports/[id].vue b/app/pages/operator/reports/[id].vue index d45ff33..af8c7c9 100644 --- a/app/pages/operator/reports/[id].vue +++ b/app/pages/operator/reports/[id].vue @@ -17,19 +17,22 @@ if (!reportId) { } const { getReportDetail, getMessages, addMessage, flagMessage } = useReports() -const { getInterventionSummary, updateReportStatus, assignTeam } = useOperator() +const { getInterventionSummary, updateReportStatus, assignTeam, getTeams } = useOperator() const { user } = useAuth() // Dati principali const { data: reportData, refresh: refreshReport } = await useAsyncData(`op-report-${reportId}`, () => getReportDetail(reportId)) const { data: messagesData, refresh: refreshMessages } = await useAsyncData(`op-messages-${reportId}`, () => getMessages(reportId)) const { data: summaryData, refresh: refreshSummary } = await useAsyncData(`op-summary-${reportId}`, () => getInterventionSummary(reportId)) +const { data: teamsData } = await useAsyncData('operator-teams', () => getTeams()) const report = computed(() => (reportData.value as any)?.report) const media = computed(() => (reportData.value as any)?.media || []) const statusHistory = computed(() => (reportData.value as any)?.history || []) 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', @@ -47,9 +50,7 @@ const isUpdatingStatus = ref(false) // Form Assegna Squadra const isAssignModalOpen = ref(false) -const teamName = ref('') -const assignNote = ref('') -const estimatedDate = ref('') +const selectedTeamId = ref(null) const isAssigning = ref(false) // Messaggi @@ -90,19 +91,15 @@ const handleStatusUpdate = async () => { } const handleAssignTeam = async () => { - if (!teamName.value) return + if (!selectedTeamId.value) return isAssigning.value = true try { const res = await assignTeam(reportId, { - assigned_to_team: teamName.value, - estimated_resolution_date: estimatedDate.value || undefined, - internal_notes: assignNote.value || undefined + team_id: selectedTeamId.value }) if (res.ok) { isAssignModalOpen.value = false - teamName.value = '' - assignNote.value = '' - estimatedDate.value = '' + selectedTeamId.value = null await Promise.all([refreshReport(), refreshSummary()]) } } catch (e) { @@ -152,6 +149,15 @@ const formatDate = (dateString: string) => { hour: '2-digit', minute: '2-digit' }) } + +const latestCitizenMessage = computed(() => { + const reportUserId = report.value?.user_id + if (!reportUserId) return null + + return [...messages.value] + .reverse() + .find((message) => message.user_id === reportUserId) || null +})