diff --git a/.gitignore b/.gitignore index c5bf6e7..5245909 100644 --- a/.gitignore +++ b/.gitignore @@ -1,111 +1,5 @@ node_modules/ -# Dependencies -**/node_modules -.pnpm-store/ - -# Build outputs dist/ -build/ -*.dist - -# Environment variables .env -.env.local -.env.development.local -.env.test.local -.env.production.local - -# IDE and editor files -.vscode/ -.idea/ -*.swp -*.swo -*~ - -# OS generated files -.DS_Store -.DS_Store? -._* -.Spotlight-V100 -.Trashes -ehthumbs.db -Thumbs.db - -# Logs -logs *.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -pnpm-debug.log* -lerna-debug.log* -# Runtime data -pids -*.pid -*.seed -*.pid.lock -*.bak - -# Coverage directory used by tools like istanbul -coverage/ -*.lcov - -# nyc test coverage -.nyc_output - -# Dependency directories -jspm_packages/ - -# TypeScript cache -*.tsbuildinfo - -# Optional npm cache directory -.npm - -# Optional eslint cache -.eslintcache - -# Microbundle cache -.rpt2_cache/ -.rts2_cache_cjs/ -.rts2_cache_es/ -.rts2_cache_umd/ - -# Optional REPL history -.node_repl_history - -# Output of 'npm pack' -*.tgz - -# Yarn Integrity file -.yarn-integrity - -# parcel-bundler cache (https://parceljs.org/) -.cache -.parcel-cache - -# Next.js build output -.next - -# Nuxt.js build / generate output -.nuxt - -# Gatsby files -.cache/ - -# Storybook build outputs -.out -.storybook-out - -# Temporary folders -tmp/ -temp/ - -# Database -*.db -*.sqlite -*.sqlite3 - -# Webdev artifacts (checkpoint zips, migrations, etc.) -.webdev/ diff --git a/.manus/db/db-query-1773685654769.json b/.manus/db/db-query-1773685654769.json deleted file mode 100644 index 343849a..0000000 --- a/.manus/db/db-query-1773685654769.json +++ /dev/null @@ -1,95 +0,0 @@ -{ - "query": "SELECT * FROM app_settings; SELECT * FROM import_logs ORDER BY startedAt DESC LIMIT 5;", - "command": "mysql --batch --raw --column-names --default-character-set=utf8mb4 --host gateway02.us-east-1.prod.aws.tidbcloud.com --port 4000 --user 4CrrYuB5tme73Qo.63b125a8f9ca --database VepzDyqR8YkJNcqpZ729Bw --execute SELECT * FROM app_settings; SELECT * FROM import_logs ORDER BY startedAt DESC LIMIT 5;", - "rows": [ - { - "id": "1", - "key": "source_type", - "value": "local", - "updatedAt": "2026-03-16 18:23:41" - }, - { - "id": "2", - "key": "veille_file_path", - "value": "D:\\OneDrive - ITINOVA\\@ITINOVA\\Projets\\@Logiciel veille réglementaire", - "updatedAt": "2026-03-16 18:23:41" - }, - { - "id": "3", - "key": "aap_file_path", - "value": "D:\\OneDrive - ITINOVA\\@ITINOVA\\Projets\\@Logiciel veille réglementaire", - "updatedAt": "2026-03-16 18:23:41" - }, - { - "id": "4", - "key": "ftp_host", - "value": "", - "updatedAt": "2026-03-16 18:23:41" - }, - { - "id": "5", - "key": "ftp_port", - "value": "21", - "updatedAt": "2026-03-16 18:23:41" - }, - { - "id": "6", - "key": "ftp_user", - "value": "", - "updatedAt": "2026-03-16 18:23:41" - }, - { - "id": "7", - "key": "ftp_password", - "value": "", - "updatedAt": "2026-03-16 18:23:41" - }, - { - "id": "8", - "key": "ftp_secure", - "value": "false", - "updatedAt": "2026-03-16 18:23:41" - }, - { - "id": "9", - "key": "onedrive_token", - "value": "", - "updatedAt": "2026-03-16 18:23:41" - }, - { - "id": "10", - "key": "sharepoint_site_url", - "value": "", - "updatedAt": "2026-03-16 18:23:41" - }, - { - "id": "11", - "key": "sharepoint_token", - "value": "", - "updatedAt": "2026-03-16 18:23:41" - }, - { - "id": "12", - "key": "auth_mode", - "value": "local", - "updatedAt": "2026-03-16 18:23:41" - }, - { - "id": "13", - "key": "import_time", - "value": "06:00", - "updatedAt": "2026-03-16 18:23:41" - } - ], - "messages": [ - "id\tfileType\tsource\tstatus\ttotalRows\tnewRows\tskippedRows\terrorMessage\tdetails\tstartedAt\tcompletedAt", - "8\tveille\tD:\\OneDrive - ITINOVA\\@ITINOVA\\Projets\\@Logiciel veille réglementaire\terror\t0\t0\t0\tFichier introuvable : D:\\OneDrive - ITINOVA\\@ITINOVA\\Projets\\@Logiciel veille réglementaire\tNULL\t2026-03-16 18:25:31\t2026-03-16 18:25:31", - "7\taap\tD:\\OneDrive - ITINOVA\\@ITINOVA\\Projets\\@Logiciel veille réglementaire\terror\t0\t0\t0\tFichier introuvable : D:\\OneDrive - ITINOVA\\@ITINOVA\\Projets\\@Logiciel veille réglementaire\tNULL\t2026-03-16 18:25:31\t2026-03-16 18:25:31", - "5\tveille\tD:\\OneDrive - ITINOVA\\@ITINOVA\\Projets\\@Logiciel veille réglementaire\terror\t0\t0\t0\tFichier introuvable : D:\\OneDrive - ITINOVA\\@ITINOVA\\Projets\\@Logiciel veille réglementaire\tNULL\t2026-03-16 18:24:39\t2026-03-16 18:24:39", - "6\taap\tD:\\OneDrive - ITINOVA\\@ITINOVA\\Projets\\@Logiciel veille réglementaire\terror\t0\t0\t0\tFichier introuvable : D:\\OneDrive - ITINOVA\\@ITINOVA\\Projets\\@Logiciel veille réglementaire\tNULL\t2026-03-16 18:24:39\t2026-03-16 18:24:39", - "4\taap\tD:\\OneDrive - ITINOVA\\@ITINOVA\\Projets\\@Logiciel veille réglementaire\terror\t0\t0\t0\tFichier introuvable : D:\\OneDrive - ITINOVA\\@ITINOVA\\Projets\\@Logiciel veille réglementaire\tNULL\t2026-03-16 18:24:15\t2026-03-16 18:24:15" - ], - "stdout": "id\tkey\tvalue\tupdatedAt\n1\tsource_type\tlocal\t2026-03-16 18:23:41\n2\tveille_file_path\tD:\\OneDrive - ITINOVA\\@ITINOVA\\Projets\\@Logiciel veille réglementaire\t2026-03-16 18:23:41\n3\taap_file_path\tD:\\OneDrive - ITINOVA\\@ITINOVA\\Projets\\@Logiciel veille réglementaire\t2026-03-16 18:23:41\n4\tftp_host\t\t2026-03-16 18:23:41\n5\tftp_port\t21\t2026-03-16 18:23:41\n6\tftp_user\t\t2026-03-16 18:23:41\n7\tftp_password\t\t2026-03-16 18:23:41\n8\tftp_secure\tfalse\t2026-03-16 18:23:41\n9\tonedrive_token\t\t2026-03-16 18:23:41\n10\tsharepoint_site_url\t\t2026-03-16 18:23:41\n11\tsharepoint_token\t\t2026-03-16 18:23:41\n12\tauth_mode\tlocal\t2026-03-16 18:23:41\n13\timport_time\t06:00\t2026-03-16 18:23:41\nid\tfileType\tsource\tstatus\ttotalRows\tnewRows\tskippedRows\terrorMessage\tdetails\tstartedAt\tcompletedAt\n8\tveille\tD:\\OneDrive - ITINOVA\\@ITINOVA\\Projets\\@Logiciel veille réglementaire\terror\t0\t0\t0\tFichier introuvable : D:\\OneDrive - ITINOVA\\@ITINOVA\\Projets\\@Logiciel veille réglementaire\tNULL\t2026-03-16 18:25:31\t2026-03-16 18:25:31\n7\taap\tD:\\OneDrive - ITINOVA\\@ITINOVA\\Projets\\@Logiciel veille réglementaire\terror\t0\t0\t0\tFichier introuvable : D:\\OneDrive - ITINOVA\\@ITINOVA\\Projets\\@Logiciel veille réglementaire\tNULL\t2026-03-16 18:25:31\t2026-03-16 18:25:31\n5\tveille\tD:\\OneDrive - ITINOVA\\@ITINOVA\\Projets\\@Logiciel veille réglementaire\terror\t0\t0\t0\tFichier introuvable : D:\\OneDrive - ITINOVA\\@ITINOVA\\Projets\\@Logiciel veille réglementaire\tNULL\t2026-03-16 18:24:39\t2026-03-16 18:24:39\n6\taap\tD:\\OneDrive - ITINOVA\\@ITINOVA\\Projets\\@Logiciel veille réglementaire\terror\t0\t0\t0\tFichier introuvable : D:\\OneDrive - ITINOVA\\@ITINOVA\\Projets\\@Logiciel veille réglementaire\tNULL\t2026-03-16 18:24:39\t2026-03-16 18:24:39\n4\taap\tD:\\OneDrive - ITINOVA\\@ITINOVA\\Projets\\@Logiciel veille réglementaire\terror\t0\t0\t0\tFichier introuvable : D:\\OneDrive - ITINOVA\\@ITINOVA\\Projets\\@Logiciel veille réglementaire\tNULL\t2026-03-16 18:24:15\t2026-03-16 18:24:15\n", - "stderr": "", - "execution_time_ms": 56 -} \ No newline at end of file diff --git a/.manus/db/db-query-1773686056762.json b/.manus/db/db-query-1773686056762.json deleted file mode 100644 index cb5f12f..0000000 --- a/.manus/db/db-query-1773686056762.json +++ /dev/null @@ -1,49 +0,0 @@ -{ - "query": "SELECT COUNT(*) as total_veille FROM veille_items; SELECT COUNT(*) as total_aap FROM aap_items; SELECT typeVeille, COUNT(*) as nb FROM veille_items GROUP BY typeVeille; SELECT categorie, COUNT(*) as nb FROM aap_items GROUP BY categorie;", - "command": "mysql --batch --raw --column-names --default-character-set=utf8mb4 --host gateway02.us-east-1.prod.aws.tidbcloud.com --port 4000 --user 4CrrYuB5tme73Qo.63b125a8f9ca --database VepzDyqR8YkJNcqpZ729Bw --execute SELECT COUNT(*) as total_veille FROM veille_items; SELECT COUNT(*) as total_aap FROM aap_items; SELECT typeVeille, COUNT(*) as nb FROM veille_items GROUP BY typeVeille; SELECT categorie, COUNT(*) as nb FROM aap_items GROUP BY categorie;", - "rows": [ - { - "total_veille": "38" - }, - { - "total_veille": "total_aap" - }, - { - "total_veille": "7" - }, - { - "total_veille": "typeVeille\tnb" - }, - { - "total_veille": "concurrentielle\t4" - }, - { - "total_veille": "technologique\t4" - }, - { - "total_veille": "reglementaire\t13" - }, - { - "total_veille": "generale\t17" - }, - { - "total_veille": "categorie\tnb" - }, - { - "total_veille": "PA\t1" - }, - { - "total_veille": "Sanitaire\t4" - }, - { - "total_veille": "Handicap\t1" - }, - { - "total_veille": "Autre\t1" - } - ], - "messages": [], - "stdout": "total_veille\n38\ntotal_aap\n7\ntypeVeille\tnb\nconcurrentielle\t4\ntechnologique\t4\nreglementaire\t13\ngenerale\t17\ncategorie\tnb\nPA\t1\nSanitaire\t4\nHandicap\t1\nAutre\t1\n", - "stderr": "", - "execution_time_ms": 66 -} \ No newline at end of file diff --git a/.manus/db/db-query-1774010427110.json b/.manus/db/db-query-1774010427110.json deleted file mode 100644 index 7a66d35..0000000 --- a/.manus/db/db-query-1774010427110.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "query": "INSERT INTO local_users (email, passwordHash, name, role, isActive, createdAt, updatedAt)\nVALUES (\n 'adminItinova',\n '$2b$12$BFswq4nzwOXHx1CHW2QIq.qSlfYgJD1iPC07Wx6Bi8V8pKJyK6BBq',\n 'Admin Itinova',\n 'admin',\n 1,\n NOW(),\n NOW()\n);", - "command": "mysql --batch --raw --column-names --default-character-set=utf8mb4 --host gateway02.us-east-1.prod.aws.tidbcloud.com --port 4000 --user 4CrrYuB5tme73Qo.63b125a8f9ca --database VepzDyqR8YkJNcqpZ729Bw --execute INSERT INTO local_users (email, passwordHash, name, role, isActive, createdAt, updatedAt)\nVALUES (\n 'adminItinova',\n '$2b$12$BFswq4nzwOXHx1CHW2QIq.qSlfYgJD1iPC07Wx6Bi8V8pKJyK6BBq',\n 'Admin Itinova',\n 'admin',\n 1,\n NOW(),\n NOW()\n);", - "rows": [], - "messages": [], - "stdout": "", - "stderr": "", - "execution_time_ms": 529 -} \ No newline at end of file diff --git a/.manus/db/db-query-1774010431849.json b/.manus/db/db-query-1774010431849.json deleted file mode 100644 index ee0f3ea..0000000 --- a/.manus/db/db-query-1774010431849.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "query": "SELECT id, email, name, role, isActive FROM local_users WHERE email = 'adminItinova';", - "command": "mysql --batch --raw --column-names --default-character-set=utf8mb4 --host gateway02.us-east-1.prod.aws.tidbcloud.com --port 4000 --user 4CrrYuB5tme73Qo.63b125a8f9ca --database VepzDyqR8YkJNcqpZ729Bw --execute SELECT id, email, name, role, isActive FROM local_users WHERE email = 'adminItinova';", - "rows": [ - { - "id": "60001", - "email": "adminItinova", - "name": "Admin Itinova", - "role": "admin", - "isActive": "1" - } - ], - "messages": [], - "stdout": "id\temail\tname\trole\tisActive\n60001\tadminItinova\tAdmin Itinova\tadmin\t1\n", - "stderr": "", - "execution_time_ms": 53 -} \ No newline at end of file diff --git a/.manus/db/db-query-error-1773685646158.json b/.manus/db/db-query-error-1773685646158.json deleted file mode 100644 index f3b85c0..0000000 --- a/.manus/db/db-query-error-1773685646158.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "query": "SELECT * FROM app_settings; SELECT * FROM import_logs ORDER BY started_at DESC LIMIT 5;", - "command": "mysql --batch --raw --column-names --default-character-set=utf8mb4 --host gateway02.us-east-1.prod.aws.tidbcloud.com --port 4000 --user 4CrrYuB5tme73Qo.63b125a8f9ca --database VepzDyqR8YkJNcqpZ729Bw --execute SELECT * FROM app_settings; SELECT * FROM import_logs ORDER BY started_at DESC LIMIT 5;", - "returncode": 1, - "logs": [ - "id\tkey\tvalue\tupdatedAt", - "1\tsource_type\tlocal\t2026-03-16 18:23:41", - "2\tveille_file_path\tD:\\OneDrive - ITINOVA\\@ITINOVA\\Projets\\@Logiciel veille réglementaire\t2026-03-16 18:23:41", - "3\taap_file_path\tD:\\OneDrive - ITINOVA\\@ITINOVA\\Projets\\@Logiciel veille réglementaire\t2026-03-16 18:23:41", - "4\tftp_host\t\t2026-03-16 18:23:41", - "5\tftp_port\t21\t2026-03-16 18:23:41", - "6\tftp_user\t\t2026-03-16 18:23:41", - "7\tftp_password\t\t2026-03-16 18:23:41", - "8\tftp_secure\tfalse\t2026-03-16 18:23:41", - "9\tonedrive_token\t\t2026-03-16 18:23:41", - "10\tsharepoint_site_url\t\t2026-03-16 18:23:41", - "11\tsharepoint_token\t\t2026-03-16 18:23:41", - "12\tauth_mode\tlocal\t2026-03-16 18:23:41", - "13\timport_time\t06:00\t2026-03-16 18:23:41", - "ERROR 1054 (42S22) at line 1: Unknown column 'started_at' in 'order clause'" - ] -} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..d2cdde4 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,56 @@ +# ─── Stage 1: Build ─────────────────────────────────────────────────────────── +FROM node:22-slim AS builder + +# Install pnpm (version correspondant au packageManager du projet) +RUN corepack enable && corepack prepare pnpm@10.4.1 --activate + +WORKDIR /app + +# Copy package files first for better layer caching +COPY package.json pnpm-lock.yaml ./ + +# Copy patches BEFORE pnpm install (referenced in pnpm-lock.yaml) +COPY patches/ ./patches/ + +# Install all dependencies (including devDependencies for build) +RUN pnpm install --frozen-lockfile + +# Copy source code +COPY . . + +# Build frontend (Vite) and backend (esbuild) +RUN pnpm run build + +# ─── Stage 2: Production ───────────────────────────────────────────────────── +FROM node:22-slim AS production + +# Install pnpm +RUN corepack enable && corepack prepare pnpm@10.4.1 --activate + +WORKDIR /app + +# Copy package files +COPY package.json pnpm-lock.yaml ./ + +# Copy patches (needed for pnpm install to apply patched dependencies) +COPY patches/ ./patches/ + +# Install ALL dependencies (vite is imported at top-level in the bundle even though +# it is only used in dev mode; esbuild marks it as external so it must be present) +RUN pnpm install --frozen-lockfile + +# Copy built artifacts from builder +COPY --from=builder /app/dist ./dist + +# Copy drizzle migrations +COPY drizzle/ ./drizzle/ +COPY drizzle.config.ts ./ + +# Set environment +ENV NODE_ENV=production +ENV PORT=3000 + +EXPOSE 3000 + +# Start the application +CMD ["node", "dist/index.js"] diff --git a/client/src/App.tsx b/client/src/App.tsx index 8a25c61..036c253 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -12,7 +12,8 @@ import AAPDashboard from "./pages/AAPDashboard"; import Settings from "./pages/Settings"; import UsersAdmin from "./pages/UsersAdmin"; import ImportLogs from "./pages/ImportLogs"; -import BoiteAIdees from "./pages/BoiteAIdees"; +import BoiteAIdees from "@/pages/BoiteAIdees"; +import RssFeeds from "@/pages/RssFeeds"; import { Loader2 } from "lucide-react"; // ─── Guard d'authentification ───────────────────────────────────────────────── @@ -108,6 +109,16 @@ function BoiteAIdeesPage() { ); } +function RssFeedsPage() { + return ( + + + + + + ); +} + // ─── Routeur principal ──────────────────────────────────────────────────────── function Router() { @@ -123,6 +134,7 @@ function Router() { + diff --git a/client/src/components/AppLayout.tsx b/client/src/components/AppLayout.tsx index b40fa44..a4f8411 100644 --- a/client/src/components/AppLayout.tsx +++ b/client/src/components/AppLayout.tsx @@ -17,6 +17,7 @@ import { Menu, X, Lightbulb, + Rss, } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; @@ -64,6 +65,7 @@ const NAV_GROUPS: NavGroup[] = [ items: [ { label: "Logs d'import", href: "/admin/logs", icon: , adminOnly: true }, { label: "Utilisateurs", href: "/admin/users", icon: , adminOnly: true }, + { label: "Flux RSS", href: "/admin/rss", icon: , adminOnly: true }, { label: "Paramètres", href: "/admin/settings", icon: , adminOnly: true }, ], }, diff --git a/client/src/const.ts b/client/src/const.ts index 9999063..65f8d71 100644 --- a/client/src/const.ts +++ b/client/src/const.ts @@ -4,6 +4,8 @@ export { COOKIE_NAME, ONE_YEAR_MS } from "@shared/const"; export const getLoginUrl = () => { const oauthPortalUrl = import.meta.env.VITE_OAUTH_PORTAL_URL; const appId = import.meta.env.VITE_APP_ID; + // Fallback to local login when OAuth is not configured + if (!oauthPortalUrl) return "/login"; const redirectUri = `${window.location.origin}/api/oauth/callback`; const state = btoa(redirectUri); diff --git a/client/src/contexts/LocalAuthContext.tsx b/client/src/contexts/LocalAuthContext.tsx index 6131396..64cff64 100644 --- a/client/src/contexts/LocalAuthContext.tsx +++ b/client/src/contexts/LocalAuthContext.tsx @@ -1,17 +1,18 @@ -import { createContext, useContext, useState, useEffect, ReactNode } from "react"; +import { createContext, useContext, useState, ReactNode } from "react"; import { trpc } from "@/lib/trpc"; interface LocalUser { id: number; name: string; - email: string; + username: string | null; + email: string | null; role: "admin" | "user" | "readonly"; } interface LocalAuthContextType { user: LocalUser | null; loading: boolean; - login: (email: string, password: string) => Promise; + login: (identifier: string, password: string) => Promise; logout: () => Promise; isAuthenticated: boolean; } @@ -34,10 +35,10 @@ export function LocalAuthProvider({ children }: { children: ReactNode }) { const loginMutation = trpc.auth.localLogin.useMutation(); const logoutMutation = trpc.auth.localLogout.useMutation(); - const login = async (email: string, password: string) => { + const login = async (identifier: string, password: string) => { setLoading(true); try { - const result = await loginMutation.mutateAsync({ email, password }); + const result = await loginMutation.mutateAsync({ identifier, password }); const localUser = result.user as LocalUser; setUser(localUser); localStorage.setItem(LOCAL_USER_KEY, JSON.stringify(localUser)); diff --git a/client/src/pages/BoiteAIdees.tsx b/client/src/pages/BoiteAIdees.tsx deleted file mode 100644 index 2f1e1be..0000000 --- a/client/src/pages/BoiteAIdees.tsx +++ /dev/null @@ -1,465 +0,0 @@ -import { useState } from "react"; -import { trpc } from "@/lib/trpc"; -import { useAuth } from "@/_core/hooks/useAuth"; -import { Button } from "@/components/ui/button"; -import { Badge } from "@/components/ui/badge"; -import { Input } from "@/components/ui/input"; -import { Textarea } from "@/components/ui/textarea"; -import { Label } from "@/components/ui/label"; -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogFooter, -} from "@/components/ui/dialog"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import { toast } from "sonner"; -import { - Lightbulb, - Plus, - MessageSquare, - Calendar, - User, - ChevronDown, - ChevronUp, - Send, - Clock, - CheckCircle2, - XCircle, - AlertCircle, -} from "lucide-react"; -import { format } from "date-fns"; -import { fr } from "date-fns/locale"; - -type Statut = "ouvert" | "en_cours" | "resolu" | "ferme"; - -const STATUT_CONFIG: Record = { - ouvert: { - label: "Ouvert", - color: "bg-blue-100 text-blue-700 border-blue-200", - icon: , - }, - en_cours: { - label: "En cours", - color: "bg-amber-100 text-amber-700 border-amber-200", - icon: , - }, - resolu: { - label: "Résolu", - color: "bg-emerald-100 text-emerald-700 border-emerald-200", - icon: , - }, - ferme: { - label: "Fermé", - color: "bg-slate-100 text-slate-600 border-slate-200", - icon: , - }, -}; - -type Idea = { - id: number; - userId: number; - userName: string; - titre: string; - message: string; - statut: Statut; - reponseAdmin: string | null; - reponduPar: string | null; - reponduAt: Date | null; - createdAt: Date; - updatedAt: Date; -}; - -export default function BoiteAIdees() { - const { user } = useAuth(); - const isAdmin = user?.role === "admin"; - - const { data: ideas = [], refetch } = trpc.ideas.list.useQuery(); - const createMutation = trpc.ideas.create.useMutation({ - onSuccess: () => { - toast.success("Votre demande a été envoyée avec succès !"); - setNewDialogOpen(false); - setNewTitre(""); - setNewMessage(""); - refetch(); - }, - onError: (e) => toast.error(e.message), - }); - const reponseMutation = trpc.ideas.repondre.useMutation({ - onSuccess: () => { - toast.success("Réponse enregistrée !"); - setReponseDialogOpen(false); - setReponseText(""); - setSelectedIdea(null); - refetch(); - }, - onError: (e) => toast.error(e.message), - }); - - // État nouvelle demande - const [newDialogOpen, setNewDialogOpen] = useState(false); - const [newTitre, setNewTitre] = useState(""); - const [newMessage, setNewMessage] = useState(""); - - // État réponse admin - const [reponseDialogOpen, setReponseDialogOpen] = useState(false); - const [selectedIdea, setSelectedIdea] = useState(null); - const [reponseText, setReponseText] = useState(""); - const [reponseStatut, setReponseStatut] = useState("resolu"); - - // État expansion des cartes - const [expandedIds, setExpandedIds] = useState>(new Set()); - - // Filtre statut - const [filtreStatut, setFiltreStatut] = useState("tous"); - const [filtreRecherche, setFiltreRecherche] = useState(""); - - const toggleExpand = (id: number) => { - setExpandedIds((prev) => { - const next = new Set(prev); - if (next.has(id)) next.delete(id); - else next.add(id); - return next; - }); - }; - - const handleRepondre = (idea: Idea) => { - setSelectedIdea(idea); - setReponseText(idea.reponseAdmin ?? ""); - setReponseStatut(idea.statut === "ouvert" ? "en_cours" : idea.statut); - setReponseDialogOpen(true); - }; - - const filteredIdeas = (ideas as Idea[]).filter((idea) => { - const matchStatut = filtreStatut === "tous" || idea.statut === filtreStatut; - const matchRecherche = - !filtreRecherche || - idea.titre.toLowerCase().includes(filtreRecherche.toLowerCase()) || - idea.message.toLowerCase().includes(filtreRecherche.toLowerCase()) || - idea.userName.toLowerCase().includes(filtreRecherche.toLowerCase()); - return matchStatut && matchRecherche; - }); - - const counts = { - tous: (ideas as Idea[]).length, - ouvert: (ideas as Idea[]).filter((i) => i.statut === "ouvert").length, - en_cours: (ideas as Idea[]).filter((i) => i.statut === "en_cours").length, - resolu: (ideas as Idea[]).filter((i) => i.statut === "resolu").length, - ferme: (ideas as Idea[]).filter((i) => i.statut === "ferme").length, - }; - - return ( -
- {/* En-tête */} -
-
-
- -
-
-

Boîte à idées

-

- {isAdmin - ? "Gérez les questions et suggestions des utilisateurs" - : "Posez vos questions et partagez vos suggestions"} -

-
-
- -
- - {/* Filtres */} -
-
- setFiltreRecherche(e.target.value)} - className="bg-white border-slate-200" - /> -
-
- {(["tous", "ouvert", "en_cours", "resolu", "ferme"] as const).map((s) => ( - - ))} -
-
- - {/* Liste des demandes */} - {filteredIdeas.length === 0 ? ( -
- -

Aucune demande pour le moment

-

Soyez le premier à soumettre une question ou une suggestion.

-
- ) : ( -
- {filteredIdeas.map((idea) => { - const isExpanded = expandedIds.has(idea.id); - const cfg = STATUT_CONFIG[idea.statut]; - return ( -
- {/* En-tête de la carte */} -
toggleExpand(idea.id)} - > -
-
- - {cfg.icon} - {cfg.label} - -

- {idea.titre} -

-
-
- - - {idea.userName} - - - - {format(new Date(idea.createdAt), "d MMM yyyy à HH:mm", { locale: fr })} - - {idea.reponseAdmin && ( - - - Réponse disponible - - )} -
-
-
- {isAdmin && ( - - )} - {isExpanded ? ( - - ) : ( - - )} -
-
- - {/* Contenu développé */} - {isExpanded && ( -
- {/* Message */} -
-

- Message -

-

{idea.message}

-
- - {/* Réponse admin */} - {idea.reponseAdmin && ( -
-

- - Réponse de l'équipe - {idea.reponduPar && ( - - — {idea.reponduPar} - - )} - {idea.reponduAt && ( - - ({format(new Date(idea.reponduAt), "d MMM yyyy", { locale: fr })}) - - )} -

-

- {idea.reponseAdmin} -

-
- )} -
- )} -
- ); - })} -
- )} - - {/* Dialog : Nouvelle demande */} - - - - - - Nouvelle demande - - -
-
- - setNewTitre(e.target.value)} - className="mt-1" - /> -
-
- -