create-crm-tmp 1.0.2 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/bin/create-crm-tmp.js +7 -3
  2. package/package.json +1 -1
  3. package/template/README.md +70 -5
  4. package/template/WORKFLOWS_CRON.md +49 -27
  5. package/template/package.json +18 -16
  6. package/template/prisma/migrations/20260210114913_add_dashboard_widget/migration.sql +20 -0
  7. package/template/prisma/schema.prisma +17 -0
  8. package/template/src/app/(dashboard)/agenda/page.tsx +279 -225
  9. package/template/src/app/(dashboard)/automatisation/[id]/page.tsx +1 -5
  10. package/template/src/app/(dashboard)/automatisation/_components/workflow-editor.tsx +20 -47
  11. package/template/src/app/(dashboard)/automatisation/new/page.tsx +0 -2
  12. package/template/src/app/(dashboard)/closing/page.tsx +5 -57
  13. package/template/src/app/(dashboard)/contacts/[id]/page.tsx +60 -44
  14. package/template/src/app/(dashboard)/contacts/page.tsx +156 -210
  15. package/template/src/app/(dashboard)/dashboard/page.tsx +438 -91
  16. package/template/src/app/(dashboard)/settings/page.tsx +179 -77
  17. package/template/src/app/(dashboard)/users/layout.tsx +30 -0
  18. package/template/src/app/(dashboard)/users/list/page.tsx +213 -159
  19. package/template/src/app/(dashboard)/users/page.tsx +13 -46
  20. package/template/src/app/(dashboard)/users/permissions/page.tsx +0 -2
  21. package/template/src/app/(dashboard)/users/roles/page.tsx +0 -2
  22. package/template/src/app/api/audit-logs/route.ts +0 -2
  23. package/template/src/app/api/auth/google/status/route.ts +46 -7
  24. package/template/src/app/api/closing-reasons/route.ts +0 -2
  25. package/template/src/app/api/contacts/[id]/files/[fileId]/route.ts +2 -1
  26. package/template/src/app/api/contacts/[id]/files/route.ts +25 -20
  27. package/template/src/app/api/contacts/[id]/route.ts +2 -3
  28. package/template/src/app/api/contacts/export/route.ts +14 -11
  29. package/template/src/app/api/contacts/import/route.ts +2 -6
  30. package/template/src/app/api/contacts/route.ts +1 -1
  31. package/template/src/app/api/dashboard/stats/route.ts +7 -0
  32. package/template/src/app/api/dashboard/widgets/[id]/route.ts +47 -0
  33. package/template/src/app/api/dashboard/widgets/route.ts +181 -0
  34. package/template/src/app/api/integrations/google-sheet/sync/route.ts +58 -28
  35. package/template/src/app/api/reminders/route.ts +4 -2
  36. package/template/src/app/api/roles/route.ts +1 -1
  37. package/template/src/app/api/settings/closing-reasons/[id]/route.ts +1 -6
  38. package/template/src/app/api/settings/closing-reasons/route.ts +0 -2
  39. package/template/src/app/api/settings/google-sheet/auto-map/route.ts +10 -5
  40. package/template/src/app/api/settings/google-sheet/route.ts +3 -3
  41. package/template/src/app/api/tasks/[id]/route.ts +4 -4
  42. package/template/src/app/api/tasks/meet/route.ts +1 -2
  43. package/template/src/app/api/tasks/route.ts +16 -18
  44. package/template/src/app/api/users/for-agenda/route.ts +1 -2
  45. package/template/src/app/api/workflows/[id]/route.ts +2 -9
  46. package/template/src/app/api/workflows/route.ts +0 -1
  47. package/template/src/app/globals.css +96 -0
  48. package/template/src/components/dashboard/activity-chart.tsx +37 -37
  49. package/template/src/components/dashboard/add-widget-dialog.tsx +161 -0
  50. package/template/src/components/dashboard/color-picker.tsx +65 -0
  51. package/template/src/components/dashboard/contacts-chart.tsx +36 -30
  52. package/template/src/components/dashboard/interactions-by-type-chart.tsx +121 -0
  53. package/template/src/components/dashboard/recent-activity.tsx +79 -86
  54. package/template/src/components/dashboard/sales-analytics-chart.tsx +4 -8
  55. package/template/src/components/dashboard/stat-card.tsx +42 -40
  56. package/template/src/components/dashboard/status-distribution-chart.tsx +64 -27
  57. package/template/src/components/dashboard/tasks-pie-chart.tsx +37 -34
  58. package/template/src/components/dashboard/top-contacts-list.tsx +41 -51
  59. package/template/src/components/dashboard/upcoming-tasks-list.tsx +71 -78
  60. package/template/src/components/dashboard/widget-wrapper.tsx +39 -0
  61. package/template/src/components/header.tsx +21 -12
  62. package/template/src/components/page-header.tsx +14 -47
  63. package/template/src/components/sidebar.tsx +3 -4
  64. package/template/src/contexts/dashboard-theme-context.tsx +58 -0
  65. package/template/src/lib/audit-log.ts +0 -2
  66. package/template/src/lib/dashboard-themes.ts +140 -0
  67. package/template/src/lib/default-widgets.ts +14 -0
  68. package/template/src/lib/google-drive.ts +38 -30
  69. package/template/src/lib/permissions.ts +56 -1
  70. package/template/src/lib/prisma.ts +0 -1
  71. package/template/src/lib/widget-registry.ts +177 -0
  72. package/template/src/lib/workflow-executor.ts +7 -13
  73. package/README.md +0 -89
@@ -1,11 +1,13 @@
1
1
  'use client';
2
2
 
3
3
  import { useSession, signOut } from '@/lib/auth-client';
4
- import { Bell, ChevronDown, LogOut, Calendar } from 'lucide-react';
4
+ import { Bell, ChevronDown, LogOut, Calendar, Menu } from 'lucide-react';
5
5
  import { useState, useEffect, useRef } from 'react';
6
6
  import { useRouter } from 'next/navigation';
7
7
  import Link from 'next/link';
8
8
  import { cn } from '@/lib/utils';
9
+ import { useMobileMenuContext } from '@/contexts/mobile-menu-context';
10
+ import { useViewAs } from '@/contexts/view-as-context';
9
11
 
10
12
  interface Reminder {
11
13
  id: string;
@@ -56,6 +58,8 @@ function formatDateTime(dateString: string) {
56
58
  export function Header() {
57
59
  const { data: session } = useSession();
58
60
  const router = useRouter();
61
+ const { toggle: toggleMobileMenu } = useMobileMenuContext();
62
+ const { viewAsUser, isViewingAsOther } = useViewAs();
59
63
  const [showRemindersDropdown, setShowRemindersDropdown] = useState(false);
60
64
  const [showUserDropdown, setShowUserDropdown] = useState(false);
61
65
  const [reminders, setReminders] = useState<Reminder[]>([]);
@@ -64,7 +68,8 @@ export function Header() {
64
68
  const remindersRef = useRef<HTMLDivElement>(null);
65
69
  const userRef = useRef<HTMLDivElement>(null);
66
70
 
67
- const userName = session?.user?.name || 'Utilisateur';
71
+ const realUserName = session?.user?.name || 'Utilisateur';
72
+ const userName = isViewingAsOther ? viewAsUser?.name || 'Utilisateur' : realUserName;
68
73
  const userEmail = session?.user?.email || '';
69
74
  const userInitial = userName?.[0]?.toUpperCase() || 'U';
70
75
 
@@ -147,8 +152,16 @@ export function Header() {
147
152
  return (
148
153
  <header className="sticky top-0 z-30 border-b border-gray-200 bg-white px-4 py-3 sm:px-6 lg:px-8">
149
154
  <div className="flex items-center justify-between gap-2 sm:gap-4">
150
- {/* Left: Logo + Greeting */}
155
+ {/* Left: Burger + Logo + Greeting */}
151
156
  <div className="flex items-center gap-2 sm:gap-3">
157
+ {/* Bouton burger - visible uniquement sur mobile/tablette */}
158
+ <button
159
+ onClick={toggleMobileMenu}
160
+ className="cursor-pointer rounded-lg p-2 text-gray-600 transition-colors hover:bg-gray-100 lg:hidden"
161
+ aria-label="Ouvrir le menu"
162
+ >
163
+ <Menu className="h-5 w-5" />
164
+ </button>
152
165
  <div className="flex items-center gap-1.5 sm:gap-2">
153
166
  {/* <Rocket className="h-5 w-5 text-indigo-600 sm:h-6 sm:w-6" /> */}
154
167
  <span className="text-base font-bold text-gray-900 sm:text-lg">CRM Template</span>
@@ -191,13 +204,9 @@ export function Header() {
191
204
  </div>
192
205
  <div className="max-h-96 overflow-y-auto">
193
206
  {loading ? (
194
- <div className="px-4 py-8 text-center text-sm text-gray-500">
195
- Chargement...
196
- </div>
207
+ <div className="px-4 py-8 text-center text-sm text-gray-500">Chargement...</div>
197
208
  ) : reminders.length === 0 ? (
198
- <div className="px-4 py-8 text-center text-sm text-gray-500">
199
- Aucun rappel
200
- </div>
209
+ <div className="px-4 py-8 text-center text-sm text-gray-500">Aucun rappel</div>
201
210
  ) : (
202
211
  <div className="divide-y divide-gray-100">
203
212
  {reminders.map((reminder) => {
@@ -285,7 +294,7 @@ export function Header() {
285
294
  <div className="relative" ref={userRef}>
286
295
  <button
287
296
  onClick={() => setShowUserDropdown(!showUserDropdown)}
288
- className="flex items-center gap-1.5 sm:gap-2 cursor-pointer"
297
+ className="flex cursor-pointer items-center gap-1.5 sm:gap-2"
289
298
  aria-label="Menu utilisateur"
290
299
  >
291
300
  <div className="flex h-8 w-8 items-center justify-center rounded-full bg-indigo-100 text-xs font-semibold text-indigo-600 sm:h-9 sm:w-9 sm:text-sm">
@@ -297,8 +306,8 @@ export function Header() {
297
306
  {/* Dropdown utilisateur */}
298
307
  {showUserDropdown && (
299
308
  <div className="absolute right-0 mt-2 w-56 rounded-lg border border-gray-200 bg-white shadow-xl">
300
- <div className="px-4 py-3 border-b border-gray-200">
301
- <p className="text-sm font-medium text-gray-900">{userName}</p>
309
+ <div className="border-b border-gray-200 px-4 py-3">
310
+ <p className="text-sm font-medium text-gray-900">{realUserName}</p>
302
311
  <p className="mt-0.5 text-xs text-gray-500">{userEmail}</p>
303
312
  </div>
304
313
  <div className="py-1">
@@ -1,60 +1,27 @@
1
- 'use client';
2
-
3
- import { useMobileMenuContext } from '@/contexts/mobile-menu-context';
4
-
5
1
  interface PageHeaderProps {
6
2
  title: string;
7
3
  description?: string;
8
4
  action?: React.ReactNode;
9
5
  }
10
6
 
11
- export function PageHeader({ title, description, action }: PageHeaderProps) {
12
- const { isOpen, toggle } = useMobileMenuContext();
13
-
7
+ export function PageHeader({ title, description, action }: Readonly<PageHeaderProps>) {
14
8
  return (
15
9
  <div className="border-b border-gray-200 bg-white px-4 py-4 sm:px-6 lg:px-8 lg:py-6">
16
- <div className="flex items-start gap-3">
17
- {/* Mobile menu button */}
18
- <button
19
- onClick={toggle}
20
- className="mt-1 shrink-0 cursor-pointer rounded-lg p-2 text-gray-700 transition-colors hover:bg-gray-100 lg:hidden"
21
- aria-label="Toggle menu"
22
- >
23
- <svg className="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
24
- {isOpen ? (
25
- <path
26
- strokeLinecap="round"
27
- strokeLinejoin="round"
28
- strokeWidth={2}
29
- d="M6 18L18 6M6 6l12 12"
30
- />
31
- ) : (
32
- <path
33
- strokeLinecap="round"
34
- strokeLinejoin="round"
35
- strokeWidth={2}
36
- d="M4 6h16M4 12h16M4 18h16"
37
- />
38
- )}
39
- </svg>
40
- </button>
41
-
42
- <div className="min-w-0 flex-1">
43
- {action ? (
44
- <div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
45
- <div className="min-w-0 flex-1">
46
- <h1 className="text-xl font-bold text-gray-900 sm:text-2xl">{title}</h1>
47
- {description && <p className="mt-1 text-sm text-gray-600">{description}</p>}
48
- </div>
49
- <div className="shrink-0">{action}</div>
50
- </div>
51
- ) : (
52
- <>
10
+ <div>
11
+ {action ? (
12
+ <div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
13
+ <div className="min-w-0 flex-1">
53
14
  <h1 className="text-xl font-bold text-gray-900 sm:text-2xl">{title}</h1>
54
15
  {description && <p className="mt-1 text-sm text-gray-600">{description}</p>}
55
- </>
56
- )}
57
- </div>
16
+ </div>
17
+ <div className="shrink-0">{action}</div>
18
+ </div>
19
+ ) : (
20
+ <>
21
+ <h1 className="text-xl font-bold text-gray-900 sm:text-2xl">{title}</h1>
22
+ {description && <p className="mt-1 text-sm text-gray-600">{description}</p>}
23
+ </>
24
+ )}
58
25
  </div>
59
26
  </div>
60
27
  );
@@ -1,9 +1,8 @@
1
1
  'use client';
2
2
 
3
3
  import Link from 'next/link';
4
- import { usePathname } from 'next/navigation';
4
+ import { usePathname, useRouter } from 'next/navigation';
5
5
  import { useSession, signOut } from '@/lib/auth-client';
6
- import { useRouter } from 'next/navigation';
7
6
  import { useMemo, useState, useEffect } from 'react';
8
7
  import { useUserRole } from '@/hooks/use-user-role';
9
8
  import { useMobileMenuContext } from '@/contexts/mobile-menu-context';
@@ -29,7 +28,7 @@ export function Sidebar() {
29
28
  const { data: session } = useSession();
30
29
  const router = useRouter();
31
30
  const { isOpen: isMobileMenuOpen, setIsOpen: setIsMobileMenuOpen } = useMobileMenuContext();
32
- const { isCollapsed, isPinned, setIsCollapsed, togglePin } = useSidebarContext();
31
+ const { isCollapsed, isPinned, setIsCollapsed } = useSidebarContext();
33
32
  const { viewAsUser, isViewingAsOther } = useViewAs();
34
33
  const [showViewAsModal, setShowViewAsModal] = useState(false);
35
34
  const [isMounted, setIsMounted] = useState(false);
@@ -128,7 +127,7 @@ export function Sidebar() {
128
127
  {/* Section Dashboard */}
129
128
  <div className={cn('px-3', isCollapsed && !isPinned && 'lg:px-2')}>
130
129
  {(!isCollapsed || isPinned) && (
131
- <h2 className="mb-2 px-3 text-xs font-semibold tracking-wider text-gray-500 uppercase">
130
+ <h2 className="mb-2 px-3 text-xs font-semibold tracking-wider text-nowrap text-gray-500 uppercase">
132
131
  CRM Template
133
132
  </h2>
134
133
  )}
@@ -0,0 +1,58 @@
1
+ 'use client';
2
+
3
+ import { createContext, useContext, useEffect, useMemo, useState, type ReactNode } from 'react';
4
+ import {
5
+ type DashboardTheme,
6
+ DASHBOARD_THEMES,
7
+ DEFAULT_THEME_KEY,
8
+ getThemeByKey,
9
+ } from '@/lib/dashboard-themes';
10
+
11
+ interface DashboardThemeContextType {
12
+ theme: DashboardTheme;
13
+ setThemeKey: (key: string) => void;
14
+ themes: DashboardTheme[];
15
+ }
16
+
17
+ const DashboardThemeContext = createContext<DashboardThemeContextType | undefined>(undefined);
18
+
19
+ const STORAGE_KEY = 'dashboard_theme';
20
+
21
+ export function DashboardThemeProvider({ children }: Readonly<{ children: ReactNode }>) {
22
+ const [themeKey, setThemeKeyState] = useState(DEFAULT_THEME_KEY);
23
+
24
+ // Charger le thème depuis le localStorage
25
+ useEffect(() => {
26
+ const stored = localStorage.getItem(STORAGE_KEY);
27
+ if (stored && DASHBOARD_THEMES.some((t) => t.key === stored)) {
28
+ setThemeKeyState(stored);
29
+ }
30
+ }, []);
31
+
32
+ const setThemeKey = (key: string) => {
33
+ setThemeKeyState(key);
34
+ localStorage.setItem(STORAGE_KEY, key);
35
+ };
36
+
37
+ const theme = getThemeByKey(themeKey);
38
+
39
+ const value = useMemo(
40
+ () => ({ theme, setThemeKey, themes: DASHBOARD_THEMES }),
41
+ // eslint-disable-next-line react-hooks/exhaustive-deps
42
+ [theme],
43
+ );
44
+
45
+ return (
46
+ <DashboardThemeContext.Provider value={value}>
47
+ {children}
48
+ </DashboardThemeContext.Provider>
49
+ );
50
+ }
51
+
52
+ export function useDashboardTheme() {
53
+ const context = useContext(DashboardThemeContext);
54
+ if (!context) {
55
+ throw new Error('useDashboardTheme doit être utilisé dans un DashboardThemeProvider');
56
+ }
57
+ return context;
58
+ }
@@ -41,5 +41,3 @@ export async function logAudit({
41
41
  console.error('Erreur lors de la création du log d’audit:', error);
42
42
  }
43
43
  }
44
-
45
-
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Thèmes de couleur pour le tableau de bord
3
+ * Chaque thème fournit des valeurs hex pour toutes les nuances nécessaires
4
+ */
5
+
6
+ export interface DashboardTheme {
7
+ key: string;
8
+ label: string;
9
+ hex: {
10
+ 50: string;
11
+ 100: string;
12
+ 200: string;
13
+ 300: string;
14
+ 400: string;
15
+ 500: string;
16
+ 600: string;
17
+ 700: string;
18
+ };
19
+ }
20
+
21
+ export const DASHBOARD_THEMES: DashboardTheme[] = [
22
+ {
23
+ key: 'orange',
24
+ label: 'Orange',
25
+ hex: {
26
+ 50: '#fff7ed',
27
+ 100: '#ffedd5',
28
+ 200: '#fed7aa',
29
+ 300: '#fdba74',
30
+ 400: '#fb923c',
31
+ 500: '#f97316',
32
+ 600: '#ea580c',
33
+ 700: '#c2410c',
34
+ },
35
+ },
36
+ {
37
+ key: 'blue',
38
+ label: 'Bleu',
39
+ hex: {
40
+ 50: '#eff6ff',
41
+ 100: '#dbeafe',
42
+ 200: '#bfdbfe',
43
+ 300: '#93c5fd',
44
+ 400: '#60a5fa',
45
+ 500: '#3b82f6',
46
+ 600: '#2563eb',
47
+ 700: '#1d4ed8',
48
+ },
49
+ },
50
+ {
51
+ key: 'violet',
52
+ label: 'Violet',
53
+ hex: {
54
+ 50: '#f5f3ff',
55
+ 100: '#ede9fe',
56
+ 200: '#ddd6fe',
57
+ 300: '#c4b5fd',
58
+ 400: '#a78bfa',
59
+ 500: '#8b5cf6',
60
+ 600: '#7c3aed',
61
+ 700: '#6d28d9',
62
+ },
63
+ },
64
+ {
65
+ key: 'emerald',
66
+ label: 'Émeraude',
67
+ hex: {
68
+ 50: '#ecfdf5',
69
+ 100: '#d1fae5',
70
+ 200: '#a7f3d0',
71
+ 300: '#6ee7b7',
72
+ 400: '#34d399',
73
+ 500: '#10b981',
74
+ 600: '#059669',
75
+ 700: '#047857',
76
+ },
77
+ },
78
+ {
79
+ key: 'rose',
80
+ label: 'Rose',
81
+ hex: {
82
+ 50: '#fff1f2',
83
+ 100: '#ffe4e6',
84
+ 200: '#fecdd3',
85
+ 300: '#fda4af',
86
+ 400: '#fb7185',
87
+ 500: '#f43f5e',
88
+ 600: '#e11d48',
89
+ 700: '#be123c',
90
+ },
91
+ },
92
+ {
93
+ key: 'cyan',
94
+ label: 'Cyan',
95
+ hex: {
96
+ 50: '#ecfeff',
97
+ 100: '#cffafe',
98
+ 200: '#a5f3fc',
99
+ 300: '#67e8f9',
100
+ 400: '#22d3ee',
101
+ 500: '#06b6d4',
102
+ 600: '#0891b2',
103
+ 700: '#0e7490',
104
+ },
105
+ },
106
+ {
107
+ key: 'amber',
108
+ label: 'Ambre',
109
+ hex: {
110
+ 50: '#fffbeb',
111
+ 100: '#fef3c7',
112
+ 200: '#fde68a',
113
+ 300: '#fcd34d',
114
+ 400: '#fbbf24',
115
+ 500: '#f59e0b',
116
+ 600: '#d97706',
117
+ 700: '#b45309',
118
+ },
119
+ },
120
+ {
121
+ key: 'indigo',
122
+ label: 'Indigo',
123
+ hex: {
124
+ 50: '#eef2ff',
125
+ 100: '#e0e7ff',
126
+ 200: '#c7d2fe',
127
+ 300: '#a5b4fc',
128
+ 400: '#818cf8',
129
+ 500: '#6366f1',
130
+ 600: '#4f46e5',
131
+ 700: '#4338ca',
132
+ },
133
+ },
134
+ ];
135
+
136
+ export const DEFAULT_THEME_KEY = 'orange';
137
+
138
+ export function getThemeByKey(key: string): DashboardTheme {
139
+ return DASHBOARD_THEMES.find((t) => t.key === key) || DASHBOARD_THEMES[0];
140
+ }
@@ -0,0 +1,14 @@
1
+ // Layout par défaut pour les nouveaux utilisateurs
2
+ // Ce fichier est séparé du widget-registry pour être importable côté serveur
3
+ export const DEFAULT_WIDGETS = [
4
+ { type: 'stat_total_contacts', x: 0, y: 0, w: 3, h: 2 },
5
+ { type: 'stat_new_contacts', x: 3, y: 0, w: 3, h: 2 },
6
+ { type: 'stat_completed_tasks', x: 6, y: 0, w: 3, h: 2 },
7
+ { type: 'stat_pending_tasks', x: 9, y: 0, w: 3, h: 2 },
8
+ { type: 'contacts_chart', x: 0, y: 2, w: 6, h: 4 },
9
+ { type: 'top_contacts', x: 6, y: 2, w: 6, h: 4 },
10
+ { type: 'activity_chart', x: 0, y: 6, w: 8, h: 4 },
11
+ { type: 'tasks_pie', x: 8, y: 6, w: 4, h: 4 },
12
+ { type: 'upcoming_tasks', x: 0, y: 10, w: 6, h: 5 },
13
+ { type: 'recent_activity', x: 6, y: 10, w: 6, h: 5 },
14
+ ];
@@ -1,5 +1,9 @@
1
1
  /**
2
2
  * Utilitaires pour gérer les fichiers avec Google Drive API
3
+ *
4
+ * Google Drive est configuré de manière centralisée :
5
+ * un seul administrateur connecte son compte Google Drive,
6
+ * et tous les utilisateurs uploadent dans ce même Drive.
3
7
  */
4
8
 
5
9
  import { prisma } from '@/lib/prisma';
@@ -8,6 +12,30 @@ import { getValidAccessToken } from './google-calendar';
8
12
  // Nom de l'application (peut être configuré via variable d'environnement)
9
13
  const APP_NAME = process.env.APP_NAME || 'CRM Template';
10
14
 
15
+ /**
16
+ * Récupère le compte Google de l'administrateur
17
+ * Tous les utilisateurs utilisent la configuration de l'admin pour Google Drive uniquement
18
+ */
19
+ export async function getAdminGoogleAccount() {
20
+ const adminUser = await prisma.user.findFirst({
21
+ where: { role: 'ADMIN' },
22
+ include: {
23
+ googleAccount: true,
24
+ },
25
+ orderBy: {
26
+ createdAt: 'asc', // Prendre le premier admin créé
27
+ },
28
+ });
29
+
30
+ if (!adminUser || !adminUser.googleAccount) {
31
+ throw new Error(
32
+ 'Aucun compte Google Drive configuré. Veuillez demander à un administrateur de connecter son compte Google Drive dans les paramètres.',
33
+ );
34
+ }
35
+
36
+ return adminUser.googleAccount;
37
+ }
38
+
11
39
  /**
12
40
  * Crée ou récupère un dossier dans Google Drive
13
41
  */
@@ -145,19 +173,14 @@ async function setFilePublicWithLink(accessToken: string, fileId: string): Promi
145
173
  * Crée un dossier dans Google Drive pour un contact
146
174
  * Structure: CRM Template > Contacts > Contact - [Nom]
147
175
  * Retourne l'ID du dossier créé ou existant
176
+ * Utilise le compte Google Drive de l'administrateur
148
177
  */
149
178
  export async function getOrCreateContactFolder(
150
179
  userId: string,
151
180
  contactId: string,
152
181
  contactName: string,
153
182
  ): Promise<string> {
154
- const googleAccount = await prisma.userGoogleAccount.findUnique({
155
- where: { userId },
156
- });
157
-
158
- if (!googleAccount) {
159
- throw new Error('Aucun compte Google connecté');
160
- }
183
+ const googleAccount = await getAdminGoogleAccount();
161
184
 
162
185
  const accessToken = await getValidAccessToken(
163
186
  googleAccount.accessToken,
@@ -170,7 +193,7 @@ export async function getOrCreateContactFolder(
170
193
  const tokenExpiresAt = new Date();
171
194
  tokenExpiresAt.setSeconds(tokenExpiresAt.getSeconds() + 3600);
172
195
  await prisma.userGoogleAccount.update({
173
- where: { userId },
196
+ where: { userId: googleAccount.userId },
174
197
  data: {
175
198
  accessToken,
176
199
  tokenExpiresAt,
@@ -193,6 +216,7 @@ export async function getOrCreateContactFolder(
193
216
 
194
217
  /**
195
218
  * Upload un fichier vers Google Drive dans le dossier du contact
219
+ * Utilise le compte Google Drive de l'administrateur
196
220
  */
197
221
  export async function uploadFileToDrive(
198
222
  userId: string,
@@ -202,13 +226,7 @@ export async function uploadFileToDrive(
202
226
  ): Promise<{ fileId: string; webViewLink: string }> {
203
227
  const folderId = await getOrCreateContactFolder(userId, contactId, contactName);
204
228
 
205
- const googleAccount = await prisma.userGoogleAccount.findUnique({
206
- where: { userId },
207
- });
208
-
209
- if (!googleAccount) {
210
- throw new Error('Aucun compte Google connecté');
211
- }
229
+ const googleAccount = await getAdminGoogleAccount();
212
230
 
213
231
  const accessToken = await getValidAccessToken(
214
232
  googleAccount.accessToken,
@@ -221,7 +239,7 @@ export async function uploadFileToDrive(
221
239
  const tokenExpiresAt = new Date();
222
240
  tokenExpiresAt.setSeconds(tokenExpiresAt.getSeconds() + 3600);
223
241
  await prisma.userGoogleAccount.update({
224
- where: { userId },
242
+ where: { userId: googleAccount.userId },
225
243
  data: {
226
244
  accessToken,
227
245
  tokenExpiresAt,
@@ -305,18 +323,13 @@ export async function uploadFileToDrive(
305
323
 
306
324
  /**
307
325
  * Récupère les informations d'un fichier depuis Google Drive
326
+ * Utilise le compte Google Drive de l'administrateur
308
327
  */
309
328
  export async function getFileInfo(
310
329
  userId: string,
311
330
  fileId: string,
312
331
  ): Promise<{ name: string; size: string; mimeType: string; webViewLink: string }> {
313
- const googleAccount = await prisma.userGoogleAccount.findUnique({
314
- where: { userId },
315
- });
316
-
317
- if (!googleAccount) {
318
- throw new Error('Aucun compte Google connecté');
319
- }
332
+ const googleAccount = await getAdminGoogleAccount();
320
333
 
321
334
  const accessToken = await getValidAccessToken(
322
335
  googleAccount.accessToken,
@@ -342,15 +355,10 @@ export async function getFileInfo(
342
355
 
343
356
  /**
344
357
  * Supprime un fichier de Google Drive
358
+ * Utilise le compte Google Drive de l'administrateur
345
359
  */
346
360
  export async function deleteFileFromDrive(userId: string, fileId: string): Promise<void> {
347
- const googleAccount = await prisma.userGoogleAccount.findUnique({
348
- where: { userId },
349
- });
350
-
351
- if (!googleAccount) {
352
- throw new Error('Aucun compte Google connecté');
353
- }
361
+ const googleAccount = await getAdminGoogleAccount();
354
362
 
355
363
  const accessToken = await getValidAccessToken(
356
364
  googleAccount.accessToken,
@@ -11,6 +11,7 @@ export interface Permission {
11
11
  }
12
12
 
13
13
  export const PERMISSION_CATEGORIES = {
14
+ DASHBOARD: 'Tableau de bord',
14
15
  ANALYTICS: 'Analytics',
15
16
  CONTACTS: 'Contacts',
16
17
  TASKS: 'Tâches',
@@ -22,6 +23,26 @@ export const PERMISSION_CATEGORIES = {
22
23
  } as const;
23
24
 
24
25
  export const PERMISSIONS: Permission[] = [
26
+ // Tableau de bord
27
+ {
28
+ code: 'dashboard.view',
29
+ name: 'Voir le tableau de bord',
30
+ description: 'Permet d\'accéder au tableau de bord et de visualiser les widgets',
31
+ category: PERMISSION_CATEGORIES.DASHBOARD,
32
+ },
33
+ {
34
+ code: 'dashboard.widgets.manage',
35
+ name: 'Gérer les widgets',
36
+ description: 'Permet d\'ajouter, supprimer et réorganiser les widgets du tableau de bord',
37
+ category: PERMISSION_CATEGORIES.DASHBOARD,
38
+ },
39
+ {
40
+ code: 'dashboard.widgets.reset',
41
+ name: 'Réinitialiser le tableau de bord',
42
+ description: 'Permet de réinitialiser la disposition des widgets aux valeurs par défaut',
43
+ category: PERMISSION_CATEGORIES.DASHBOARD,
44
+ },
45
+
25
46
  // Analytics
26
47
  {
27
48
  code: 'analytics.view',
@@ -127,7 +148,8 @@ export const PERMISSIONS: Permission[] = [
127
148
  {
128
149
  code: 'tasks.view_other_users_events',
129
150
  name: 'Voir les événements des autres utilisateurs',
130
- description: 'Permet de voir les événements (tâches, rendez-vous) des autres utilisateurs dans l\'agenda',
151
+ description:
152
+ "Permet de voir les événements (tâches, rendez-vous) des autres utilisateurs dans l'agenda",
131
153
  category: PERMISSION_CATEGORIES.TASKS,
132
154
  },
133
155
  {
@@ -236,6 +258,13 @@ export const PERMISSIONS: Permission[] = [
236
258
  description: 'Permet de configurer les synchronisations Google Sheets',
237
259
  category: PERMISSION_CATEGORIES.INTEGRATIONS,
238
260
  },
261
+ {
262
+ code: 'integrations.google_drive.manage',
263
+ name: 'Gérer Google Drive partagé',
264
+ description:
265
+ 'Permet de configurer le compte Google Drive administrateur utilisé pour le stockage des fichiers',
266
+ category: PERMISSION_CATEGORIES.INTEGRATIONS,
267
+ },
239
268
 
240
269
  // Utilisateurs
241
270
  {
@@ -300,6 +329,18 @@ export const PERMISSIONS: Permission[] = [
300
329
  description: 'Permet de créer, modifier et supprimer des statuts',
301
330
  category: PERMISSION_CATEGORIES.SETTINGS,
302
331
  },
332
+ {
333
+ code: 'settings.closing_reasons.manage',
334
+ name: 'Gérer les motifs de fermeture',
335
+ description: 'Permet de créer, modifier et supprimer des motifs de fermeture de contacts',
336
+ category: PERMISSION_CATEGORIES.SETTINGS,
337
+ },
338
+ {
339
+ code: 'settings.workflows.manage',
340
+ name: 'Gérer les automatisations',
341
+ description: 'Permet de créer, modifier, activer et supprimer des workflows (automatisations)',
342
+ category: PERMISSION_CATEGORIES.SETTINGS,
343
+ },
303
344
 
304
345
  // Général
305
346
  {
@@ -309,6 +350,13 @@ export const PERMISSIONS: Permission[] = [
309
350
  'Permet de voir tous les contacts de toutes les entreprises (réservé aux administrateurs)',
310
351
  category: PERMISSION_CATEGORIES.GENERAL,
311
352
  },
353
+ {
354
+ code: 'general.audit_logs.view',
355
+ name: 'Voir les logs d’audit',
356
+ description:
357
+ 'Permet de consulter les journaux d’audit (actions des utilisateurs, historique des modifications)',
358
+ category: PERMISSION_CATEGORIES.GENERAL,
359
+ },
312
360
  ];
313
361
 
314
362
  // Regrouper les permissions par catégorie
@@ -334,6 +382,9 @@ export const DEFAULT_ROLES = {
334
382
  name: 'Manager',
335
383
  description: "Gestion d'équipe et accès étendu aux leads",
336
384
  permissions: [
385
+ 'dashboard.view',
386
+ 'dashboard.widgets.manage',
387
+ 'dashboard.widgets.reset',
337
388
  'analytics.view',
338
389
  'contacts.view_all',
339
390
  'contacts.create',
@@ -360,6 +411,8 @@ export const DEFAULT_ROLES = {
360
411
  name: 'Commercial',
361
412
  description: 'Accès de base pour la gestion des leads personnels',
362
413
  permissions: [
414
+ 'dashboard.view',
415
+ 'dashboard.widgets.manage',
363
416
  'contacts.view_own',
364
417
  'contacts.view_unassigned',
365
418
  'contacts.create',
@@ -380,6 +433,7 @@ export const DEFAULT_ROLES = {
380
433
  name: 'Télépro',
381
434
  description: 'Accès limité pour la qualification de leads',
382
435
  permissions: [
436
+ 'dashboard.view',
383
437
  'contacts.view_own',
384
438
  'contacts.view_unassigned',
385
439
  'contacts.create',
@@ -395,6 +449,7 @@ export const DEFAULT_ROLES = {
395
449
  name: 'Comptable',
396
450
  description: 'Accès limité aux informations financières et reporting',
397
451
  permissions: [
452
+ 'dashboard.view',
398
453
  'analytics.view',
399
454
  'analytics.export',
400
455
  'contacts.view_all',
@@ -11,7 +11,6 @@ const pool = new Pool({
11
11
  max: 10, // Nombre maximum de connexions dans le pool
12
12
  idleTimeoutMillis: 30000, // Fermer les connexions inactives après 30s
13
13
  connectionTimeoutMillis: 10000, // Timeout de connexion de 10s
14
-
15
14
  });
16
15
 
17
16
  const adapter = new PrismaPg(pool);