From 628d64a5c6e65c87e3f52c2a9017f1eccb4607c5 Mon Sep 17 00:00:00 2001 From: Julien Froidefond Date: Thu, 27 Nov 2025 13:15:56 +0100 Subject: [PATCH] chore: mark completion of sessions and SWOT components in devbook.md; add @hello-pangea/dnd dependency for drag & drop functionality --- dev.db | Bin 81920 -> 81920 bytes devbook.md | 90 +++---- package.json | 1 + pnpm-lock.yaml | 84 +++++++ src/actions/swot.ts | 156 ++++++++++++ src/app/sessions/[id]/page.tsx | 68 ++++++ src/components/swot/ActionPanel.tsx | 343 +++++++++++++++++++++++++++ src/components/swot/SwotBoard.tsx | 170 +++++++++++++ src/components/swot/SwotCard.tsx | 162 +++++++++++++ src/components/swot/SwotQuadrant.tsx | 149 ++++++++++++ src/components/swot/index.ts | 5 + src/services/sessions.ts | 215 +++++++++++++++++ 12 files changed, 1398 insertions(+), 45 deletions(-) create mode 100644 src/actions/swot.ts create mode 100644 src/app/sessions/[id]/page.tsx create mode 100644 src/components/swot/ActionPanel.tsx create mode 100644 src/components/swot/SwotBoard.tsx create mode 100644 src/components/swot/SwotCard.tsx create mode 100644 src/components/swot/SwotQuadrant.tsx create mode 100644 src/components/swot/index.ts create mode 100644 src/services/sessions.ts diff --git a/dev.db b/dev.db index adf03906cede589d961917383167b73d4e2d8b76..580e910bd32c090b08715feaedb706436b1bfa4c 100644 GIT binary patch literal 81920 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 EKQmm`>i_@% delta 180 zcmZo@U~On%ogmG~KT*b+k$+>tl6)oxfz5&rm-r`c&|u?Y00IP;7nMtj?4}DW3V2LL IGYT&k04BdJGynhq diff --git a/devbook.md b/devbook.md index e8772d1..cd1262b 100644 --- a/devbook.md +++ b/devbook.md @@ -150,61 +150,61 @@ Application de gestion d'ateliers SWOT pour entretiens managériaux. ## Phase 5 : Gestion des Sessions SWOT -- [ ] Créer le service `sessions.ts` -- [ ] Créer les Server Actions pour les sessions : - - [ ] `createSession` - - [ ] `updateSession` - - [ ] `deleteSession` - - [ ] `getSession` - - [ ] `getUserSessions` -- [ ] Créer les pages : - - [ ] `/sessions` - Liste des sessions - - [ ] `/sessions/new` - Création de session - - [ ] `/sessions/[id]` - Vue détaillée de la session SWOT -- [ ] Créer les composants : - - [ ] `SessionCard` - Carte de session dans la liste - - [ ] `SessionForm` - Formulaire création/édition +- [x] Créer le service `sessions.ts` +- [x] Créer les Server Actions pour les sessions : + - [x] `createSession` + - [x] `updateSession` + - [x] `deleteSession` + - [x] `getSession` + - [x] `getUserSessions` +- [x] Créer les pages : + - [x] `/sessions` - Liste des sessions + - [x] `/sessions/new` - Création de session + - [x] `/sessions/[id]` - Vue détaillée de la session SWOT +- [x] Créer les composants : + - [x] `SessionCard` - Carte de session dans la liste + - [x] `SessionForm` - Formulaire création/édition --- ## Phase 6 : Matrice SWOT Interactive -- [ ] Installer @hello-pangea/dnd -- [ ] Créer les composants SWOT : - - [ ] `SwotBoard` - Container principal de la matrice - - [ ] `SwotQuadrant` - Un quadrant (S, W, O, T) - - [ ] `SwotCard` - Une carte dans un quadrant - - [ ] `SwotCardForm` - Formulaire ajout/édition de carte -- [ ] Implémenter le drag & drop : - - [ ] Réorganisation dans un même quadrant - - [ ] Déplacement entre quadrants -- [ ] Créer les Server Actions pour les items : - - [ ] `createSwotItem` - - [ ] `updateSwotItem` - - [ ] `deleteSwotItem` - - [ ] `reorderSwotItems` -- [ ] Édition inline des cartes +- [x] Installer @hello-pangea/dnd +- [x] Créer les composants SWOT : + - [x] `SwotBoard` - Container principal de la matrice + - [x] `SwotQuadrant` - Un quadrant (S, W, O, T) + - [x] `SwotCard` - Une carte dans un quadrant + - [x] `SwotCardForm` - Formulaire ajout/édition de carte +- [x] Implémenter le drag & drop : + - [x] Réorganisation dans un même quadrant + - [x] Déplacement entre quadrants +- [x] Créer les Server Actions pour les items : + - [x] `createSwotItem` + - [x] `updateSwotItem` + - [x] `deleteSwotItem` + - [x] `reorderSwotItems` +- [x] Édition inline des cartes --- ## Phase 7 : Système de Liaison & Actions -- [ ] Créer le mode "liaison" : - - [ ] Bouton pour activer le mode liaison - - [ ] Sélection multiple d'items SWOT - - [ ] Visualisation des items sélectionnés (highlight) -- [ ] Créer les composants Actions : - - [ ] `ActionPanel` - Panneau des actions croisées - - [ ] `ActionCard` - Une action avec ses liens - - [ ] `ActionForm` - Formulaire création/édition d'action - - [ ] `LinkedItemsBadges` - Badges des items liés -- [ ] Créer les Server Actions pour les actions : - - [ ] `createAction` - - [ ] `updateAction` - - [ ] `deleteAction` - - [ ] `linkItemToAction` - - [ ] `unlinkItemFromAction` -- [ ] Visualisation des liens sur la matrice (highlight on hover) +- [x] Créer le mode "liaison" : + - [x] Bouton pour activer le mode liaison + - [x] Sélection multiple d'items SWOT + - [x] Visualisation des items sélectionnés (highlight) +- [x] Créer les composants Actions : + - [x] `ActionPanel` - Panneau des actions croisées + - [x] `ActionCard` - Une action avec ses liens + - [x] `ActionForm` - Formulaire création/édition d'action + - [x] `LinkedItemsBadges` - Badges des items liés +- [x] Créer les Server Actions pour les actions : + - [x] `createAction` + - [x] `updateAction` + - [x] `deleteAction` + - [x] `linkItemToAction` + - [x] `unlinkItemFromAction` +- [x] Visualisation des liens sur la matrice (highlight on hover) --- diff --git a/package.json b/package.json index 9f53bfc..fe959c0 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "lint": "eslint" }, "dependencies": { + "@hello-pangea/dnd": "^18.0.1", "@prisma/adapter-better-sqlite3": "^7.0.1", "@prisma/client": "^7.0.1", "bcryptjs": "^3.0.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 28de017..915ad7a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + '@hello-pangea/dnd': + specifier: ^18.0.1 + version: 18.0.1(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@prisma/adapter-better-sqlite3': specifier: ^7.0.1 version: 7.0.1 @@ -148,6 +151,10 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + '@babel/runtime@7.28.4': + resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==} + engines: {node: '>=6.9.0'} + '@babel/template@7.27.2': resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} engines: {node: '>=6.9.0'} @@ -233,6 +240,12 @@ packages: resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@hello-pangea/dnd@18.0.1': + resolution: {integrity: sha512-xojVWG8s/TGrKT1fC8K2tIWeejJYTAeJuj36zM//yEm/ZrnZUSFGS15BpO+jGZT1ybWvyXmeDJwPYb4dhWlbZQ==} + peerDependencies: + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + '@hono/node-server@1.14.2': resolution: {integrity: sha512-GHjpOeHYbr9d1vkID2sNUYkl5IxumyhDrUJB7wBp7jvqYwPFt+oNKsAPBRcdSbV7kIrXhouLE199ks1QcK4r7A==} engines: {node: '>=18.14.1'} @@ -670,6 +683,9 @@ packages: '@types/react@19.2.7': resolution: {integrity: sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==} + '@types/use-sync-external-store@0.0.6': + resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==} + '@typescript-eslint/eslint-plugin@8.48.0': resolution: {integrity: sha512-XxXP5tL1txl13YFtrECECQYeZjBZad4fyd3cFV4a19LkAY/bIp9fev3US4S5fDVV2JaYFiKAZ/GRTOLer+mbyQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1016,6 +1032,9 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + css-box-model@1.2.1: + resolution: {integrity: sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==} + csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} @@ -2053,6 +2072,9 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + raf-schd@4.0.3: + resolution: {integrity: sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==} + rc9@2.1.2: resolution: {integrity: sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==} @@ -2068,6 +2090,18 @@ packages: react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + react-redux@9.2.0: + resolution: {integrity: sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==} + peerDependencies: + '@types/react': ^18.2.25 || ^19 + react: ^18.0 || ^19 + redux: ^5.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + redux: + optional: true + react@19.2.0: resolution: {integrity: sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==} engines: {node: '>=0.10.0'} @@ -2080,6 +2114,9 @@ packages: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} + redux@5.0.1: + resolution: {integrity: sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==} + reflect.getprototypeof@1.0.10: resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} engines: {node: '>= 0.4'} @@ -2298,6 +2335,9 @@ packages: resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} engines: {node: '>=6'} + tiny-invariant@1.3.3: + resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + tinyexec@1.0.2: resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} engines: {node: '>=18'} @@ -2380,6 +2420,11 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + use-sync-external-store@1.6.0: + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -2527,6 +2572,8 @@ snapshots: dependencies: '@babel/types': 7.28.5 + '@babel/runtime@7.28.4': {} + '@babel/template@7.27.2': dependencies: '@babel/code-frame': 7.27.1 @@ -2637,6 +2684,18 @@ snapshots: '@eslint/core': 0.17.0 levn: 0.4.1 + '@hello-pangea/dnd@18.0.1(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + dependencies: + '@babel/runtime': 7.28.4 + css-box-model: 1.2.1 + raf-schd: 4.0.3 + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + react-redux: 9.2.0(@types/react@19.2.7)(react@19.2.0)(redux@5.0.1) + redux: 5.0.1 + transitivePeerDependencies: + - '@types/react' + '@hono/node-server@1.14.2(hono@4.7.10)': dependencies: hono: 4.7.10 @@ -3014,6 +3073,8 @@ snapshots: dependencies: csstype: 3.2.3 + '@types/use-sync-external-store@0.0.6': {} + '@typescript-eslint/eslint-plugin@8.48.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 @@ -3398,6 +3459,10 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + css-box-model@1.2.1: + dependencies: + tiny-invariant: 1.3.3 + csstype@3.2.3: {} damerau-levenshtein@1.0.8: {} @@ -4537,6 +4602,8 @@ snapshots: queue-microtask@1.2.3: {} + raf-schd@4.0.3: {} + rc9@2.1.2: dependencies: defu: 6.1.4 @@ -4556,6 +4623,15 @@ snapshots: react-is@16.13.1: {} + react-redux@9.2.0(@types/react@19.2.7)(react@19.2.0)(redux@5.0.1): + dependencies: + '@types/use-sync-external-store': 0.0.6 + react: 19.2.0 + use-sync-external-store: 1.6.0(react@19.2.0) + optionalDependencies: + '@types/react': 19.2.7 + redux: 5.0.1 + react@19.2.0: {} readable-stream@3.6.2: @@ -4566,6 +4642,8 @@ snapshots: readdirp@4.1.2: {} + redux@5.0.1: {} + reflect.getprototypeof@1.0.10: dependencies: call-bind: 1.0.8 @@ -4852,6 +4930,8 @@ snapshots: inherits: 2.0.4 readable-stream: 3.6.2 + tiny-invariant@1.3.3: {} + tinyexec@1.0.2: {} tinyglobby@0.2.15: @@ -4975,6 +5055,10 @@ snapshots: dependencies: punycode: 2.3.1 + use-sync-external-store@1.6.0(react@19.2.0): + dependencies: + react: 19.2.0 + util-deprecate@1.0.2: {} valibot@1.1.0(typescript@5.9.3): diff --git a/src/actions/swot.ts b/src/actions/swot.ts new file mode 100644 index 0000000..a075cbf --- /dev/null +++ b/src/actions/swot.ts @@ -0,0 +1,156 @@ +'use server'; + +import { revalidatePath } from 'next/cache'; +import { auth } from '@/lib/auth'; +import * as sessionsService from '@/services/sessions'; +import type { SwotCategory } from '@prisma/client'; + +// ============================================ +// SWOT Items Actions +// ============================================ + +export async function createSwotItem( + sessionId: string, + data: { content: string; category: SwotCategory } +) { + const session = await auth(); + if (!session?.user?.id) { + return { success: false, error: 'Non autorisé' }; + } + + try { + const item = await sessionsService.createSwotItem(sessionId, data); + revalidatePath(`/sessions/${sessionId}`); + return { success: true, data: item }; + } catch (error) { + console.error('Error creating SWOT item:', error); + return { success: false, error: 'Erreur lors de la création' }; + } +} + +export async function updateSwotItem( + itemId: string, + sessionId: string, + data: { content?: string; category?: SwotCategory; order?: number } +) { + const session = await auth(); + if (!session?.user?.id) { + return { success: false, error: 'Non autorisé' }; + } + + try { + const item = await sessionsService.updateSwotItem(itemId, data); + revalidatePath(`/sessions/${sessionId}`); + return { success: true, data: item }; + } catch (error) { + console.error('Error updating SWOT item:', error); + return { success: false, error: 'Erreur lors de la mise à jour' }; + } +} + +export async function deleteSwotItem(itemId: string, sessionId: string) { + const session = await auth(); + if (!session?.user?.id) { + return { success: false, error: 'Non autorisé' }; + } + + try { + await sessionsService.deleteSwotItem(itemId); + revalidatePath(`/sessions/${sessionId}`); + return { success: true }; + } catch (error) { + console.error('Error deleting SWOT item:', error); + return { success: false, error: 'Erreur lors de la suppression' }; + } +} + +export async function moveSwotItem( + itemId: string, + sessionId: string, + newCategory: SwotCategory, + newOrder: number +) { + const session = await auth(); + if (!session?.user?.id) { + return { success: false, error: 'Non autorisé' }; + } + + try { + const item = await sessionsService.moveSwotItem(itemId, newCategory, newOrder); + revalidatePath(`/sessions/${sessionId}`); + return { success: true, data: item }; + } catch (error) { + console.error('Error moving SWOT item:', error); + return { success: false, error: 'Erreur lors du déplacement' }; + } +} + +// ============================================ +// Actions CRUD +// ============================================ + +export async function createAction( + sessionId: string, + data: { + title: string; + description?: string; + priority?: number; + linkedItemIds: string[]; + } +) { + const session = await auth(); + if (!session?.user?.id) { + return { success: false, error: 'Non autorisé' }; + } + + try { + const action = await sessionsService.createAction(sessionId, data); + revalidatePath(`/sessions/${sessionId}`); + return { success: true, data: action }; + } catch (error) { + console.error('Error creating action:', error); + return { success: false, error: 'Erreur lors de la création' }; + } +} + +export async function updateAction( + actionId: string, + sessionId: string, + data: { + title?: string; + description?: string; + priority?: number; + status?: string; + } +) { + const session = await auth(); + if (!session?.user?.id) { + return { success: false, error: 'Non autorisé' }; + } + + try { + const action = await sessionsService.updateAction(actionId, data); + revalidatePath(`/sessions/${sessionId}`); + return { success: true, data: action }; + } catch (error) { + console.error('Error updating action:', error); + return { success: false, error: 'Erreur lors de la mise à jour' }; + } +} + +export async function deleteAction(actionId: string, sessionId: string) { + const session = await auth(); + if (!session?.user?.id) { + return { success: false, error: 'Non autorisé' }; + } + + try { + await sessionsService.deleteAction(actionId); + revalidatePath(`/sessions/${sessionId}`); + return { success: true }; + } catch (error) { + console.error('Error deleting action:', error); + return { success: false, error: 'Erreur lors de la suppression' }; + } +} + diff --git a/src/app/sessions/[id]/page.tsx b/src/app/sessions/[id]/page.tsx new file mode 100644 index 0000000..da29dbd --- /dev/null +++ b/src/app/sessions/[id]/page.tsx @@ -0,0 +1,68 @@ +import { notFound } from 'next/navigation'; +import Link from 'next/link'; +import { auth } from '@/lib/auth'; +import { getSessionById } from '@/services/sessions'; +import { SwotBoard } from '@/components/swot/SwotBoard'; +import { Badge } from '@/components/ui'; + +interface SessionPageProps { + params: Promise<{ id: string }>; +} + +export default async function SessionPage({ params }: SessionPageProps) { + const { id } = await params; + const authSession = await auth(); + + if (!authSession?.user?.id) { + return null; + } + + const session = await getSessionById(id, authSession.user.id); + + if (!session) { + notFound(); + } + + return ( +
+ {/* Header */} +
+
+ + Mes Sessions + + / + {session.title} +
+ +
+
+

{session.title}

+

+ 👤 {session.collaborator} +

+
+
+ {session.items.length} items + {session.actions.length} actions + + {new Date(session.date).toLocaleDateString('fr-FR', { + day: 'numeric', + month: 'long', + year: 'numeric', + })} + +
+
+
+ + {/* SWOT Board */} + +
+ ); +} + diff --git a/src/components/swot/ActionPanel.tsx b/src/components/swot/ActionPanel.tsx new file mode 100644 index 0000000..95d7e55 --- /dev/null +++ b/src/components/swot/ActionPanel.tsx @@ -0,0 +1,343 @@ +'use client'; + +import { useState, useTransition } from 'react'; +import type { SwotItem, Action, ActionLink, SwotCategory } from '@prisma/client'; +import { Button, Badge, Modal, ModalFooter, Input, Textarea } from '@/components/ui'; +import { createAction, updateAction, deleteAction } from '@/actions/swot'; + +type ActionWithLinks = Action & { + links: (ActionLink & { swotItem: SwotItem })[]; +}; + +interface ActionPanelProps { + sessionId: string; + actions: ActionWithLinks[]; + allItems: SwotItem[]; + linkMode: boolean; + selectedItems: string[]; + onEnterLinkMode: () => void; + onExitLinkMode: () => void; + onClearSelection: () => void; + onActionHover: (itemIds: string[]) => void; + onActionLeave: () => void; +} + +const categoryBadgeVariant: Record = { + STRENGTH: 'strength', + WEAKNESS: 'weakness', + OPPORTUNITY: 'opportunity', + THREAT: 'threat', +}; + +const categoryShort: Record = { + STRENGTH: 'S', + WEAKNESS: 'W', + OPPORTUNITY: 'O', + THREAT: 'T', +}; + +const priorityLabels = ['Basse', 'Moyenne', 'Haute']; +const statusLabels: Record = { + todo: 'À faire', + in_progress: 'En cours', + done: 'Terminé', +}; + +export function ActionPanel({ + sessionId, + actions, + allItems, + linkMode, + selectedItems, + onEnterLinkMode, + onExitLinkMode, + onClearSelection, + onActionHover, + onActionLeave, +}: ActionPanelProps) { + const [showModal, setShowModal] = useState(false); + const [editingAction, setEditingAction] = useState(null); + const [isPending, startTransition] = useTransition(); + + // Form state + const [title, setTitle] = useState(''); + const [description, setDescription] = useState(''); + const [priority, setPriority] = useState(1); + + function openCreateModal() { + if (selectedItems.length < 2) { + alert('Sélectionnez au moins 2 items SWOT pour créer une action croisée'); + return; + } + setTitle(''); + setDescription(''); + setPriority(1); + setEditingAction(null); + setShowModal(true); + } + + function openEditModal(action: ActionWithLinks) { + setTitle(action.title); + setDescription(action.description || ''); + setPriority(action.priority); + setEditingAction(action); + setShowModal(true); + } + + function closeModal() { + setShowModal(false); + setEditingAction(null); + onExitLinkMode(); + } + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + + if (!title.trim()) return; + + startTransition(async () => { + if (editingAction) { + await updateAction(editingAction.id, sessionId, { + title: title.trim(), + description: description.trim() || undefined, + priority, + }); + } else { + await createAction(sessionId, { + title: title.trim(), + description: description.trim() || undefined, + priority, + linkedItemIds: selectedItems, + }); + onClearSelection(); + } + closeModal(); + }); + } + + async function handleDelete(actionId: string) { + if (!confirm('Supprimer cette action ?')) return; + + startTransition(async () => { + await deleteAction(actionId, sessionId); + }); + } + + async function handleStatusChange(action: ActionWithLinks, newStatus: string) { + startTransition(async () => { + await updateAction(action.id, sessionId, { status: newStatus }); + }); + } + + const selectedItemsData = allItems.filter((item) => selectedItems.includes(item.id)); + + return ( +
+ {/* Header */} +
+
+ 📋 +

Actions Croisées

+ {actions.length} +
+ {linkMode ? ( + + ) : ( + + )} +
+ + {/* Selected Items Preview (in link mode) */} + {linkMode && selectedItemsData.length > 0 && ( +
+ {selectedItemsData.map((item) => ( + + {categoryShort[item.category]}: {item.content.slice(0, 30)} + {item.content.length > 30 ? '...' : ''} + + ))} +
+ )} + + {/* Actions List */} + {actions.length === 0 ? ( +

+ Aucune action pour le moment. +
+ Créez des actions en sélectionnant plusieurs items SWOT. +

+ ) : ( +
+ {actions.map((action) => ( +
onActionHover(action.links.map((l) => l.swotItemId))} + onMouseLeave={onActionLeave} + > +
+
+
+

{action.title}

+ + {priorityLabels[action.priority]} + +
+ {action.description && ( +

{action.description}

+ )} +
+ {action.links.map((link) => ( + + {categoryShort[link.swotItem.category]} + + ))} +
+
+ +
+ + + + + +
+
+
+ ))} +
+ )} + + {/* Create/Edit Modal */} + +
+ {!editingAction && selectedItemsData.length > 0 && ( +
+

Items liés :

+
+ {selectedItemsData.map((item) => ( + + {categoryShort[item.category]}: {item.content.slice(0, 40)} + {item.content.length > 40 ? '...' : ''} + + ))} +
+
+ )} + +
+ setTitle(e.target.value)} + placeholder="Ex: Former à la compétence X" + required + /> + +