create-crm-tmp 1.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 (187) hide show
  1. package/bin/create-crm-tmp.js +93 -0
  2. package/package.json +25 -0
  3. package/template/.prettierignore +33 -0
  4. package/template/.prettierrc.json +25 -0
  5. package/template/README.md +173 -0
  6. package/template/eslint.config.mjs +18 -0
  7. package/template/exemple-contacts.csv +11 -0
  8. package/template/next.config.ts +8 -0
  9. package/template/package.json +64 -0
  10. package/template/postcss.config.mjs +7 -0
  11. package/template/prisma/migrations/20251126144728_init/migration.sql +78 -0
  12. package/template/prisma/migrations/20251126155204_add_user_roles/migration.sql +5 -0
  13. package/template/prisma/migrations/20251128095126_add_company_info/migration.sql +19 -0
  14. package/template/prisma/migrations/20251128123321_add_smtp_config/migration.sql +22 -0
  15. package/template/prisma/migrations/20251128132303_add_status/migration.sql +23 -0
  16. package/template/prisma/migrations/20251201102207_add_user_active/migration.sql +75 -0
  17. package/template/prisma/migrations/20251201105507_add_email_signature/migration.sql +2 -0
  18. package/template/prisma/migrations/20251201151122_add_tasks/migration.sql +45 -0
  19. package/template/prisma/migrations/20251202111854_add_task_reminder/migration.sql +2 -0
  20. package/template/prisma/migrations/20251202135859_add_google_meet_integration/migration.sql +27 -0
  21. package/template/prisma/migrations/20251203103317_add_meta_lead_integration/migration.sql +20 -0
  22. package/template/prisma/migrations/20251203104002_add_google_ads_integration/migration.sql +18 -0
  23. package/template/prisma/migrations/20251203112122_add_google_sheet_integration/migration.sql +32 -0
  24. package/template/prisma/migrations/20251203153853_allow_multiple_integration_configs/migration.sql +20 -0
  25. package/template/prisma/migrations/20251205141705_update_user_roles/migration.sql +12 -0
  26. package/template/prisma/migrations/20251205150000_add_commercial_and_telepro_assignment/migration.sql +21 -0
  27. package/template/prisma/migrations/20251205160000_add_interaction_logging/migration.sql +11 -0
  28. package/template/prisma/migrations/20251208090314_add_automatic_interaction_types/migration.sql +12 -0
  29. package/template/prisma/migrations/20251208094843_mg/migration.sql +14 -0
  30. package/template/prisma/migrations/20251208100000_add_company_support/migration.sql +14 -0
  31. package/template/prisma/migrations/20251208110000_add_templates/migration.sql +26 -0
  32. package/template/prisma/migrations/20251208141304_add_video_conference_task_type/migration.sql +2 -0
  33. package/template/prisma/migrations/20251209104759_add_internal_note_to_task/migration.sql +2 -0
  34. package/template/prisma/migrations/20251209134803_add_company_field/migration.sql +2 -0
  35. package/template/prisma/migrations/20251209150000_rename_company_to_company_name/migration.sql +3 -0
  36. package/template/prisma/migrations/20251209150016_add_email_tracking/migration.sql +21 -0
  37. package/template/prisma/migrations/20251209155908_add_notify_contact_to_task/migration.sql +2 -0
  38. package/template/prisma/migrations/20251210110019_add_appointment_types/migration.sql +10 -0
  39. package/template/prisma/migrations/20251210113928_add_contact_files/migration.sql +26 -0
  40. package/template/prisma/migrations/20251212132339_add_custom_roles/migration.sql +24 -0
  41. package/template/prisma/migrations/20251215104448_add_file_interaction_types/migration.sql +11 -0
  42. package/template/prisma/migrations/20251215145616_add_closing_reasons/migration.sql +12 -0
  43. package/template/prisma/migrations/20251216140850_add_log_users/migration.sql +25 -0
  44. package/template/prisma/migrations/20251216151000_rename_perdu_to_ferme/migration.sql +8 -0
  45. package/template/prisma/migrations/20251216162318_add_column_mappings_to_google_sheet/migration.sql +2 -0
  46. package/template/prisma/migrations/20251216185127_add_workflows/migration.sql +80 -0
  47. package/template/prisma/migrations/20251216192237_add_scheduled_workflow_actions/migration.sql +32 -0
  48. package/template/prisma/migrations/migration_lock.toml +3 -0
  49. package/template/prisma/schema.prisma +582 -0
  50. package/template/prisma.config.ts +14 -0
  51. package/template/src/app/(auth)/invite/[token]/page.tsx +200 -0
  52. package/template/src/app/(auth)/layout.tsx +3 -0
  53. package/template/src/app/(auth)/reset-password/complete/page.tsx +213 -0
  54. package/template/src/app/(auth)/reset-password/page.tsx +146 -0
  55. package/template/src/app/(auth)/reset-password/verify/page.tsx +183 -0
  56. package/template/src/app/(auth)/signin/page.tsx +166 -0
  57. package/template/src/app/(dashboard)/agenda/page.tsx +3051 -0
  58. package/template/src/app/(dashboard)/automatisation/[id]/page.tsx +24 -0
  59. package/template/src/app/(dashboard)/automatisation/_components/workflow-editor.tsx +905 -0
  60. package/template/src/app/(dashboard)/automatisation/new/page.tsx +20 -0
  61. package/template/src/app/(dashboard)/automatisation/page.tsx +337 -0
  62. package/template/src/app/(dashboard)/closing/page.tsx +1052 -0
  63. package/template/src/app/(dashboard)/contacts/[id]/page.tsx +6028 -0
  64. package/template/src/app/(dashboard)/contacts/page.tsx +3713 -0
  65. package/template/src/app/(dashboard)/dashboard/page.tsx +186 -0
  66. package/template/src/app/(dashboard)/layout.tsx +30 -0
  67. package/template/src/app/(dashboard)/settings/page.tsx +4070 -0
  68. package/template/src/app/(dashboard)/templates/page.tsx +567 -0
  69. package/template/src/app/(dashboard)/users/list/page.tsx +507 -0
  70. package/template/src/app/(dashboard)/users/page.tsx +457 -0
  71. package/template/src/app/(dashboard)/users/permissions/page.tsx +181 -0
  72. package/template/src/app/(dashboard)/users/roles/page.tsx +434 -0
  73. package/template/src/app/api/audit-logs/route.ts +57 -0
  74. package/template/src/app/api/auth/[...all]/route.ts +4 -0
  75. package/template/src/app/api/auth/check-active/route.ts +31 -0
  76. package/template/src/app/api/auth/google/callback/route.ts +94 -0
  77. package/template/src/app/api/auth/google/disconnect/route.ts +32 -0
  78. package/template/src/app/api/auth/google/route.ts +34 -0
  79. package/template/src/app/api/auth/google/status/route.ts +32 -0
  80. package/template/src/app/api/closing-reasons/route.ts +27 -0
  81. package/template/src/app/api/contacts/[id]/files/[fileId]/route.ts +94 -0
  82. package/template/src/app/api/contacts/[id]/files/route.ts +269 -0
  83. package/template/src/app/api/contacts/[id]/interactions/[interactionId]/route.ts +91 -0
  84. package/template/src/app/api/contacts/[id]/interactions/route.ts +103 -0
  85. package/template/src/app/api/contacts/[id]/meet/route.ts +296 -0
  86. package/template/src/app/api/contacts/[id]/route.ts +322 -0
  87. package/template/src/app/api/contacts/[id]/send-email/route.ts +254 -0
  88. package/template/src/app/api/contacts/export/route.ts +270 -0
  89. package/template/src/app/api/contacts/import/route.ts +381 -0
  90. package/template/src/app/api/contacts/route.ts +283 -0
  91. package/template/src/app/api/dashboard/stats/route.ts +299 -0
  92. package/template/src/app/api/email/track/[id]/route.ts +68 -0
  93. package/template/src/app/api/integrations/google-sheet/sync/route.ts +526 -0
  94. package/template/src/app/api/invite/complete/route.ts +88 -0
  95. package/template/src/app/api/invite/validate/route.ts +55 -0
  96. package/template/src/app/api/reminders/route.ts +95 -0
  97. package/template/src/app/api/reset-password/complete/route.ts +73 -0
  98. package/template/src/app/api/reset-password/request/route.ts +84 -0
  99. package/template/src/app/api/reset-password/validate/route.ts +49 -0
  100. package/template/src/app/api/reset-password/verify/route.ts +74 -0
  101. package/template/src/app/api/roles/[id]/route.ts +183 -0
  102. package/template/src/app/api/roles/route.ts +140 -0
  103. package/template/src/app/api/send/route.ts +282 -0
  104. package/template/src/app/api/settings/change-password/route.ts +95 -0
  105. package/template/src/app/api/settings/closing-reasons/[id]/route.ts +84 -0
  106. package/template/src/app/api/settings/closing-reasons/route.ts +74 -0
  107. package/template/src/app/api/settings/company/route.ts +121 -0
  108. package/template/src/app/api/settings/google-ads/[id]/route.ts +117 -0
  109. package/template/src/app/api/settings/google-ads/route.ts +122 -0
  110. package/template/src/app/api/settings/google-sheet/[id]/route.ts +230 -0
  111. package/template/src/app/api/settings/google-sheet/auto-map/route.ts +196 -0
  112. package/template/src/app/api/settings/google-sheet/route.ts +254 -0
  113. package/template/src/app/api/settings/meta-leads/[id]/route.ts +123 -0
  114. package/template/src/app/api/settings/meta-leads/route.ts +132 -0
  115. package/template/src/app/api/settings/profile/route.ts +42 -0
  116. package/template/src/app/api/settings/smtp/route.ts +130 -0
  117. package/template/src/app/api/settings/smtp/test/route.ts +121 -0
  118. package/template/src/app/api/settings/statuses/[id]/route.ts +101 -0
  119. package/template/src/app/api/settings/statuses/route.ts +83 -0
  120. package/template/src/app/api/statuses/route.ts +25 -0
  121. package/template/src/app/api/tasks/[id]/attendees/route.ts +76 -0
  122. package/template/src/app/api/tasks/[id]/route.ts +728 -0
  123. package/template/src/app/api/tasks/meet/route.ts +240 -0
  124. package/template/src/app/api/tasks/route.ts +417 -0
  125. package/template/src/app/api/templates/[id]/route.ts +140 -0
  126. package/template/src/app/api/templates/route.ts +91 -0
  127. package/template/src/app/api/users/[id]/route.ts +168 -0
  128. package/template/src/app/api/users/list/route.ts +45 -0
  129. package/template/src/app/api/users/me/route.ts +48 -0
  130. package/template/src/app/api/users/route.ts +250 -0
  131. package/template/src/app/api/webhooks/google-ads/route.ts +208 -0
  132. package/template/src/app/api/webhooks/meta-leads/route.ts +258 -0
  133. package/template/src/app/api/workflows/[id]/route.ts +192 -0
  134. package/template/src/app/api/workflows/process/route.ts +293 -0
  135. package/template/src/app/api/workflows/route.ts +124 -0
  136. package/template/src/app/favicon.ico +0 -0
  137. package/template/src/app/globals.css +1416 -0
  138. package/template/src/app/layout.tsx +31 -0
  139. package/template/src/app/page.tsx +32 -0
  140. package/template/src/components/dashboard/activity-chart.tsx +67 -0
  141. package/template/src/components/dashboard/contacts-chart.tsx +63 -0
  142. package/template/src/components/dashboard/recent-activity.tsx +164 -0
  143. package/template/src/components/dashboard/sales-analytics-chart.tsx +81 -0
  144. package/template/src/components/dashboard/stat-card.tsx +61 -0
  145. package/template/src/components/dashboard/status-distribution-chart.tsx +45 -0
  146. package/template/src/components/dashboard/tasks-pie-chart.tsx +88 -0
  147. package/template/src/components/dashboard/top-contacts-list.tsx +129 -0
  148. package/template/src/components/dashboard/upcoming-tasks-list.tsx +126 -0
  149. package/template/src/components/editor.tsx +856 -0
  150. package/template/src/components/email-template.tsx +35 -0
  151. package/template/src/components/header.tsx +320 -0
  152. package/template/src/components/invitation-email-template.tsx +79 -0
  153. package/template/src/components/meet-cancellation-email-template.tsx +120 -0
  154. package/template/src/components/meet-confirmation-email-template.tsx +156 -0
  155. package/template/src/components/meet-update-email-template.tsx +209 -0
  156. package/template/src/components/page-header.tsx +61 -0
  157. package/template/src/components/reset-password-email-template.tsx +79 -0
  158. package/template/src/components/sidebar.tsx +294 -0
  159. package/template/src/components/skeleton.tsx +380 -0
  160. package/template/src/components/ui/commands.tsx +396 -0
  161. package/template/src/components/ui/components.tsx +150 -0
  162. package/template/src/components/ui/theme.tsx +5 -0
  163. package/template/src/components/view-as-banner.tsx +45 -0
  164. package/template/src/components/view-as-modal.tsx +186 -0
  165. package/template/src/contexts/mobile-menu-context.tsx +31 -0
  166. package/template/src/contexts/sidebar-context.tsx +107 -0
  167. package/template/src/contexts/task-reminder-context.tsx +239 -0
  168. package/template/src/contexts/view-as-context.tsx +84 -0
  169. package/template/src/hooks/use-user-role.ts +82 -0
  170. package/template/src/lib/audit-log.ts +45 -0
  171. package/template/src/lib/auth-client.ts +16 -0
  172. package/template/src/lib/auth.ts +35 -0
  173. package/template/src/lib/check-permission.ts +193 -0
  174. package/template/src/lib/contact-duplicate.ts +112 -0
  175. package/template/src/lib/contact-interactions.ts +371 -0
  176. package/template/src/lib/encryption.ts +99 -0
  177. package/template/src/lib/google-calendar.ts +300 -0
  178. package/template/src/lib/google-drive.ts +372 -0
  179. package/template/src/lib/permissions.ts +412 -0
  180. package/template/src/lib/prisma.ts +32 -0
  181. package/template/src/lib/roles.ts +120 -0
  182. package/template/src/lib/template-variables.ts +76 -0
  183. package/template/src/lib/utils.ts +46 -0
  184. package/template/src/lib/workflow-executor.ts +482 -0
  185. package/template/src/proxy.ts +91 -0
  186. package/template/tsconfig.json +34 -0
  187. package/template/vercel.json +8 -0
@@ -0,0 +1,294 @@
1
+ 'use client';
2
+
3
+ import Link from 'next/link';
4
+ import { usePathname } from 'next/navigation';
5
+ import { useSession, signOut } from '@/lib/auth-client';
6
+ import { useRouter } from 'next/navigation';
7
+ import { useMemo, useState, useEffect } from 'react';
8
+ import { useUserRole } from '@/hooks/use-user-role';
9
+ import { useMobileMenuContext } from '@/contexts/mobile-menu-context';
10
+ import { useSidebarContext } from '@/contexts/sidebar-context';
11
+ import { useViewAs } from '@/contexts/view-as-context';
12
+ import { ViewAsModal } from '@/components/view-as-modal';
13
+ import {
14
+ LayoutDashboard,
15
+ Users,
16
+ UserCog,
17
+ Settings,
18
+ Calendar as CalendarIcon,
19
+ FileText,
20
+ Eye,
21
+ Zap,
22
+ Columns3,
23
+ X,
24
+ } from 'lucide-react';
25
+ import { cn } from '@/lib/utils';
26
+
27
+ export function Sidebar() {
28
+ const pathname = usePathname();
29
+ const { data: session } = useSession();
30
+ const router = useRouter();
31
+ const { isOpen: isMobileMenuOpen, setIsOpen: setIsMobileMenuOpen } = useMobileMenuContext();
32
+ const { isCollapsed, isPinned, setIsCollapsed, togglePin } = useSidebarContext();
33
+ const { viewAsUser, isViewingAsOther } = useViewAs();
34
+ const [showViewAsModal, setShowViewAsModal] = useState(false);
35
+ const [isMounted, setIsMounted] = useState(false);
36
+
37
+ // Éviter l'erreur d'hydratation
38
+ useEffect(() => {
39
+ setIsMounted(true);
40
+ }, []);
41
+
42
+ // Obtenir le rôle de l'utilisateur via le hook personnalisé
43
+ const { isAdmin, isRealAdmin } = useUserRole();
44
+
45
+ // Navigation principale (Dashboard section)
46
+ const dashboardNav = useMemo(() => {
47
+ const baseNav = [
48
+ { name: 'Dashboard', href: '/dashboard', icon: LayoutDashboard },
49
+ { name: 'Contacts', href: '/contacts', icon: Users },
50
+ { name: 'Agenda', href: '/agenda', icon: CalendarIcon },
51
+ { name: 'Closing', href: '/closing', icon: Columns3 },
52
+ { name: 'Automatisations', href: '/automatisation', icon: Zap },
53
+ { name: 'Templates', href: '/templates', icon: FileText },
54
+ ];
55
+
56
+ // Ajouter la gestion des droits d'accès seulement pour les admins
57
+ if (isAdmin) {
58
+ baseNav.push({
59
+ name: "Droits d'accès",
60
+ href: '/users',
61
+ icon: UserCog,
62
+ });
63
+ }
64
+
65
+ baseNav.push({
66
+ name: 'Paramètres',
67
+ href: '/settings',
68
+ icon: Settings,
69
+ });
70
+
71
+ return baseNav;
72
+ }, [isAdmin]);
73
+
74
+ const handleSignOut = async () => {
75
+ await signOut();
76
+ router.push('/signin');
77
+ };
78
+
79
+ const handleLinkClick = () => {
80
+ setIsMobileMenuOpen(false);
81
+ };
82
+
83
+ return (
84
+ <>
85
+ {/* Overlay for mobile */}
86
+ {isMobileMenuOpen && (
87
+ <div
88
+ className="fixed inset-0 z-40 bg-gray-500/20 backdrop-blur-sm lg:hidden"
89
+ onClick={() => setIsMobileMenuOpen(false)}
90
+ />
91
+ )}
92
+
93
+ {/* Sidebar */}
94
+ <div
95
+ className={cn(
96
+ 'fixed top-0 left-0 z-40 flex h-screen flex-col border-r border-gray-200 bg-white transition-all duration-300 ease-in-out lg:relative lg:translate-x-0',
97
+ isMobileMenuOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0',
98
+ isCollapsed && !isPinned ? 'w-64 lg:w-16' : 'w-64 lg:w-64',
99
+ )}
100
+ onMouseEnter={() => {
101
+ if (typeof window !== 'undefined' && window.innerWidth >= 1024) {
102
+ if (!isPinned && isCollapsed) {
103
+ setIsCollapsed(false);
104
+ }
105
+ }
106
+ }}
107
+ onMouseLeave={() => {
108
+ if (typeof window !== 'undefined' && window.innerWidth >= 1024) {
109
+ if (!isPinned && !isCollapsed) {
110
+ setIsCollapsed(true);
111
+ }
112
+ }
113
+ }}
114
+ >
115
+ {/* Bouton fermer - Mobile seulement */}
116
+ <div className="flex h-16 items-center justify-end border-b border-gray-200 px-4 lg:hidden">
117
+ <button
118
+ onClick={() => setIsMobileMenuOpen(false)}
119
+ className="cursor-pointer rounded-lg p-2 text-gray-500 transition-colors hover:bg-gray-100"
120
+ aria-label="Close menu"
121
+ >
122
+ <X className="h-5 w-5" />
123
+ </button>
124
+ </div>
125
+
126
+ {/* Navigation principale */}
127
+ <nav className="flex-1 space-y-6 overflow-y-auto py-4">
128
+ {/* Section Dashboard */}
129
+ <div className={cn('px-3', isCollapsed && !isPinned && 'lg:px-2')}>
130
+ {(!isCollapsed || isPinned) && (
131
+ <h2 className="mb-2 px-3 text-xs font-semibold tracking-wider text-gray-500 uppercase">
132
+ CRM Template
133
+ </h2>
134
+ )}
135
+ <div className="space-y-1">
136
+ {dashboardNav.map((item) => {
137
+ const isActive = pathname === item.href;
138
+ const Icon = item.icon;
139
+ return (
140
+ <Link
141
+ key={item.name}
142
+ href={item.href}
143
+ onClick={handleLinkClick}
144
+ className={cn(
145
+ 'flex items-center gap-3 rounded-lg py-2 text-sm font-medium transition-colors',
146
+ isCollapsed && !isPinned ? 'px-3 lg:justify-center lg:px-2' : 'px-3',
147
+ isActive
148
+ ? 'bg-indigo-50 text-indigo-600'
149
+ : 'text-gray-700 hover:bg-gray-50 hover:text-gray-900',
150
+ )}
151
+ title={isCollapsed && !isPinned ? item.name : undefined}
152
+ >
153
+ <Icon className="h-5 w-5 shrink-0" />
154
+ {(!isCollapsed || isPinned) && (
155
+ <span className="whitespace-nowrap">{item.name}</span>
156
+ )}
157
+ </Link>
158
+ );
159
+ })}
160
+ </div>
161
+ </div>
162
+ </nav>
163
+
164
+ {/* Vue active - pour les admins seulement */}
165
+ {isRealAdmin && (
166
+ <div
167
+ className={cn(
168
+ 'border-t border-gray-200 transition-all duration-300',
169
+ isCollapsed && !isPinned ? 'p-3 lg:p-2' : 'p-3',
170
+ )}
171
+ >
172
+ {!isCollapsed || isPinned ? (
173
+ <button
174
+ onClick={() => setShowViewAsModal(true)}
175
+ className={cn(
176
+ 'w-full cursor-pointer rounded-lg border-2 p-3 text-left transition-all',
177
+ isViewingAsOther
178
+ ? 'border-indigo-600 bg-indigo-600 text-white hover:border-indigo-700 hover:bg-indigo-700'
179
+ : 'border-gray-300 bg-white text-gray-900 hover:border-indigo-300 hover:bg-indigo-50',
180
+ )}
181
+ aria-label="Changer de vue"
182
+ >
183
+ <div className="flex items-center gap-3">
184
+ <div
185
+ className={cn(
186
+ 'flex h-10 w-10 shrink-0 items-center justify-center rounded-full',
187
+ isViewingAsOther ? 'bg-white/20 text-white' : 'bg-indigo-100 text-indigo-600',
188
+ )}
189
+ >
190
+ {!isMounted
191
+ ? 'U'
192
+ : isViewingAsOther
193
+ ? viewAsUser?.name?.[0]?.toUpperCase() || 'U'
194
+ : session?.user?.name?.[0]?.toUpperCase() || 'U'}
195
+ </div>
196
+ <div className="min-w-0 flex-1">
197
+ <p
198
+ className={cn(
199
+ 'text-xs font-medium',
200
+ isViewingAsOther ? 'text-white/80' : 'text-gray-500',
201
+ )}
202
+ >
203
+ {isViewingAsOther ? 'Vue:' : 'Ma vue'}
204
+ </p>
205
+ <p className={`truncate text-sm font-semibold`}>
206
+ {!isMounted
207
+ ? 'Utilisateur'
208
+ : isViewingAsOther
209
+ ? viewAsUser?.name
210
+ : session?.user?.name || 'Utilisateur'}
211
+ </p>
212
+ </div>
213
+ <Eye className="h-5 w-5 shrink-0" />
214
+ </div>
215
+ </button>
216
+ ) : (
217
+ <button
218
+ onClick={() => setShowViewAsModal(true)}
219
+ className={cn(
220
+ 'w-full cursor-pointer rounded-lg p-2 transition-colors',
221
+ isViewingAsOther
222
+ ? 'bg-indigo-600 text-white hover:bg-indigo-700'
223
+ : 'text-gray-500 hover:bg-gray-100',
224
+ )}
225
+ title="Changer de vue"
226
+ aria-label="Changer de vue"
227
+ >
228
+ <div className="flex items-center justify-center">
229
+ <Eye className="h-5 w-5" />
230
+ </div>
231
+ </button>
232
+ )}
233
+ </div>
234
+ )}
235
+
236
+ {/* User Profile */}
237
+ <div
238
+ className={cn(
239
+ 'border-t border-gray-200 transition-all duration-300',
240
+ isCollapsed && !isPinned ? 'p-4 lg:p-2' : 'p-4',
241
+ )}
242
+ >
243
+ {!isCollapsed || isPinned ? (
244
+ <>
245
+ <div className="flex items-center gap-3">
246
+ <div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-indigo-100 text-indigo-600">
247
+ {!isMounted ? 'U' : session?.user?.name?.[0]?.toUpperCase() || 'U'}
248
+ </div>
249
+ <div className="min-w-0 flex-1">
250
+ <p className="truncate text-sm font-medium text-gray-900">
251
+ {!isMounted ? 'Utilisateur' : session?.user?.name || 'Utilisateur'}
252
+ </p>
253
+ <p className="truncate text-xs text-gray-500">
254
+ {!isMounted ? '' : session?.user?.email}
255
+ </p>
256
+ </div>
257
+ </div>
258
+ <button
259
+ onClick={handleSignOut}
260
+ className="mt-3 w-full cursor-pointer rounded-lg bg-gray-100 px-3 py-2 text-sm font-medium text-gray-700 transition-colors hover:bg-gray-200"
261
+ >
262
+ Déconnexion
263
+ </button>
264
+ </>
265
+ ) : (
266
+ <div className="flex flex-col items-center gap-2">
267
+ <div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-indigo-100 text-indigo-600">
268
+ {!isMounted ? 'U' : session?.user?.name?.[0]?.toUpperCase() || 'U'}
269
+ </div>
270
+ <button
271
+ onClick={handleSignOut}
272
+ className="cursor-pointer rounded-lg p-2 text-gray-500 transition-colors hover:bg-gray-100"
273
+ title="Déconnexion"
274
+ aria-label="Déconnexion"
275
+ >
276
+ <svg className="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
277
+ <path
278
+ strokeLinecap="round"
279
+ strokeLinejoin="round"
280
+ strokeWidth={2}
281
+ d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"
282
+ />
283
+ </svg>
284
+ </button>
285
+ </div>
286
+ )}
287
+ </div>
288
+ </div>
289
+
290
+ {/* Modal de changement de vue */}
291
+ <ViewAsModal isOpen={showViewAsModal} onClose={() => setShowViewAsModal(false)} />
292
+ </>
293
+ );
294
+ }
@@ -0,0 +1,380 @@
1
+ /**
2
+ * Composants Skeleton pour les états de chargement
3
+ */
4
+
5
+ import { cn } from '@/lib/utils';
6
+
7
+ export function Skeleton({ className }: { className?: string }) {
8
+ return (
9
+ <div
10
+ className={cn('animate-pulse rounded bg-gray-200', className)}
11
+ aria-label="Chargement..."
12
+ />
13
+ );
14
+ }
15
+
16
+ export function ContactTableSkeleton() {
17
+ return (
18
+ <div className="overflow-x-auto rounded-lg bg-white shadow">
19
+ <table className="min-w-full divide-y divide-gray-200">
20
+ <thead className="bg-gray-50">
21
+ <tr>
22
+ <th className="px-3 py-3 sm:px-6">
23
+ <Skeleton className="h-4 w-4" />
24
+ </th>
25
+ <th className="px-3 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase sm:px-6">
26
+ Contact
27
+ </th>
28
+ <th className="px-3 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase sm:px-6">
29
+ Téléphone
30
+ </th>
31
+ <th className="px-3 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase sm:px-6">
32
+ Email
33
+ </th>
34
+ <th className="px-3 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase sm:px-6">
35
+ Statut
36
+ </th>
37
+ <th className="px-3 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase sm:px-6">
38
+ Origine
39
+ </th>
40
+ <th className="px-3 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase sm:px-6">
41
+ COMMERCIAL
42
+ </th>
43
+ <th className="px-3 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase sm:px-6">
44
+ TÉLÉPRO
45
+ </th>
46
+ <th className="px-3 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase sm:px-6">
47
+ CRÉÉ LE
48
+ </th>
49
+ <th className="px-3 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase sm:px-6">
50
+ MODIFIÉ LE
51
+ </th>
52
+ </tr>
53
+ </thead>
54
+ <tbody className="divide-y divide-gray-200 bg-white">
55
+ {Array.from({ length: 8 }).map((_, i) => (
56
+ <tr key={i} className="hover:bg-gray-50">
57
+ <td className="px-3 py-4 whitespace-nowrap sm:px-6">
58
+ <Skeleton className="h-4 w-4" />
59
+ </td>
60
+ <td className="px-3 py-4 whitespace-nowrap sm:px-6">
61
+ <div className="flex items-center">
62
+ <Skeleton className="h-10 w-10 rounded-full" />
63
+ <div className="ml-3 space-y-2 sm:ml-4">
64
+ <Skeleton className="h-4 w-32" />
65
+ <Skeleton className="h-3 w-24" />
66
+ </div>
67
+ </div>
68
+ </td>
69
+ <td className="px-3 py-4 whitespace-nowrap sm:px-6">
70
+ <Skeleton className="h-4 w-28" />
71
+ </td>
72
+ <td className="px-3 py-4 whitespace-nowrap sm:px-6">
73
+ <Skeleton className="h-4 w-40" />
74
+ </td>
75
+ <td className="px-3 py-4 whitespace-nowrap sm:px-6">
76
+ <Skeleton className="h-6 w-20 rounded-full" />
77
+ </td>
78
+ <td className="px-3 py-4 whitespace-nowrap sm:px-6">
79
+ <Skeleton className="h-4 w-28" />
80
+ </td>
81
+ <td className="px-3 py-4 whitespace-nowrap sm:px-6">
82
+ <Skeleton className="h-5 w-24 rounded-full" />
83
+ </td>
84
+ <td className="px-3 py-4 whitespace-nowrap sm:px-6">
85
+ <Skeleton className="h-5 w-24 rounded-full" />
86
+ </td>
87
+ <td className="px-3 py-4 whitespace-nowrap sm:px-6">
88
+ <Skeleton className="h-4 w-32" />
89
+ </td>
90
+ <td className="px-3 py-4 whitespace-nowrap sm:px-6">
91
+ <Skeleton className="h-4 w-32" />
92
+ </td>
93
+ </tr>
94
+ ))}
95
+ </tbody>
96
+ </table>
97
+ </div>
98
+ );
99
+ }
100
+
101
+ export function ContactCardsSkeleton() {
102
+ return (
103
+ <div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
104
+ {Array.from({ length: 9 }).map((_, i) => (
105
+ <div key={i} className="rounded-lg border border-gray-200 bg-white p-6 shadow-sm">
106
+ {/* En-tête */}
107
+ <div className="mb-4 flex items-start justify-between">
108
+ <div className="flex items-center gap-3">
109
+ <Skeleton className="h-12 w-12 rounded-full" />
110
+ <div className="space-y-2">
111
+ <Skeleton className="h-5 w-32" />
112
+ <Skeleton className="h-3 w-24" />
113
+ </div>
114
+ </div>
115
+ </div>
116
+
117
+ {/* Informations de contact */}
118
+ <div className="mb-4 space-y-2">
119
+ <div className="flex items-center">
120
+ <Skeleton className="mr-2 h-4 w-4" />
121
+ <Skeleton className="h-4 w-28" />
122
+ </div>
123
+ <div className="flex items-center">
124
+ <Skeleton className="mr-2 h-4 w-4" />
125
+ <Skeleton className="h-4 w-36" />
126
+ </div>
127
+ <div className="flex items-center">
128
+ <Skeleton className="mr-2 h-4 w-4" />
129
+ <Skeleton className="h-4 w-32" />
130
+ </div>
131
+ <div className="flex items-center">
132
+ <Skeleton className="mr-2 h-4 w-4" />
133
+ <Skeleton className="h-4 w-24" />
134
+ </div>
135
+ </div>
136
+
137
+ {/* Badge statut */}
138
+ <div className="mb-4">
139
+ <Skeleton className="h-6 w-24 rounded-full" />
140
+ </div>
141
+
142
+ {/* Pied de carte avec utilisateurs assignés */}
143
+ <div className="flex items-start justify-between border-t border-gray-100 pt-4">
144
+ <div className="space-y-2">
145
+ <div className="flex items-center gap-2">
146
+ <Skeleton className="h-7 w-7 rounded-full" />
147
+ <div className="space-y-1">
148
+ <Skeleton className="h-4 w-20" />
149
+ </div>
150
+ </div>
151
+ <div className="flex items-center gap-2">
152
+ <Skeleton className="h-7 w-7 rounded-full" />
153
+ <div className="space-y-1">
154
+ <Skeleton className="h-4 w-20" />
155
+ </div>
156
+ </div>
157
+ </div>
158
+ </div>
159
+ </div>
160
+ ))}
161
+ </div>
162
+ );
163
+ }
164
+
165
+ export function AgendaMonthSkeleton() {
166
+ return (
167
+ <div className="rounded-lg bg-white shadow">
168
+ <div className="grid grid-cols-7 border-b border-gray-200">
169
+ {['Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam', 'Dim'].map((day) => (
170
+ <div
171
+ key={day}
172
+ className="border-r border-gray-200 p-3 text-center text-sm font-semibold text-gray-700 last:border-r-0"
173
+ >
174
+ {day}
175
+ </div>
176
+ ))}
177
+ </div>
178
+ <div className="grid grid-cols-7">
179
+ {Array.from({ length: 42 }).map((_, i) => (
180
+ <div
181
+ key={i}
182
+ className="min-h-[100px] border-r border-b border-gray-200 p-2 last:border-r-0"
183
+ >
184
+ <Skeleton className="mb-2 h-5 w-6" />
185
+ <div className="space-y-1">
186
+ <Skeleton className="h-5 w-full" />
187
+ <Skeleton className="h-5 w-3/4" />
188
+ </div>
189
+ </div>
190
+ ))}
191
+ </div>
192
+ </div>
193
+ );
194
+ }
195
+
196
+ export function AgendaWeekSkeleton() {
197
+ const HOURS = Array.from({ length: 17 }, (_, i) => i + 6); // 6h à 22h
198
+ // Utiliser un pattern déterministe pour éviter les erreurs d'hydratation
199
+ // On affiche un skeleton toutes les 3 heures pour chaque jour
200
+ const shouldShowSkeleton = (dayIndex: number, hourIndex: number) => {
201
+ return (dayIndex + hourIndex) % 3 === 0;
202
+ };
203
+
204
+ return (
205
+ <div className="overflow-auto rounded-lg bg-white shadow">
206
+ <div className="grid grid-cols-8 border-b border-gray-200 bg-gray-50 text-xs font-medium text-gray-500">
207
+ <div className="px-3 py-2 text-right">(UTC+01:00) Hr</div>
208
+ {Array.from({ length: 7 }).map((_, i) => (
209
+ <div key={i} className="border-l border-gray-200 px-3 py-2 text-center">
210
+ <Skeleton className="mx-auto h-4 w-12" />
211
+ <Skeleton className="mx-auto mt-1 h-8 w-8 rounded-full" />
212
+ </div>
213
+ ))}
214
+ </div>
215
+ <div className="grid grid-cols-8 text-xs">
216
+ <div className="border-r border-gray-200 bg-gray-50">
217
+ {HOURS.map((hour) => (
218
+ <div
219
+ key={hour}
220
+ className="flex h-16 items-start justify-end border-b border-gray-200 pr-2"
221
+ >
222
+ <Skeleton className="h-3 w-10" />
223
+ </div>
224
+ ))}
225
+ </div>
226
+ {Array.from({ length: 7 }).map((_, dayIndex) => (
227
+ <div key={dayIndex} className="border-l border-gray-200">
228
+ {HOURS.map((hour, hourIndex) => (
229
+ <div key={hour} className="relative h-16 border-b border-gray-100 px-1.5 py-0.5">
230
+ {shouldShowSkeleton(dayIndex, hourIndex) && (
231
+ <Skeleton className="h-12 w-full rounded" />
232
+ )}
233
+ </div>
234
+ ))}
235
+ </div>
236
+ ))}
237
+ </div>
238
+ </div>
239
+ );
240
+ }
241
+
242
+ export function AgendaDaySkeleton() {
243
+ return (
244
+ <div className="space-y-4">
245
+ {Array.from({ length: 5 }).map((_, i) => (
246
+ <div key={i} className="rounded-lg border border-gray-200 bg-white p-4 shadow">
247
+ <div className="flex items-start justify-between">
248
+ <div className="flex-1">
249
+ <div className="flex items-center gap-2">
250
+ <Skeleton className="h-5 w-5 rounded-full" />
251
+ <div className="flex-1 space-y-2">
252
+ <div className="flex items-center gap-2">
253
+ <Skeleton className="h-5 w-16 rounded-full" />
254
+ <Skeleton className="h-4 w-20" />
255
+ </div>
256
+ <Skeleton className="h-5 w-48" />
257
+ <Skeleton className="h-4 w-32" />
258
+ <div className="mt-2 flex items-center gap-4">
259
+ <Skeleton className="h-4 w-20" />
260
+ <Skeleton className="h-4 w-24" />
261
+ </div>
262
+ </div>
263
+ </div>
264
+ </div>
265
+ </div>
266
+ </div>
267
+ ))}
268
+ </div>
269
+ );
270
+ }
271
+
272
+ export function UsersTableSkeleton() {
273
+ return (
274
+ <div className="overflow-x-auto rounded-lg bg-white shadow">
275
+ <table className="min-w-full divide-y divide-gray-200">
276
+ <thead className="bg-gray-50">
277
+ <tr>
278
+ <th className="px-3 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase sm:px-6">
279
+ Utilisateur
280
+ </th>
281
+ <th className="px-3 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase sm:px-6">
282
+ Email
283
+ </th>
284
+ <th className="px-3 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase sm:px-6">
285
+ Rôle
286
+ </th>
287
+ <th className="px-3 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase sm:px-6">
288
+ Email vérifié
289
+ </th>
290
+ <th className="px-3 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase sm:px-6">
291
+ Compte
292
+ </th>
293
+ </tr>
294
+ </thead>
295
+ <tbody className="divide-y divide-gray-200 bg-white">
296
+ {Array.from({ length: 6 }).map((_, i) => (
297
+ <tr key={i} className="hover:bg-gray-50">
298
+ <td className="px-3 py-4 whitespace-nowrap sm:px-6">
299
+ <div className="flex items-center">
300
+ <Skeleton className="h-8 w-8 rounded-full sm:h-10 sm:w-10" />
301
+ <div className="ml-2 space-y-1 sm:ml-4">
302
+ <Skeleton className="h-4 w-32" />
303
+ </div>
304
+ </div>
305
+ </td>
306
+ <td className="px-3 py-4 whitespace-nowrap sm:px-6">
307
+ <Skeleton className="h-4 w-40" />
308
+ </td>
309
+ <td className="px-3 py-4 whitespace-nowrap sm:px-6">
310
+ <Skeleton className="h-8 w-24 rounded-md" />
311
+ </td>
312
+ <td className="px-3 py-4 whitespace-nowrap sm:px-6">
313
+ <Skeleton className="h-6 w-20 rounded-full" />
314
+ </td>
315
+ <td className="px-3 py-4 whitespace-nowrap sm:px-6">
316
+ <div className="flex items-center gap-2">
317
+ <Skeleton className="h-5 w-9 rounded-full" />
318
+ <Skeleton className="h-4 w-12" />
319
+ </div>
320
+ </td>
321
+ </tr>
322
+ ))}
323
+ </tbody>
324
+ </table>
325
+ </div>
326
+ );
327
+ }
328
+
329
+ export function TemplatesPageSkeleton() {
330
+ return (
331
+ <div className="h-full">
332
+ {/* Header Skeleton */}
333
+ <div className="border-b border-gray-200 bg-white px-4 py-4 sm:px-6 lg:px-8">
334
+ <div className="flex items-center justify-between">
335
+ <div className="space-y-2">
336
+ <Skeleton className="h-8 w-48" />
337
+ <Skeleton className="h-4 w-64" />
338
+ </div>
339
+ <Skeleton className="h-10 w-40 rounded-lg" />
340
+ </div>
341
+ </div>
342
+
343
+ <div className="p-4 sm:p-6 lg:p-8">
344
+ {/* Filtres Skeleton */}
345
+ <div className="mb-6 flex flex-wrap gap-2">
346
+ {Array.from({ length: 4 }).map((_, i) => (
347
+ <Skeleton key={i} className="h-10 w-24 rounded-lg" />
348
+ ))}
349
+ </div>
350
+
351
+ {/* Grille de templates Skeleton */}
352
+ <div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
353
+ {Array.from({ length: 6 }).map((_, i) => (
354
+ <div
355
+ key={i}
356
+ className="rounded-lg border border-gray-200 bg-white p-4 shadow transition-shadow"
357
+ >
358
+ <div className="flex items-start justify-between">
359
+ <div className="flex-1">
360
+ <div className="mb-2 flex items-center gap-2">
361
+ <Skeleton className="h-5 w-5" />
362
+ <Skeleton className="h-6 w-32" />
363
+ </div>
364
+ <Skeleton className="mb-2 h-6 w-20 rounded-full" />
365
+ <Skeleton className="mb-1 h-4 w-full" />
366
+ <Skeleton className="mb-1 h-4 w-3/4" />
367
+ <Skeleton className="h-4 w-1/2" />
368
+ </div>
369
+ </div>
370
+ <div className="mt-4 flex items-center justify-end gap-2">
371
+ <Skeleton className="h-8 w-8 rounded-lg" />
372
+ <Skeleton className="h-8 w-8 rounded-lg" />
373
+ </div>
374
+ </div>
375
+ ))}
376
+ </div>
377
+ </div>
378
+ </div>
379
+ );
380
+ }