create-crm-tmp 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (190) hide show
  1. package/bin/create-crm-tmp.js +56 -35
  2. package/package.json +1 -1
  3. package/template/README.md +230 -115
  4. package/template/eslint.config.mjs +13 -0
  5. package/template/next.config.ts +14 -0
  6. package/template/package.json +15 -2
  7. package/template/prisma/migrations/20260318095700_init_db/migration.sql +978 -0
  8. package/template/prisma/migrations/migration_lock.toml +3 -0
  9. package/template/prisma/schema.prisma +132 -637
  10. package/template/src/app/(auth)/invite/[token]/page.tsx +10 -8
  11. package/template/src/app/(auth)/layout.tsx +1 -1
  12. package/template/src/app/(auth)/reset-password/complete/page.tsx +11 -8
  13. package/template/src/app/(auth)/reset-password/page.tsx +4 -4
  14. package/template/src/app/(auth)/reset-password/verify/page.tsx +4 -4
  15. package/template/src/app/(auth)/signin/page.tsx +14 -6
  16. package/template/src/app/(dashboard)/agenda/page.tsx +2243 -988
  17. package/template/src/app/(dashboard)/automatisation/_components/workflow-editor.tsx +18 -104
  18. package/template/src/app/(dashboard)/automatisation/page.tsx +10 -26
  19. package/template/src/app/(dashboard)/closing/page.tsx +78 -62
  20. package/template/src/app/(dashboard)/contacts/[id]/page.tsx +2082 -1080
  21. package/template/src/app/(dashboard)/contacts/companies/[id]/page.tsx +46 -47
  22. package/template/src/app/(dashboard)/contacts/page.tsx +1062 -780
  23. package/template/src/app/(dashboard)/dashboard/page.tsx +533 -37
  24. package/template/src/app/(dashboard)/dev/page.tsx +1291 -0
  25. package/template/src/app/(dashboard)/layout.tsx +6 -2
  26. package/template/src/app/(dashboard)/settings/page.tsx +797 -2582
  27. package/template/src/app/(dashboard)/templates/page.tsx +55 -54
  28. package/template/src/app/(dashboard)/users/list/page.tsx +51 -48
  29. package/template/src/app/(dashboard)/users/page.tsx +1 -1
  30. package/template/src/app/(dashboard)/users/permissions/page.tsx +2 -2
  31. package/template/src/app/(dashboard)/users/roles/page.tsx +7 -5
  32. package/template/src/app/api/agenda/google-events/route.ts +92 -0
  33. package/template/src/app/api/auth/check-active/route.ts +3 -2
  34. package/template/src/app/api/auth/google/route.ts +2 -1
  35. package/template/src/app/api/auth/google/status/route.ts +7 -31
  36. package/template/src/app/api/companies/[id]/activities/route.ts +1 -3
  37. package/template/src/app/api/companies/[id]/route.ts +1 -2
  38. package/template/src/app/api/companies/route.ts +42 -12
  39. package/template/src/app/api/contacts/[id]/files/[fileId]/preview/route.ts +9 -31
  40. package/template/src/app/api/contacts/[id]/files/[fileId]/route.ts +14 -32
  41. package/template/src/app/api/contacts/[id]/files/route.ts +112 -212
  42. package/template/src/app/api/contacts/[id]/interactions/[interactionId]/route.ts +27 -1
  43. package/template/src/app/api/contacts/[id]/interactions/route.ts +16 -16
  44. package/template/src/app/api/contacts/[id]/kyc/route.ts +21 -11
  45. package/template/src/app/api/contacts/[id]/meet/route.ts +19 -2
  46. package/template/src/app/api/contacts/[id]/route.ts +106 -34
  47. package/template/src/app/api/contacts/[id]/send-email/route.ts +27 -11
  48. package/template/src/app/api/contacts/[id]/workflows/run/route.ts +6 -0
  49. package/template/src/app/api/contacts/export/route.ts +9 -13
  50. package/template/src/app/api/contacts/import/route.ts +55 -25
  51. package/template/src/app/api/contacts/import-preview/route.ts +1 -1
  52. package/template/src/app/api/contacts/origins/route.ts +63 -0
  53. package/template/src/app/api/contacts/route.ts +153 -41
  54. package/template/src/app/api/cron/cleanup-editor-images/route.ts +166 -0
  55. package/template/src/app/api/dashboard/widgets/[id]/route.ts +44 -0
  56. package/template/src/app/api/dashboard/widgets/route.ts +181 -0
  57. package/template/src/app/api/dev/reminders/test/route.ts +114 -0
  58. package/template/src/app/api/editor/upload-image/route.ts +61 -0
  59. package/template/src/app/api/integrations/google-sheet/jobs/[jobId]/route.ts +47 -0
  60. package/template/src/app/api/integrations/google-sheet/jobs/usage/route.ts +50 -0
  61. package/template/src/app/api/integrations/google-sheet/sync/route.ts +24 -556
  62. package/template/src/app/api/jobs/google-sheet/process/route.ts +84 -0
  63. package/template/src/app/api/jobs/google-sheet/schedule/route.ts +50 -0
  64. package/template/src/app/api/reminders/clear/route.ts +120 -0
  65. package/template/src/app/api/reminders/clear/undo/route.ts +112 -0
  66. package/template/src/app/api/reminders/route.ts +164 -39
  67. package/template/src/app/api/reminders/state/route.ts +164 -0
  68. package/template/src/app/api/reset-password/request/route.ts +1 -1
  69. package/template/src/app/api/reset-password/verify/route.ts +1 -1
  70. package/template/src/app/api/send/route.ts +16 -4
  71. package/template/src/app/api/settings/google-ads/route.ts +14 -0
  72. package/template/src/app/api/settings/google-calendar/calendars/route.ts +97 -0
  73. package/template/src/app/api/settings/google-calendar/route.ts +124 -0
  74. package/template/src/app/api/settings/google-sheet/[id]/route.ts +28 -0
  75. package/template/src/app/api/settings/google-sheet/auto-map/route.ts +37 -4
  76. package/template/src/app/api/settings/google-sheet/preview/route.ts +9 -3
  77. package/template/src/app/api/settings/google-sheet/route.ts +14 -0
  78. package/template/src/app/api/settings/integrations/logs/route.ts +93 -0
  79. package/template/src/app/api/settings/integrations/notifications/route.ts +67 -0
  80. package/template/src/app/api/settings/meta-leads/[id]/route.ts +0 -1
  81. package/template/src/app/api/settings/meta-leads/route.ts +14 -2
  82. package/template/src/app/api/settings/smtp/route.ts +53 -6
  83. package/template/src/app/api/tasks/[id]/attendees/route.ts +24 -8
  84. package/template/src/app/api/tasks/[id]/route.ts +234 -58
  85. package/template/src/app/api/tasks/meet/route.ts +27 -19
  86. package/template/src/app/api/tasks/route.ts +62 -17
  87. package/template/src/app/api/users/[id]/route.ts +20 -14
  88. package/template/src/app/api/users/list/route.ts +57 -19
  89. package/template/src/app/api/webhooks/google-ads/route.ts +34 -14
  90. package/template/src/app/api/webhooks/meta-leads/route.ts +32 -12
  91. package/template/src/app/api/workflows/[id]/route.ts +0 -4
  92. package/template/src/app/api/workflows/process/route.ts +22 -51
  93. package/template/src/app/api/workflows/route.ts +0 -4
  94. package/template/src/app/globals.css +342 -4
  95. package/template/src/app/layout.tsx +11 -3
  96. package/template/src/app/page.tsx +1 -1
  97. package/template/src/components/address-autocomplete.tsx +7 -6
  98. package/template/src/components/config-error-alert.tsx +46 -0
  99. package/template/src/components/contacts/filter-bar.tsx +12 -3
  100. package/template/src/components/contacts/filter-builder.tsx +28 -43
  101. package/template/src/components/contacts/save-view-dialog.tsx +1 -1
  102. package/template/src/components/contacts/views-tab-bar.tsx +15 -6
  103. package/template/src/components/dashboard/activity-chart.tsx +41 -28
  104. package/template/src/components/dashboard/add-widget-dialog.tsx +157 -0
  105. package/template/src/components/dashboard/color-picker.tsx +64 -0
  106. package/template/src/components/dashboard/contacts-chart.tsx +69 -0
  107. package/template/src/components/dashboard/interactions-by-type-chart.tsx +121 -0
  108. package/template/src/components/dashboard/recent-activity.tsx +154 -0
  109. package/template/src/components/dashboard/stat-card.tsx +40 -40
  110. package/template/src/components/dashboard/status-distribution-chart.tsx +81 -0
  111. package/template/src/components/dashboard/tasks-pie-chart.tsx +37 -34
  112. package/template/src/components/dashboard/top-contacts-list.tsx +113 -0
  113. package/template/src/components/dashboard/upcoming-tasks-list.tsx +72 -81
  114. package/template/src/components/dashboard/widget-wrapper.tsx +36 -0
  115. package/template/src/components/date-picker.tsx +9 -6
  116. package/template/src/components/editor/upload-editor-image.ts +42 -0
  117. package/template/src/components/editor.tsx +161 -22
  118. package/template/src/components/email-template.tsx +2 -2
  119. package/template/src/components/global-search.tsx +30 -28
  120. package/template/src/components/header.tsx +178 -80
  121. package/template/src/components/inactive-account-guard.tsx +58 -0
  122. package/template/src/components/integration-notifications-listener.tsx +12 -0
  123. package/template/src/components/invitation-email-template.tsx +2 -2
  124. package/template/src/components/meet-cancellation-email-template.tsx +3 -3
  125. package/template/src/components/meet-confirmation-email-template.tsx +3 -3
  126. package/template/src/components/meet-update-email-template.tsx +3 -3
  127. package/template/src/components/page-header.tsx +5 -5
  128. package/template/src/components/protected-page.tsx +1 -1
  129. package/template/src/components/reset-password-email-template.tsx +2 -2
  130. package/template/src/components/settings/integrations/GoogleAdsIntegration.tsx +428 -0
  131. package/template/src/components/settings/integrations/GoogleSheetConfigMonitoringModal.tsx +680 -0
  132. package/template/src/components/settings/integrations/GoogleSheetIntegration.tsx +809 -0
  133. package/template/src/components/settings/integrations/ImportResultDialog.tsx +124 -0
  134. package/template/src/components/settings/integrations/IntegrationLogPanel.tsx +57 -0
  135. package/template/src/components/settings/integrations/IntegrationLogsTable.tsx +186 -0
  136. package/template/src/components/settings/integrations/MetaLeadIntegration.tsx +451 -0
  137. package/template/src/components/sidebar.tsx +45 -26
  138. package/template/src/components/skeleton.tsx +40 -43
  139. package/template/src/components/ui/accordion.tsx +2 -2
  140. package/template/src/components/ui/alert-dialog.tsx +1 -1
  141. package/template/src/components/ui/button.tsx +20 -9
  142. package/template/src/components/ui/components.tsx +1 -1
  143. package/template/src/components/ui/date-picker.tsx +422 -0
  144. package/template/src/components/ui/datetime-picker.tsx +338 -0
  145. package/template/src/components/ui/status-select.tsx +271 -0
  146. package/template/src/components/ui/tooltip.tsx +37 -0
  147. package/template/src/components/view-as-modal.tsx +13 -7
  148. package/template/src/contexts/app-toast-context.tsx +245 -57
  149. package/template/src/contexts/dashboard-theme-context.tsx +53 -0
  150. package/template/src/contexts/sidebar-context.tsx +22 -17
  151. package/template/src/contexts/task-reminder-context.tsx +134 -160
  152. package/template/src/contexts/view-as-context.tsx +33 -6
  153. package/template/src/hooks/use-focus-trap.ts +2 -2
  154. package/template/src/hooks/useIntegrationNotifications.ts +49 -0
  155. package/template/src/lib/auth.ts +8 -1
  156. package/template/src/lib/config-links.ts +14 -0
  157. package/template/src/lib/contact-duplicate.ts +79 -61
  158. package/template/src/lib/contact-interactions.ts +21 -21
  159. package/template/src/lib/contact-view-filters.ts +24 -64
  160. package/template/src/lib/contacts-list-url.ts +190 -0
  161. package/template/src/lib/dashboard-stats.ts +65 -7
  162. package/template/src/lib/dashboard-themes.ts +135 -0
  163. package/template/src/lib/date-utils.ts +127 -0
  164. package/template/src/lib/default-widgets.ts +12 -0
  165. package/template/src/lib/editor-html-image-dimensions.ts +172 -0
  166. package/template/src/lib/editor-image-limits.ts +19 -0
  167. package/template/src/lib/email-html-sanitize.ts +19 -0
  168. package/template/src/lib/encryption.ts +9 -6
  169. package/template/src/lib/fr-geography.ts +192 -0
  170. package/template/src/lib/google-calendar-agenda.ts +201 -0
  171. package/template/src/lib/google-calendar.ts +255 -5
  172. package/template/src/lib/google-sheet-sync-jobs.ts +96 -0
  173. package/template/src/lib/google-sheet-sync-runner.ts +514 -0
  174. package/template/src/lib/integration-import-log.ts +21 -0
  175. package/template/src/lib/permissions.ts +40 -10
  176. package/template/src/lib/prisma.ts +4 -1
  177. package/template/src/lib/qstash.ts +65 -0
  178. package/template/src/lib/reminder-state-server.ts +80 -0
  179. package/template/src/lib/reminder-state.ts +29 -0
  180. package/template/src/lib/supabase-storage.ts +113 -0
  181. package/template/src/lib/template-variables.ts +164 -23
  182. package/template/src/lib/utils.ts +45 -0
  183. package/template/src/lib/widget-registry.ts +173 -0
  184. package/template/src/lib/workflow-executor.ts +16 -70
  185. package/template/src/proxy.ts +1 -0
  186. package/template/vercel.json +3 -10
  187. package/template/skills-lock.json +0 -25
  188. package/template/src/components/dashboard/dashboard-content.tsx +0 -79
  189. package/template/src/lib/google-drive.ts +0 -1101
  190. package/template/src/types/yousign.ts +0 -52
@@ -6,6 +6,7 @@ import {
6
6
  assignContactImmediate,
7
7
  addNoteImmediate,
8
8
  notifyUserImmediate,
9
+ getContactVariables,
9
10
  } from '@/lib/workflow-executor';
10
11
  import { decrypt } from '@/lib/encryption';
11
12
  import nodemailer from 'nodemailer';
@@ -18,21 +19,22 @@ import { replaceTemplateVariables } from '@/lib/template-variables';
18
19
  */
19
20
  export async function GET(request: NextRequest) {
20
21
  try {
22
+ const isDevelopment = process.env.NODE_ENV === 'development';
21
23
  const cronSecret = process.env.CRON_SECRET;
24
+ const authHeader = request.headers.get('authorization');
22
25
 
23
- if (cronSecret) {
24
- const authHeader = request.headers.get('authorization');
25
- const expectedAuth = `Bearer ${cronSecret}`;
26
+ if (!cronSecret && !isDevelopment) {
27
+ return NextResponse.json(
28
+ { error: 'Unauthorized - CRON_SECRET manquant en production' },
29
+ { status: 401 },
30
+ );
31
+ }
26
32
 
27
- if (authHeader !== expectedAuth) {
28
- const isDevelopment = process.env.NODE_ENV === 'development';
29
- if (!isDevelopment) {
30
- return NextResponse.json(
31
- { error: 'Unauthorized - Secret manquant ou invalide' },
32
- { status: 401 },
33
- );
34
- }
35
- }
33
+ if (cronSecret && authHeader !== `Bearer ${cronSecret}` && !isDevelopment) {
34
+ return NextResponse.json(
35
+ { error: 'Unauthorized - Secret manquant ou invalide' },
36
+ { status: 401 },
37
+ );
36
38
  }
37
39
 
38
40
  const now = new Date();
@@ -72,7 +74,9 @@ async function processScheduledActions(now: Date) {
72
74
  contact: {
73
75
  include: {
74
76
  status: true,
75
- company: { select: { name: true } },
77
+ company: { select: { name: true, address: true, city: true, postalCode: true } },
78
+ assignedCommercial: { select: { name: true } },
79
+ assignedTelepro: { select: { name: true } },
76
80
  },
77
81
  },
78
82
  workflow: {
@@ -139,18 +143,7 @@ async function processScheduledActions(now: Date) {
139
143
  break;
140
144
 
141
145
  case 'ADD_NOTE': {
142
- const variables = {
143
- firstName: contact.firstName || '',
144
- lastName: contact.lastName || '',
145
- civility: contact.civility || '',
146
- email: contact.email || '',
147
- phone: contact.phone || '',
148
- secondaryPhone: contact.secondaryPhone || '',
149
- address: contact.address || '',
150
- city: contact.city || '',
151
- postalCode: contact.postalCode || '',
152
- companyName: contact.company?.name || '',
153
- };
146
+ const variables = getContactVariables(contact);
154
147
  const noteContent = replaceTemplateVariables(actionData.noteContent || '', variables);
155
148
  await addNoteImmediate(
156
149
  contact.id,
@@ -352,18 +345,7 @@ async function getExecuteWorkflowActions() {
352
345
  (action.delayDays || 0) * 24 * 60 * 60 * 1000 + (action.delayHours || 0) * 60 * 60 * 1000;
353
346
  const executeAt = new Date(Date.now() + delayMs);
354
347
 
355
- const variables = {
356
- firstName: contact.firstName || '',
357
- lastName: contact.lastName || '',
358
- civility: contact.civility || '',
359
- email: contact.email || '',
360
- phone: contact.phone || '',
361
- secondaryPhone: contact.secondaryPhone || '',
362
- address: contact.address || '',
363
- city: contact.city || '',
364
- postalCode: contact.postalCode || '',
365
- companyName: contact.company?.name || '',
366
- };
348
+ const variables = getContactVariables(contact);
367
349
 
368
350
  switch (action.actionType) {
369
351
  case 'SEND_EMAIL':
@@ -527,7 +509,7 @@ async function getExecuteWorkflowActions() {
527
509
  if (action.smsMessage && contact.phone) {
528
510
  let message = action.smsMessage;
529
511
  for (const [key, value] of Object.entries(variables)) {
530
- message = message.replace(new RegExp(`{${key}}`, 'g'), value as string);
512
+ message = message.replaceAll(`{${key}}`, value as string);
531
513
  }
532
514
  if (executeAt <= new Date()) {
533
515
  await mod.sendSMSImmediate(workflow, contact, message);
@@ -571,18 +553,7 @@ async function executeScheduledEmail(
571
553
  throw new Error('Configuration SMTP non trouvée');
572
554
  }
573
555
 
574
- const variables = {
575
- firstName: contact.firstName || '',
576
- lastName: contact.lastName || '',
577
- civility: contact.civility || '',
578
- email: contact.email || '',
579
- phone: contact.phone || '',
580
- secondaryPhone: contact.secondaryPhone || '',
581
- address: contact.address || '',
582
- city: contact.city || '',
583
- postalCode: contact.postalCode || '',
584
- companyName: contact.company?.name || '',
585
- };
556
+ const variables = getContactVariables(contact);
586
557
 
587
558
  const subject = replaceTemplateVariables(actionData.templateSubject || '', variables);
588
559
  const content = replaceTemplateVariables(actionData.templateContent || '', variables);
@@ -638,7 +609,7 @@ async function executeScheduledSMS(
638
609
  };
639
610
 
640
611
  for (const [key, value] of Object.entries(variables)) {
641
- message = message.replace(new RegExp(`{${key}}`, 'g'), value);
612
+ message = message.replaceAll(`{${key}}`, value);
642
613
  }
643
614
 
644
615
  console.log(`SMS à envoyer à ${contact.phone}: ${message}`);
@@ -83,8 +83,6 @@ export async function POST(request: NextRequest) {
83
83
  triggerTimeHours,
84
84
  triggerTimeReference,
85
85
  triggerTaskType,
86
- triggerTransactionFromStatus,
87
- triggerTransactionToStatus,
88
86
  actions = [],
89
87
  } = body;
90
88
 
@@ -108,8 +106,6 @@ export async function POST(request: NextRequest) {
108
106
  triggerTimeHours: triggerTimeHours || null,
109
107
  triggerTimeReference: triggerTimeReference || null,
110
108
  triggerTaskType: triggerTaskType || null,
111
- triggerTransactionFromStatus: triggerTransactionFromStatus || null,
112
- triggerTransactionToStatus: triggerTransactionToStatus || null,
113
109
  actions: {
114
110
  create: actions.map((action: any, index: number) => ({
115
111
  actionType: action.actionType,
@@ -1,4 +1,5 @@
1
1
  @import 'tailwindcss';
2
+ @import 'react-grid-layout/css/styles.css';
2
3
 
3
4
  @custom-variant dark (&:is(.dark *));
4
5
 
@@ -48,7 +49,7 @@
48
49
  --radius-4xl: calc(var(--radius) + 16px);
49
50
  }
50
51
 
51
- h1,
52
+ /* h1,
52
53
  h2,
53
54
  h3,
54
55
  h4,
@@ -56,7 +57,7 @@ h5,
56
57
  h6 {
57
58
  font-family: var(--font-display), serif;
58
59
  letter-spacing: -0.02em;
59
- }
60
+ } */
60
61
 
61
62
  /* LexKit Editor - Clean Framework-Agnostic Styles */
62
63
 
@@ -1487,9 +1488,12 @@ table td[data-lexical-table-cell-selection] {
1487
1488
 
1488
1489
  :root {
1489
1490
  --radius: 0.625rem;
1490
- --duration-fast: 150ms;
1491
- --duration-normal: 250ms;
1491
+ --duration-fast: 120ms;
1492
+ --duration-normal: 200ms;
1493
+ --duration-slow: 320ms;
1492
1494
  --ease-standard: cubic-bezier(0.4, 0, 0.2, 1);
1495
+ --ease-emphasized: cubic-bezier(0.2, 0, 0, 1);
1496
+ --ease-decelerate: cubic-bezier(0, 0, 0.2, 1);
1493
1497
  --shadow-card: 0 10px 30px -18px rgb(15 23 42 / 0.22);
1494
1498
  --shadow-dropdown: 0 18px 45px -24px rgb(15 23 42 / 0.3);
1495
1499
  --background: oklch(1 0 0);
@@ -1574,6 +1578,15 @@ table td[data-lexical-table-cell-selection] {
1574
1578
  body {
1575
1579
  @apply bg-background text-foreground;
1576
1580
  }
1581
+ /* Eliminate 300ms tap-delay on all interactive elements */
1582
+ button,
1583
+ a,
1584
+ [role='button'],
1585
+ input,
1586
+ select,
1587
+ textarea {
1588
+ touch-action: manipulation;
1589
+ }
1577
1590
  }
1578
1591
 
1579
1592
  @layer utilities {
@@ -1619,3 +1632,328 @@ table td[data-lexical-table-cell-selection] {
1619
1632
  scroll-margin-top: 6rem;
1620
1633
  }
1621
1634
  }
1635
+
1636
+ /* ===== DASHBOARD THEME UTILITIES ===== */
1637
+
1638
+ .dash-link {
1639
+ color: var(--dash-600);
1640
+ transition: color 150ms;
1641
+ }
1642
+ .dash-link:hover {
1643
+ color: var(--dash-700);
1644
+ }
1645
+
1646
+ .dash-btn {
1647
+ background-color: var(--dash-500);
1648
+ color: white;
1649
+ transition: background-color 150ms;
1650
+ }
1651
+ .dash-btn:hover {
1652
+ background-color: var(--dash-600);
1653
+ }
1654
+
1655
+ .dash-accent-bar {
1656
+ background-color: var(--dash-500);
1657
+ }
1658
+
1659
+ .dash-icon-box {
1660
+ background-color: var(--dash-100);
1661
+ transition: background-color 150ms;
1662
+ }
1663
+ .group:hover .dash-icon-box-gh {
1664
+ background-color: var(--dash-200);
1665
+ }
1666
+
1667
+ .dash-icon-color {
1668
+ color: var(--dash-600);
1669
+ }
1670
+
1671
+ .dash-avatar {
1672
+ background-color: var(--dash-100);
1673
+ color: var(--dash-700);
1674
+ }
1675
+
1676
+ .dash-pill-active {
1677
+ background-color: var(--dash-500);
1678
+ color: white;
1679
+ }
1680
+
1681
+ .dash-hover-bg:hover {
1682
+ background-color: color-mix(in srgb, var(--dash-50) 30%, transparent);
1683
+ }
1684
+ .dash-hover-border:hover {
1685
+ border-color: var(--dash-200);
1686
+ }
1687
+ .dash-hover-border-left:hover {
1688
+ border-left-color: var(--dash-400);
1689
+ background-color: color-mix(in srgb, var(--dash-50) 30%, transparent);
1690
+ }
1691
+ .dash-hover-border-light:hover {
1692
+ border-color: var(--dash-100);
1693
+ }
1694
+ .dash-hover-text:hover {
1695
+ color: var(--dash-600);
1696
+ }
1697
+ .group:hover .dash-hover-text-gh {
1698
+ color: var(--dash-500);
1699
+ }
1700
+
1701
+ .dash-legend-bg {
1702
+ background-color: color-mix(in srgb, var(--dash-50) 80%, transparent);
1703
+ }
1704
+ .dash-legend-dot {
1705
+ background-color: var(--dash-500);
1706
+ }
1707
+
1708
+ /* ===== REACT GRID LAYOUT OVERRIDES ===== */
1709
+
1710
+ .react-grid-layout {
1711
+ position: relative;
1712
+ transition: height 200ms ease;
1713
+ }
1714
+
1715
+ .react-grid-item {
1716
+ transition: all 200ms ease;
1717
+ transition-property: left, top, width, height;
1718
+ }
1719
+
1720
+ .react-grid-item.cssTransforms {
1721
+ transition-property: transform, width, height;
1722
+ }
1723
+
1724
+ .react-grid-item.resizing {
1725
+ z-index: 1;
1726
+ will-change: width, height;
1727
+ opacity: 0.9;
1728
+ }
1729
+
1730
+ .react-grid-item.react-draggable-dragging {
1731
+ transition: none;
1732
+ z-index: 3;
1733
+ will-change: transform;
1734
+ opacity: 0.9;
1735
+ box-shadow: 0 20px 40px rgba(0, 0, 0, 0.12);
1736
+ border-radius: 16px;
1737
+ }
1738
+
1739
+ .react-grid-item > .react-resizable-handle {
1740
+ position: absolute;
1741
+ width: 20px;
1742
+ height: 20px;
1743
+ bottom: 0;
1744
+ right: 0;
1745
+ cursor: se-resize;
1746
+ opacity: 0;
1747
+ transition: opacity 150ms ease;
1748
+ }
1749
+
1750
+ .react-grid-item:hover > .react-resizable-handle {
1751
+ opacity: 1;
1752
+ }
1753
+
1754
+ .react-grid-item > .react-resizable-handle::after {
1755
+ content: '';
1756
+ position: absolute;
1757
+ right: 4px;
1758
+ bottom: 4px;
1759
+ width: 8px;
1760
+ height: 8px;
1761
+ border-right: 2px solid color-mix(in srgb, var(--dash-500, #f97316) 50%, transparent);
1762
+ border-bottom: 2px solid color-mix(in srgb, var(--dash-500, #f97316) 50%, transparent);
1763
+ border-radius: 0 0 2px 0;
1764
+ }
1765
+
1766
+ .react-grid-placeholder {
1767
+ background: color-mix(in srgb, var(--dash-500, #f97316) 10%, transparent) !important;
1768
+ border: 2px dashed color-mix(in srgb, var(--dash-500, #f97316) 30%, transparent) !important;
1769
+ border-radius: 16px !important;
1770
+ opacity: 1 !important;
1771
+ }
1772
+
1773
+ @keyframes gs-field-in {
1774
+ from {
1775
+ opacity: 0;
1776
+ transform: translateY(6px) scale(0.995);
1777
+ }
1778
+ to {
1779
+ opacity: 1;
1780
+ transform: translateY(0) scale(1);
1781
+ }
1782
+ }
1783
+
1784
+ .gs-import-field {
1785
+ opacity: 0;
1786
+ animation: gs-field-in 340ms cubic-bezier(0.22, 1, 0.36, 1) forwards;
1787
+ animation-delay: 0ms;
1788
+ }
1789
+
1790
+ @media (prefers-reduced-motion: reduce) {
1791
+ .gs-import-field {
1792
+ opacity: 1;
1793
+ animation: none;
1794
+ }
1795
+ }
1796
+
1797
+ /* ===== MOTION SYSTEM — minimal pro ===== */
1798
+
1799
+ @keyframes ui-fade-in {
1800
+ from { opacity: 0; }
1801
+ to { opacity: 1; }
1802
+ }
1803
+
1804
+ @keyframes ui-slide-up-sm {
1805
+ from { opacity: 0; transform: translateY(6px); }
1806
+ to { opacity: 1; transform: translateY(0); }
1807
+ }
1808
+
1809
+ @keyframes ui-slide-down-sm {
1810
+ from { opacity: 0; transform: translateY(-6px); }
1811
+ to { opacity: 1; transform: translateY(0); }
1812
+ }
1813
+
1814
+ @keyframes ui-scale-in {
1815
+ from { opacity: 0; transform: scale(0.96); }
1816
+ to { opacity: 1; transform: scale(1); }
1817
+ }
1818
+
1819
+ @keyframes ui-slide-up-fade-out {
1820
+ from { opacity: 1; transform: translateY(0); }
1821
+ to { opacity: 0; transform: translateY(-4px); }
1822
+ }
1823
+
1824
+ @keyframes ui-count-pop {
1825
+ 0% { transform: scale(1); }
1826
+ 40% { transform: scale(1.2); }
1827
+ 100% { transform: scale(1); }
1828
+ }
1829
+
1830
+ /* Cloche notifications : léger balancement périodique pour attirer l’œil */
1831
+ @keyframes ui-bell-nudge {
1832
+ 0%,
1833
+ 12% {
1834
+ transform: rotate(0deg);
1835
+ }
1836
+ 16% {
1837
+ transform: rotate(-9deg);
1838
+ }
1839
+ 22% {
1840
+ transform: rotate(9deg);
1841
+ }
1842
+ 28% {
1843
+ transform: rotate(-5deg);
1844
+ }
1845
+ 34%,
1846
+ 100% {
1847
+ transform: rotate(0deg);
1848
+ }
1849
+ }
1850
+
1851
+ .ui-bell-notify {
1852
+ display: inline-flex;
1853
+ transform-origin: top center;
1854
+ will-change: transform;
1855
+ animation: ui-bell-nudge 4s ease-in-out infinite;
1856
+ }
1857
+
1858
+ .ui-fade-in {
1859
+ animation: ui-fade-in var(--duration-normal) var(--ease-standard) both;
1860
+ }
1861
+
1862
+ .ui-slide-up {
1863
+ animation: ui-slide-up-sm var(--duration-normal) var(--ease-emphasized) both;
1864
+ }
1865
+
1866
+ .ui-slide-down {
1867
+ animation: ui-slide-down-sm var(--duration-normal) var(--ease-emphasized) both;
1868
+ }
1869
+
1870
+ .ui-scale-in {
1871
+ animation: ui-scale-in var(--duration-normal) var(--ease-emphasized) both;
1872
+ }
1873
+
1874
+ .ui-stagger-1 { animation-delay: 30ms; }
1875
+ .ui-stagger-2 { animation-delay: 60ms; }
1876
+ .ui-stagger-3 { animation-delay: 90ms; }
1877
+ .ui-stagger-4 { animation-delay: 120ms; }
1878
+ .ui-stagger-5 { animation-delay: 150ms; }
1879
+ .ui-stagger-6 { animation-delay: 180ms; }
1880
+
1881
+ .ui-pressable {
1882
+ transition: transform var(--duration-fast) var(--ease-standard);
1883
+ }
1884
+ .ui-pressable:active {
1885
+ transform: scale(0.97);
1886
+ }
1887
+
1888
+ .ui-lift-hover {
1889
+ transition: box-shadow var(--duration-normal) var(--ease-standard),
1890
+ transform var(--duration-normal) var(--ease-standard);
1891
+ }
1892
+ .ui-lift-hover:hover {
1893
+ transform: translateY(-1px);
1894
+ box-shadow: 0 4px 16px -6px rgb(0 0 0 / 0.12);
1895
+ }
1896
+
1897
+ .ui-dropdown-enter {
1898
+ animation: ui-slide-up-sm var(--duration-fast) var(--ease-decelerate) both;
1899
+ }
1900
+ .ui-dropdown-exit {
1901
+ animation: ui-slide-up-fade-out var(--duration-fast) var(--ease-standard) both;
1902
+ }
1903
+
1904
+ .ui-count-pop {
1905
+ animation: ui-count-pop var(--duration-normal) var(--ease-emphasized);
1906
+ }
1907
+
1908
+ .ui-row-hover {
1909
+ transition: background-color var(--duration-fast) var(--ease-standard);
1910
+ }
1911
+
1912
+ .ui-tab-indicator {
1913
+ transition: color var(--duration-normal) var(--ease-standard),
1914
+ border-color var(--duration-normal) var(--ease-standard);
1915
+ }
1916
+
1917
+ @media (prefers-reduced-motion: reduce) {
1918
+ .ui-fade-in,
1919
+ .ui-slide-up,
1920
+ .ui-slide-down,
1921
+ .ui-scale-in,
1922
+ .ui-dropdown-enter,
1923
+ .ui-dropdown-exit,
1924
+ .ui-count-pop,
1925
+ .ui-bell-notify {
1926
+ animation: none !important;
1927
+ opacity: 1 !important;
1928
+ transform: none !important;
1929
+ }
1930
+
1931
+ .ui-pressable:active {
1932
+ transform: none;
1933
+ }
1934
+
1935
+ .ui-lift-hover:hover {
1936
+ transform: none;
1937
+ }
1938
+
1939
+ .ui-stagger-1,
1940
+ .ui-stagger-2,
1941
+ .ui-stagger-3,
1942
+ .ui-stagger-4,
1943
+ .ui-stagger-5,
1944
+ .ui-stagger-6 {
1945
+ animation-delay: 0ms !important;
1946
+ }
1947
+ }
1948
+
1949
+ /* Respect user's reduced motion preferences */
1950
+ @media (prefers-reduced-motion: reduce) {
1951
+ *,
1952
+ *::before,
1953
+ *::after {
1954
+ animation-duration: 0.01ms !important;
1955
+ animation-iteration-count: 1 !important;
1956
+ transition-duration: 0.01ms !important;
1957
+ scroll-behavior: auto !important;
1958
+ }
1959
+ }
@@ -1,4 +1,4 @@
1
- import type { Metadata } from 'next';
1
+ import type { Metadata, Viewport } from 'next';
2
2
  import { DM_Sans, Playfair_Display, JetBrains_Mono } from 'next/font/google';
3
3
  import './globals.css';
4
4
  import { cn } from '@/lib/utils';
@@ -20,9 +20,17 @@ const monoFont = JetBrains_Mono({
20
20
  });
21
21
 
22
22
  export const metadata: Metadata = {
23
- title: 'Gold Blessing',
23
+ title: 'CRM Template',
24
24
  description:
25
- 'Le CRM Gold Blessing : gérez vos campagnes, paiements et relations clients de manière efficace et intuitive.',
25
+ 'Un CRM moderne pour gérer vos contacts et relations clients.',
26
+ };
27
+
28
+ export const viewport: Viewport = {
29
+ colorScheme: 'light',
30
+ themeColor: [
31
+ { media: '(prefers-color-scheme: light)', color: '#ffffff' },
32
+ { media: '(prefers-color-scheme: dark)', color: '#0a0a0a' },
33
+ ],
26
34
  };
27
35
 
28
36
  export default function RootLayout({
@@ -47,7 +47,7 @@ export default function HomePage() {
47
47
  } else if (has('tasks.view_all') || has('tasks.view_own')) {
48
48
  router.push('/agenda');
49
49
  } else {
50
- router.push('/contacts');
50
+ router.push('/dashboard');
51
51
  }
52
52
  }, [permissionsLoaded, permissions, router]);
53
53
 
@@ -4,7 +4,7 @@ import { useState, useEffect, useRef } from 'react';
4
4
  import { MapPin, X } from 'lucide-react';
5
5
  import { Spinner } from '@/components/skeleton';
6
6
  import { searchAddress, type AddressFeature, extractAddressComponents } from '@/lib/address-api';
7
- import { cn } from '@/lib/utils';
7
+ import { cn, devToast } from '@/lib/utils';
8
8
  import { useAppToast } from '@/contexts/app-toast-context';
9
9
 
10
10
  interface AddressAutocompleteProps {
@@ -95,8 +95,7 @@ export default function AddressAutocomplete({
95
95
  setSuggestions(result.features);
96
96
  setShowSuggestions(true);
97
97
  } catch (error) {
98
- const message = error instanceof Error ? error.message : 'Erreur lors de la recherche.';
99
- setSearchError(message);
98
+ setSearchError(devToast('Impossible de rechercher l\'adresse. Veuillez réessayer.', error));
100
99
  setSuggestions([]);
101
100
  } finally {
102
101
  setLoading(false);
@@ -160,7 +159,7 @@ export default function AddressAutocomplete({
160
159
  return (
161
160
  <div className="relative">
162
161
  <div className="relative">
163
- <MapPin className="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-gray-400" />
162
+ <MapPin aria-hidden="true" className="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-gray-400" />
164
163
  <input
165
164
  ref={inputRef}
166
165
  data-ui-skip-token="true"
@@ -176,9 +175,10 @@ export default function AddressAutocomplete({
176
175
  onBlur={handleBlur}
177
176
  placeholder={placeholder}
178
177
  disabled={disabled}
178
+ autoComplete="street-address"
179
179
  className={cn(
180
180
  'w-full rounded-lg border border-gray-300 py-2 pr-10 pl-10 text-sm transition-colors',
181
- 'focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500',
181
+ 'focus-visible:border-blue-500 focus-visible:ring-2 focus-visible:ring-ring/50 focus-visible:outline-none',
182
182
  'disabled:cursor-not-allowed disabled:bg-gray-50 disabled:text-gray-500',
183
183
  className,
184
184
  )}
@@ -201,6 +201,7 @@ export default function AddressAutocomplete({
201
201
  <div
202
202
  ref={suggestionsRef}
203
203
  className="absolute z-50 mt-1 w-full rounded-lg border border-gray-200 bg-white shadow-lg"
204
+ aria-live="polite"
204
205
  >
205
206
  <ul className="max-h-60 overflow-y-auto py-1">
206
207
  {suggestions.map((suggestion, index) => (
@@ -214,7 +215,7 @@ export default function AddressAutocomplete({
214
215
  selectedIndex === index && 'bg-blue-50',
215
216
  )}
216
217
  >
217
- <MapPin className="mt-1 h-4 w-4 shrink-0 text-gray-400" />
218
+ <MapPin aria-hidden="true" className="mt-1 h-4 w-4 shrink-0 text-gray-400" />
218
219
  <div className="flex-1">
219
220
  <p className="text-sm font-medium text-gray-900">
220
221
  {suggestion.properties.name}
@@ -0,0 +1,46 @@
1
+ 'use client';
2
+
3
+ import Link from 'next/link';
4
+ import { AlertCircle } from 'lucide-react';
5
+
6
+ interface ConfigErrorAlertProps {
7
+ /** Message d'erreur à afficher */
8
+ message: string;
9
+ /** Lien vers la section de configuration (ex: /settings?section=system) */
10
+ configLink?: string;
11
+ /** Texte du lien de redirection */
12
+ linkLabel?: string;
13
+ /** ClassName additionnel pour le conteneur */
14
+ className?: string;
15
+ }
16
+
17
+ /**
18
+ * Affiche un message d'erreur lorsqu'une action nécessite une configuration manquante.
19
+ * Inclut un lien cliquable vers la section de configuration appropriée.
20
+ */
21
+ export function ConfigErrorAlert({
22
+ message,
23
+ configLink,
24
+ linkLabel = 'Configurer dans les paramètres',
25
+ className = '',
26
+ }: ConfigErrorAlertProps) {
27
+ return (
28
+ <div
29
+ role="alert"
30
+ className={`flex items-start gap-3 rounded-lg border border-red-200 bg-red-50 p-4 text-sm text-red-800 ${className}`}
31
+ >
32
+ <AlertCircle className="mt-0.5 h-5 w-5 shrink-0 text-red-600" />
33
+ <div className="min-w-0 flex-1">
34
+ <p className="font-medium">{message}</p>
35
+ {configLink && (
36
+ <Link
37
+ href={configLink}
38
+ className="mt-2 inline-flex items-center font-medium text-red-700 underline underline-offset-2 hover:text-red-900"
39
+ >
40
+ {linkLabel} →
41
+ </Link>
42
+ )}
43
+ </div>
44
+ </div>
45
+ );
46
+ }