diff --git a/config/sidebar.php b/config/sidebar.php index 2f1e15f..945b5f9 100644 --- a/config/sidebar.php +++ b/config/sidebar.php @@ -25,6 +25,7 @@ return [ ['label' => 'sidebar.general.dashboard', 'to' => '/', 'icon' => 'mdi:view-dashboard-outline'], ['label' => 'sidebar.general.myTasks', 'to' => '/my-tasks', 'icon' => 'mdi:clipboard-check-outline', 'module' => 'project-management'], ['label' => 'sidebar.general.projects', 'to' => '/projects', 'icon' => 'mdi:folder-outline', 'module' => 'project-management'], + ['label' => 'sidebar.general.portal', 'to' => '/portal', 'icon' => 'mdi:account-box-outline', 'module' => 'client-portal'], ['label' => 'sidebar.general.timeTracking', 'to' => '/time-tracking', 'icon' => 'mdi:calendar-edit-outline', 'module' => 'time-tracking'], // Gating module uniquement (cf. en-tête) : rendu visuel + badge gérés côté layout. ['label' => 'sidebar.general.mail', 'to' => '/mail', 'icon' => 'mdi:email-outline', 'module' => 'mail'], diff --git a/frontend/app/middleware/auth.global.ts b/frontend/app/middleware/auth.global.ts index 4c98f1e..0fec47c 100644 --- a/frontend/app/middleware/auth.global.ts +++ b/frontend/app/middleware/auth.global.ts @@ -14,6 +14,22 @@ export default defineNuxtRouteMiddleware(async (to) => { return navigateTo('/') } + // Cloisonnement portail client : un utilisateur ROLE_CLIENT "pur" (a ROLE_CLIENT + // mais PAS ROLE_USER) n'a accès qu'aux pages /portal. Toute autre route interne + // est redirigée vers /portal. Les ROLE_ADMIN / ROLE_USER ne sont pas concernés + // (ils peuvent aussi visiter /portal pour prévisualiser). + if (auth.isAuthenticated && !isLogin) { + const roles = auth.user?.roles ?? [] + const isPureClient = roles.includes('ROLE_CLIENT') && !roles.includes('ROLE_USER') + + if (isPureClient) { + const isPortalRoute = to.path === '/portal' || to.path.startsWith('/portal/') + if (!isPortalRoute) { + return navigateTo('/portal') + } + } + } + const { loaded: sidebarLoaded, loadSidebar, resetSidebar } = useSidebar() const { loaded: modulesLoaded, loadModules, resetModules } = useModules() diff --git a/frontend/app/middleware/portal.ts b/frontend/app/middleware/portal.ts new file mode 100644 index 0000000..4210bf2 --- /dev/null +++ b/frontend/app/middleware/portal.ts @@ -0,0 +1,12 @@ +/** + * Named middleware for portal pages (`/portal/**`). + * Ensures the user is authenticated. Access is open to every authenticated user + * (ROLE_CLIENT see their portal, ROLE_ADMIN/ROLE_USER may preview it). + */ +export default defineNuxtRouteMiddleware(() => { + const auth = useAuthStore() + + if (!auth.isAuthenticated) { + return navigateTo('/login') + } +}) diff --git a/frontend/components/admin/AdminClientTicketTab.vue b/frontend/components/admin/AdminClientTicketTab.vue new file mode 100644 index 0000000..cdb744d --- /dev/null +++ b/frontend/components/admin/AdminClientTicketTab.vue @@ -0,0 +1,256 @@ + + + diff --git a/frontend/components/user/UserDrawer.vue b/frontend/components/user/UserDrawer.vue index 964f818..beda546 100644 --- a/frontend/components/user/UserDrawer.vue +++ b/frontend/components/user/UserDrawer.vue @@ -48,6 +48,26 @@ + +
+

{{ $t('users.clientAccount') }}

+ +
+ +
+
+
@@ -71,6 +91,8 @@ diff --git a/frontend/modules/client-portal/components/ClientTicketFormModal.vue b/frontend/modules/client-portal/components/ClientTicketFormModal.vue new file mode 100644 index 0000000..4d5d496 --- /dev/null +++ b/frontend/modules/client-portal/components/ClientTicketFormModal.vue @@ -0,0 +1,158 @@ + + + diff --git a/frontend/modules/client-portal/components/ClientTicketStatusBadge.vue b/frontend/modules/client-portal/components/ClientTicketStatusBadge.vue new file mode 100644 index 0000000..f7d35b9 --- /dev/null +++ b/frontend/modules/client-portal/components/ClientTicketStatusBadge.vue @@ -0,0 +1,25 @@ + + + diff --git a/frontend/modules/client-portal/components/ClientTicketTypeBadge.vue b/frontend/modules/client-portal/components/ClientTicketTypeBadge.vue new file mode 100644 index 0000000..ed9d76c --- /dev/null +++ b/frontend/modules/client-portal/components/ClientTicketTypeBadge.vue @@ -0,0 +1,25 @@ + + + diff --git a/frontend/modules/client-portal/nuxt.config.ts b/frontend/modules/client-portal/nuxt.config.ts new file mode 100644 index 0000000..268da7f --- /dev/null +++ b/frontend/modules/client-portal/nuxt.config.ts @@ -0,0 +1 @@ +export default defineNuxtConfig({}) diff --git a/frontend/modules/client-portal/pages/portal.vue b/frontend/modules/client-portal/pages/portal.vue new file mode 100644 index 0000000..b1d5de4 --- /dev/null +++ b/frontend/modules/client-portal/pages/portal.vue @@ -0,0 +1,93 @@ + + + diff --git a/frontend/modules/client-portal/pages/portal/projects/[id].vue b/frontend/modules/client-portal/pages/portal/projects/[id].vue new file mode 100644 index 0000000..c3efba8 --- /dev/null +++ b/frontend/modules/client-portal/pages/portal/projects/[id].vue @@ -0,0 +1,133 @@ + + + diff --git a/frontend/modules/client-portal/services/client-tickets.ts b/frontend/modules/client-portal/services/client-tickets.ts new file mode 100644 index 0000000..2bbd652 --- /dev/null +++ b/frontend/modules/client-portal/services/client-tickets.ts @@ -0,0 +1,60 @@ +import type { + ClientTicket, + ClientTicketCreate, + ClientTicketStatusUpdate, +} from './dto/client-ticket' +import type { HydraCollection } from '~/utils/api' +import { extractHydraMembers } from '~/utils/api' + +export type ClientTicketFilters = { + project?: number | string + status?: string + submittedBy?: number | string +} + +export function useClientTicketService() { + const api = useApi() + + async function getAll(params?: ClientTicketFilters): Promise { + const query: Record = {} + if (params?.project !== undefined && params.project !== '') { + query.project = typeof params.project === 'number' + ? `/api/projects/${params.project}` + : params.project + } + if (params?.status) { + query.status = params.status + } + if (params?.submittedBy !== undefined && params.submittedBy !== '') { + query.submittedBy = typeof params.submittedBy === 'number' + ? `/api/users/${params.submittedBy}` + : params.submittedBy + } + const data = await api.get>('/client_tickets', query) + return extractHydraMembers(data) + } + + async function getById(id: number): Promise { + return api.get(`/client_tickets/${id}`) + } + + async function create(payload: ClientTicketCreate): Promise { + return api.post('/client_tickets', payload as Record, { + toastSuccessKey: 'clientTicket.created', + }) + } + + async function updateStatus(id: number, payload: ClientTicketStatusUpdate): Promise { + return api.patch(`/client_tickets/${id}`, payload as Record, { + toastSuccessKey: 'clientTicket.statusChanged', + }) + } + + async function remove(id: number): Promise { + await api.delete(`/client_tickets/${id}`, {}, { + toastSuccessKey: 'clientTicket.deleted', + }) + } + + return { getAll, getById, create, updateStatus, remove } +} diff --git a/frontend/modules/client-portal/services/dto/client-ticket.ts b/frontend/modules/client-portal/services/dto/client-ticket.ts new file mode 100644 index 0000000..25f90e0 --- /dev/null +++ b/frontend/modules/client-portal/services/dto/client-ticket.ts @@ -0,0 +1,34 @@ +import type { TaskDocument } from '~/modules/project-management/services/dto/task-document' + +export type ClientTicketType = 'bug' | 'improvement' | 'other' +export type ClientTicketStatus = 'new' | 'in_progress' | 'done' | 'rejected' + +export type ClientTicket = { + '@id'?: string + id: number + number: number + type: ClientTicketType + title: string + description: string + url: string | null + status: ClientTicketStatus + statusComment: string | null + project: string // IRI + submittedBy: string | null // IRI, nullable (ON DELETE SET NULL) + createdAt: string + updatedAt: string + documents?: TaskDocument[] +} + +export type ClientTicketCreate = { + type: ClientTicketType + title: string + description: string + url?: string | null + project: string // IRI +} + +export type ClientTicketStatusUpdate = { + status: ClientTicketStatus + statusComment?: string | null +} diff --git a/frontend/modules/client-portal/utils/ticket.ts b/frontend/modules/client-portal/utils/ticket.ts new file mode 100644 index 0000000..04bc09c --- /dev/null +++ b/frontend/modules/client-portal/utils/ticket.ts @@ -0,0 +1,18 @@ +/** + * Format a client-ticket number for display, e.g. `CT-001`. + */ +export function formatTicketNumber(value: number): string { + return `CT-${String(value).padStart(3, '0')}` +} + +/** + * Extract the numeric id from an API Platform IRI (e.g. `/api/projects/5` → 5). + * Returns null when the IRI cannot be parsed. + */ +export function iriToId(iri: string | null | undefined): number | null { + if (!iri) { + return null + } + const match = iri.match(/(\d+)$/) + return match ? Number(match[1]) : null +} diff --git a/frontend/modules/project-management/components/TaskCard.vue b/frontend/modules/project-management/components/TaskCard.vue index 777087c..4c8a970 100644 --- a/frontend/modules/project-management/components/TaskCard.vue +++ b/frontend/modules/project-management/components/TaskCard.vue @@ -20,6 +20,12 @@ name="mdi:flag-variant" class="h-3.5 w-3.5 text-red-600" /> +

{{ task.title }}

@@ -103,6 +109,7 @@