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.
- package/bin/create-crm-tmp.js +7 -3
- package/package.json +1 -1
- package/template/README.md +70 -5
- package/template/WORKFLOWS_CRON.md +49 -27
- package/template/package.json +18 -16
- package/template/prisma/migrations/20260210114913_add_dashboard_widget/migration.sql +20 -0
- package/template/prisma/schema.prisma +17 -0
- package/template/src/app/(dashboard)/agenda/page.tsx +279 -225
- package/template/src/app/(dashboard)/automatisation/[id]/page.tsx +1 -5
- package/template/src/app/(dashboard)/automatisation/_components/workflow-editor.tsx +20 -47
- package/template/src/app/(dashboard)/automatisation/new/page.tsx +0 -2
- package/template/src/app/(dashboard)/closing/page.tsx +5 -57
- package/template/src/app/(dashboard)/contacts/[id]/page.tsx +60 -44
- package/template/src/app/(dashboard)/contacts/page.tsx +156 -210
- package/template/src/app/(dashboard)/dashboard/page.tsx +438 -91
- package/template/src/app/(dashboard)/settings/page.tsx +179 -77
- package/template/src/app/(dashboard)/users/layout.tsx +30 -0
- package/template/src/app/(dashboard)/users/list/page.tsx +213 -159
- package/template/src/app/(dashboard)/users/page.tsx +13 -46
- package/template/src/app/(dashboard)/users/permissions/page.tsx +0 -2
- package/template/src/app/(dashboard)/users/roles/page.tsx +0 -2
- package/template/src/app/api/audit-logs/route.ts +0 -2
- package/template/src/app/api/auth/google/status/route.ts +46 -7
- package/template/src/app/api/closing-reasons/route.ts +0 -2
- package/template/src/app/api/contacts/[id]/files/[fileId]/route.ts +2 -1
- package/template/src/app/api/contacts/[id]/files/route.ts +25 -20
- package/template/src/app/api/contacts/[id]/route.ts +2 -3
- package/template/src/app/api/contacts/export/route.ts +14 -11
- package/template/src/app/api/contacts/import/route.ts +2 -6
- package/template/src/app/api/contacts/route.ts +1 -1
- package/template/src/app/api/dashboard/stats/route.ts +7 -0
- package/template/src/app/api/dashboard/widgets/[id]/route.ts +47 -0
- package/template/src/app/api/dashboard/widgets/route.ts +181 -0
- package/template/src/app/api/integrations/google-sheet/sync/route.ts +58 -28
- package/template/src/app/api/reminders/route.ts +4 -2
- package/template/src/app/api/roles/route.ts +1 -1
- package/template/src/app/api/settings/closing-reasons/[id]/route.ts +1 -6
- package/template/src/app/api/settings/closing-reasons/route.ts +0 -2
- package/template/src/app/api/settings/google-sheet/auto-map/route.ts +10 -5
- package/template/src/app/api/settings/google-sheet/route.ts +3 -3
- package/template/src/app/api/tasks/[id]/route.ts +4 -4
- package/template/src/app/api/tasks/meet/route.ts +1 -2
- package/template/src/app/api/tasks/route.ts +16 -18
- package/template/src/app/api/users/for-agenda/route.ts +1 -2
- package/template/src/app/api/workflows/[id]/route.ts +2 -9
- package/template/src/app/api/workflows/route.ts +0 -1
- package/template/src/app/globals.css +96 -0
- package/template/src/components/dashboard/activity-chart.tsx +37 -37
- package/template/src/components/dashboard/add-widget-dialog.tsx +161 -0
- package/template/src/components/dashboard/color-picker.tsx +65 -0
- package/template/src/components/dashboard/contacts-chart.tsx +36 -30
- package/template/src/components/dashboard/interactions-by-type-chart.tsx +121 -0
- package/template/src/components/dashboard/recent-activity.tsx +79 -86
- package/template/src/components/dashboard/sales-analytics-chart.tsx +4 -8
- package/template/src/components/dashboard/stat-card.tsx +42 -40
- package/template/src/components/dashboard/status-distribution-chart.tsx +64 -27
- package/template/src/components/dashboard/tasks-pie-chart.tsx +37 -34
- package/template/src/components/dashboard/top-contacts-list.tsx +41 -51
- package/template/src/components/dashboard/upcoming-tasks-list.tsx +71 -78
- package/template/src/components/dashboard/widget-wrapper.tsx +39 -0
- package/template/src/components/header.tsx +21 -12
- package/template/src/components/page-header.tsx +14 -47
- package/template/src/components/sidebar.tsx +3 -4
- package/template/src/contexts/dashboard-theme-context.tsx +58 -0
- package/template/src/lib/audit-log.ts +0 -2
- package/template/src/lib/dashboard-themes.ts +140 -0
- package/template/src/lib/default-widgets.ts +14 -0
- package/template/src/lib/google-drive.ts +38 -30
- package/template/src/lib/permissions.ts +56 -1
- package/template/src/lib/prisma.ts +0 -1
- package/template/src/lib/widget-registry.ts +177 -0
- package/template/src/lib/workflow-executor.ts +7 -13
- 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
|
|
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
|
|
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="
|
|
301
|
-
<p className="text-sm font-medium text-gray-900">{
|
|
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
|
|
17
|
-
{
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
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
|
|
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
|
+
}
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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);
|