create-crm-tmp 1.1.2 → 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.
Files changed (220) hide show
  1. package/package.json +1 -1
  2. package/template/.prettierignore +2 -0
  3. package/template/README.md +53 -67
  4. package/template/components.json +22 -0
  5. package/template/exemple-contacts.csv +54 -0
  6. package/template/next.config.ts +27 -1
  7. package/template/package.json +64 -27
  8. package/template/prisma/schema.prisma +821 -72
  9. package/template/skills-lock.json +25 -0
  10. package/template/src/app/(auth)/invite/[token]/page.tsx +21 -24
  11. package/template/src/app/(auth)/reset-password/complete/page.tsx +12 -21
  12. package/template/src/app/(auth)/reset-password/page.tsx +12 -8
  13. package/template/src/app/(auth)/reset-password/verify/page.tsx +12 -8
  14. package/template/src/app/(auth)/signin/page.tsx +20 -17
  15. package/template/src/app/(dashboard)/agenda/page.tsx +2231 -2188
  16. package/template/src/app/(dashboard)/automatisation/[id]/page.tsx +10 -7
  17. package/template/src/app/(dashboard)/automatisation/_components/workflow-editor.tsx +680 -323
  18. package/template/src/app/(dashboard)/automatisation/new/page.tsx +11 -8
  19. package/template/src/app/(dashboard)/automatisation/page.tsx +473 -180
  20. package/template/src/app/(dashboard)/closing/page.tsx +500 -468
  21. package/template/src/app/(dashboard)/contacts/[id]/page.tsx +5035 -4126
  22. package/template/src/app/(dashboard)/contacts/companies/[id]/page.tsx +1703 -0
  23. package/template/src/app/(dashboard)/contacts/loading.tsx +13 -0
  24. package/template/src/app/(dashboard)/contacts/page.tsx +3776 -2064
  25. package/template/src/app/(dashboard)/dashboard/page.tsx +37 -519
  26. package/template/src/app/(dashboard)/error.tsx +37 -0
  27. package/template/src/app/(dashboard)/layout.tsx +1 -1
  28. package/template/src/app/(dashboard)/loading.tsx +5 -0
  29. package/template/src/app/(dashboard)/settings/loading.tsx +19 -0
  30. package/template/src/app/(dashboard)/settings/page.tsx +2685 -2489
  31. package/template/src/app/(dashboard)/templates/page.tsx +500 -300
  32. package/template/src/app/(dashboard)/users/list/page.tsx +356 -350
  33. package/template/src/app/(dashboard)/users/page.tsx +279 -310
  34. package/template/src/app/(dashboard)/users/permissions/page.tsx +104 -99
  35. package/template/src/app/(dashboard)/users/roles/page.tsx +164 -137
  36. package/template/src/app/api/audit-logs/route.ts +1 -1
  37. package/template/src/app/api/auth/google/callback/route.ts +8 -5
  38. package/template/src/app/api/auth/google/disconnect/route.ts +2 -2
  39. package/template/src/app/api/companies/[id]/activities/route.ts +131 -0
  40. package/template/src/app/api/companies/[id]/route.ts +195 -0
  41. package/template/src/app/api/companies/export/route.ts +206 -0
  42. package/template/src/app/api/companies/route.ts +166 -0
  43. package/template/src/app/api/contact-views/[id]/pin/route.ts +69 -0
  44. package/template/src/app/api/contact-views/[id]/route.ts +197 -0
  45. package/template/src/app/api/contact-views/route.ts +146 -0
  46. package/template/src/app/api/contacts/[id]/files/[fileId]/preview/route.ts +77 -0
  47. package/template/src/app/api/contacts/[id]/files/[fileId]/route.ts +7 -17
  48. package/template/src/app/api/contacts/[id]/files/route.ts +83 -44
  49. package/template/src/app/api/contacts/[id]/interactions/route.ts +37 -0
  50. package/template/src/app/api/contacts/[id]/kyc/route.ts +71 -0
  51. package/template/src/app/api/contacts/[id]/meet/route.ts +38 -29
  52. package/template/src/app/api/contacts/[id]/route.ts +111 -20
  53. package/template/src/app/api/contacts/[id]/send-email/route.ts +6 -0
  54. package/template/src/app/api/contacts/[id]/workflows/run/route.ts +61 -0
  55. package/template/src/app/api/contacts/export/route.ts +12 -17
  56. package/template/src/app/api/contacts/import/route.ts +22 -19
  57. package/template/src/app/api/contacts/import-preview/route.ts +139 -0
  58. package/template/src/app/api/contacts/route.ts +202 -49
  59. package/template/src/app/api/dashboard/stats/route.ts +9 -292
  60. package/template/src/app/api/integrations/google-sheet/sync/route.ts +203 -185
  61. package/template/src/app/api/invite/complete/route.ts +20 -23
  62. package/template/src/app/api/reminders/route.ts +1 -0
  63. package/template/src/app/api/reset-password/complete/route.ts +11 -13
  64. package/template/src/app/api/send/route.ts +9 -85
  65. package/template/src/app/api/settings/closing-reasons/[id]/route.ts +10 -21
  66. package/template/src/app/api/settings/closing-reasons/route.ts +10 -21
  67. package/template/src/app/api/settings/company/route.ts +19 -26
  68. package/template/src/app/api/settings/google-ads/[id]/route.ts +20 -23
  69. package/template/src/app/api/settings/google-ads/route.ts +20 -23
  70. package/template/src/app/api/settings/google-sheet/[id]/route.ts +20 -23
  71. package/template/src/app/api/settings/google-sheet/auto-map/route.ts +23 -32
  72. package/template/src/app/api/settings/google-sheet/preview/route.ts +104 -0
  73. package/template/src/app/api/settings/google-sheet/route.ts +20 -23
  74. package/template/src/app/api/settings/meta-leads/[id]/route.ts +20 -23
  75. package/template/src/app/api/settings/meta-leads/route.ts +20 -23
  76. package/template/src/app/api/settings/statuses/[id]/route.ts +33 -23
  77. package/template/src/app/api/settings/statuses/route.ts +24 -22
  78. package/template/src/app/api/statuses/route.ts +2 -5
  79. package/template/src/app/api/tasks/[id]/attendees/route.ts +14 -7
  80. package/template/src/app/api/tasks/[id]/route.ts +161 -137
  81. package/template/src/app/api/tasks/meet/route.ts +11 -8
  82. package/template/src/app/api/tasks/route.ts +155 -95
  83. package/template/src/app/api/templates/[id]/route.ts +22 -13
  84. package/template/src/app/api/templates/route.ts +22 -5
  85. package/template/src/app/api/users/[id]/resend-invite/route.ts +95 -0
  86. package/template/src/app/api/users/[id]/route.ts +16 -1
  87. package/template/src/app/api/users/commercials/route.ts +38 -0
  88. package/template/src/app/api/users/for-agenda/route.ts +1 -2
  89. package/template/src/app/api/users/route.ts +94 -55
  90. package/template/src/app/api/webhooks/google-ads/route.ts +20 -1
  91. package/template/src/app/api/webhooks/meta-leads/route.ts +18 -1
  92. package/template/src/app/api/workflows/[id]/route.ts +33 -6
  93. package/template/src/app/api/workflows/process/route.ts +509 -146
  94. package/template/src/app/api/workflows/route.ts +46 -4
  95. package/template/src/app/globals.css +210 -101
  96. package/template/src/app/layout.tsx +19 -8
  97. package/template/src/app/page.tsx +37 -7
  98. package/template/src/components/address-autocomplete.tsx +232 -0
  99. package/template/src/components/contacts/filter-bar.tsx +181 -0
  100. package/template/src/components/contacts/filter-builder.tsx +589 -0
  101. package/template/src/components/contacts/save-view-dialog.tsx +160 -0
  102. package/template/src/components/contacts/views-tab-bar.tsx +440 -0
  103. package/template/src/components/dashboard/activity-chart.tsx +31 -39
  104. package/template/src/components/dashboard/dashboard-content.tsx +79 -0
  105. package/template/src/components/dashboard/stat-card.tsx +40 -42
  106. package/template/src/components/dashboard/tasks-pie-chart.tsx +34 -37
  107. package/template/src/components/dashboard/upcoming-tasks-list.tsx +78 -72
  108. package/template/src/components/date-picker.tsx +396 -0
  109. package/template/src/components/editor.tsx +27 -13
  110. package/template/src/components/email-template.tsx +4 -2
  111. package/template/src/components/global-search.tsx +358 -0
  112. package/template/src/components/header.tsx +57 -62
  113. package/template/src/components/invitation-email-template.tsx +4 -2
  114. package/template/src/components/lazy-editor.tsx +11 -0
  115. package/template/src/components/meet-cancellation-email-template.tsx +11 -3
  116. package/template/src/components/meet-confirmation-email-template.tsx +10 -3
  117. package/template/src/components/meet-update-email-template.tsx +10 -3
  118. package/template/src/components/page-header.tsx +19 -15
  119. package/template/src/components/protected-page.tsx +94 -0
  120. package/template/src/components/reset-password-email-template.tsx +4 -2
  121. package/template/src/components/sidebar.tsx +92 -94
  122. package/template/src/components/skeleton.tsx +128 -42
  123. package/template/src/components/ui/accordion.tsx +64 -0
  124. package/template/src/components/ui/alert-dialog.tsx +139 -0
  125. package/template/src/components/ui/button.tsx +60 -0
  126. package/template/src/components/view-as-banner.tsx +1 -1
  127. package/template/src/components/view-as-modal.tsx +21 -16
  128. package/template/src/config/nav-pages.ts +108 -0
  129. package/template/src/contexts/app-toast-context.tsx +174 -0
  130. package/template/src/contexts/sidebar-context.tsx +16 -47
  131. package/template/src/contexts/task-reminder-context.tsx +6 -6
  132. package/template/src/contexts/view-as-context.tsx +11 -16
  133. package/template/src/hooks/use-alert.tsx +65 -0
  134. package/template/src/hooks/use-confirm.tsx +87 -0
  135. package/template/src/hooks/use-contact-views.ts +140 -0
  136. package/template/src/hooks/use-contacts.ts +69 -0
  137. package/template/src/hooks/use-fetch.ts +17 -0
  138. package/template/src/hooks/use-focus-trap.ts +73 -0
  139. package/template/src/hooks/use-statuses.ts +22 -0
  140. package/template/src/lib/address-api.ts +155 -0
  141. package/template/src/lib/cache.ts +73 -0
  142. package/template/src/lib/check-permission.ts +12 -177
  143. package/template/src/lib/contact-interactions.ts +3 -1
  144. package/template/src/lib/contact-view-filters.ts +341 -0
  145. package/template/src/lib/dashboard-stats.ts +224 -0
  146. package/template/src/lib/date-utils.ts +49 -0
  147. package/template/src/lib/get-auth-user.ts +25 -0
  148. package/template/src/lib/google-calendar.ts +54 -12
  149. package/template/src/lib/google-drive.ts +796 -75
  150. package/template/src/lib/google-fetch.ts +63 -0
  151. package/template/src/lib/local-storage.ts +34 -0
  152. package/template/src/lib/permissions.ts +245 -47
  153. package/template/src/lib/prisma.ts +11 -11
  154. package/template/src/lib/roles.ts +14 -39
  155. package/template/src/lib/template-variables.ts +67 -33
  156. package/template/src/lib/utils.ts +26 -2
  157. package/template/src/lib/workflow-executor.ts +445 -229
  158. package/template/src/proxy.ts +34 -73
  159. package/template/src/types/contact-views.ts +351 -0
  160. package/template/src/types/yousign.ts +52 -0
  161. package/template/vercel.json +12 -0
  162. package/template/WORKFLOWS_CRON.md +0 -185
  163. package/template/prisma/migrations/20251126144728_init/migration.sql +0 -78
  164. package/template/prisma/migrations/20251126155204_add_user_roles/migration.sql +0 -5
  165. package/template/prisma/migrations/20251128095126_add_company_info/migration.sql +0 -19
  166. package/template/prisma/migrations/20251128123321_add_smtp_config/migration.sql +0 -22
  167. package/template/prisma/migrations/20251128132303_add_status/migration.sql +0 -23
  168. package/template/prisma/migrations/20251201102207_add_user_active/migration.sql +0 -75
  169. package/template/prisma/migrations/20251201105507_add_email_signature/migration.sql +0 -2
  170. package/template/prisma/migrations/20251201151122_add_tasks/migration.sql +0 -45
  171. package/template/prisma/migrations/20251202111854_add_task_reminder/migration.sql +0 -2
  172. package/template/prisma/migrations/20251202135859_add_google_meet_integration/migration.sql +0 -27
  173. package/template/prisma/migrations/20251203103317_add_meta_lead_integration/migration.sql +0 -20
  174. package/template/prisma/migrations/20251203104002_add_google_ads_integration/migration.sql +0 -18
  175. package/template/prisma/migrations/20251203112122_add_google_sheet_integration/migration.sql +0 -32
  176. package/template/prisma/migrations/20251203153853_allow_multiple_integration_configs/migration.sql +0 -20
  177. package/template/prisma/migrations/20251205141705_update_user_roles/migration.sql +0 -12
  178. package/template/prisma/migrations/20251205150000_add_commercial_and_telepro_assignment/migration.sql +0 -21
  179. package/template/prisma/migrations/20251205160000_add_interaction_logging/migration.sql +0 -11
  180. package/template/prisma/migrations/20251208090314_add_automatic_interaction_types/migration.sql +0 -12
  181. package/template/prisma/migrations/20251208094843_mg/migration.sql +0 -14
  182. package/template/prisma/migrations/20251208100000_add_company_support/migration.sql +0 -14
  183. package/template/prisma/migrations/20251208110000_add_templates/migration.sql +0 -26
  184. package/template/prisma/migrations/20251208141304_add_video_conference_task_type/migration.sql +0 -2
  185. package/template/prisma/migrations/20251209104759_add_internal_note_to_task/migration.sql +0 -2
  186. package/template/prisma/migrations/20251209134803_add_company_field/migration.sql +0 -2
  187. package/template/prisma/migrations/20251209150000_rename_company_to_company_name/migration.sql +0 -3
  188. package/template/prisma/migrations/20251209150016_add_email_tracking/migration.sql +0 -21
  189. package/template/prisma/migrations/20251209155908_add_notify_contact_to_task/migration.sql +0 -2
  190. package/template/prisma/migrations/20251210110019_add_appointment_types/migration.sql +0 -10
  191. package/template/prisma/migrations/20251210113928_add_contact_files/migration.sql +0 -26
  192. package/template/prisma/migrations/20251212132339_add_custom_roles/migration.sql +0 -24
  193. package/template/prisma/migrations/20251215104448_add_file_interaction_types/migration.sql +0 -11
  194. package/template/prisma/migrations/20251215145616_add_closing_reasons/migration.sql +0 -12
  195. package/template/prisma/migrations/20251216140850_add_log_users/migration.sql +0 -25
  196. package/template/prisma/migrations/20251216151000_rename_perdu_to_ferme/migration.sql +0 -8
  197. package/template/prisma/migrations/20251216162318_add_column_mappings_to_google_sheet/migration.sql +0 -2
  198. package/template/prisma/migrations/20251216185127_add_workflows/migration.sql +0 -80
  199. package/template/prisma/migrations/20251216192237_add_scheduled_workflow_actions/migration.sql +0 -32
  200. package/template/prisma/migrations/20251220000000_add_task_interaction_type/migration.sql +0 -4
  201. package/template/prisma/migrations/20251221000000_add_task_type/migration.sql +0 -3
  202. package/template/prisma/migrations/20251221000001_add_event_color/migration.sql +0 -23
  203. package/template/prisma/migrations/20260210114913_add_dashboard_widget/migration.sql +0 -20
  204. package/template/prisma/migrations/migration_lock.toml +0 -3
  205. package/template/src/app/(dashboard)/users/layout.tsx +0 -30
  206. package/template/src/app/api/dashboard/widgets/[id]/route.ts +0 -47
  207. package/template/src/app/api/dashboard/widgets/route.ts +0 -181
  208. package/template/src/components/dashboard/add-widget-dialog.tsx +0 -161
  209. package/template/src/components/dashboard/color-picker.tsx +0 -65
  210. package/template/src/components/dashboard/contacts-chart.tsx +0 -69
  211. package/template/src/components/dashboard/interactions-by-type-chart.tsx +0 -121
  212. package/template/src/components/dashboard/recent-activity.tsx +0 -157
  213. package/template/src/components/dashboard/sales-analytics-chart.tsx +0 -77
  214. package/template/src/components/dashboard/status-distribution-chart.tsx +0 -82
  215. package/template/src/components/dashboard/top-contacts-list.tsx +0 -119
  216. package/template/src/components/dashboard/widget-wrapper.tsx +0 -39
  217. package/template/src/contexts/dashboard-theme-context.tsx +0 -58
  218. package/template/src/lib/dashboard-themes.ts +0 -140
  219. package/template/src/lib/default-widgets.ts +0 -14
  220. package/template/src/lib/widget-registry.ts +0 -177
@@ -1,9 +1,32 @@
1
1
  import { NextRequest, NextResponse } from 'next/server';
2
+ import { z } from 'zod';
2
3
  import { auth } from '@/lib/auth';
3
4
  import { prisma } from '@/lib/prisma';
5
+ import { checkPermission } from '@/lib/check-permission';
4
6
  import { handleContactDuplicate } from '@/lib/contact-duplicate';
5
7
  import { executeWorkflowsOnContactCreated } from '@/lib/workflow-executor';
6
8
  import { normalizePhoneNumber } from '@/lib/utils';
9
+ import { buildPrismaWhereFromFilters } from '@/lib/contact-view-filters';
10
+ import type { ViewFilter, ViewSortConfig } from '@/types/contact-views';
11
+
12
+ const createContactSchema = z.object({
13
+ civility: z.enum(['M', 'MME', 'MLLE']).optional().nullable(),
14
+ firstName: z.string().trim().min(1).optional().nullable(),
15
+ lastName: z.string().trim().min(1).optional().nullable(),
16
+ phone: z.string().trim().min(3, 'Le téléphone est obligatoire'),
17
+ secondaryPhone: z.string().trim().optional().nullable(),
18
+ email: z.string().email().optional().nullable(),
19
+ address: z.string().optional().nullable(),
20
+ city: z.string().optional().nullable(),
21
+ postalCode: z.string().optional().nullable(),
22
+ origin: z.string().optional().nullable(),
23
+ companyId: z.string().optional().nullable(),
24
+ jobTitle: z.string().trim().optional().nullable(),
25
+ statusId: z.string().optional().nullable(),
26
+ closingReason: z.string().optional().nullable(),
27
+ assignedCommercialId: z.string().optional().nullable(),
28
+ assignedTeleproId: z.string().optional().nullable(),
29
+ });
7
30
 
8
31
  // GET /api/contacts - Récupérer tous les contacts avec filtres
9
32
  export async function GET(request: NextRequest) {
@@ -16,8 +39,24 @@ export async function GET(request: NextRequest) {
16
39
  return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
17
40
  }
18
41
 
42
+ const [canViewAll, canViewOwn, canViewUnassigned] = await Promise.all([
43
+ checkPermission('contacts.view_all'),
44
+ checkPermission('contacts.view_own'),
45
+ checkPermission('contacts.view_unassigned'),
46
+ ]);
47
+
48
+ if (!canViewAll && !canViewOwn) {
49
+ return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
50
+ }
51
+
19
52
  const { searchParams } = new URL(request.url);
20
53
  const search = searchParams.get('search') || '';
54
+ const viewId = searchParams.get('viewId');
55
+ const filtersParam = searchParams.get('filters');
56
+ const statusIds = searchParams.get('statusIds');
57
+ const assignedCommercialIds = searchParams.get('assignedCommercialIds');
58
+ const assignedTeleproIds = searchParams.get('assignedTeleproIds');
59
+ const origins = searchParams.get('origins');
21
60
  const statusId = searchParams.get('statusId');
22
61
  const assignedCommercialId = searchParams.get('assignedCommercialId');
23
62
  const assignedTeleproId = searchParams.get('assignedTeleproId');
@@ -26,40 +65,122 @@ export async function GET(request: NextRequest) {
26
65
  const createdAtEnd = searchParams.get('createdAtEnd');
27
66
  const updatedAtStart = searchParams.get('updatedAtStart');
28
67
  const updatedAtEnd = searchParams.get('updatedAtEnd');
29
- // const isCompany = searchParams.get('isCompany');
30
- const page = parseInt(searchParams.get('page') || '1');
31
- const limit = parseInt(searchParams.get('limit') || '50');
68
+ const sortFieldParam = searchParams.get('sortField');
69
+ const sortDirParam = searchParams.get('sortDir');
70
+ const page = Number.parseInt(searchParams.get('page') || '1');
71
+ const limit = Number.parseInt(searchParams.get('limit') || '50');
32
72
  const skip = (page - 1) * limit;
33
73
 
34
- // Construire les filtres
35
74
  const where: any = {};
75
+ let viewSortConfig: ViewSortConfig | null = null;
36
76
 
37
- if (search) {
38
- where.OR = [
39
- { firstName: { contains: search, mode: 'insensitive' } },
40
- { lastName: { contains: search, mode: 'insensitive' } },
41
- { email: { contains: search, mode: 'insensitive' } },
42
- { phone: { contains: search, mode: 'insensitive' } },
77
+ // Resolve view filters if viewId or inline filters are provided
78
+ if (viewId) {
79
+ const view = await prisma.contactView.findUnique({ where: { id: viewId } });
80
+ if (view && (view.userId === session.user.id || view.isPublic)) {
81
+ const viewFilters = (view.filters ?? []) as unknown as ViewFilter[];
82
+ const viewWhere = buildPrismaWhereFromFilters(viewFilters);
83
+ if (Object.keys(viewWhere).length > 0) {
84
+ where.AND = where.AND || [];
85
+ where.AND.push(viewWhere);
86
+ }
87
+ if (view.sortConfig) {
88
+ viewSortConfig = view.sortConfig as unknown as ViewSortConfig;
89
+ }
90
+ }
91
+ } else if (filtersParam) {
92
+ try {
93
+ const parsedFilters = JSON.parse(filtersParam) as ViewFilter[];
94
+ if (Array.isArray(parsedFilters) && parsedFilters.length > 0) {
95
+ const filtersWhere = buildPrismaWhereFromFilters(parsedFilters);
96
+ if (Object.keys(filtersWhere).length > 0) {
97
+ where.AND = where.AND || [];
98
+ where.AND.push(filtersWhere);
99
+ }
100
+ }
101
+ } catch {
102
+ // Invalid JSON, ignore
103
+ }
104
+ }
105
+
106
+ if (!canViewAll && canViewOwn) {
107
+ const ownershipConditions: any[] = [
108
+ { assignedCommercialId: session.user.id },
109
+ { assignedTeleproId: session.user.id },
110
+ { createdById: session.user.id },
43
111
  ];
112
+ if (canViewUnassigned) {
113
+ ownershipConditions.push({
114
+ AND: [{ assignedCommercialId: null }, { assignedTeleproId: null }],
115
+ });
116
+ }
117
+ where.AND = where.AND || [];
118
+ where.AND.push({ OR: ownershipConditions });
44
119
  }
45
120
 
46
- if (statusId) {
121
+ if (search) {
122
+ where.AND = where.AND || [];
123
+ where.AND.push({
124
+ OR: [
125
+ { firstName: { contains: search, mode: 'insensitive' } },
126
+ { lastName: { contains: search, mode: 'insensitive' } },
127
+ { email: { contains: search, mode: 'insensitive' } },
128
+ { phone: { contains: search, mode: 'insensitive' } },
129
+ ],
130
+ });
131
+ }
132
+
133
+ // Legacy query-param filters (rétrocompatibilité)
134
+ if (statusIds) {
135
+ const ids = statusIds.split(',').filter(Boolean);
136
+ where.statusId = ids.length === 1 ? ids[0] : { in: ids };
137
+ } else if (statusId) {
47
138
  where.statusId = statusId;
48
139
  }
49
140
 
50
- if (assignedCommercialId) {
141
+ if (assignedCommercialIds) {
142
+ const ids = assignedCommercialIds.split(',').filter(Boolean);
143
+ const hasUnassigned = ids.includes('UNASSIGNED');
144
+ const realIds = ids.filter((id) => id !== 'UNASSIGNED');
145
+ if (hasUnassigned && realIds.length > 0) {
146
+ where.AND = where.AND || [];
147
+ where.AND.push({
148
+ OR: [{ assignedCommercialId: null }, { assignedCommercialId: { in: realIds } }],
149
+ });
150
+ } else if (hasUnassigned) {
151
+ where.assignedCommercialId = null;
152
+ } else {
153
+ where.assignedCommercialId = realIds.length === 1 ? realIds[0] : { in: realIds };
154
+ }
155
+ } else if (assignedCommercialId) {
51
156
  where.assignedCommercialId = assignedCommercialId;
52
157
  }
53
158
 
54
- if (assignedTeleproId) {
159
+ if (assignedTeleproIds) {
160
+ const ids = assignedTeleproIds.split(',').filter(Boolean);
161
+ const hasUnassigned = ids.includes('UNASSIGNED');
162
+ const realIds = ids.filter((id) => id !== 'UNASSIGNED');
163
+ if (hasUnassigned && realIds.length > 0) {
164
+ where.AND = where.AND || [];
165
+ where.AND.push({
166
+ OR: [{ assignedTeleproId: null }, { assignedTeleproId: { in: realIds } }],
167
+ });
168
+ } else if (hasUnassigned) {
169
+ where.assignedTeleproId = null;
170
+ } else {
171
+ where.assignedTeleproId = realIds.length === 1 ? realIds[0] : { in: realIds };
172
+ }
173
+ } else if (assignedTeleproId) {
55
174
  where.assignedTeleproId = assignedTeleproId;
56
175
  }
57
176
 
58
- if (origin) {
177
+ if (origins) {
178
+ const vals = origins.split(',').filter(Boolean);
179
+ where.origin = vals.length === 1 ? vals[0] : { in: vals };
180
+ } else if (origin) {
59
181
  where.origin = origin;
60
182
  }
61
183
 
62
- // Filtres de date pour createdAt
63
184
  if (createdAtStart || createdAtEnd) {
64
185
  where.createdAt = {};
65
186
  if (createdAtStart) {
@@ -69,7 +190,6 @@ export async function GET(request: NextRequest) {
69
190
  }
70
191
  }
71
192
  if (createdAtEnd) {
72
- // Ajouter 23h59m59s pour inclure toute la journée
73
193
  const endDate = new Date(createdAtEnd);
74
194
  if (!isNaN(endDate.getTime())) {
75
195
  endDate.setHours(23, 59, 59, 999);
@@ -78,7 +198,6 @@ export async function GET(request: NextRequest) {
78
198
  }
79
199
  }
80
200
 
81
- // Filtres de date pour updatedAt
82
201
  if (updatedAtStart || updatedAtEnd) {
83
202
  where.updatedAt = {};
84
203
  if (updatedAtStart) {
@@ -88,7 +207,6 @@ export async function GET(request: NextRequest) {
88
207
  }
89
208
  }
90
209
  if (updatedAtEnd) {
91
- // Ajouter 23h59m59s pour inclure toute la journée
92
210
  const endDate = new Date(updatedAtEnd);
93
211
  if (!isNaN(endDate.getTime())) {
94
212
  endDate.setHours(23, 59, 59, 999);
@@ -97,21 +215,20 @@ export async function GET(request: NextRequest) {
97
215
  }
98
216
  }
99
217
 
100
- // if (isCompany === 'true') {
101
- // where = { ...where, isCompany: true };
102
- // }
218
+ // Determine sort order: explicit param > view config > default
219
+ let orderBy: any = { createdAt: 'desc' as const };
220
+ if (sortFieldParam && sortDirParam) {
221
+ orderBy = { [sortFieldParam]: sortDirParam };
222
+ } else if (viewSortConfig) {
223
+ orderBy = { [viewSortConfig.field]: viewSortConfig.direction };
224
+ }
103
225
 
104
226
  const [contacts, total] = await Promise.all([
105
227
  prisma.contact.findMany({
106
- where: {
107
- ...where,
108
- isCompany: false,
109
- },
228
+ where,
110
229
  include: {
111
230
  status: true,
112
- companyRelation: {
113
- select: { id: true, firstName: true, lastName: true, isCompany: true },
114
- },
231
+ company: { select: { id: true, name: true } },
115
232
  assignedCommercial: {
116
233
  select: { id: true, name: true, email: true },
117
234
  },
@@ -121,12 +238,34 @@ export async function GET(request: NextRequest) {
121
238
  createdBy: {
122
239
  select: { id: true, name: true, email: true },
123
240
  },
241
+ tourLinks: {
242
+ include: {
243
+ tour: {
244
+ select: {
245
+ id: true,
246
+ number: true,
247
+ },
248
+ },
249
+ },
250
+ },
251
+ transactions: {
252
+ select: {
253
+ id: true,
254
+ status: true,
255
+ totalAmountCents: true,
256
+ createdAt: true,
257
+ },
258
+ orderBy: {
259
+ createdAt: 'desc',
260
+ },
261
+ take: 1,
262
+ },
124
263
  },
125
- orderBy: { createdAt: 'desc' },
264
+ orderBy,
126
265
  skip,
127
266
  take: limit,
128
267
  }),
129
- prisma.contact.count({ where: { ...where, isCompany: false } }),
268
+ prisma.contact.count({ where }),
130
269
  ]);
131
270
 
132
271
  return NextResponse.json({
@@ -155,7 +294,24 @@ export async function POST(request: NextRequest) {
155
294
  return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
156
295
  }
157
296
 
158
- const body = await request.json();
297
+ const canCreate = await checkPermission('contacts.create');
298
+ if (!canCreate) {
299
+ return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
300
+ }
301
+
302
+ const json = await request.json();
303
+ const parseResult = createContactSchema.safeParse(json);
304
+
305
+ if (!parseResult.success) {
306
+ return NextResponse.json(
307
+ {
308
+ error: 'Données invalides',
309
+ details: parseResult.error.flatten(),
310
+ },
311
+ { status: 400 },
312
+ );
313
+ }
314
+
159
315
  const {
160
316
  civility,
161
317
  firstName,
@@ -167,19 +323,13 @@ export async function POST(request: NextRequest) {
167
323
  city,
168
324
  postalCode,
169
325
  origin,
170
- companyName,
171
- isCompany,
172
326
  companyId,
327
+ jobTitle,
173
328
  statusId,
174
329
  closingReason,
175
330
  assignedCommercialId,
176
331
  assignedTeleproId,
177
- } = body;
178
-
179
- // Validation
180
- if (!phone) {
181
- return NextResponse.json({ error: 'Le téléphone est obligatoire' }, { status: 400 });
182
- }
332
+ } = parseResult.data;
183
333
 
184
334
  // Vérifier si c'est un doublon (nom, prénom ET email)
185
335
  const duplicateContactId = await handleContactDuplicate(
@@ -196,9 +346,7 @@ export async function POST(request: NextRequest) {
196
346
  where: { id: duplicateContactId },
197
347
  include: {
198
348
  status: true,
199
- companyRelation: {
200
- select: { id: true, firstName: true, lastName: true, isCompany: true },
201
- },
349
+ company: { select: { id: true, name: true } },
202
350
  assignedCommercial: {
203
351
  select: { id: true, name: true, email: true },
204
352
  },
@@ -216,7 +364,7 @@ export async function POST(request: NextRequest) {
216
364
  // Sinon, créer un nouveau contact
217
365
  const contact = await prisma.contact.create({
218
366
  data: {
219
- civility: civility || null,
367
+ civility: civility ?? null,
220
368
  firstName: firstName || null,
221
369
  lastName: lastName || null,
222
370
  phone: normalizePhoneNumber(phone),
@@ -226,9 +374,8 @@ export async function POST(request: NextRequest) {
226
374
  city: city || null,
227
375
  postalCode: postalCode || null,
228
376
  origin: origin || null,
229
- companyName: companyName || null,
230
- isCompany: isCompany === true,
231
377
  companyId: companyId || null,
378
+ jobTitle: jobTitle || null,
232
379
  statusId: statusId || null,
233
380
  closingReason: closingReason || null,
234
381
  assignedCommercialId: assignedCommercialId || null,
@@ -252,9 +399,7 @@ export async function POST(request: NextRequest) {
252
399
  },
253
400
  include: {
254
401
  status: true,
255
- companyRelation: {
256
- select: { id: true, firstName: true, lastName: true, isCompany: true },
257
- },
402
+ company: { select: { id: true, name: true } },
258
403
  assignedCommercial: {
259
404
  select: { id: true, name: true, email: true },
260
405
  },
@@ -278,6 +423,14 @@ export async function POST(request: NextRequest) {
278
423
  return NextResponse.json(contact, { status: 201 });
279
424
  } catch (error: any) {
280
425
  console.error('Erreur lors de la création du contact:', error);
281
- return NextResponse.json({ error: error.message || 'Erreur serveur' }, { status: 500 });
426
+ return NextResponse.json(
427
+ {
428
+ error:
429
+ process.env.NODE_ENV === 'development'
430
+ ? error.message || 'Erreur serveur'
431
+ : 'Erreur serveur',
432
+ },
433
+ { status: 500 },
434
+ );
282
435
  }
283
436
  }
@@ -1,304 +1,21 @@
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 { checkPermission } from '@/lib/check-permission';
1
+ import { NextResponse } from 'next/server';
2
+ import { getAuthUser } from '@/lib/get-auth-user';
3
+ import { getDashboardStats } from '@/lib/dashboard-stats';
6
4
 
7
- export async function GET(req: NextRequest) {
5
+ export async function GET() {
8
6
  try {
9
- const session = await auth.api.getSession({
10
- headers: await headers(),
11
- });
7
+ const authUser = await getAuthUser();
12
8
 
13
- if (!session) {
9
+ if (!authUser) {
14
10
  return NextResponse.json({ error: 'Non autorisé' }, { status: 401 });
15
11
  }
16
12
 
17
- // Vérifier la permission d'accès au tableau de bord
18
- const canView = await checkPermission('dashboard.view');
19
- if (!canView) {
13
+ if (!authUser.permissions.includes('dashboard.view')) {
20
14
  return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
21
15
  }
22
16
 
23
- const userId = session.user.id;
24
- const userRole = session.user.role;
25
-
26
- // Déterminer les contacts visibles selon le rôle
27
- const contactFilter =
28
- userRole === 'ADMIN'
29
- ? {}
30
- : userRole === 'MANAGER'
31
- ? {}
32
- : userRole === 'COMMERCIAL'
33
- ? { assignedCommercialId: userId }
34
- : userRole === 'TELEPRO'
35
- ? { assignedTeleproId: userId }
36
- : { createdById: userId };
37
-
38
- // 1. Total des contacts
39
- const totalContacts = await prisma.contact.count({
40
- where: contactFilter,
41
- });
42
-
43
- // 2. Contacts créés ce mois
44
- const startOfMonth = new Date();
45
- startOfMonth.setDate(1);
46
- startOfMonth.setHours(0, 0, 0, 0);
47
-
48
- const contactsThisMonth = await prisma.contact.count({
49
- where: {
50
- ...contactFilter,
51
- createdAt: { gte: startOfMonth },
52
- },
53
- });
54
-
55
- const lastMonthStart = new Date(startOfMonth);
56
- lastMonthStart.setMonth(lastMonthStart.getMonth() - 1);
57
-
58
- const contactsLastMonth = await prisma.contact.count({
59
- where: {
60
- ...contactFilter,
61
- createdAt: {
62
- gte: lastMonthStart,
63
- lt: startOfMonth,
64
- },
65
- },
66
- });
67
-
68
- const contactsGrowth =
69
- contactsLastMonth > 0
70
- ? ((contactsThisMonth - contactsLastMonth) / contactsLastMonth) * 100
71
- : 0;
72
-
73
- // 3. Contacts par mois (12 derniers mois)
74
- const monthsData = [];
75
- for (let i = 11; i >= 0; i--) {
76
- const monthStart = new Date();
77
- monthStart.setMonth(monthStart.getMonth() - i);
78
- monthStart.setDate(1);
79
- monthStart.setHours(0, 0, 0, 0);
80
-
81
- const monthEnd = new Date(monthStart);
82
- monthEnd.setMonth(monthEnd.getMonth() + 1);
83
-
84
- const count = await prisma.contact.count({
85
- where: {
86
- ...contactFilter,
87
- createdAt: {
88
- gte: monthStart,
89
- lt: monthEnd,
90
- },
91
- },
92
- });
93
-
94
- monthsData.push({
95
- month: monthStart.toLocaleString('fr-FR', { month: 'short' }),
96
- count,
97
- });
98
- }
99
-
100
- // 4. Répartition par statut (pour le radar chart)
101
- const statusDistribution = await prisma.contact.groupBy({
102
- by: ['statusId'],
103
- where: contactFilter,
104
- _count: true,
105
- });
106
-
107
- const statuses = await prisma.status.findMany();
108
- const statusData = statuses.map((status) => ({
109
- name: status.name,
110
- value: statusDistribution.find((s) => s.statusId === status.id)?._count || 0,
111
- }));
112
-
113
- // 5. Tâches à venir (Top tasks)
114
- const upcomingTasks = await prisma.task.findMany({
115
- where: {
116
- assignedUserId: userId,
117
- completed: false,
118
- scheduledAt: { gte: new Date() },
119
- },
120
- include: {
121
- contact: true,
122
- assignedUser: true,
123
- },
124
- orderBy: { scheduledAt: 'asc' },
125
- take: 6,
126
- });
127
-
128
- // 6. Interactions récentes
129
- const recentInteractions = await prisma.interaction.findMany({
130
- where: {
131
- userId,
132
- },
133
- include: {
134
- contact: true,
135
- user: true,
136
- },
137
- orderBy: { createdAt: 'desc' },
138
- take: 5,
139
- });
140
-
141
- // 7. Statistiques des tâches
142
- const totalTasks = await prisma.task.count({
143
- where: { assignedUserId: userId },
144
- });
145
-
146
- const completedTasks = await prisma.task.count({
147
- where: {
148
- assignedUserId: userId,
149
- completed: true,
150
- },
151
- });
152
-
153
- const pendingTasks = totalTasks - completedTasks;
154
-
155
- // 8. Tâches par type ce mois
156
- const tasksThisMonthByType = await prisma.task.groupBy({
157
- by: ['type'],
158
- where: {
159
- assignedUserId: userId,
160
- createdAt: { gte: startOfMonth },
161
- },
162
- _count: true,
163
- });
164
-
165
- const tasksByType = tasksThisMonthByType.map((t) => ({
166
- type: t.type,
167
- count: t._count,
168
- }));
169
-
170
- // 9. Interactions par type ce mois
171
- const interactionsThisMonth = await prisma.interaction.groupBy({
172
- by: ['type'],
173
- where: {
174
- userId,
175
- createdAt: { gte: startOfMonth },
176
- },
177
- _count: true,
178
- });
179
-
180
- const interactionsByType = interactionsThisMonth.map((i) => ({
181
- type: i.type,
182
- count: i._count,
183
- }));
184
-
185
- // 10. Activité des 7 derniers jours
186
- const last7Days = [];
187
- for (let i = 6; i >= 0; i--) {
188
- const dayStart = new Date();
189
- dayStart.setDate(dayStart.getDate() - i);
190
- dayStart.setHours(0, 0, 0, 0);
191
-
192
- const dayEnd = new Date(dayStart);
193
- dayEnd.setDate(dayEnd.getDate() + 1);
194
-
195
- const interactionsCount = await prisma.interaction.count({
196
- where: {
197
- userId,
198
- createdAt: {
199
- gte: dayStart,
200
- lt: dayEnd,
201
- },
202
- },
203
- });
204
-
205
- const tasksCount = await prisma.task.count({
206
- where: {
207
- assignedUserId: userId,
208
- createdAt: {
209
- gte: dayStart,
210
- lt: dayEnd,
211
- },
212
- },
213
- });
214
-
215
- last7Days.push({
216
- date: dayStart.toLocaleDateString('fr-FR', {
217
- day: 'numeric',
218
- month: 'short',
219
- }),
220
- interactions: interactionsCount,
221
- tasks: tasksCount,
222
- });
223
- }
224
-
225
- // 11. Top contacts (ceux avec le plus d'interactions)
226
- const topContacts = await prisma.contact.findMany({
227
- where: contactFilter,
228
- include: {
229
- _count: {
230
- select: { interactions: true },
231
- },
232
- status: true,
233
- assignedCommercial: true,
234
- assignedTelepro: true,
235
- },
236
- orderBy: {
237
- createdAt: 'desc',
238
- },
239
- take: 4,
240
- });
241
-
242
- return NextResponse.json({
243
- overview: {
244
- totalContacts,
245
- contactsThisMonth,
246
- contactsGrowth: Math.round(contactsGrowth * 10) / 10,
247
- monthsData,
248
- },
249
- statusDistribution: statusData,
250
- tasks: {
251
- total: totalTasks,
252
- completed: completedTasks,
253
- pending: pendingTasks,
254
- upcoming: upcomingTasks.map((task) => ({
255
- id: task.id,
256
- title: task.title || 'Sans titre',
257
- description: task.description,
258
- type: task.type,
259
- scheduledAt: task.scheduledAt,
260
- contact: task.contact
261
- ? {
262
- id: task.contact.id,
263
- name:
264
- `${task.contact.firstName || ''} ${task.contact.lastName || ''}`.trim() ||
265
- task.contact.phone,
266
- }
267
- : null,
268
- priority: task.priority,
269
- })),
270
- byType: tasksByType,
271
- },
272
- interactions: {
273
- recent: recentInteractions.map((interaction) => ({
274
- id: interaction.id,
275
- type: interaction.type,
276
- title: interaction.title,
277
- content: interaction.content,
278
- date: interaction.date || interaction.createdAt,
279
- contact: {
280
- id: interaction.contact.id,
281
- name:
282
- `${interaction.contact.firstName || ''} ${interaction.contact.lastName || ''}`.trim() ||
283
- interaction.contact.phone,
284
- },
285
- })),
286
- byType: interactionsByType,
287
- },
288
- activity: {
289
- last7Days,
290
- },
291
- topContacts: topContacts.map((contact) => ({
292
- id: contact.id,
293
- name: `${contact.firstName || ''} ${contact.lastName || ''}`.trim() || contact.phone,
294
- phone: contact.phone,
295
- email: contact.email,
296
- status: contact.status?.name || 'Non défini',
297
- interactionsCount: contact._count.interactions,
298
- assignedCommercial: contact.assignedCommercial?.name,
299
- assignedTelepro: contact.assignedTelepro?.name,
300
- })),
301
- });
17
+ const stats = await getDashboardStats(authUser.session.user.id, authUser.permissions);
18
+ return NextResponse.json(stats);
302
19
  } catch (error) {
303
20
  console.error('Erreur lors de la récupération des statistiques:', error);
304
21
  return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 });