create-crm-tmp 1.1.3 → 2.0.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/package.json +1 -1
- package/template/.prettierignore +2 -0
- package/template/README.md +1 -1
- package/template/components.json +22 -0
- package/template/exemple-contacts.csv +54 -0
- package/template/next.config.ts +27 -1
- package/template/package.json +51 -16
- package/template/prisma/schema.prisma +807 -58
- package/template/skills-lock.json +25 -0
- package/template/src/app/(auth)/invite/[token]/page.tsx +21 -24
- package/template/src/app/(auth)/reset-password/complete/page.tsx +12 -21
- package/template/src/app/(auth)/reset-password/page.tsx +12 -8
- package/template/src/app/(auth)/reset-password/verify/page.tsx +12 -8
- package/template/src/app/(auth)/signin/page.tsx +20 -17
- package/template/src/app/(dashboard)/agenda/page.tsx +2232 -2189
- package/template/src/app/(dashboard)/automatisation/[id]/page.tsx +10 -7
- package/template/src/app/(dashboard)/automatisation/_components/workflow-editor.tsx +680 -323
- package/template/src/app/(dashboard)/automatisation/new/page.tsx +11 -8
- package/template/src/app/(dashboard)/automatisation/page.tsx +473 -180
- package/template/src/app/(dashboard)/closing/page.tsx +500 -468
- package/template/src/app/(dashboard)/contacts/[id]/page.tsx +5049 -4110
- package/template/src/app/(dashboard)/contacts/companies/[id]/page.tsx +1703 -0
- package/template/src/app/(dashboard)/contacts/loading.tsx +13 -0
- package/template/src/app/(dashboard)/contacts/page.tsx +3776 -2064
- package/template/src/app/(dashboard)/dashboard/page.tsx +37 -519
- package/template/src/app/(dashboard)/error.tsx +37 -0
- package/template/src/app/(dashboard)/layout.tsx +1 -1
- package/template/src/app/(dashboard)/loading.tsx +5 -0
- package/template/src/app/(dashboard)/settings/loading.tsx +19 -0
- package/template/src/app/(dashboard)/settings/page.tsx +2685 -2489
- package/template/src/app/(dashboard)/templates/page.tsx +500 -300
- package/template/src/app/(dashboard)/users/list/page.tsx +356 -350
- package/template/src/app/(dashboard)/users/page.tsx +279 -310
- package/template/src/app/(dashboard)/users/permissions/page.tsx +104 -99
- package/template/src/app/(dashboard)/users/roles/page.tsx +164 -137
- package/template/src/app/api/audit-logs/route.ts +1 -1
- package/template/src/app/api/auth/google/callback/route.ts +8 -5
- package/template/src/app/api/auth/google/disconnect/route.ts +2 -2
- package/template/src/app/api/companies/[id]/activities/route.ts +131 -0
- package/template/src/app/api/companies/[id]/route.ts +195 -0
- package/template/src/app/api/companies/export/route.ts +206 -0
- package/template/src/app/api/companies/route.ts +166 -0
- package/template/src/app/api/contact-views/[id]/pin/route.ts +69 -0
- package/template/src/app/api/contact-views/[id]/route.ts +197 -0
- package/template/src/app/api/contact-views/route.ts +146 -0
- package/template/src/app/api/contacts/[id]/files/[fileId]/preview/route.ts +77 -0
- package/template/src/app/api/contacts/[id]/files/[fileId]/route.ts +7 -17
- package/template/src/app/api/contacts/[id]/files/route.ts +83 -44
- package/template/src/app/api/contacts/[id]/interactions/route.ts +37 -0
- package/template/src/app/api/contacts/[id]/kyc/route.ts +71 -0
- package/template/src/app/api/contacts/[id]/meet/route.ts +38 -29
- package/template/src/app/api/contacts/[id]/route.ts +111 -20
- package/template/src/app/api/contacts/[id]/send-email/route.ts +6 -0
- package/template/src/app/api/contacts/[id]/workflows/run/route.ts +61 -0
- package/template/src/app/api/contacts/export/route.ts +13 -18
- package/template/src/app/api/contacts/import/route.ts +22 -19
- package/template/src/app/api/contacts/import-preview/route.ts +139 -0
- package/template/src/app/api/contacts/route.ts +202 -49
- package/template/src/app/api/dashboard/stats/route.ts +9 -292
- package/template/src/app/api/integrations/google-sheet/sync/route.ts +203 -185
- package/template/src/app/api/invite/complete/route.ts +20 -23
- package/template/src/app/api/reminders/route.ts +1 -0
- package/template/src/app/api/reset-password/complete/route.ts +11 -13
- package/template/src/app/api/send/route.ts +9 -43
- package/template/src/app/api/settings/closing-reasons/[id]/route.ts +10 -21
- package/template/src/app/api/settings/closing-reasons/route.ts +10 -21
- package/template/src/app/api/settings/company/route.ts +19 -26
- package/template/src/app/api/settings/google-ads/[id]/route.ts +20 -23
- package/template/src/app/api/settings/google-ads/route.ts +20 -23
- package/template/src/app/api/settings/google-sheet/[id]/route.ts +20 -23
- package/template/src/app/api/settings/google-sheet/auto-map/route.ts +23 -32
- package/template/src/app/api/settings/google-sheet/preview/route.ts +104 -0
- package/template/src/app/api/settings/google-sheet/route.ts +20 -23
- package/template/src/app/api/settings/meta-leads/[id]/route.ts +20 -23
- package/template/src/app/api/settings/meta-leads/route.ts +20 -23
- package/template/src/app/api/settings/statuses/[id]/route.ts +29 -32
- package/template/src/app/api/settings/statuses/route.ts +24 -22
- package/template/src/app/api/statuses/route.ts +2 -5
- package/template/src/app/api/tasks/[id]/attendees/route.ts +14 -7
- package/template/src/app/api/tasks/[id]/route.ts +173 -137
- package/template/src/app/api/tasks/meet/route.ts +11 -8
- package/template/src/app/api/tasks/route.ts +155 -95
- package/template/src/app/api/templates/[id]/route.ts +22 -13
- package/template/src/app/api/templates/route.ts +22 -5
- package/template/src/app/api/users/[id]/resend-invite/route.ts +95 -0
- package/template/src/app/api/users/[id]/route.ts +2 -2
- package/template/src/app/api/users/commercials/route.ts +38 -0
- package/template/src/app/api/users/for-agenda/route.ts +1 -2
- package/template/src/app/api/users/route.ts +89 -34
- package/template/src/app/api/webhooks/google-ads/route.ts +20 -1
- package/template/src/app/api/webhooks/meta-leads/route.ts +18 -1
- package/template/src/app/api/workflows/[id]/route.ts +33 -6
- package/template/src/app/api/workflows/process/route.ts +510 -146
- package/template/src/app/api/workflows/route.ts +46 -4
- package/template/src/app/globals.css +243 -101
- package/template/src/app/layout.tsx +19 -8
- package/template/src/app/page.tsx +37 -7
- package/template/src/components/address-autocomplete.tsx +232 -0
- package/template/src/components/contacts/filter-bar.tsx +181 -0
- package/template/src/components/contacts/filter-builder.tsx +589 -0
- package/template/src/components/contacts/save-view-dialog.tsx +160 -0
- package/template/src/components/contacts/views-tab-bar.tsx +440 -0
- package/template/src/components/dashboard/activity-chart.tsx +31 -39
- package/template/src/components/dashboard/dashboard-content.tsx +79 -0
- package/template/src/components/dashboard/stat-card.tsx +40 -42
- package/template/src/components/dashboard/tasks-pie-chart.tsx +34 -37
- package/template/src/components/dashboard/upcoming-tasks-list.tsx +78 -72
- package/template/src/components/date-picker.tsx +396 -0
- package/template/src/components/editor.tsx +27 -13
- package/template/src/components/email-template.tsx +4 -2
- package/template/src/components/global-search.tsx +358 -0
- package/template/src/components/header.tsx +57 -62
- package/template/src/components/invitation-email-template.tsx +4 -2
- package/template/src/components/lazy-editor.tsx +11 -0
- package/template/src/components/meet-cancellation-email-template.tsx +11 -3
- package/template/src/components/meet-confirmation-email-template.tsx +10 -3
- package/template/src/components/meet-update-email-template.tsx +10 -3
- package/template/src/components/page-header.tsx +19 -15
- package/template/src/components/protected-page.tsx +94 -0
- package/template/src/components/reset-password-email-template.tsx +4 -2
- package/template/src/components/sidebar.tsx +92 -94
- package/template/src/components/skeleton.tsx +128 -42
- package/template/src/components/ui/accordion.tsx +64 -0
- package/template/src/components/ui/alert-dialog.tsx +139 -0
- package/template/src/components/ui/button.tsx +60 -0
- package/template/src/components/view-as-banner.tsx +1 -1
- package/template/src/components/view-as-modal.tsx +21 -16
- package/template/src/config/nav-pages.ts +108 -0
- package/template/src/contexts/app-toast-context.tsx +174 -0
- package/template/src/contexts/sidebar-context.tsx +16 -47
- package/template/src/contexts/task-reminder-context.tsx +6 -6
- package/template/src/contexts/view-as-context.tsx +11 -16
- package/template/src/hooks/use-alert.tsx +65 -0
- package/template/src/hooks/use-confirm.tsx +87 -0
- package/template/src/hooks/use-contact-views.ts +140 -0
- package/template/src/hooks/use-contacts.ts +69 -0
- package/template/src/hooks/use-fetch.ts +17 -0
- package/template/src/hooks/use-focus-trap.ts +73 -0
- package/template/src/hooks/use-statuses.ts +22 -0
- package/template/src/lib/address-api.ts +155 -0
- package/template/src/lib/cache.ts +73 -0
- package/template/src/lib/check-permission.ts +12 -177
- package/template/src/lib/contact-interactions.ts +3 -1
- package/template/src/lib/contact-view-filters.ts +341 -0
- package/template/src/lib/dashboard-stats.ts +224 -0
- package/template/src/lib/date-utils.ts +49 -0
- package/template/src/lib/get-auth-user.ts +25 -0
- package/template/src/lib/google-calendar.ts +54 -12
- package/template/src/lib/google-drive.ts +796 -75
- package/template/src/lib/google-fetch.ts +63 -0
- package/template/src/lib/local-storage.ts +34 -0
- package/template/src/lib/permissions.ts +245 -47
- package/template/src/lib/prisma.ts +11 -11
- package/template/src/lib/roles.ts +12 -15
- package/template/src/lib/template-variables.ts +67 -33
- package/template/src/lib/utils.ts +26 -11
- package/template/src/lib/workflow-executor.ts +445 -228
- package/template/src/proxy.ts +34 -73
- package/template/src/types/contact-views.ts +351 -0
- package/template/src/types/yousign.ts +52 -0
- package/template/vercel.json +12 -0
- package/template/WORKFLOWS_CRON.md +0 -185
- package/template/prisma/migrations/20251126144728_init/migration.sql +0 -78
- package/template/prisma/migrations/20251126155204_add_user_roles/migration.sql +0 -5
- package/template/prisma/migrations/20251128095126_add_company_info/migration.sql +0 -19
- package/template/prisma/migrations/20251128123321_add_smtp_config/migration.sql +0 -22
- package/template/prisma/migrations/20251128132303_add_status/migration.sql +0 -23
- package/template/prisma/migrations/20251201102207_add_user_active/migration.sql +0 -75
- package/template/prisma/migrations/20251201105507_add_email_signature/migration.sql +0 -2
- package/template/prisma/migrations/20251201151122_add_tasks/migration.sql +0 -45
- package/template/prisma/migrations/20251202111854_add_task_reminder/migration.sql +0 -2
- package/template/prisma/migrations/20251202135859_add_google_meet_integration/migration.sql +0 -27
- package/template/prisma/migrations/20251203103317_add_meta_lead_integration/migration.sql +0 -20
- package/template/prisma/migrations/20251203104002_add_google_ads_integration/migration.sql +0 -18
- package/template/prisma/migrations/20251203112122_add_google_sheet_integration/migration.sql +0 -32
- package/template/prisma/migrations/20251203153853_allow_multiple_integration_configs/migration.sql +0 -20
- package/template/prisma/migrations/20251205141705_update_user_roles/migration.sql +0 -12
- package/template/prisma/migrations/20251205150000_add_commercial_and_telepro_assignment/migration.sql +0 -21
- package/template/prisma/migrations/20251205160000_add_interaction_logging/migration.sql +0 -11
- package/template/prisma/migrations/20251208090314_add_automatic_interaction_types/migration.sql +0 -12
- package/template/prisma/migrations/20251208094843_mg/migration.sql +0 -14
- package/template/prisma/migrations/20251208100000_add_company_support/migration.sql +0 -14
- package/template/prisma/migrations/20251208110000_add_templates/migration.sql +0 -26
- package/template/prisma/migrations/20251208141304_add_video_conference_task_type/migration.sql +0 -2
- package/template/prisma/migrations/20251209104759_add_internal_note_to_task/migration.sql +0 -2
- package/template/prisma/migrations/20251209134803_add_company_field/migration.sql +0 -2
- package/template/prisma/migrations/20251209150000_rename_company_to_company_name/migration.sql +0 -3
- package/template/prisma/migrations/20251209150016_add_email_tracking/migration.sql +0 -21
- package/template/prisma/migrations/20251209155908_add_notify_contact_to_task/migration.sql +0 -2
- package/template/prisma/migrations/20251210110019_add_appointment_types/migration.sql +0 -10
- package/template/prisma/migrations/20251210113928_add_contact_files/migration.sql +0 -26
- package/template/prisma/migrations/20251212132339_add_custom_roles/migration.sql +0 -24
- package/template/prisma/migrations/20251215104448_add_file_interaction_types/migration.sql +0 -11
- package/template/prisma/migrations/20251215145616_add_closing_reasons/migration.sql +0 -12
- package/template/prisma/migrations/20251216140850_add_log_users/migration.sql +0 -25
- package/template/prisma/migrations/20251216151000_rename_perdu_to_ferme/migration.sql +0 -8
- package/template/prisma/migrations/20251216162318_add_column_mappings_to_google_sheet/migration.sql +0 -2
- package/template/prisma/migrations/20251216185127_add_workflows/migration.sql +0 -80
- package/template/prisma/migrations/20251216192237_add_scheduled_workflow_actions/migration.sql +0 -32
- package/template/prisma/migrations/20251220000000_add_task_interaction_type/migration.sql +0 -4
- package/template/prisma/migrations/20251221000000_add_task_type/migration.sql +0 -3
- package/template/prisma/migrations/20251221000001_add_event_color/migration.sql +0 -23
- package/template/prisma/migrations/20260210114913_add_dashboard_widget/migration.sql +0 -20
- package/template/prisma/migrations/20260226093949_fix_cascade_on_user_delete/migration.sql +0 -69
- package/template/prisma/migrations/migration_lock.toml +0 -3
- package/template/src/app/(dashboard)/users/layout.tsx +0 -30
- package/template/src/app/api/dashboard/widgets/[id]/route.ts +0 -47
- package/template/src/app/api/dashboard/widgets/route.ts +0 -181
- package/template/src/components/dashboard/add-widget-dialog.tsx +0 -161
- package/template/src/components/dashboard/color-picker.tsx +0 -65
- package/template/src/components/dashboard/contacts-chart.tsx +0 -69
- package/template/src/components/dashboard/interactions-by-type-chart.tsx +0 -121
- package/template/src/components/dashboard/recent-activity.tsx +0 -157
- package/template/src/components/dashboard/status-distribution-chart.tsx +0 -82
- package/template/src/components/dashboard/top-contacts-list.tsx +0 -119
- package/template/src/components/dashboard/widget-wrapper.tsx +0 -39
- package/template/src/contexts/dashboard-theme-context.tsx +0 -58
- package/template/src/lib/dashboard-themes.ts +0 -140
- package/template/src/lib/default-widgets.ts +0 -14
- package/template/src/lib/widget-registry.ts +0 -177
|
@@ -1,181 +0,0 @@
|
|
|
1
|
-
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
-
import { auth } from '@/lib/auth';
|
|
3
|
-
import { headers } from 'next/headers';
|
|
4
|
-
import { prisma } from '@/lib/prisma';
|
|
5
|
-
import { DEFAULT_WIDGETS } from '@/lib/default-widgets';
|
|
6
|
-
import { checkPermission } from '@/lib/check-permission';
|
|
7
|
-
|
|
8
|
-
// GET - Récupérer les widgets de l'utilisateur
|
|
9
|
-
export async function GET() {
|
|
10
|
-
try {
|
|
11
|
-
const session = await auth.api.getSession({
|
|
12
|
-
headers: await headers(),
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
if (!session) {
|
|
16
|
-
return NextResponse.json({ error: 'Non autorisé' }, { status: 401 });
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
// Vérifier la permission d'accès au tableau de bord
|
|
20
|
-
const canView = await checkPermission('dashboard.view');
|
|
21
|
-
if (!canView) {
|
|
22
|
-
return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const widgets = await prisma.dashboardWidget.findMany({
|
|
26
|
-
where: { userId: session.user.id },
|
|
27
|
-
orderBy: [{ y: 'asc' }, { x: 'asc' }],
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
return NextResponse.json(widgets);
|
|
31
|
-
} catch (error) {
|
|
32
|
-
console.error('Erreur lors de la récupération des widgets:', error);
|
|
33
|
-
return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 });
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// POST - Créer un widget (ou initialiser le layout par défaut)
|
|
38
|
-
export async function POST(req: NextRequest) {
|
|
39
|
-
try {
|
|
40
|
-
const session = await auth.api.getSession({
|
|
41
|
-
headers: await headers(),
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
if (!session) {
|
|
45
|
-
return NextResponse.json({ error: 'Non autorisé' }, { status: 401 });
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// Vérifier la permission de gestion des widgets
|
|
49
|
-
const canManage = await checkPermission('dashboard.widgets.manage');
|
|
50
|
-
if (!canManage) {
|
|
51
|
-
return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const body = await req.json();
|
|
55
|
-
|
|
56
|
-
// Si c'est une initialisation par défaut
|
|
57
|
-
if (body.initDefault) {
|
|
58
|
-
const existing = await prisma.dashboardWidget.count({
|
|
59
|
-
where: { userId: session.user.id },
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
if (existing > 0) {
|
|
63
|
-
const widgets = await prisma.dashboardWidget.findMany({
|
|
64
|
-
where: { userId: session.user.id },
|
|
65
|
-
orderBy: [{ y: 'asc' }, { x: 'asc' }],
|
|
66
|
-
});
|
|
67
|
-
return NextResponse.json(widgets);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const created = await prisma.$transaction(
|
|
71
|
-
DEFAULT_WIDGETS.map((dw) =>
|
|
72
|
-
prisma.dashboardWidget.create({
|
|
73
|
-
data: {
|
|
74
|
-
userId: session.user.id,
|
|
75
|
-
type: dw.type,
|
|
76
|
-
x: dw.x,
|
|
77
|
-
y: dw.y,
|
|
78
|
-
w: dw.w,
|
|
79
|
-
h: dw.h,
|
|
80
|
-
},
|
|
81
|
-
}),
|
|
82
|
-
),
|
|
83
|
-
);
|
|
84
|
-
|
|
85
|
-
return NextResponse.json(created, { status: 201 });
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// Création d'un widget unique
|
|
89
|
-
const {
|
|
90
|
-
type,
|
|
91
|
-
x,
|
|
92
|
-
y,
|
|
93
|
-
w: width,
|
|
94
|
-
h: height,
|
|
95
|
-
} = body as {
|
|
96
|
-
type?: string;
|
|
97
|
-
x?: number;
|
|
98
|
-
y?: number;
|
|
99
|
-
w?: number;
|
|
100
|
-
h?: number;
|
|
101
|
-
};
|
|
102
|
-
|
|
103
|
-
if (!type) {
|
|
104
|
-
return NextResponse.json({ error: 'Le type de widget est requis' }, { status: 400 });
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// Calculer la position Y max pour placer le nouveau widget en bas
|
|
108
|
-
const lastWidget = await prisma.dashboardWidget.findFirst({
|
|
109
|
-
where: { userId: session.user.id },
|
|
110
|
-
orderBy: { y: 'desc' },
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
const newY = lastWidget ? lastWidget.y + lastWidget.h : 0;
|
|
114
|
-
|
|
115
|
-
const widget = await prisma.dashboardWidget.create({
|
|
116
|
-
data: {
|
|
117
|
-
userId: session.user.id,
|
|
118
|
-
type,
|
|
119
|
-
x: x ?? 0,
|
|
120
|
-
y: y ?? newY,
|
|
121
|
-
w: width ?? 6,
|
|
122
|
-
h: height ?? 4,
|
|
123
|
-
},
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
return NextResponse.json(widget, { status: 201 });
|
|
127
|
-
} catch (error) {
|
|
128
|
-
console.error('Erreur lors de la création du widget:', error);
|
|
129
|
-
return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 });
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// PUT - Mettre à jour les positions des widgets (batch)
|
|
134
|
-
export async function PUT(req: NextRequest) {
|
|
135
|
-
try {
|
|
136
|
-
const session = await auth.api.getSession({
|
|
137
|
-
headers: await headers(),
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
if (!session) {
|
|
141
|
-
return NextResponse.json({ error: 'Non autorisé' }, { status: 401 });
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// Vérifier la permission de gestion des widgets
|
|
145
|
-
const canManage = await checkPermission('dashboard.widgets.manage');
|
|
146
|
-
if (!canManage) {
|
|
147
|
-
return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
const body = await req.json();
|
|
151
|
-
const { widgets } = body as {
|
|
152
|
-
widgets: Array<{ id: string; x: number; y: number; w: number; h: number }>;
|
|
153
|
-
};
|
|
154
|
-
|
|
155
|
-
if (!widgets || !Array.isArray(widgets)) {
|
|
156
|
-
return NextResponse.json({ error: 'Format invalide' }, { status: 400 });
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
await prisma.$transaction(
|
|
160
|
-
widgets.map((w) =>
|
|
161
|
-
prisma.dashboardWidget.updateMany({
|
|
162
|
-
where: {
|
|
163
|
-
id: w.id,
|
|
164
|
-
userId: session.user.id,
|
|
165
|
-
},
|
|
166
|
-
data: {
|
|
167
|
-
x: w.x,
|
|
168
|
-
y: w.y,
|
|
169
|
-
w: w.w,
|
|
170
|
-
h: w.h,
|
|
171
|
-
},
|
|
172
|
-
}),
|
|
173
|
-
),
|
|
174
|
-
);
|
|
175
|
-
|
|
176
|
-
return NextResponse.json({ success: true });
|
|
177
|
-
} catch (error) {
|
|
178
|
-
console.error('Erreur lors de la mise à jour des widgets:', error);
|
|
179
|
-
return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 });
|
|
180
|
-
}
|
|
181
|
-
}
|
|
@@ -1,161 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { useState } from 'react';
|
|
4
|
-
import { X, Plus, BarChart3, ListTodo, TrendingUp } from 'lucide-react';
|
|
5
|
-
import { cn } from '@/lib/utils';
|
|
6
|
-
import { WIDGET_REGISTRY, type WidgetDefinition } from '@/lib/widget-registry';
|
|
7
|
-
|
|
8
|
-
interface AddWidgetDialogProps {
|
|
9
|
-
readonly isOpen: boolean;
|
|
10
|
-
readonly onClose: () => void;
|
|
11
|
-
readonly onAdd: (type: string, w: number, h: number) => void;
|
|
12
|
-
readonly existingTypes: string[];
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
const CATEGORY_LABELS: Record<string, { label: string; icon: typeof BarChart3 }> = {
|
|
16
|
-
stat: { label: 'Statistiques', icon: TrendingUp },
|
|
17
|
-
chart: { label: 'Graphiques', icon: BarChart3 },
|
|
18
|
-
list: { label: 'Listes', icon: ListTodo },
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
export function AddWidgetDialog({ isOpen, onClose, onAdd, existingTypes }: AddWidgetDialogProps) {
|
|
22
|
-
const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
|
|
23
|
-
|
|
24
|
-
if (!isOpen) return null;
|
|
25
|
-
|
|
26
|
-
const categories = ['stat', 'chart', 'list'] as const;
|
|
27
|
-
|
|
28
|
-
const filteredWidgets = selectedCategory
|
|
29
|
-
? WIDGET_REGISTRY.filter((w) => w.category === selectedCategory)
|
|
30
|
-
: WIDGET_REGISTRY;
|
|
31
|
-
|
|
32
|
-
const handleAdd = (widget: WidgetDefinition) => {
|
|
33
|
-
onAdd(widget.type, widget.defaultW, widget.defaultH);
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
return (
|
|
37
|
-
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
|
38
|
-
{/* Backdrop */}
|
|
39
|
-
<div
|
|
40
|
-
className="absolute inset-0 cursor-pointer bg-black/40 backdrop-blur-sm"
|
|
41
|
-
role="button"
|
|
42
|
-
tabIndex={0}
|
|
43
|
-
onClick={onClose}
|
|
44
|
-
onKeyDown={(e) => {
|
|
45
|
-
if (e.key === 'Escape') onClose();
|
|
46
|
-
}}
|
|
47
|
-
/>
|
|
48
|
-
|
|
49
|
-
{/* Dialog */}
|
|
50
|
-
<div className="relative z-10 w-full max-w-2xl rounded-2xl bg-white shadow-2xl">
|
|
51
|
-
{/* Header */}
|
|
52
|
-
<div className="flex items-center justify-between border-b border-gray-100 px-6 py-4">
|
|
53
|
-
<div>
|
|
54
|
-
<h2 className="text-lg font-semibold text-gray-900">Ajouter un Widget</h2>
|
|
55
|
-
<p className="mt-0.5 text-sm text-gray-500">
|
|
56
|
-
Choisissez un widget à ajouter au tableau de bord
|
|
57
|
-
</p>
|
|
58
|
-
</div>
|
|
59
|
-
<button
|
|
60
|
-
onClick={onClose}
|
|
61
|
-
className="flex h-8 w-8 cursor-pointer items-center justify-center rounded-lg text-gray-400 transition-colors hover:bg-gray-100 hover:text-gray-600"
|
|
62
|
-
>
|
|
63
|
-
<X className="h-4 w-4" />
|
|
64
|
-
</button>
|
|
65
|
-
</div>
|
|
66
|
-
|
|
67
|
-
{/* Filtres catégories */}
|
|
68
|
-
<div className="flex gap-2 border-b border-gray-100 px-6 py-3">
|
|
69
|
-
<button
|
|
70
|
-
onClick={() => setSelectedCategory(null)}
|
|
71
|
-
className={cn(
|
|
72
|
-
'cursor-pointer rounded-full px-3.5 py-1.5 text-xs font-medium transition-colors',
|
|
73
|
-
selectedCategory === null
|
|
74
|
-
? 'dash-pill-active'
|
|
75
|
-
: 'bg-gray-100 text-gray-600 hover:bg-gray-200',
|
|
76
|
-
)}
|
|
77
|
-
>
|
|
78
|
-
Tous
|
|
79
|
-
</button>
|
|
80
|
-
{categories.map((cat) => {
|
|
81
|
-
const { label, icon: Icon } = CATEGORY_LABELS[cat];
|
|
82
|
-
return (
|
|
83
|
-
<button
|
|
84
|
-
key={cat}
|
|
85
|
-
onClick={() => setSelectedCategory(cat)}
|
|
86
|
-
className={cn(
|
|
87
|
-
'flex cursor-pointer items-center gap-1.5 rounded-full px-3.5 py-1.5 text-xs font-medium transition-colors',
|
|
88
|
-
selectedCategory === cat
|
|
89
|
-
? 'dash-pill-active'
|
|
90
|
-
: 'bg-gray-100 text-gray-600 hover:bg-gray-200',
|
|
91
|
-
)}
|
|
92
|
-
>
|
|
93
|
-
<Icon className="h-3 w-3" />
|
|
94
|
-
{label}
|
|
95
|
-
</button>
|
|
96
|
-
);
|
|
97
|
-
})}
|
|
98
|
-
</div>
|
|
99
|
-
|
|
100
|
-
{/* Grille de widgets */}
|
|
101
|
-
<div className="max-h-[400px] overflow-y-auto p-6">
|
|
102
|
-
<div className="grid grid-cols-2 gap-3">
|
|
103
|
-
{filteredWidgets.map((widget) => {
|
|
104
|
-
const isAlreadyAdded = existingTypes.includes(widget.type);
|
|
105
|
-
const Icon = widget.icon;
|
|
106
|
-
|
|
107
|
-
return (
|
|
108
|
-
<button
|
|
109
|
-
key={widget.type}
|
|
110
|
-
onClick={() => !isAlreadyAdded && handleAdd(widget)}
|
|
111
|
-
disabled={isAlreadyAdded}
|
|
112
|
-
className={cn(
|
|
113
|
-
'group flex items-start gap-3 rounded-xl border p-4 text-left transition-all duration-150',
|
|
114
|
-
isAlreadyAdded
|
|
115
|
-
? 'cursor-not-allowed border-gray-100 bg-gray-50 opacity-50'
|
|
116
|
-
: 'border-gray-100 bg-white dash-hover-border dash-hover-bg hover:shadow-sm',
|
|
117
|
-
)}
|
|
118
|
-
>
|
|
119
|
-
<div
|
|
120
|
-
className={cn(
|
|
121
|
-
'flex h-9 w-9 shrink-0 items-center justify-center rounded-lg',
|
|
122
|
-
isAlreadyAdded ? 'bg-gray-100' : 'dash-icon-box dash-icon-box-gh',
|
|
123
|
-
)}
|
|
124
|
-
>
|
|
125
|
-
<Icon
|
|
126
|
-
className={cn(
|
|
127
|
-
'h-4 w-4',
|
|
128
|
-
isAlreadyAdded ? 'text-gray-400' : 'dash-icon-color',
|
|
129
|
-
)}
|
|
130
|
-
/>
|
|
131
|
-
</div>
|
|
132
|
-
<div className="min-w-0 flex-1">
|
|
133
|
-
<div className="flex items-center justify-between">
|
|
134
|
-
<p className="text-sm font-medium text-gray-900">{widget.label}</p>
|
|
135
|
-
{isAlreadyAdded ? (
|
|
136
|
-
<span className="text-[10px] font-medium text-gray-400">Déjà ajouté</span>
|
|
137
|
-
) : (
|
|
138
|
-
<Plus className="h-4 w-4 text-gray-300 transition-colors dash-hover-text-gh" />
|
|
139
|
-
)}
|
|
140
|
-
</div>
|
|
141
|
-
<p className="mt-0.5 text-xs text-gray-500">{widget.description}</p>
|
|
142
|
-
<p className="mt-1 text-[10px] text-gray-400">
|
|
143
|
-
Taille : {widget.defaultW}x{widget.defaultH}
|
|
144
|
-
</p>
|
|
145
|
-
</div>
|
|
146
|
-
</button>
|
|
147
|
-
);
|
|
148
|
-
})}
|
|
149
|
-
</div>
|
|
150
|
-
</div>
|
|
151
|
-
|
|
152
|
-
{/* Footer */}
|
|
153
|
-
<div className="border-t border-gray-100 px-6 py-3">
|
|
154
|
-
<p className="text-center text-xs text-gray-400">
|
|
155
|
-
Les widgets peuvent être déplacés et redimensionnés après ajout
|
|
156
|
-
</p>
|
|
157
|
-
</div>
|
|
158
|
-
</div>
|
|
159
|
-
</div>
|
|
160
|
-
);
|
|
161
|
-
}
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { useState, useRef, useEffect } from 'react';
|
|
4
|
-
import { Palette, Check } from 'lucide-react';
|
|
5
|
-
import { useDashboardTheme } from '@/contexts/dashboard-theme-context';
|
|
6
|
-
|
|
7
|
-
export function DashboardColorPicker() {
|
|
8
|
-
const { theme, setThemeKey, themes } = useDashboardTheme();
|
|
9
|
-
const [isOpen, setIsOpen] = useState(false);
|
|
10
|
-
const ref = useRef<HTMLDivElement>(null);
|
|
11
|
-
|
|
12
|
-
// Fermer le dropdown au clic extérieur
|
|
13
|
-
useEffect(() => {
|
|
14
|
-
function handleClickOutside(event: MouseEvent) {
|
|
15
|
-
if (ref.current && !ref.current.contains(event.target as Node)) {
|
|
16
|
-
setIsOpen(false);
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
if (isOpen) {
|
|
20
|
-
document.addEventListener('mousedown', handleClickOutside);
|
|
21
|
-
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
22
|
-
}
|
|
23
|
-
}, [isOpen]);
|
|
24
|
-
|
|
25
|
-
return (
|
|
26
|
-
<div ref={ref} className="relative">
|
|
27
|
-
<button
|
|
28
|
-
onClick={() => setIsOpen(!isOpen)}
|
|
29
|
-
className="inline-flex h-10 w-10 cursor-pointer items-center justify-center rounded-xl border border-gray-200 bg-white shadow-sm transition-all duration-150 hover:bg-gray-50 hover:shadow-md active:scale-[0.98]"
|
|
30
|
-
title="Changer la couleur du thème"
|
|
31
|
-
>
|
|
32
|
-
<Palette className="h-4 w-4 text-gray-600" />
|
|
33
|
-
</button>
|
|
34
|
-
|
|
35
|
-
{isOpen && (
|
|
36
|
-
<div className="absolute right-0 z-50 mt-2 w-56 rounded-xl border border-gray-100 bg-white p-3 shadow-xl">
|
|
37
|
-
<p className="mb-2.5 text-[11px] font-medium tracking-wider text-gray-400 uppercase">
|
|
38
|
-
Couleur d'accent
|
|
39
|
-
</p>
|
|
40
|
-
<div className="grid grid-cols-4 gap-2">
|
|
41
|
-
{themes.map((t) => (
|
|
42
|
-
<button
|
|
43
|
-
key={t.key}
|
|
44
|
-
onClick={() => {
|
|
45
|
-
setThemeKey(t.key);
|
|
46
|
-
setIsOpen(false);
|
|
47
|
-
}}
|
|
48
|
-
className="group relative flex h-10 w-10 cursor-pointer items-center justify-center rounded-xl transition-all duration-150 hover:scale-110"
|
|
49
|
-
style={{ backgroundColor: t.hex[500] }}
|
|
50
|
-
title={t.label}
|
|
51
|
-
>
|
|
52
|
-
{theme.key === t.key && (
|
|
53
|
-
<Check className="h-4 w-4 text-white drop-shadow-sm" />
|
|
54
|
-
)}
|
|
55
|
-
<span className="absolute -bottom-5 left-1/2 -translate-x-1/2 whitespace-nowrap text-[10px] text-gray-500 opacity-0 transition-opacity group-hover:opacity-100">
|
|
56
|
-
{t.label}
|
|
57
|
-
</span>
|
|
58
|
-
</button>
|
|
59
|
-
))}
|
|
60
|
-
</div>
|
|
61
|
-
</div>
|
|
62
|
-
)}
|
|
63
|
-
</div>
|
|
64
|
-
);
|
|
65
|
-
}
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
Area,
|
|
5
|
-
AreaChart,
|
|
6
|
-
ResponsiveContainer,
|
|
7
|
-
Tooltip,
|
|
8
|
-
XAxis,
|
|
9
|
-
YAxis,
|
|
10
|
-
CartesianGrid,
|
|
11
|
-
} from 'recharts';
|
|
12
|
-
import { useDashboardTheme } from '@/contexts/dashboard-theme-context';
|
|
13
|
-
|
|
14
|
-
interface ContactsChartProps {
|
|
15
|
-
readonly data: Array<{ month: string; count: number }>;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export function ContactsChart({ data }: Readonly<ContactsChartProps>) {
|
|
19
|
-
const { theme } = useDashboardTheme();
|
|
20
|
-
|
|
21
|
-
return (
|
|
22
|
-
<div className="flex h-full flex-col rounded-2xl border border-gray-100 bg-white p-5 shadow-sm">
|
|
23
|
-
<div className="mb-4">
|
|
24
|
-
<h3 className="text-base font-semibold text-gray-900">Évolution des Contacts</h3>
|
|
25
|
-
<p className="mt-0.5 text-xs text-gray-400">Contacts créés par mois</p>
|
|
26
|
-
</div>
|
|
27
|
-
<div className="min-h-0 flex-1">
|
|
28
|
-
<ResponsiveContainer width="100%" height="100%">
|
|
29
|
-
<AreaChart data={data} margin={{ top: 5, right: 5, left: -20, bottom: 0 }}>
|
|
30
|
-
<defs>
|
|
31
|
-
<linearGradient id="colorContactsAccent" x1="0" y1="0" x2="0" y2="1">
|
|
32
|
-
<stop offset="0%" stopColor={theme.hex[500]} stopOpacity={0.3} />
|
|
33
|
-
<stop offset="95%" stopColor={theme.hex[500]} stopOpacity={0.02} />
|
|
34
|
-
</linearGradient>
|
|
35
|
-
</defs>
|
|
36
|
-
<CartesianGrid strokeDasharray="3 3" stroke="#f3f4f6" vertical={false} />
|
|
37
|
-
<XAxis
|
|
38
|
-
dataKey="month"
|
|
39
|
-
stroke="#d1d5db"
|
|
40
|
-
fontSize={11}
|
|
41
|
-
tickLine={false}
|
|
42
|
-
axisLine={false}
|
|
43
|
-
dy={8}
|
|
44
|
-
/>
|
|
45
|
-
<YAxis stroke="#d1d5db" fontSize={11} tickLine={false} axisLine={false} width={40} />
|
|
46
|
-
<Tooltip
|
|
47
|
-
contentStyle={{
|
|
48
|
-
backgroundColor: '#fff',
|
|
49
|
-
border: '1px solid #f3f4f6',
|
|
50
|
-
borderRadius: '12px',
|
|
51
|
-
boxShadow: '0 4px 12px rgba(0,0,0,0.08)',
|
|
52
|
-
fontSize: '13px',
|
|
53
|
-
}}
|
|
54
|
-
labelStyle={{ color: '#374151', fontWeight: 600 }}
|
|
55
|
-
/>
|
|
56
|
-
<Area
|
|
57
|
-
type="monotone"
|
|
58
|
-
dataKey="count"
|
|
59
|
-
stroke={theme.hex[500]}
|
|
60
|
-
strokeWidth={2.5}
|
|
61
|
-
fill="url(#colorContactsAccent)"
|
|
62
|
-
name="Contacts"
|
|
63
|
-
/>
|
|
64
|
-
</AreaChart>
|
|
65
|
-
</ResponsiveContainer>
|
|
66
|
-
</div>
|
|
67
|
-
</div>
|
|
68
|
-
);
|
|
69
|
-
}
|
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
Bar,
|
|
5
|
-
BarChart,
|
|
6
|
-
ResponsiveContainer,
|
|
7
|
-
Tooltip,
|
|
8
|
-
XAxis,
|
|
9
|
-
YAxis,
|
|
10
|
-
CartesianGrid,
|
|
11
|
-
Cell,
|
|
12
|
-
} from 'recharts';
|
|
13
|
-
|
|
14
|
-
interface InteractionsByTypeChartProps {
|
|
15
|
-
readonly data: Array<{ type: string; count: number }>;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const TYPE_LABELS: Record<string, string> = {
|
|
19
|
-
CALL: 'Appels',
|
|
20
|
-
SMS: 'SMS',
|
|
21
|
-
EMAIL: 'Emails',
|
|
22
|
-
MEETING: 'Réunions',
|
|
23
|
-
NOTE: 'Notes',
|
|
24
|
-
TASK: 'Tâches',
|
|
25
|
-
STATUS_CHANGE: 'Changements',
|
|
26
|
-
APPOINTMENT_CREATED: 'RDV créés',
|
|
27
|
-
APPOINTMENT_DELETED: 'RDV supprimés',
|
|
28
|
-
APPOINTMENT_CHANGED: 'RDV modifiés',
|
|
29
|
-
ASSIGNMENT_CHANGE: 'Assignations',
|
|
30
|
-
CONTACT_UPDATE: 'Mises à jour',
|
|
31
|
-
FILE_UPLOADED: 'Fichiers',
|
|
32
|
-
FILE_REPLACED: 'Fichiers remplacés',
|
|
33
|
-
FILE_DELETED: 'Fichiers supprimés',
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
const TYPE_COLORS: Record<string, string> = {
|
|
37
|
-
CALL: '#3b82f6',
|
|
38
|
-
SMS: '#10b981',
|
|
39
|
-
EMAIL: '#f97316',
|
|
40
|
-
MEETING: '#8b5cf6',
|
|
41
|
-
NOTE: '#6b7280',
|
|
42
|
-
TASK: '#f59e0b',
|
|
43
|
-
STATUS_CHANGE: '#ef4444',
|
|
44
|
-
APPOINTMENT_CREATED: '#14b8a6',
|
|
45
|
-
APPOINTMENT_DELETED: '#ef4444',
|
|
46
|
-
APPOINTMENT_CHANGED: '#f97316',
|
|
47
|
-
ASSIGNMENT_CHANGE: '#ec4899',
|
|
48
|
-
CONTACT_UPDATE: '#06b6d4',
|
|
49
|
-
FILE_UPLOADED: '#84cc16',
|
|
50
|
-
FILE_REPLACED: '#a855f7',
|
|
51
|
-
FILE_DELETED: '#dc2626',
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
export function InteractionsByTypeChart({ data }: Readonly<InteractionsByTypeChartProps>) {
|
|
55
|
-
const chartData = data
|
|
56
|
-
.filter((d) => d.count > 0)
|
|
57
|
-
.map((d) => ({
|
|
58
|
-
...d,
|
|
59
|
-
label: TYPE_LABELS[d.type] || d.type,
|
|
60
|
-
fill: TYPE_COLORS[d.type] || '#f97316',
|
|
61
|
-
}))
|
|
62
|
-
.sort((a, b) => b.count - a.count);
|
|
63
|
-
|
|
64
|
-
return (
|
|
65
|
-
<div className="flex h-full flex-col rounded-2xl border border-gray-100 bg-white p-5 shadow-sm">
|
|
66
|
-
<div className="mb-4">
|
|
67
|
-
<h3 className="text-base font-semibold text-gray-900">Interactions par Type</h3>
|
|
68
|
-
<p className="mt-0.5 text-xs text-gray-400">Répartition des interactions ce mois</p>
|
|
69
|
-
</div>
|
|
70
|
-
|
|
71
|
-
{chartData.length === 0 ? (
|
|
72
|
-
<div className="flex flex-1 items-center justify-center">
|
|
73
|
-
<p className="text-sm text-gray-400">Aucune interaction ce mois</p>
|
|
74
|
-
</div>
|
|
75
|
-
) : (
|
|
76
|
-
<div className="min-h-0 flex-1">
|
|
77
|
-
<ResponsiveContainer width="100%" height="100%">
|
|
78
|
-
<BarChart
|
|
79
|
-
data={chartData}
|
|
80
|
-
layout="vertical"
|
|
81
|
-
margin={{ top: 0, right: 20, left: 0, bottom: 0 }}
|
|
82
|
-
>
|
|
83
|
-
<CartesianGrid strokeDasharray="3 3" stroke="#f3f4f6" horizontal={false} />
|
|
84
|
-
<XAxis
|
|
85
|
-
type="number"
|
|
86
|
-
stroke="#d1d5db"
|
|
87
|
-
fontSize={11}
|
|
88
|
-
tickLine={false}
|
|
89
|
-
axisLine={false}
|
|
90
|
-
/>
|
|
91
|
-
<YAxis
|
|
92
|
-
dataKey="label"
|
|
93
|
-
type="category"
|
|
94
|
-
stroke="#d1d5db"
|
|
95
|
-
fontSize={11}
|
|
96
|
-
tickLine={false}
|
|
97
|
-
axisLine={false}
|
|
98
|
-
width={80}
|
|
99
|
-
/>
|
|
100
|
-
<Tooltip
|
|
101
|
-
contentStyle={{
|
|
102
|
-
backgroundColor: '#fff',
|
|
103
|
-
border: '1px solid #f3f4f6',
|
|
104
|
-
borderRadius: '12px',
|
|
105
|
-
boxShadow: '0 4px 12px rgba(0,0,0,0.08)',
|
|
106
|
-
fontSize: '13px',
|
|
107
|
-
}}
|
|
108
|
-
formatter={(value: number) => [value, 'Interactions']}
|
|
109
|
-
/>
|
|
110
|
-
<Bar dataKey="count" radius={[0, 6, 6, 0]} barSize={16}>
|
|
111
|
-
{chartData.map((entry) => (
|
|
112
|
-
<Cell key={`type-${entry.label}`} fill={entry.fill} />
|
|
113
|
-
))}
|
|
114
|
-
</Bar>
|
|
115
|
-
</BarChart>
|
|
116
|
-
</ResponsiveContainer>
|
|
117
|
-
</div>
|
|
118
|
-
)}
|
|
119
|
-
</div>
|
|
120
|
-
);
|
|
121
|
-
}
|