From a18e1f575ff734828c4deb4c771cd124866ca5f3 Mon Sep 17 00:00:00 2001 From: Matthieu Date: Mon, 22 Jun 2026 09:49:44 +0200 Subject: [PATCH] refactor(client-portal) : remove client portal feature entirely - drop ClientPortal module, ClientTicket entity, ROLE_CLIENT and all couplings (Task, TaskDocument, User, Notification) back to an internal-only model - migration drops client_ticket / user_allowed_projects / related FK columns and removes leftover external client accounts (would otherwise be promoted to ROLE_USER) - remove client-portal frontend module, admin tickets tab, user portal section, portal nav item and portal/clientTicket i18n keys - fix directory nav icon (invalid mdi:contact-multiple-outline -> mdi:card-account-details-outline) - add 'make sync-permissions' target, wire it into install/db-reset and the prod deploy script --- config/modules.php | 2 - config/packages/doctrine.yaml | 6 - config/packages/security.yaml | 2 +- config/services.yaml | 2 - config/sidebar.php | 3 +- frontend/app/middleware/auth.global.ts | 16 - frontend/app/middleware/portal.ts | 12 - .../components/admin/AdminClientTicketTab.vue | 256 --------------- .../notification/NotificationBell.vue | 11 - frontend/components/user/UserDrawer.vue | 76 +---- frontend/i18n/locales/fr.json | 52 +--- .../components/ClientTicketDetailModal.vue | 132 -------- .../components/ClientTicketFormModal.vue | 158 ---------- .../components/ClientTicketStatusBadge.vue | 25 -- .../components/ClientTicketTypeBadge.vue | 25 -- frontend/modules/client-portal/nuxt.config.ts | 1 - .../client-portal/pages/portal/index.vue | 93 ------ .../pages/portal/projects/[id].vue | 133 -------- .../client-portal/services/client-tickets.ts | 60 ---- .../services/dto/client-ticket.ts | 34 -- .../modules/client-portal/utils/ticket.ts | 18 -- .../components/TaskCard.vue | 7 - .../components/TaskDocumentUpload.vue | 5 +- .../components/TaskListItem.vue | 7 - .../project-management/services/dto/task.ts | 8 - .../services/task-documents.ts | 13 +- frontend/pages/admin.vue | 2 - frontend/services/dto/notification.ts | 3 +- frontend/services/dto/user-data.ts | 16 - infra/prod/deploy.sh | 3 + makefile | 7 +- migrations/Version20260622090000.php | 120 +++++++ src/DataFixtures/AppFixtures.php | 50 --- .../ClientPortal/ClientPortalModule.php | 41 --- .../Domain/Entity/ClientTicket.php | 245 --------------- .../Domain/Enum/ClientTicketStatus.php | 37 --- .../Domain/Enum/ClientTicketType.php | 21 -- .../ClientTicketRepositoryInterface.php | 19 -- .../State/ClientTicketNumberProcessor.php | 126 -------- .../State/ClientTicketProvider.php | 124 -------- .../State/ClientTicketStatusProcessor.php | 88 ------ .../DoctrineClientTicketRepository.php | 46 --- .../Core/Domain/Entity/Notification.php | 20 -- src/Module/Core/Domain/Entity/User.php | 68 +--- src/Module/Core/Infrastructure/Notifier.php | 3 - .../ProjectManagement/Domain/Entity/Task.php | 23 -- .../Domain/Entity/TaskDocument.php | 31 +- .../State/TaskDocumentProcessor.php | 113 ++----- .../State/TaskDocumentProvider.php | 45 +-- .../TaskDocumentDownloadController.php | 16 - .../Domain/Contract/ClientTicketInterface.php | 24 -- .../Domain/Contract/NotifierInterface.php | 1 - src/Shared/Domain/Contract/UserInterface.php | 14 - .../ClientPortal/ClientTicketApiTest.php | 293 ------------------ .../TimestampableBlamableSubscriberTest.php | 13 - 55 files changed, 170 insertions(+), 2599 deletions(-) delete mode 100644 frontend/app/middleware/portal.ts delete mode 100644 frontend/components/admin/AdminClientTicketTab.vue delete mode 100644 frontend/modules/client-portal/components/ClientTicketDetailModal.vue delete mode 100644 frontend/modules/client-portal/components/ClientTicketFormModal.vue delete mode 100644 frontend/modules/client-portal/components/ClientTicketStatusBadge.vue delete mode 100644 frontend/modules/client-portal/components/ClientTicketTypeBadge.vue delete mode 100644 frontend/modules/client-portal/nuxt.config.ts delete mode 100644 frontend/modules/client-portal/pages/portal/index.vue delete mode 100644 frontend/modules/client-portal/pages/portal/projects/[id].vue delete mode 100644 frontend/modules/client-portal/services/client-tickets.ts delete mode 100644 frontend/modules/client-portal/services/dto/client-ticket.ts delete mode 100644 frontend/modules/client-portal/utils/ticket.ts create mode 100644 migrations/Version20260622090000.php delete mode 100644 src/Module/ClientPortal/ClientPortalModule.php delete mode 100644 src/Module/ClientPortal/Domain/Entity/ClientTicket.php delete mode 100644 src/Module/ClientPortal/Domain/Enum/ClientTicketStatus.php delete mode 100644 src/Module/ClientPortal/Domain/Enum/ClientTicketType.php delete mode 100644 src/Module/ClientPortal/Domain/Repository/ClientTicketRepositoryInterface.php delete mode 100644 src/Module/ClientPortal/Infrastructure/ApiPlatform/State/ClientTicketNumberProcessor.php delete mode 100644 src/Module/ClientPortal/Infrastructure/ApiPlatform/State/ClientTicketProvider.php delete mode 100644 src/Module/ClientPortal/Infrastructure/ApiPlatform/State/ClientTicketStatusProcessor.php delete mode 100644 src/Module/ClientPortal/Infrastructure/Doctrine/DoctrineClientTicketRepository.php delete mode 100644 src/Shared/Domain/Contract/ClientTicketInterface.php delete mode 100644 tests/Functional/Module/ClientPortal/ClientTicketApiTest.php diff --git a/config/modules.php b/config/modules.php index ffef4f3..b0b6e90 100644 --- a/config/modules.php +++ b/config/modules.php @@ -8,7 +8,6 @@ declare(strict_types=1); */ use App\Module\Absence\AbsenceModule; -use App\Module\ClientPortal\ClientPortalModule; use App\Module\Core\CoreModule; use App\Module\Directory\DirectoryModule; use App\Module\Integration\IntegrationModule; @@ -26,5 +25,4 @@ return [ MailModule::class, IntegrationModule::class, ReportingModule::class, - ClientPortalModule::class, ]; diff --git a/config/packages/doctrine.yaml b/config/packages/doctrine.yaml index 48948ad..2aeef63 100644 --- a/config/packages/doctrine.yaml +++ b/config/packages/doctrine.yaml @@ -26,7 +26,6 @@ doctrine: App\Shared\Domain\Contract\TaskInterface: App\Module\ProjectManagement\Domain\Entity\Task App\Shared\Domain\Contract\TaskTagInterface: App\Module\ProjectManagement\Domain\Entity\TaskTag App\Shared\Domain\Contract\ClientInterface: App\Module\Directory\Domain\Entity\Client - App\Shared\Domain\Contract\ClientTicketInterface: App\Module\ClientPortal\Domain\Entity\ClientTicket mappings: Core: type: attribute @@ -63,11 +62,6 @@ doctrine: is_bundle: false dir: '%kernel.project_dir%/src/Module/Integration/Domain/Entity' prefix: 'App\Module\Integration\Domain\Entity' - ClientPortal: - type: attribute - is_bundle: false - dir: '%kernel.project_dir%/src/Module/ClientPortal/Domain/Entity' - prefix: 'App\Module\ClientPortal\Domain\Entity' controller_resolver: auto_mapping: false diff --git a/config/packages/security.yaml b/config/packages/security.yaml index 18306fb..24cb6de 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -1,6 +1,6 @@ security: role_hierarchy: - ROLE_ADMIN: [ROLE_USER, ROLE_CLIENT] + ROLE_ADMIN: [ROLE_USER] # https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords password_hashers: diff --git a/config/services.yaml b/config/services.yaml index 4e2b5af..6d11bb1 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -111,8 +111,6 @@ services: App\Module\Directory\Domain\Repository\ClientRepositoryInterface: '@App\Module\Directory\Infrastructure\Doctrine\DoctrineClientRepository' - App\Module\ClientPortal\Domain\Repository\ClientTicketRepositoryInterface: '@App\Module\ClientPortal\Infrastructure\Doctrine\DoctrineClientTicketRepository' - App\Module\Directory\Domain\Repository\ProspectRepositoryInterface: '@App\Module\Directory\Infrastructure\Doctrine\DoctrineProspectRepository' App\Module\Mail\Domain\Repository\MailConfigurationRepositoryInterface: '@App\Module\Mail\Infrastructure\Doctrine\DoctrineMailConfigurationRepository' diff --git a/config/sidebar.php b/config/sidebar.php index 945b5f9..1d9ad69 100644 --- a/config/sidebar.php +++ b/config/sidebar.php @@ -25,7 +25,6 @@ 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'], @@ -37,7 +36,7 @@ return [ 'roles' => ['ROLE_ADMIN'], 'items' => [ ['label' => 'sidebar.admin.teamAbsences', 'to' => '/team-absences', 'icon' => 'mdi:calendar-account-outline', 'module' => 'absence'], - ['label' => 'sidebar.admin.directory', 'to' => '/directory', 'icon' => 'mdi:contact-multiple-outline', 'module' => 'directory'], + ['label' => 'sidebar.admin.directory', 'to' => '/directory', 'icon' => 'mdi:card-account-details-outline', 'module' => 'directory'], ['label' => 'sidebar.admin.administration', 'to' => '/admin', 'icon' => 'mdi:cog-outline', 'permission' => 'core.users.view'], ['label' => 'sidebar.admin.reporting', 'to' => '/reporting', 'icon' => 'mdi:chart-line', 'module' => 'reporting', 'permission' => 'reporting.view'], ], diff --git a/frontend/app/middleware/auth.global.ts b/frontend/app/middleware/auth.global.ts index 0fec47c..4c98f1e 100644 --- a/frontend/app/middleware/auth.global.ts +++ b/frontend/app/middleware/auth.global.ts @@ -14,22 +14,6 @@ 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 deleted file mode 100644 index 4210bf2..0000000 --- a/frontend/app/middleware/portal.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * 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 deleted file mode 100644 index cdb744d..0000000 --- a/frontend/components/admin/AdminClientTicketTab.vue +++ /dev/null @@ -1,256 +0,0 @@ - - - diff --git a/frontend/components/notification/NotificationBell.vue b/frontend/components/notification/NotificationBell.vue index e3e9b6d..38383e9 100644 --- a/frontend/components/notification/NotificationBell.vue +++ b/frontend/components/notification/NotificationBell.vue @@ -102,22 +102,11 @@ function toggleDropdown() { } } -const auth = useAuthStore() - -const isAdmin = computed(() => (auth.user?.roles ?? []).includes('ROLE_ADMIN')) - function handleClick(notif: Notification) { if (!notif.isRead) { markAsRead(notif.id) } isOpen.value = false - - // Deep-link to the related ticket when present. The notification payload does - // not carry the ticket's project, so we route to the relevant list view: - // admins to the client-tickets admin tab, clients to their portal. - if (notif.relatedTicket) { - navigateTo(isAdmin.value ? '/admin' : '/portal') - } } async function handleMarkAllRead() { diff --git a/frontend/components/user/UserDrawer.vue b/frontend/components/user/UserDrawer.vue index beda546..149f7e3 100644 --- a/frontend/components/user/UserDrawer.vue +++ b/frontend/components/user/UserDrawer.vue @@ -48,26 +48,6 @@ - -
-

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

- -
- -
-
-
@@ -91,8 +71,6 @@ diff --git a/frontend/modules/client-portal/components/ClientTicketFormModal.vue b/frontend/modules/client-portal/components/ClientTicketFormModal.vue deleted file mode 100644 index 4d5d496..0000000 --- a/frontend/modules/client-portal/components/ClientTicketFormModal.vue +++ /dev/null @@ -1,158 +0,0 @@ - - - diff --git a/frontend/modules/client-portal/components/ClientTicketStatusBadge.vue b/frontend/modules/client-portal/components/ClientTicketStatusBadge.vue deleted file mode 100644 index f7d35b9..0000000 --- a/frontend/modules/client-portal/components/ClientTicketStatusBadge.vue +++ /dev/null @@ -1,25 +0,0 @@ - - - diff --git a/frontend/modules/client-portal/components/ClientTicketTypeBadge.vue b/frontend/modules/client-portal/components/ClientTicketTypeBadge.vue deleted file mode 100644 index ed9d76c..0000000 --- a/frontend/modules/client-portal/components/ClientTicketTypeBadge.vue +++ /dev/null @@ -1,25 +0,0 @@ - - - diff --git a/frontend/modules/client-portal/nuxt.config.ts b/frontend/modules/client-portal/nuxt.config.ts deleted file mode 100644 index 268da7f..0000000 --- a/frontend/modules/client-portal/nuxt.config.ts +++ /dev/null @@ -1 +0,0 @@ -export default defineNuxtConfig({}) diff --git a/frontend/modules/client-portal/pages/portal/index.vue b/frontend/modules/client-portal/pages/portal/index.vue deleted file mode 100644 index b1d5de4..0000000 --- a/frontend/modules/client-portal/pages/portal/index.vue +++ /dev/null @@ -1,93 +0,0 @@ - - - diff --git a/frontend/modules/client-portal/pages/portal/projects/[id].vue b/frontend/modules/client-portal/pages/portal/projects/[id].vue deleted file mode 100644 index c3efba8..0000000 --- a/frontend/modules/client-portal/pages/portal/projects/[id].vue +++ /dev/null @@ -1,133 +0,0 @@ - - - diff --git a/frontend/modules/client-portal/services/client-tickets.ts b/frontend/modules/client-portal/services/client-tickets.ts deleted file mode 100644 index 2bbd652..0000000 --- a/frontend/modules/client-portal/services/client-tickets.ts +++ /dev/null @@ -1,60 +0,0 @@ -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 deleted file mode 100644 index 25f90e0..0000000 --- a/frontend/modules/client-portal/services/dto/client-ticket.ts +++ /dev/null @@ -1,34 +0,0 @@ -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 deleted file mode 100644 index 04bc09c..0000000 --- a/frontend/modules/client-portal/utils/ticket.ts +++ /dev/null @@ -1,18 +0,0 @@ -/** - * 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 4c8a970..777087c 100644 --- a/frontend/modules/project-management/components/TaskCard.vue +++ b/frontend/modules/project-management/components/TaskCard.vue @@ -20,12 +20,6 @@ name="mdi:flag-variant" class="h-3.5 w-3.5 text-red-600" /> -

{{ task.title }}

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