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.
- package/bin/create-crm-tmp.js +56 -35
- package/package.json +1 -1
- package/template/README.md +230 -115
- package/template/eslint.config.mjs +13 -0
- package/template/next.config.ts +14 -0
- package/template/package.json +15 -2
- package/template/prisma/migrations/20260318095700_init_db/migration.sql +978 -0
- package/template/prisma/migrations/migration_lock.toml +3 -0
- package/template/prisma/schema.prisma +132 -637
- package/template/src/app/(auth)/invite/[token]/page.tsx +10 -8
- package/template/src/app/(auth)/layout.tsx +1 -1
- package/template/src/app/(auth)/reset-password/complete/page.tsx +11 -8
- package/template/src/app/(auth)/reset-password/page.tsx +4 -4
- package/template/src/app/(auth)/reset-password/verify/page.tsx +4 -4
- package/template/src/app/(auth)/signin/page.tsx +14 -6
- package/template/src/app/(dashboard)/agenda/page.tsx +2243 -988
- package/template/src/app/(dashboard)/automatisation/_components/workflow-editor.tsx +18 -104
- package/template/src/app/(dashboard)/automatisation/page.tsx +10 -26
- package/template/src/app/(dashboard)/closing/page.tsx +78 -62
- package/template/src/app/(dashboard)/contacts/[id]/page.tsx +2082 -1080
- package/template/src/app/(dashboard)/contacts/companies/[id]/page.tsx +46 -47
- package/template/src/app/(dashboard)/contacts/page.tsx +1062 -780
- package/template/src/app/(dashboard)/dashboard/page.tsx +533 -37
- package/template/src/app/(dashboard)/dev/page.tsx +1291 -0
- package/template/src/app/(dashboard)/layout.tsx +6 -2
- package/template/src/app/(dashboard)/settings/page.tsx +797 -2582
- package/template/src/app/(dashboard)/templates/page.tsx +55 -54
- package/template/src/app/(dashboard)/users/list/page.tsx +51 -48
- package/template/src/app/(dashboard)/users/page.tsx +1 -1
- package/template/src/app/(dashboard)/users/permissions/page.tsx +2 -2
- package/template/src/app/(dashboard)/users/roles/page.tsx +7 -5
- package/template/src/app/api/agenda/google-events/route.ts +92 -0
- package/template/src/app/api/auth/check-active/route.ts +3 -2
- package/template/src/app/api/auth/google/route.ts +2 -1
- package/template/src/app/api/auth/google/status/route.ts +7 -31
- package/template/src/app/api/companies/[id]/activities/route.ts +1 -3
- package/template/src/app/api/companies/[id]/route.ts +1 -2
- package/template/src/app/api/companies/route.ts +42 -12
- package/template/src/app/api/contacts/[id]/files/[fileId]/preview/route.ts +9 -31
- package/template/src/app/api/contacts/[id]/files/[fileId]/route.ts +14 -32
- package/template/src/app/api/contacts/[id]/files/route.ts +112 -212
- package/template/src/app/api/contacts/[id]/interactions/[interactionId]/route.ts +27 -1
- package/template/src/app/api/contacts/[id]/interactions/route.ts +16 -16
- package/template/src/app/api/contacts/[id]/kyc/route.ts +21 -11
- package/template/src/app/api/contacts/[id]/meet/route.ts +19 -2
- package/template/src/app/api/contacts/[id]/route.ts +106 -34
- package/template/src/app/api/contacts/[id]/send-email/route.ts +27 -11
- package/template/src/app/api/contacts/[id]/workflows/run/route.ts +6 -0
- package/template/src/app/api/contacts/export/route.ts +9 -13
- package/template/src/app/api/contacts/import/route.ts +55 -25
- package/template/src/app/api/contacts/import-preview/route.ts +1 -1
- package/template/src/app/api/contacts/origins/route.ts +63 -0
- package/template/src/app/api/contacts/route.ts +153 -41
- package/template/src/app/api/cron/cleanup-editor-images/route.ts +166 -0
- package/template/src/app/api/dashboard/widgets/[id]/route.ts +44 -0
- package/template/src/app/api/dashboard/widgets/route.ts +181 -0
- package/template/src/app/api/dev/reminders/test/route.ts +114 -0
- package/template/src/app/api/editor/upload-image/route.ts +61 -0
- package/template/src/app/api/integrations/google-sheet/jobs/[jobId]/route.ts +47 -0
- package/template/src/app/api/integrations/google-sheet/jobs/usage/route.ts +50 -0
- package/template/src/app/api/integrations/google-sheet/sync/route.ts +24 -556
- package/template/src/app/api/jobs/google-sheet/process/route.ts +84 -0
- package/template/src/app/api/jobs/google-sheet/schedule/route.ts +50 -0
- package/template/src/app/api/reminders/clear/route.ts +120 -0
- package/template/src/app/api/reminders/clear/undo/route.ts +112 -0
- package/template/src/app/api/reminders/route.ts +164 -39
- package/template/src/app/api/reminders/state/route.ts +164 -0
- package/template/src/app/api/reset-password/request/route.ts +1 -1
- package/template/src/app/api/reset-password/verify/route.ts +1 -1
- package/template/src/app/api/send/route.ts +16 -4
- package/template/src/app/api/settings/google-ads/route.ts +14 -0
- package/template/src/app/api/settings/google-calendar/calendars/route.ts +97 -0
- package/template/src/app/api/settings/google-calendar/route.ts +124 -0
- package/template/src/app/api/settings/google-sheet/[id]/route.ts +28 -0
- package/template/src/app/api/settings/google-sheet/auto-map/route.ts +37 -4
- package/template/src/app/api/settings/google-sheet/preview/route.ts +9 -3
- package/template/src/app/api/settings/google-sheet/route.ts +14 -0
- package/template/src/app/api/settings/integrations/logs/route.ts +93 -0
- package/template/src/app/api/settings/integrations/notifications/route.ts +67 -0
- package/template/src/app/api/settings/meta-leads/[id]/route.ts +0 -1
- package/template/src/app/api/settings/meta-leads/route.ts +14 -2
- package/template/src/app/api/settings/smtp/route.ts +53 -6
- package/template/src/app/api/tasks/[id]/attendees/route.ts +24 -8
- package/template/src/app/api/tasks/[id]/route.ts +234 -58
- package/template/src/app/api/tasks/meet/route.ts +27 -19
- package/template/src/app/api/tasks/route.ts +62 -17
- package/template/src/app/api/users/[id]/route.ts +20 -14
- package/template/src/app/api/users/list/route.ts +57 -19
- package/template/src/app/api/webhooks/google-ads/route.ts +34 -14
- package/template/src/app/api/webhooks/meta-leads/route.ts +32 -12
- package/template/src/app/api/workflows/[id]/route.ts +0 -4
- package/template/src/app/api/workflows/process/route.ts +22 -51
- package/template/src/app/api/workflows/route.ts +0 -4
- package/template/src/app/globals.css +342 -4
- package/template/src/app/layout.tsx +11 -3
- package/template/src/app/page.tsx +1 -1
- package/template/src/components/address-autocomplete.tsx +7 -6
- package/template/src/components/config-error-alert.tsx +46 -0
- package/template/src/components/contacts/filter-bar.tsx +12 -3
- package/template/src/components/contacts/filter-builder.tsx +28 -43
- package/template/src/components/contacts/save-view-dialog.tsx +1 -1
- package/template/src/components/contacts/views-tab-bar.tsx +15 -6
- package/template/src/components/dashboard/activity-chart.tsx +41 -28
- package/template/src/components/dashboard/add-widget-dialog.tsx +157 -0
- package/template/src/components/dashboard/color-picker.tsx +64 -0
- package/template/src/components/dashboard/contacts-chart.tsx +69 -0
- package/template/src/components/dashboard/interactions-by-type-chart.tsx +121 -0
- package/template/src/components/dashboard/recent-activity.tsx +154 -0
- package/template/src/components/dashboard/stat-card.tsx +40 -40
- package/template/src/components/dashboard/status-distribution-chart.tsx +81 -0
- package/template/src/components/dashboard/tasks-pie-chart.tsx +37 -34
- package/template/src/components/dashboard/top-contacts-list.tsx +113 -0
- package/template/src/components/dashboard/upcoming-tasks-list.tsx +72 -81
- package/template/src/components/dashboard/widget-wrapper.tsx +36 -0
- package/template/src/components/date-picker.tsx +9 -6
- package/template/src/components/editor/upload-editor-image.ts +42 -0
- package/template/src/components/editor.tsx +161 -22
- package/template/src/components/email-template.tsx +2 -2
- package/template/src/components/global-search.tsx +30 -28
- package/template/src/components/header.tsx +178 -80
- package/template/src/components/inactive-account-guard.tsx +58 -0
- package/template/src/components/integration-notifications-listener.tsx +12 -0
- package/template/src/components/invitation-email-template.tsx +2 -2
- package/template/src/components/meet-cancellation-email-template.tsx +3 -3
- package/template/src/components/meet-confirmation-email-template.tsx +3 -3
- package/template/src/components/meet-update-email-template.tsx +3 -3
- package/template/src/components/page-header.tsx +5 -5
- package/template/src/components/protected-page.tsx +1 -1
- package/template/src/components/reset-password-email-template.tsx +2 -2
- package/template/src/components/settings/integrations/GoogleAdsIntegration.tsx +428 -0
- package/template/src/components/settings/integrations/GoogleSheetConfigMonitoringModal.tsx +680 -0
- package/template/src/components/settings/integrations/GoogleSheetIntegration.tsx +809 -0
- package/template/src/components/settings/integrations/ImportResultDialog.tsx +124 -0
- package/template/src/components/settings/integrations/IntegrationLogPanel.tsx +57 -0
- package/template/src/components/settings/integrations/IntegrationLogsTable.tsx +186 -0
- package/template/src/components/settings/integrations/MetaLeadIntegration.tsx +451 -0
- package/template/src/components/sidebar.tsx +45 -26
- package/template/src/components/skeleton.tsx +40 -43
- package/template/src/components/ui/accordion.tsx +2 -2
- package/template/src/components/ui/alert-dialog.tsx +1 -1
- package/template/src/components/ui/button.tsx +20 -9
- package/template/src/components/ui/components.tsx +1 -1
- package/template/src/components/ui/date-picker.tsx +422 -0
- package/template/src/components/ui/datetime-picker.tsx +338 -0
- package/template/src/components/ui/status-select.tsx +271 -0
- package/template/src/components/ui/tooltip.tsx +37 -0
- package/template/src/components/view-as-modal.tsx +13 -7
- package/template/src/contexts/app-toast-context.tsx +245 -57
- package/template/src/contexts/dashboard-theme-context.tsx +53 -0
- package/template/src/contexts/sidebar-context.tsx +22 -17
- package/template/src/contexts/task-reminder-context.tsx +134 -160
- package/template/src/contexts/view-as-context.tsx +33 -6
- package/template/src/hooks/use-focus-trap.ts +2 -2
- package/template/src/hooks/useIntegrationNotifications.ts +49 -0
- package/template/src/lib/auth.ts +8 -1
- package/template/src/lib/config-links.ts +14 -0
- package/template/src/lib/contact-duplicate.ts +79 -61
- package/template/src/lib/contact-interactions.ts +21 -21
- package/template/src/lib/contact-view-filters.ts +24 -64
- package/template/src/lib/contacts-list-url.ts +190 -0
- package/template/src/lib/dashboard-stats.ts +65 -7
- package/template/src/lib/dashboard-themes.ts +135 -0
- package/template/src/lib/date-utils.ts +127 -0
- package/template/src/lib/default-widgets.ts +12 -0
- package/template/src/lib/editor-html-image-dimensions.ts +172 -0
- package/template/src/lib/editor-image-limits.ts +19 -0
- package/template/src/lib/email-html-sanitize.ts +19 -0
- package/template/src/lib/encryption.ts +9 -6
- package/template/src/lib/fr-geography.ts +192 -0
- package/template/src/lib/google-calendar-agenda.ts +201 -0
- package/template/src/lib/google-calendar.ts +255 -5
- package/template/src/lib/google-sheet-sync-jobs.ts +96 -0
- package/template/src/lib/google-sheet-sync-runner.ts +514 -0
- package/template/src/lib/integration-import-log.ts +21 -0
- package/template/src/lib/permissions.ts +40 -10
- package/template/src/lib/prisma.ts +4 -1
- package/template/src/lib/qstash.ts +65 -0
- package/template/src/lib/reminder-state-server.ts +80 -0
- package/template/src/lib/reminder-state.ts +29 -0
- package/template/src/lib/supabase-storage.ts +113 -0
- package/template/src/lib/template-variables.ts +164 -23
- package/template/src/lib/utils.ts +45 -0
- package/template/src/lib/widget-registry.ts +173 -0
- package/template/src/lib/workflow-executor.ts +16 -70
- package/template/src/proxy.ts +1 -0
- package/template/vercel.json +3 -10
- package/template/skills-lock.json +0 -25
- package/template/src/components/dashboard/dashboard-content.tsx +0 -79
- package/template/src/lib/google-drive.ts +0 -1101
- package/template/src/types/yousign.ts +0 -52
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { X, Plus, Filter, Save, RotateCcw } from 'lucide-react';
|
|
4
|
+
import { cn } from '@/lib/utils';
|
|
4
5
|
import {
|
|
5
6
|
FILTER_FIELD_DEFINITIONS,
|
|
6
7
|
OPERATOR_LABELS,
|
|
@@ -20,6 +21,8 @@ interface FilterBarProps {
|
|
|
20
21
|
isViewOwner?: boolean;
|
|
21
22
|
statusOptions?: { id: string; name: string; color: string }[];
|
|
22
23
|
userOptions?: { id: string; name: string }[];
|
|
24
|
+
/** Quand true, pas de padding (intégré sur une ligne avec la barre d'outils) */
|
|
25
|
+
inline?: boolean;
|
|
23
26
|
}
|
|
24
27
|
|
|
25
28
|
function getFieldLabel(field: string): string {
|
|
@@ -89,10 +92,16 @@ export function FilterBar({
|
|
|
89
92
|
isViewOwner,
|
|
90
93
|
statusOptions,
|
|
91
94
|
userOptions,
|
|
95
|
+
inline = false,
|
|
92
96
|
}: FilterBarProps) {
|
|
97
|
+
const rootClass = cn(
|
|
98
|
+
'flex flex-wrap items-center gap-2',
|
|
99
|
+
!inline && 'px-4 py-2 sm:px-6 lg:px-8',
|
|
100
|
+
inline && 'shrink-0',
|
|
101
|
+
);
|
|
93
102
|
if (filters.length === 0 && !hasUnsavedChanges) {
|
|
94
103
|
return (
|
|
95
|
-
<div className=
|
|
104
|
+
<div className={rootClass}>
|
|
96
105
|
<button
|
|
97
106
|
onClick={onAddFilter}
|
|
98
107
|
className="flex cursor-pointer items-center gap-1.5 rounded-md border border-gray-200 px-3 py-1.5 text-sm text-gray-600 transition-colors hover:bg-gray-50"
|
|
@@ -105,7 +114,7 @@ export function FilterBar({
|
|
|
105
114
|
}
|
|
106
115
|
|
|
107
116
|
return (
|
|
108
|
-
<div className=
|
|
117
|
+
<div className={rootClass}>
|
|
109
118
|
<Filter className="h-3.5 w-3.5 shrink-0 text-gray-400" />
|
|
110
119
|
|
|
111
120
|
{filters.map((filter, index) => (
|
|
@@ -120,7 +129,7 @@ export function FilterBar({
|
|
|
120
129
|
</span>
|
|
121
130
|
<button
|
|
122
131
|
onClick={() => onRemoveFilter(index)}
|
|
123
|
-
|
|
132
|
+
className="ml-0.5 cursor-pointer rounded p-0.5 text-blue-400 hover:bg-blue-100 hover:text-blue-600"
|
|
124
133
|
>
|
|
125
134
|
<X className="h-3 w-3" />
|
|
126
135
|
</button>
|
|
@@ -14,9 +14,9 @@ import {
|
|
|
14
14
|
OPERATOR_LABELS,
|
|
15
15
|
DATE_PRESET_LABELS,
|
|
16
16
|
} from '@/types/contact-views';
|
|
17
|
-
import type { Region, Department } from '@/lib/french-regions';
|
|
18
|
-
import { FRENCH_REGIONS, FRENCH_DEPARTMENTS } from '@/lib/french-regions';
|
|
19
17
|
import { useFocusTrap } from '@/hooks/use-focus-trap';
|
|
18
|
+
import { FR_DEPARTMENTS, FR_REGIONS } from '@/lib/fr-geography';
|
|
19
|
+
import { DatePicker } from '@/components/ui/date-picker';
|
|
20
20
|
|
|
21
21
|
type Step = 'field' | 'operator' | 'value';
|
|
22
22
|
|
|
@@ -226,16 +226,10 @@ export function FilterBuilder({
|
|
|
226
226
|
return originOptions.map((o) => ({ value: o, label: o }));
|
|
227
227
|
}
|
|
228
228
|
if (selectedField.field === 'region') {
|
|
229
|
-
return
|
|
230
|
-
value: r.code,
|
|
231
|
-
label: r.name,
|
|
232
|
-
}));
|
|
229
|
+
return FR_REGIONS.map((r) => ({ value: r.code, label: r.name }));
|
|
233
230
|
}
|
|
234
231
|
if (selectedField.field === 'department') {
|
|
235
|
-
return
|
|
236
|
-
value: d.code,
|
|
237
|
-
label: `${d.code} - ${d.name}`,
|
|
238
|
-
}));
|
|
232
|
+
return FR_DEPARTMENTS.map((d) => ({ value: d.code, label: `${d.code} — ${d.name}` }));
|
|
239
233
|
}
|
|
240
234
|
return [];
|
|
241
235
|
}
|
|
@@ -308,7 +302,7 @@ export function FilterBuilder({
|
|
|
308
302
|
placeholder="Rechercher une propriété..."
|
|
309
303
|
value={fieldSearch}
|
|
310
304
|
onChange={(e) => setFieldSearch(e.target.value)}
|
|
311
|
-
className="w-full rounded-md border border-gray-200 py-2 pr-3 pl-9 text-sm focus:
|
|
305
|
+
className="w-full rounded-md border border-gray-200 py-2 pr-3 pl-9 text-sm focus:ring-2 focus:ring-gray-400/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
|
|
312
306
|
autoFocus
|
|
313
307
|
/>
|
|
314
308
|
</div>
|
|
@@ -369,8 +363,8 @@ export function FilterBuilder({
|
|
|
369
363
|
<button
|
|
370
364
|
key={preset}
|
|
371
365
|
onClick={() => applyFilter('date_preset', null, preset)}
|
|
372
|
-
|
|
373
|
-
|
|
366
|
+
className={cn(
|
|
367
|
+
'flex w-full cursor-pointer items-center justify-between px-4 py-2 text-sm hover:bg-gray-50',
|
|
374
368
|
selectedPreset === preset ? 'font-medium text-blue-600' : 'text-gray-700',
|
|
375
369
|
)}
|
|
376
370
|
>
|
|
@@ -394,7 +388,7 @@ export function FilterBuilder({
|
|
|
394
388
|
placeholder="Rechercher..."
|
|
395
389
|
value={selectSearch}
|
|
396
390
|
onChange={(e) => setSelectSearch(e.target.value)}
|
|
397
|
-
className="w-full rounded-md border border-gray-200 py-2 pr-3 pl-9 text-sm focus:
|
|
391
|
+
className="w-full rounded-md border border-gray-200 py-2 pr-3 pl-9 text-sm focus:ring-2 focus:ring-gray-400/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
|
|
398
392
|
autoFocus
|
|
399
393
|
/>
|
|
400
394
|
</div>
|
|
@@ -409,8 +403,8 @@ export function FilterBuilder({
|
|
|
409
403
|
<button
|
|
410
404
|
key={option.value}
|
|
411
405
|
onClick={() => handleToggleSelectValue(option.value)}
|
|
412
|
-
|
|
413
|
-
|
|
406
|
+
className={cn(
|
|
407
|
+
'flex w-full cursor-pointer items-center gap-2 px-4 py-2 text-sm hover:bg-gray-50',
|
|
414
408
|
isSelected ? 'text-blue-600' : 'text-gray-700',
|
|
415
409
|
)}
|
|
416
410
|
>
|
|
@@ -469,7 +463,7 @@ export function FilterBuilder({
|
|
|
469
463
|
applyFilter(undefined, textInput.trim(), null);
|
|
470
464
|
}
|
|
471
465
|
}}
|
|
472
|
-
className="w-full rounded-md border border-gray-200 px-3 py-2 text-sm focus:
|
|
466
|
+
className="w-full rounded-md border border-gray-200 px-3 py-2 text-sm focus:ring-2 focus:ring-gray-400/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
|
|
473
467
|
autoFocus
|
|
474
468
|
/>
|
|
475
469
|
<button
|
|
@@ -503,7 +497,7 @@ export function FilterBuilder({
|
|
|
503
497
|
applyFilter(undefined, textInput.trim(), null);
|
|
504
498
|
}
|
|
505
499
|
}}
|
|
506
|
-
className="w-full rounded-md border border-gray-200 px-3 py-2 text-sm focus:
|
|
500
|
+
className="w-full rounded-md border border-gray-200 px-3 py-2 text-sm focus:ring-2 focus:ring-gray-400/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
|
|
507
501
|
autoFocus
|
|
508
502
|
/>
|
|
509
503
|
<button
|
|
@@ -525,12 +519,11 @@ export function FilterBuilder({
|
|
|
525
519
|
selectedOperator !== 'date_preset' &&
|
|
526
520
|
selectedOperator !== 'between' && (
|
|
527
521
|
<div className="p-4">
|
|
528
|
-
<
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
autoFocus
|
|
522
|
+
<DatePicker
|
|
523
|
+
embedded
|
|
524
|
+
startDate={dateInputStart}
|
|
525
|
+
onDateChange={(start) => setDateInputStart(start)}
|
|
526
|
+
label="Choisir une date"
|
|
534
527
|
/>
|
|
535
528
|
<button
|
|
536
529
|
onClick={() => {
|
|
@@ -549,25 +542,17 @@ export function FilterBuilder({
|
|
|
549
542
|
{/* Date between */}
|
|
550
543
|
{selectedField.type === 'date' && selectedOperator === 'between' && (
|
|
551
544
|
<div className="space-y-3 p-4">
|
|
552
|
-
<
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
<label className="mb-1 block text-xs font-medium text-gray-500">Au</label>
|
|
564
|
-
<input
|
|
565
|
-
type="date"
|
|
566
|
-
value={dateInputEnd}
|
|
567
|
-
onChange={(e) => setDateInputEnd(e.target.value)}
|
|
568
|
-
className="w-full rounded-md border border-gray-200 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
|
|
569
|
-
/>
|
|
570
|
-
</div>
|
|
545
|
+
<DatePicker
|
|
546
|
+
embedded
|
|
547
|
+
isPeriod
|
|
548
|
+
startDate={dateInputStart}
|
|
549
|
+
endDate={dateInputEnd}
|
|
550
|
+
label="Période"
|
|
551
|
+
onDateChange={(start, end) => {
|
|
552
|
+
setDateInputStart(start);
|
|
553
|
+
if (end) setDateInputEnd(end);
|
|
554
|
+
}}
|
|
555
|
+
/>
|
|
571
556
|
<button
|
|
572
557
|
onClick={() => {
|
|
573
558
|
if (dateInputStart && dateInputEnd) {
|
|
@@ -91,7 +91,7 @@ export function SaveViewDialog({
|
|
|
91
91
|
value={name}
|
|
92
92
|
onChange={(e) => setName(e.target.value)}
|
|
93
93
|
placeholder="Ex: Nouveaux contacts cette semaine"
|
|
94
|
-
className="w-full rounded-lg border border-gray-200 px-3 py-2.5 text-sm focus:
|
|
94
|
+
className="w-full rounded-lg border border-gray-200 px-3 py-2.5 text-sm focus:ring-2 focus:ring-gray-400/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
|
|
95
95
|
/>
|
|
96
96
|
</div>
|
|
97
97
|
|
|
@@ -44,6 +44,8 @@ interface ViewsTabBarProps {
|
|
|
44
44
|
hasUnsavedChanges?: boolean;
|
|
45
45
|
activeFilters?: ViewFilter[];
|
|
46
46
|
entityType?: 'contacts' | 'companies';
|
|
47
|
+
/** Quand true, pas de bordure ni padding (intégré sur une ligne avec le titre) */
|
|
48
|
+
inline?: boolean;
|
|
47
49
|
}
|
|
48
50
|
|
|
49
51
|
export type { ViewPermissions };
|
|
@@ -65,6 +67,7 @@ export function ViewsTabBar({
|
|
|
65
67
|
hasUnsavedChanges,
|
|
66
68
|
activeFilters,
|
|
67
69
|
entityType = 'contacts',
|
|
70
|
+
inline = false,
|
|
68
71
|
}: ViewsTabBarProps) {
|
|
69
72
|
const [showAllViews, setShowAllViews] = useState(false);
|
|
70
73
|
const [contextMenu, setContextMenu] = useState<string | null>(null);
|
|
@@ -120,12 +123,18 @@ export function ViewsTabBar({
|
|
|
120
123
|
canEdit(view) || canDelete(view) || permissions.canCreate;
|
|
121
124
|
|
|
122
125
|
return (
|
|
123
|
-
<div
|
|
126
|
+
<div
|
|
127
|
+
className={cn(
|
|
128
|
+
'flex min-w-0 items-center gap-1',
|
|
129
|
+
!inline && 'border-b border-gray-200 bg-white px-4 sm:px-6 lg:px-8',
|
|
130
|
+
inline && 'flex-1 overflow-x-clip',
|
|
131
|
+
)}
|
|
132
|
+
>
|
|
124
133
|
{/* Vue "Tous les contacts" (défaut) */}
|
|
125
134
|
<button
|
|
126
135
|
onClick={() => onSelectView(null)}
|
|
127
136
|
className={cn(
|
|
128
|
-
'relative flex shrink-0 cursor-pointer items-center gap-1.5 border-b-2 px-3 py-2.5 text-sm font-medium whitespace-nowrap
|
|
137
|
+
'ui-tab-indicator relative flex shrink-0 cursor-pointer items-center gap-1.5 border-b-2 px-3 py-2.5 text-sm font-medium whitespace-nowrap',
|
|
129
138
|
activeViewId === null
|
|
130
139
|
? 'border-blue-600 text-blue-600'
|
|
131
140
|
: 'border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700',
|
|
@@ -150,7 +159,7 @@ export function ViewsTabBar({
|
|
|
150
159
|
<button
|
|
151
160
|
onClick={() => onSelectView(view.id)}
|
|
152
161
|
className={cn(
|
|
153
|
-
'relative flex cursor-pointer items-center gap-1.5 border-b-2 px-3 py-2.5 text-sm font-medium whitespace-nowrap
|
|
162
|
+
'ui-tab-indicator relative flex cursor-pointer items-center gap-1.5 border-b-2 px-3 py-2.5 text-sm font-medium whitespace-nowrap',
|
|
154
163
|
activeViewId === view.id
|
|
155
164
|
? 'border-blue-600 text-blue-600'
|
|
156
165
|
: 'border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700',
|
|
@@ -188,7 +197,7 @@ export function ViewsTabBar({
|
|
|
188
197
|
{contextMenu === view.id && (
|
|
189
198
|
<div
|
|
190
199
|
ref={contextMenuRef}
|
|
191
|
-
className="absolute top-full right-0 z-50 mt-1 w-48 rounded-lg border border-gray-200 bg-white py-1 shadow-lg"
|
|
200
|
+
className="ui-dropdown-enter absolute top-full right-0 z-50 mt-1 w-48 rounded-lg border border-gray-200 bg-white py-1 shadow-lg"
|
|
192
201
|
>
|
|
193
202
|
{canEdit(view) && (
|
|
194
203
|
<button
|
|
@@ -289,7 +298,7 @@ export function ViewsTabBar({
|
|
|
289
298
|
</button>
|
|
290
299
|
|
|
291
300
|
{showAllViews && (
|
|
292
|
-
<div className="absolute top-full left-0 z-50 mt-1 w-72 rounded-lg border border-gray-200 bg-white py-1 shadow-lg">
|
|
301
|
+
<div className="ui-dropdown-enter absolute top-full left-0 z-50 mt-1 w-72 rounded-lg border border-gray-200 bg-white py-1 shadow-lg">
|
|
293
302
|
<div className="px-3 py-2 text-xs font-semibold tracking-wide text-gray-400 uppercase">
|
|
294
303
|
Vues enregistrées
|
|
295
304
|
</div>
|
|
@@ -338,7 +347,7 @@ export function ViewsTabBar({
|
|
|
338
347
|
{contextMenu === view.id && (
|
|
339
348
|
<div
|
|
340
349
|
ref={contextMenuRef}
|
|
341
|
-
className="absolute top-full right-0 z-50 mt-1 w-48 rounded-lg border border-gray-200 bg-white py-1 shadow-lg"
|
|
350
|
+
className="ui-dropdown-enter absolute top-full right-0 z-50 mt-1 w-48 rounded-lg border border-gray-200 bg-white py-1 shadow-lg"
|
|
342
351
|
>
|
|
343
352
|
{canEdit(view) && (
|
|
344
353
|
<button
|
|
@@ -1,56 +1,69 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
Bar,
|
|
5
|
+
BarChart,
|
|
6
|
+
ResponsiveContainer,
|
|
7
|
+
Tooltip,
|
|
8
|
+
XAxis,
|
|
9
|
+
YAxis,
|
|
10
|
+
CartesianGrid,
|
|
11
|
+
Legend,
|
|
12
|
+
} from 'recharts';
|
|
13
|
+
import { useDashboardTheme } from '@/contexts/dashboard-theme-context';
|
|
4
14
|
|
|
5
15
|
interface ActivityChartProps {
|
|
6
|
-
data: Array<{ date: string; interactions: number; tasks: number }>;
|
|
16
|
+
readonly data: Array<{ date: string; interactions: number; tasks: number }>;
|
|
7
17
|
}
|
|
8
18
|
|
|
9
|
-
export function ActivityChart({ data }: ActivityChartProps) {
|
|
19
|
+
export function ActivityChart({ data }: Readonly<ActivityChartProps>) {
|
|
20
|
+
const { theme } = useDashboardTheme();
|
|
21
|
+
|
|
10
22
|
return (
|
|
11
|
-
<div className="rounded-
|
|
23
|
+
<div className="flex h-full flex-col rounded-2xl border border-gray-100 bg-white p-5 shadow-sm">
|
|
12
24
|
<div className="mb-4">
|
|
13
|
-
<h3 className="text-
|
|
14
|
-
<p className="mt-
|
|
25
|
+
<h3 className="text-base font-semibold text-gray-900">Activité des 7 Derniers Jours</h3>
|
|
26
|
+
<p className="mt-0.5 text-xs text-gray-400">Interactions et tâches créées</p>
|
|
15
27
|
</div>
|
|
16
|
-
<div className="h-
|
|
28
|
+
<div className="min-h-0 flex-1">
|
|
17
29
|
<ResponsiveContainer width="100%" height="100%">
|
|
18
|
-
<BarChart
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
<linearGradient id="tasksGradient" x1="0" y1="0" x2="0" y2="1">
|
|
25
|
-
<stop offset="0%" stopColor="#8b5cf6" stopOpacity={1} />
|
|
26
|
-
<stop offset="100%" stopColor="#7c3aed" stopOpacity={0.8} />
|
|
27
|
-
</linearGradient>
|
|
28
|
-
</defs>
|
|
30
|
+
<BarChart
|
|
31
|
+
data={data}
|
|
32
|
+
barCategoryGap="25%"
|
|
33
|
+
margin={{ top: 5, right: 5, left: -20, bottom: 0 }}
|
|
34
|
+
>
|
|
35
|
+
<CartesianGrid strokeDasharray="3 3" stroke="#f3f4f6" vertical={false} />
|
|
29
36
|
<XAxis
|
|
30
37
|
dataKey="date"
|
|
31
|
-
stroke="#
|
|
32
|
-
fontSize={
|
|
38
|
+
stroke="#d1d5db"
|
|
39
|
+
fontSize={11}
|
|
33
40
|
tickLine={false}
|
|
34
41
|
axisLine={false}
|
|
42
|
+
dy={8}
|
|
35
43
|
/>
|
|
36
|
-
<YAxis stroke="#
|
|
44
|
+
<YAxis stroke="#d1d5db" fontSize={11} tickLine={false} axisLine={false} width={40} />
|
|
37
45
|
<Tooltip
|
|
38
46
|
contentStyle={{
|
|
39
|
-
backgroundColor: '
|
|
40
|
-
border: '1px solid #
|
|
47
|
+
backgroundColor: '#fff',
|
|
48
|
+
border: '1px solid #f3f4f6',
|
|
41
49
|
borderRadius: '12px',
|
|
42
|
-
boxShadow: '0 4px
|
|
50
|
+
boxShadow: '0 4px 12px rgba(0,0,0,0.08)',
|
|
51
|
+
fontSize: '13px',
|
|
43
52
|
}}
|
|
44
53
|
labelStyle={{ color: '#374151', fontWeight: 600 }}
|
|
45
54
|
/>
|
|
46
|
-
<Legend
|
|
55
|
+
<Legend
|
|
56
|
+
wrapperStyle={{ paddingTop: '12px', fontSize: '12px' }}
|
|
57
|
+
iconType="circle"
|
|
58
|
+
iconSize={8}
|
|
59
|
+
/>
|
|
47
60
|
<Bar
|
|
48
61
|
dataKey="interactions"
|
|
49
|
-
fill=
|
|
50
|
-
radius={[
|
|
62
|
+
fill={theme.hex[500]}
|
|
63
|
+
radius={[6, 6, 0, 0]}
|
|
51
64
|
name="Interactions"
|
|
52
65
|
/>
|
|
53
|
-
<Bar dataKey="tasks" fill=
|
|
66
|
+
<Bar dataKey="tasks" fill={theme.hex[300]} radius={[6, 6, 0, 0]} name="Tâches" />
|
|
54
67
|
</BarChart>
|
|
55
68
|
</ResponsiveContainer>
|
|
56
69
|
</div>
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { X, Plus, BarChart3, ListTodo, TrendingUp } from 'lucide-react';
|
|
5
|
+
import { cn } from '@/lib/utils';
|
|
6
|
+
import { WIDGET_REGISTRY, type WidgetDefinition } from '@/lib/widget-registry';
|
|
7
|
+
|
|
8
|
+
interface AddWidgetDialogProps {
|
|
9
|
+
readonly isOpen: boolean;
|
|
10
|
+
readonly onClose: () => void;
|
|
11
|
+
readonly onAdd: (type: string, w: number, h: number) => void;
|
|
12
|
+
readonly existingTypes: string[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const CATEGORY_LABELS: Record<string, { label: string; icon: typeof BarChart3 }> = {
|
|
16
|
+
stat: { label: 'Statistiques', icon: TrendingUp },
|
|
17
|
+
chart: { label: 'Graphiques', icon: BarChart3 },
|
|
18
|
+
list: { label: 'Listes', icon: ListTodo },
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export function AddWidgetDialog({ isOpen, onClose, onAdd, existingTypes }: AddWidgetDialogProps) {
|
|
22
|
+
const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
|
|
23
|
+
|
|
24
|
+
if (!isOpen) return null;
|
|
25
|
+
|
|
26
|
+
const categories = ['stat', 'chart', 'list'] as const;
|
|
27
|
+
|
|
28
|
+
const filteredWidgets = selectedCategory
|
|
29
|
+
? WIDGET_REGISTRY.filter((w) => w.category === selectedCategory)
|
|
30
|
+
: WIDGET_REGISTRY;
|
|
31
|
+
|
|
32
|
+
const handleAdd = (widget: WidgetDefinition) => {
|
|
33
|
+
onAdd(widget.type, widget.defaultW, widget.defaultH);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<div className="ui-fade-in fixed inset-0 z-50 flex items-center justify-center">
|
|
38
|
+
<button
|
|
39
|
+
type="button"
|
|
40
|
+
className="absolute inset-0 cursor-pointer bg-black/40 backdrop-blur-sm"
|
|
41
|
+
onClick={onClose}
|
|
42
|
+
aria-label="Fermer la fenêtre"
|
|
43
|
+
/>
|
|
44
|
+
|
|
45
|
+
<div className="ui-scale-in relative z-10 w-full max-w-2xl rounded-2xl bg-white shadow-2xl">
|
|
46
|
+
<div className="flex items-center justify-between border-b border-gray-100 px-6 py-4">
|
|
47
|
+
<div>
|
|
48
|
+
<h2 className="text-lg font-semibold text-gray-900">Ajouter un Widget</h2>
|
|
49
|
+
<p className="mt-0.5 text-sm text-gray-500">
|
|
50
|
+
Choisissez un widget à ajouter au tableau de bord
|
|
51
|
+
</p>
|
|
52
|
+
</div>
|
|
53
|
+
<button
|
|
54
|
+
type="button"
|
|
55
|
+
onClick={onClose}
|
|
56
|
+
aria-label="Fermer"
|
|
57
|
+
className="flex h-8 w-8 cursor-pointer items-center justify-center rounded-lg text-gray-400 transition-colors hover:bg-gray-100 hover:text-gray-600"
|
|
58
|
+
>
|
|
59
|
+
<X className="h-4 w-4" />
|
|
60
|
+
</button>
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
<div className="flex gap-2 border-b border-gray-100 px-6 py-3">
|
|
64
|
+
<button
|
|
65
|
+
type="button"
|
|
66
|
+
onClick={() => setSelectedCategory(null)}
|
|
67
|
+
className={cn(
|
|
68
|
+
'cursor-pointer rounded-full px-3.5 py-1.5 text-xs font-medium transition-colors',
|
|
69
|
+
selectedCategory === null
|
|
70
|
+
? 'dash-pill-active'
|
|
71
|
+
: 'bg-gray-100 text-gray-600 hover:bg-gray-200',
|
|
72
|
+
)}
|
|
73
|
+
>
|
|
74
|
+
Tous
|
|
75
|
+
</button>
|
|
76
|
+
{categories.map((cat) => {
|
|
77
|
+
const { label, icon: Icon } = CATEGORY_LABELS[cat];
|
|
78
|
+
return (
|
|
79
|
+
<button
|
|
80
|
+
type="button"
|
|
81
|
+
key={cat}
|
|
82
|
+
onClick={() => setSelectedCategory(cat)}
|
|
83
|
+
className={cn(
|
|
84
|
+
'flex cursor-pointer items-center gap-1.5 rounded-full px-3.5 py-1.5 text-xs font-medium transition-colors',
|
|
85
|
+
selectedCategory === cat
|
|
86
|
+
? 'dash-pill-active'
|
|
87
|
+
: 'bg-gray-100 text-gray-600 hover:bg-gray-200',
|
|
88
|
+
)}
|
|
89
|
+
>
|
|
90
|
+
<Icon className="h-3 w-3" />
|
|
91
|
+
{label}
|
|
92
|
+
</button>
|
|
93
|
+
);
|
|
94
|
+
})}
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
<div className="max-h-[400px] overflow-y-auto p-6">
|
|
98
|
+
<div className="grid grid-cols-2 gap-3">
|
|
99
|
+
{filteredWidgets.map((widget) => {
|
|
100
|
+
const isAlreadyAdded = existingTypes.includes(widget.type);
|
|
101
|
+
const Icon = widget.icon;
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<button
|
|
105
|
+
type="button"
|
|
106
|
+
key={widget.type}
|
|
107
|
+
onClick={() => !isAlreadyAdded && handleAdd(widget)}
|
|
108
|
+
disabled={isAlreadyAdded}
|
|
109
|
+
className={cn(
|
|
110
|
+
'group flex items-start gap-3 rounded-xl border p-4 text-left transition-[color,background-color,border-color,box-shadow] duration-150',
|
|
111
|
+
isAlreadyAdded
|
|
112
|
+
? 'cursor-not-allowed border-gray-100 bg-gray-50 opacity-50'
|
|
113
|
+
: 'dash-hover-border dash-hover-bg border-gray-100 bg-white hover:shadow-sm',
|
|
114
|
+
)}
|
|
115
|
+
>
|
|
116
|
+
<div
|
|
117
|
+
className={cn(
|
|
118
|
+
'flex h-9 w-9 shrink-0 items-center justify-center rounded-lg',
|
|
119
|
+
isAlreadyAdded ? 'bg-gray-100' : 'dash-icon-box dash-icon-box-gh',
|
|
120
|
+
)}
|
|
121
|
+
>
|
|
122
|
+
<Icon
|
|
123
|
+
className={cn(
|
|
124
|
+
'h-4 w-4',
|
|
125
|
+
isAlreadyAdded ? 'text-gray-400' : 'dash-icon-color',
|
|
126
|
+
)}
|
|
127
|
+
/>
|
|
128
|
+
</div>
|
|
129
|
+
<div className="min-w-0 flex-1">
|
|
130
|
+
<div className="flex items-center justify-between">
|
|
131
|
+
<p className="text-sm font-medium text-gray-900">{widget.label}</p>
|
|
132
|
+
{isAlreadyAdded ? (
|
|
133
|
+
<span className="text-[10px] font-medium text-gray-400">Déjà ajouté</span>
|
|
134
|
+
) : (
|
|
135
|
+
<Plus className="dash-hover-text-gh h-4 w-4 text-gray-300 transition-colors" />
|
|
136
|
+
)}
|
|
137
|
+
</div>
|
|
138
|
+
<p className="mt-0.5 text-xs text-gray-500">{widget.description}</p>
|
|
139
|
+
<p className="mt-1 text-[10px] text-gray-400">
|
|
140
|
+
Taille : {widget.defaultW}x{widget.defaultH}
|
|
141
|
+
</p>
|
|
142
|
+
</div>
|
|
143
|
+
</button>
|
|
144
|
+
);
|
|
145
|
+
})}
|
|
146
|
+
</div>
|
|
147
|
+
</div>
|
|
148
|
+
|
|
149
|
+
<div className="border-t border-gray-100 px-6 py-3">
|
|
150
|
+
<p className="text-center text-xs text-gray-400">
|
|
151
|
+
Les widgets peuvent être déplacés et redimensionnés après ajout
|
|
152
|
+
</p>
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
);
|
|
157
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useRef, useEffect } from 'react';
|
|
4
|
+
import { Palette, Check } from 'lucide-react';
|
|
5
|
+
import { useDashboardTheme } from '@/contexts/dashboard-theme-context';
|
|
6
|
+
|
|
7
|
+
export function DashboardColorPicker() {
|
|
8
|
+
const { theme, setThemeKey, themes } = useDashboardTheme();
|
|
9
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
10
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
11
|
+
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
function handleClickOutside(event: MouseEvent) {
|
|
14
|
+
if (ref.current && !ref.current.contains(event.target as Node)) {
|
|
15
|
+
setIsOpen(false);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
if (isOpen) {
|
|
19
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
20
|
+
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
21
|
+
}
|
|
22
|
+
}, [isOpen]);
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<div ref={ref} className="relative">
|
|
26
|
+
<button
|
|
27
|
+
onClick={() => setIsOpen(!isOpen)}
|
|
28
|
+
className="inline-flex h-10 w-10 cursor-pointer items-center justify-center rounded-xl border border-gray-200 bg-white shadow-sm transition-[background-color,box-shadow,transform] duration-150 hover:bg-gray-50 hover:shadow-md active:scale-[0.98]"
|
|
29
|
+
title="Changer la couleur du thème"
|
|
30
|
+
aria-label="Changer la couleur du thème"
|
|
31
|
+
>
|
|
32
|
+
<Palette aria-hidden="true" className="h-4 w-4 text-gray-600" />
|
|
33
|
+
</button>
|
|
34
|
+
|
|
35
|
+
{isOpen && (
|
|
36
|
+
<div className="absolute right-0 z-50 mt-2 w-56 rounded-xl border border-gray-100 bg-white p-3 shadow-xl">
|
|
37
|
+
<p className="mb-2.5 text-[11px] font-medium tracking-wider text-gray-400 uppercase">
|
|
38
|
+
Couleur d'accent
|
|
39
|
+
</p>
|
|
40
|
+
<div className="grid grid-cols-4 gap-2">
|
|
41
|
+
{themes.map((t) => (
|
|
42
|
+
<button
|
|
43
|
+
key={t.key}
|
|
44
|
+
onClick={() => {
|
|
45
|
+
setThemeKey(t.key);
|
|
46
|
+
setIsOpen(false);
|
|
47
|
+
}}
|
|
48
|
+
className="group relative flex h-10 w-10 cursor-pointer items-center justify-center rounded-xl transition-transform duration-150 hover:scale-110"
|
|
49
|
+
style={{ backgroundColor: t.hex[500] }}
|
|
50
|
+
title={t.label}
|
|
51
|
+
aria-label={t.label}
|
|
52
|
+
>
|
|
53
|
+
{theme.key === t.key && <Check aria-hidden="true" className="h-4 w-4 text-white drop-shadow-sm" />}
|
|
54
|
+
<span className="absolute -bottom-5 left-1/2 -translate-x-1/2 text-[10px] whitespace-nowrap text-gray-500 opacity-0 transition-opacity group-hover:opacity-100">
|
|
55
|
+
{t.label}
|
|
56
|
+
</span>
|
|
57
|
+
</button>
|
|
58
|
+
))}
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
)}
|
|
62
|
+
</div>
|
|
63
|
+
);
|
|
64
|
+
}
|