create-crm-tmp 1.1.3 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/template/.prettierignore +2 -0
- package/template/README.md +1 -1
- package/template/components.json +22 -0
- package/template/exemple-contacts.csv +54 -0
- package/template/next.config.ts +27 -1
- package/template/package.json +51 -16
- package/template/prisma/schema.prisma +807 -58
- package/template/skills-lock.json +25 -0
- package/template/src/app/(auth)/invite/[token]/page.tsx +21 -24
- package/template/src/app/(auth)/reset-password/complete/page.tsx +12 -21
- package/template/src/app/(auth)/reset-password/page.tsx +12 -8
- package/template/src/app/(auth)/reset-password/verify/page.tsx +12 -8
- package/template/src/app/(auth)/signin/page.tsx +20 -17
- package/template/src/app/(dashboard)/agenda/page.tsx +2232 -2189
- package/template/src/app/(dashboard)/automatisation/[id]/page.tsx +10 -7
- package/template/src/app/(dashboard)/automatisation/_components/workflow-editor.tsx +680 -323
- package/template/src/app/(dashboard)/automatisation/new/page.tsx +11 -8
- package/template/src/app/(dashboard)/automatisation/page.tsx +473 -180
- package/template/src/app/(dashboard)/closing/page.tsx +500 -468
- package/template/src/app/(dashboard)/contacts/[id]/page.tsx +5049 -4110
- package/template/src/app/(dashboard)/contacts/companies/[id]/page.tsx +1703 -0
- package/template/src/app/(dashboard)/contacts/loading.tsx +13 -0
- package/template/src/app/(dashboard)/contacts/page.tsx +3776 -2064
- package/template/src/app/(dashboard)/dashboard/page.tsx +37 -519
- package/template/src/app/(dashboard)/error.tsx +37 -0
- package/template/src/app/(dashboard)/layout.tsx +1 -1
- package/template/src/app/(dashboard)/loading.tsx +5 -0
- package/template/src/app/(dashboard)/settings/loading.tsx +19 -0
- package/template/src/app/(dashboard)/settings/page.tsx +2685 -2489
- package/template/src/app/(dashboard)/templates/page.tsx +500 -300
- package/template/src/app/(dashboard)/users/list/page.tsx +356 -350
- package/template/src/app/(dashboard)/users/page.tsx +279 -310
- package/template/src/app/(dashboard)/users/permissions/page.tsx +104 -99
- package/template/src/app/(dashboard)/users/roles/page.tsx +164 -137
- package/template/src/app/api/audit-logs/route.ts +1 -1
- package/template/src/app/api/auth/google/callback/route.ts +8 -5
- package/template/src/app/api/auth/google/disconnect/route.ts +2 -2
- package/template/src/app/api/companies/[id]/activities/route.ts +131 -0
- package/template/src/app/api/companies/[id]/route.ts +195 -0
- package/template/src/app/api/companies/export/route.ts +206 -0
- package/template/src/app/api/companies/route.ts +166 -0
- package/template/src/app/api/contact-views/[id]/pin/route.ts +69 -0
- package/template/src/app/api/contact-views/[id]/route.ts +197 -0
- package/template/src/app/api/contact-views/route.ts +146 -0
- package/template/src/app/api/contacts/[id]/files/[fileId]/preview/route.ts +77 -0
- package/template/src/app/api/contacts/[id]/files/[fileId]/route.ts +7 -17
- package/template/src/app/api/contacts/[id]/files/route.ts +83 -44
- package/template/src/app/api/contacts/[id]/interactions/route.ts +37 -0
- package/template/src/app/api/contacts/[id]/kyc/route.ts +71 -0
- package/template/src/app/api/contacts/[id]/meet/route.ts +38 -29
- package/template/src/app/api/contacts/[id]/route.ts +111 -20
- package/template/src/app/api/contacts/[id]/send-email/route.ts +6 -0
- package/template/src/app/api/contacts/[id]/workflows/run/route.ts +61 -0
- package/template/src/app/api/contacts/export/route.ts +13 -18
- package/template/src/app/api/contacts/import/route.ts +22 -19
- package/template/src/app/api/contacts/import-preview/route.ts +139 -0
- package/template/src/app/api/contacts/route.ts +202 -49
- package/template/src/app/api/dashboard/stats/route.ts +9 -292
- package/template/src/app/api/integrations/google-sheet/sync/route.ts +203 -185
- package/template/src/app/api/invite/complete/route.ts +20 -23
- package/template/src/app/api/reminders/route.ts +1 -0
- package/template/src/app/api/reset-password/complete/route.ts +11 -13
- package/template/src/app/api/send/route.ts +9 -43
- package/template/src/app/api/settings/closing-reasons/[id]/route.ts +10 -21
- package/template/src/app/api/settings/closing-reasons/route.ts +10 -21
- package/template/src/app/api/settings/company/route.ts +19 -26
- package/template/src/app/api/settings/google-ads/[id]/route.ts +20 -23
- package/template/src/app/api/settings/google-ads/route.ts +20 -23
- package/template/src/app/api/settings/google-sheet/[id]/route.ts +20 -23
- package/template/src/app/api/settings/google-sheet/auto-map/route.ts +23 -32
- package/template/src/app/api/settings/google-sheet/preview/route.ts +104 -0
- package/template/src/app/api/settings/google-sheet/route.ts +20 -23
- package/template/src/app/api/settings/meta-leads/[id]/route.ts +20 -23
- package/template/src/app/api/settings/meta-leads/route.ts +20 -23
- package/template/src/app/api/settings/statuses/[id]/route.ts +29 -32
- package/template/src/app/api/settings/statuses/route.ts +24 -22
- package/template/src/app/api/statuses/route.ts +2 -5
- package/template/src/app/api/tasks/[id]/attendees/route.ts +14 -7
- package/template/src/app/api/tasks/[id]/route.ts +173 -137
- package/template/src/app/api/tasks/meet/route.ts +11 -8
- package/template/src/app/api/tasks/route.ts +155 -95
- package/template/src/app/api/templates/[id]/route.ts +22 -13
- package/template/src/app/api/templates/route.ts +22 -5
- package/template/src/app/api/users/[id]/resend-invite/route.ts +95 -0
- package/template/src/app/api/users/[id]/route.ts +2 -2
- package/template/src/app/api/users/commercials/route.ts +38 -0
- package/template/src/app/api/users/for-agenda/route.ts +1 -2
- package/template/src/app/api/users/route.ts +89 -34
- package/template/src/app/api/webhooks/google-ads/route.ts +20 -1
- package/template/src/app/api/webhooks/meta-leads/route.ts +18 -1
- package/template/src/app/api/workflows/[id]/route.ts +33 -6
- package/template/src/app/api/workflows/process/route.ts +510 -146
- package/template/src/app/api/workflows/route.ts +46 -4
- package/template/src/app/globals.css +243 -101
- package/template/src/app/layout.tsx +19 -8
- package/template/src/app/page.tsx +37 -7
- package/template/src/components/address-autocomplete.tsx +232 -0
- package/template/src/components/contacts/filter-bar.tsx +181 -0
- package/template/src/components/contacts/filter-builder.tsx +589 -0
- package/template/src/components/contacts/save-view-dialog.tsx +160 -0
- package/template/src/components/contacts/views-tab-bar.tsx +440 -0
- package/template/src/components/dashboard/activity-chart.tsx +31 -39
- package/template/src/components/dashboard/dashboard-content.tsx +79 -0
- package/template/src/components/dashboard/stat-card.tsx +40 -42
- package/template/src/components/dashboard/tasks-pie-chart.tsx +34 -37
- package/template/src/components/dashboard/upcoming-tasks-list.tsx +78 -72
- package/template/src/components/date-picker.tsx +396 -0
- package/template/src/components/editor.tsx +27 -13
- package/template/src/components/email-template.tsx +4 -2
- package/template/src/components/global-search.tsx +358 -0
- package/template/src/components/header.tsx +57 -62
- package/template/src/components/invitation-email-template.tsx +4 -2
- package/template/src/components/lazy-editor.tsx +11 -0
- package/template/src/components/meet-cancellation-email-template.tsx +11 -3
- package/template/src/components/meet-confirmation-email-template.tsx +10 -3
- package/template/src/components/meet-update-email-template.tsx +10 -3
- package/template/src/components/page-header.tsx +19 -15
- package/template/src/components/protected-page.tsx +94 -0
- package/template/src/components/reset-password-email-template.tsx +4 -2
- package/template/src/components/sidebar.tsx +92 -94
- package/template/src/components/skeleton.tsx +128 -42
- package/template/src/components/ui/accordion.tsx +64 -0
- package/template/src/components/ui/alert-dialog.tsx +139 -0
- package/template/src/components/ui/button.tsx +60 -0
- package/template/src/components/view-as-banner.tsx +1 -1
- package/template/src/components/view-as-modal.tsx +21 -16
- package/template/src/config/nav-pages.ts +108 -0
- package/template/src/contexts/app-toast-context.tsx +174 -0
- package/template/src/contexts/sidebar-context.tsx +16 -47
- package/template/src/contexts/task-reminder-context.tsx +6 -6
- package/template/src/contexts/view-as-context.tsx +11 -16
- package/template/src/hooks/use-alert.tsx +65 -0
- package/template/src/hooks/use-confirm.tsx +87 -0
- package/template/src/hooks/use-contact-views.ts +140 -0
- package/template/src/hooks/use-contacts.ts +69 -0
- package/template/src/hooks/use-fetch.ts +17 -0
- package/template/src/hooks/use-focus-trap.ts +73 -0
- package/template/src/hooks/use-statuses.ts +22 -0
- package/template/src/lib/address-api.ts +155 -0
- package/template/src/lib/cache.ts +73 -0
- package/template/src/lib/check-permission.ts +12 -177
- package/template/src/lib/contact-interactions.ts +3 -1
- package/template/src/lib/contact-view-filters.ts +341 -0
- package/template/src/lib/dashboard-stats.ts +224 -0
- package/template/src/lib/date-utils.ts +49 -0
- package/template/src/lib/get-auth-user.ts +25 -0
- package/template/src/lib/google-calendar.ts +54 -12
- package/template/src/lib/google-drive.ts +796 -75
- package/template/src/lib/google-fetch.ts +63 -0
- package/template/src/lib/local-storage.ts +34 -0
- package/template/src/lib/permissions.ts +245 -47
- package/template/src/lib/prisma.ts +11 -11
- package/template/src/lib/roles.ts +12 -15
- package/template/src/lib/template-variables.ts +67 -33
- package/template/src/lib/utils.ts +26 -11
- package/template/src/lib/workflow-executor.ts +445 -228
- package/template/src/proxy.ts +34 -73
- package/template/src/types/contact-views.ts +351 -0
- package/template/src/types/yousign.ts +52 -0
- package/template/vercel.json +12 -0
- package/template/WORKFLOWS_CRON.md +0 -185
- package/template/prisma/migrations/20251126144728_init/migration.sql +0 -78
- package/template/prisma/migrations/20251126155204_add_user_roles/migration.sql +0 -5
- package/template/prisma/migrations/20251128095126_add_company_info/migration.sql +0 -19
- package/template/prisma/migrations/20251128123321_add_smtp_config/migration.sql +0 -22
- package/template/prisma/migrations/20251128132303_add_status/migration.sql +0 -23
- package/template/prisma/migrations/20251201102207_add_user_active/migration.sql +0 -75
- package/template/prisma/migrations/20251201105507_add_email_signature/migration.sql +0 -2
- package/template/prisma/migrations/20251201151122_add_tasks/migration.sql +0 -45
- package/template/prisma/migrations/20251202111854_add_task_reminder/migration.sql +0 -2
- package/template/prisma/migrations/20251202135859_add_google_meet_integration/migration.sql +0 -27
- package/template/prisma/migrations/20251203103317_add_meta_lead_integration/migration.sql +0 -20
- package/template/prisma/migrations/20251203104002_add_google_ads_integration/migration.sql +0 -18
- package/template/prisma/migrations/20251203112122_add_google_sheet_integration/migration.sql +0 -32
- package/template/prisma/migrations/20251203153853_allow_multiple_integration_configs/migration.sql +0 -20
- package/template/prisma/migrations/20251205141705_update_user_roles/migration.sql +0 -12
- package/template/prisma/migrations/20251205150000_add_commercial_and_telepro_assignment/migration.sql +0 -21
- package/template/prisma/migrations/20251205160000_add_interaction_logging/migration.sql +0 -11
- package/template/prisma/migrations/20251208090314_add_automatic_interaction_types/migration.sql +0 -12
- package/template/prisma/migrations/20251208094843_mg/migration.sql +0 -14
- package/template/prisma/migrations/20251208100000_add_company_support/migration.sql +0 -14
- package/template/prisma/migrations/20251208110000_add_templates/migration.sql +0 -26
- package/template/prisma/migrations/20251208141304_add_video_conference_task_type/migration.sql +0 -2
- package/template/prisma/migrations/20251209104759_add_internal_note_to_task/migration.sql +0 -2
- package/template/prisma/migrations/20251209134803_add_company_field/migration.sql +0 -2
- package/template/prisma/migrations/20251209150000_rename_company_to_company_name/migration.sql +0 -3
- package/template/prisma/migrations/20251209150016_add_email_tracking/migration.sql +0 -21
- package/template/prisma/migrations/20251209155908_add_notify_contact_to_task/migration.sql +0 -2
- package/template/prisma/migrations/20251210110019_add_appointment_types/migration.sql +0 -10
- package/template/prisma/migrations/20251210113928_add_contact_files/migration.sql +0 -26
- package/template/prisma/migrations/20251212132339_add_custom_roles/migration.sql +0 -24
- package/template/prisma/migrations/20251215104448_add_file_interaction_types/migration.sql +0 -11
- package/template/prisma/migrations/20251215145616_add_closing_reasons/migration.sql +0 -12
- package/template/prisma/migrations/20251216140850_add_log_users/migration.sql +0 -25
- package/template/prisma/migrations/20251216151000_rename_perdu_to_ferme/migration.sql +0 -8
- package/template/prisma/migrations/20251216162318_add_column_mappings_to_google_sheet/migration.sql +0 -2
- package/template/prisma/migrations/20251216185127_add_workflows/migration.sql +0 -80
- package/template/prisma/migrations/20251216192237_add_scheduled_workflow_actions/migration.sql +0 -32
- package/template/prisma/migrations/20251220000000_add_task_interaction_type/migration.sql +0 -4
- package/template/prisma/migrations/20251221000000_add_task_type/migration.sql +0 -3
- package/template/prisma/migrations/20251221000001_add_event_color/migration.sql +0 -23
- package/template/prisma/migrations/20260210114913_add_dashboard_widget/migration.sql +0 -20
- package/template/prisma/migrations/20260226093949_fix_cascade_on_user_delete/migration.sql +0 -69
- package/template/prisma/migrations/migration_lock.toml +0 -3
- package/template/src/app/(dashboard)/users/layout.tsx +0 -30
- package/template/src/app/api/dashboard/widgets/[id]/route.ts +0 -47
- package/template/src/app/api/dashboard/widgets/route.ts +0 -181
- package/template/src/components/dashboard/add-widget-dialog.tsx +0 -161
- package/template/src/components/dashboard/color-picker.tsx +0 -65
- package/template/src/components/dashboard/contacts-chart.tsx +0 -69
- package/template/src/components/dashboard/interactions-by-type-chart.tsx +0 -121
- package/template/src/components/dashboard/recent-activity.tsx +0 -157
- package/template/src/components/dashboard/status-distribution-chart.tsx +0 -82
- package/template/src/components/dashboard/top-contacts-list.tsx +0 -119
- package/template/src/components/dashboard/widget-wrapper.tsx +0 -39
- package/template/src/contexts/dashboard-theme-context.tsx +0 -58
- package/template/src/lib/dashboard-themes.ts +0 -140
- package/template/src/lib/default-widgets.ts +0 -14
- package/template/src/lib/widget-registry.ts +0 -177
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useRef, useEffect } from 'react';
|
|
4
|
+
import { X, Globe, Lock } from 'lucide-react';
|
|
5
|
+
import { useFocusTrap } from '@/hooks/use-focus-trap';
|
|
6
|
+
|
|
7
|
+
interface SaveViewDialogProps {
|
|
8
|
+
open: boolean;
|
|
9
|
+
onClose: () => void;
|
|
10
|
+
onSave: (name: string, isPublic: boolean) => void;
|
|
11
|
+
mode: 'create' | 'save_as' | 'rename';
|
|
12
|
+
initialName?: string;
|
|
13
|
+
initialPublic?: boolean;
|
|
14
|
+
loading?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function SaveViewDialog({
|
|
18
|
+
open,
|
|
19
|
+
onClose,
|
|
20
|
+
onSave,
|
|
21
|
+
mode,
|
|
22
|
+
initialName = '',
|
|
23
|
+
initialPublic = false,
|
|
24
|
+
loading = false,
|
|
25
|
+
}: SaveViewDialogProps) {
|
|
26
|
+
const [name, setName] = useState(initialName);
|
|
27
|
+
const [isPublic, setIsPublic] = useState(initialPublic);
|
|
28
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
29
|
+
const panelRef = useRef<HTMLDivElement>(null);
|
|
30
|
+
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
if (open) {
|
|
33
|
+
setName(initialName);
|
|
34
|
+
setIsPublic(initialPublic);
|
|
35
|
+
}
|
|
36
|
+
}, [open, initialName, initialPublic]);
|
|
37
|
+
|
|
38
|
+
useFocusTrap(open, panelRef, { onClose, initialFocusRef: inputRef });
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
function handleClickOutside(e: MouseEvent) {
|
|
42
|
+
if (panelRef.current && !panelRef.current.contains(e.target as Node)) {
|
|
43
|
+
onClose();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (open) {
|
|
47
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
48
|
+
}
|
|
49
|
+
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
50
|
+
}, [open, onClose]);
|
|
51
|
+
|
|
52
|
+
function handleSubmit(e: React.FormEvent) {
|
|
53
|
+
e.preventDefault();
|
|
54
|
+
if (!name.trim()) return;
|
|
55
|
+
onSave(name.trim(), isPublic);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (!open) return null;
|
|
59
|
+
|
|
60
|
+
const titles = {
|
|
61
|
+
create: 'Créer une vue',
|
|
62
|
+
save_as: 'Enregistrer sous',
|
|
63
|
+
rename: 'Renommer la vue',
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/30">
|
|
68
|
+
<div
|
|
69
|
+
ref={panelRef}
|
|
70
|
+
className="w-full max-w-sm rounded-xl border border-gray-200 bg-white shadow-xl"
|
|
71
|
+
>
|
|
72
|
+
<div className="flex items-center justify-between border-b border-gray-100 px-5 py-4">
|
|
73
|
+
<h3 className="text-base font-semibold text-gray-900">{titles[mode]}</h3>
|
|
74
|
+
<button
|
|
75
|
+
onClick={onClose}
|
|
76
|
+
className="cursor-pointer rounded p-1 text-gray-400 hover:bg-gray-100 hover:text-gray-600"
|
|
77
|
+
>
|
|
78
|
+
<X className="h-4 w-4" />
|
|
79
|
+
</button>
|
|
80
|
+
</div>
|
|
81
|
+
|
|
82
|
+
<form onSubmit={handleSubmit} className="space-y-4 p-5">
|
|
83
|
+
<div>
|
|
84
|
+
<label htmlFor="view-name" className="mb-1.5 block text-sm font-medium text-gray-700">
|
|
85
|
+
Nom de la vue
|
|
86
|
+
</label>
|
|
87
|
+
<input
|
|
88
|
+
ref={inputRef}
|
|
89
|
+
id="view-name"
|
|
90
|
+
type="text"
|
|
91
|
+
value={name}
|
|
92
|
+
onChange={(e) => setName(e.target.value)}
|
|
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:outline-none focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
|
|
95
|
+
/>
|
|
96
|
+
</div>
|
|
97
|
+
|
|
98
|
+
{mode !== 'rename' && (
|
|
99
|
+
<div>
|
|
100
|
+
<label className="mb-2 block text-sm font-medium text-gray-700">Visibilité</label>
|
|
101
|
+
<div className="space-y-2">
|
|
102
|
+
<label className="flex cursor-pointer items-start gap-3 rounded-lg border border-gray-200 p-3 transition-colors hover:bg-gray-50 has-[:checked]:border-blue-200 has-[:checked]:bg-blue-50">
|
|
103
|
+
<input
|
|
104
|
+
type="radio"
|
|
105
|
+
name="visibility"
|
|
106
|
+
checked={!isPublic}
|
|
107
|
+
onChange={() => setIsPublic(false)}
|
|
108
|
+
className="mt-0.5 accent-blue-600"
|
|
109
|
+
/>
|
|
110
|
+
<div className="flex-1">
|
|
111
|
+
<div className="flex items-center gap-1.5 text-sm font-medium text-gray-900">
|
|
112
|
+
<Lock className="h-3.5 w-3.5" />
|
|
113
|
+
Privée
|
|
114
|
+
</div>
|
|
115
|
+
<p className="mt-0.5 text-xs text-gray-500">Visible uniquement par vous</p>
|
|
116
|
+
</div>
|
|
117
|
+
</label>
|
|
118
|
+
<label className="flex cursor-pointer items-start gap-3 rounded-lg border border-gray-200 p-3 transition-colors hover:bg-gray-50 has-[:checked]:border-blue-200 has-[:checked]:bg-blue-50">
|
|
119
|
+
<input
|
|
120
|
+
type="radio"
|
|
121
|
+
name="visibility"
|
|
122
|
+
checked={isPublic}
|
|
123
|
+
onChange={() => setIsPublic(true)}
|
|
124
|
+
className="mt-0.5 accent-blue-600"
|
|
125
|
+
/>
|
|
126
|
+
<div className="flex-1">
|
|
127
|
+
<div className="flex items-center gap-1.5 text-sm font-medium text-gray-900">
|
|
128
|
+
<Globe className="h-3.5 w-3.5" />
|
|
129
|
+
Publique
|
|
130
|
+
</div>
|
|
131
|
+
<p className="mt-0.5 text-xs text-gray-500">
|
|
132
|
+
Visible par tous les utilisateurs du CRM
|
|
133
|
+
</p>
|
|
134
|
+
</div>
|
|
135
|
+
</label>
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
)}
|
|
139
|
+
|
|
140
|
+
<div className="flex items-center justify-end gap-3 pt-2">
|
|
141
|
+
<button
|
|
142
|
+
type="button"
|
|
143
|
+
onClick={onClose}
|
|
144
|
+
className="cursor-pointer rounded-lg px-4 py-2 text-sm font-medium text-gray-700 transition-colors hover:bg-gray-100"
|
|
145
|
+
>
|
|
146
|
+
Annuler
|
|
147
|
+
</button>
|
|
148
|
+
<button
|
|
149
|
+
type="submit"
|
|
150
|
+
disabled={!name.trim() || loading}
|
|
151
|
+
className="cursor-pointer rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-700 disabled:cursor-not-allowed disabled:opacity-50"
|
|
152
|
+
>
|
|
153
|
+
{loading ? 'Enregistrement...' : mode === 'rename' ? 'Renommer' : 'Enregistrer'}
|
|
154
|
+
</button>
|
|
155
|
+
</div>
|
|
156
|
+
</form>
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
);
|
|
160
|
+
}
|
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useRef, useEffect } from 'react';
|
|
4
|
+
import {
|
|
5
|
+
Plus,
|
|
6
|
+
ChevronDown,
|
|
7
|
+
MoreHorizontal,
|
|
8
|
+
Pencil,
|
|
9
|
+
Copy,
|
|
10
|
+
Trash2,
|
|
11
|
+
Globe,
|
|
12
|
+
Lock,
|
|
13
|
+
Pin,
|
|
14
|
+
PinOff,
|
|
15
|
+
Users,
|
|
16
|
+
Building2,
|
|
17
|
+
} from 'lucide-react';
|
|
18
|
+
import { cn } from '@/lib/utils';
|
|
19
|
+
import type { ContactViewData, ViewFilter } from '@/types/contact-views';
|
|
20
|
+
|
|
21
|
+
interface ViewPermissions {
|
|
22
|
+
canCreate: boolean;
|
|
23
|
+
canEditOwn: boolean;
|
|
24
|
+
canEditAll: boolean;
|
|
25
|
+
canDeleteOwn: boolean;
|
|
26
|
+
canDeleteAll: boolean;
|
|
27
|
+
canShare: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface ViewsTabBarProps {
|
|
31
|
+
views: ContactViewData[];
|
|
32
|
+
activeViewId: string | null;
|
|
33
|
+
currentUserId: string;
|
|
34
|
+
permissions: ViewPermissions;
|
|
35
|
+
onSelectView: (viewId: string | null) => void;
|
|
36
|
+
onCreateView: () => void;
|
|
37
|
+
onRenameView: (view: ContactViewData) => void;
|
|
38
|
+
onCloneView: (view: ContactViewData) => void;
|
|
39
|
+
onDeleteView: (view: ContactViewData) => void;
|
|
40
|
+
onTogglePublic: (view: ContactViewData) => void;
|
|
41
|
+
onTogglePin: (view: ContactViewData) => void;
|
|
42
|
+
onDropdownOpenChange?: (open: boolean) => void;
|
|
43
|
+
onContextMenuOpenChange?: (open: boolean) => void;
|
|
44
|
+
hasUnsavedChanges?: boolean;
|
|
45
|
+
activeFilters?: ViewFilter[];
|
|
46
|
+
entityType?: 'contacts' | 'companies';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export type { ViewPermissions };
|
|
50
|
+
|
|
51
|
+
export function ViewsTabBar({
|
|
52
|
+
views,
|
|
53
|
+
activeViewId,
|
|
54
|
+
currentUserId,
|
|
55
|
+
permissions,
|
|
56
|
+
onSelectView,
|
|
57
|
+
onCreateView,
|
|
58
|
+
onRenameView,
|
|
59
|
+
onCloneView,
|
|
60
|
+
onDeleteView,
|
|
61
|
+
onTogglePublic,
|
|
62
|
+
onTogglePin,
|
|
63
|
+
onDropdownOpenChange,
|
|
64
|
+
onContextMenuOpenChange,
|
|
65
|
+
hasUnsavedChanges,
|
|
66
|
+
activeFilters,
|
|
67
|
+
entityType = 'contacts',
|
|
68
|
+
}: ViewsTabBarProps) {
|
|
69
|
+
const [showAllViews, setShowAllViews] = useState(false);
|
|
70
|
+
const [contextMenu, setContextMenu] = useState<string | null>(null);
|
|
71
|
+
const allViewsRef = useRef<HTMLDivElement>(null);
|
|
72
|
+
const contextMenuRef = useRef<HTMLDivElement>(null);
|
|
73
|
+
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
onDropdownOpenChange?.(showAllViews);
|
|
76
|
+
}, [showAllViews, onDropdownOpenChange]);
|
|
77
|
+
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
onContextMenuOpenChange?.(contextMenu !== null);
|
|
80
|
+
}, [contextMenu, onContextMenuOpenChange]);
|
|
81
|
+
|
|
82
|
+
const pinnedViews = views
|
|
83
|
+
.filter((v) => v.pinOrder != null)
|
|
84
|
+
.sort((a, b) => (a.pinOrder ?? 0) - (b.pinOrder ?? 0));
|
|
85
|
+
const unpinnedViews = views
|
|
86
|
+
.filter((v) => v.pinOrder == null)
|
|
87
|
+
.sort((a, b) => {
|
|
88
|
+
const byCreated = new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
|
|
89
|
+
if (byCreated !== 0) return byCreated;
|
|
90
|
+
return (a.name || '').localeCompare(b.name || '', 'fr');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
useEffect(() => {
|
|
94
|
+
function handleClickOutside(e: MouseEvent) {
|
|
95
|
+
if (allViewsRef.current && !allViewsRef.current.contains(e.target as Node)) {
|
|
96
|
+
setShowAllViews(false);
|
|
97
|
+
}
|
|
98
|
+
if (contextMenuRef.current && !contextMenuRef.current.contains(e.target as Node)) {
|
|
99
|
+
setContextMenu(null);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
103
|
+
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
104
|
+
}, []);
|
|
105
|
+
|
|
106
|
+
const getFilterCount = (view: ContactViewData) => {
|
|
107
|
+
const filters = view.filters as ViewFilter[];
|
|
108
|
+
return Array.isArray(filters) ? filters.length : 0;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const isOwner = (view: ContactViewData) => view.userId === currentUserId;
|
|
112
|
+
|
|
113
|
+
const canEdit = (view: ContactViewData) =>
|
|
114
|
+
isOwner(view) ? permissions.canEditOwn : permissions.canEditAll;
|
|
115
|
+
|
|
116
|
+
const canDelete = (view: ContactViewData) =>
|
|
117
|
+
isOwner(view) ? permissions.canDeleteOwn : permissions.canDeleteAll;
|
|
118
|
+
|
|
119
|
+
const hasContextActions = (view: ContactViewData) =>
|
|
120
|
+
canEdit(view) || canDelete(view) || permissions.canCreate;
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
<div className="flex items-center gap-1 border-b border-gray-200 bg-white px-4 sm:px-6 lg:px-8">
|
|
124
|
+
{/* Vue "Tous les contacts" (défaut) */}
|
|
125
|
+
<button
|
|
126
|
+
onClick={() => onSelectView(null)}
|
|
127
|
+
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 transition-colors',
|
|
129
|
+
activeViewId === null
|
|
130
|
+
? 'border-blue-600 text-blue-600'
|
|
131
|
+
: 'border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700',
|
|
132
|
+
)}
|
|
133
|
+
>
|
|
134
|
+
{entityType === 'companies' ? (
|
|
135
|
+
<Building2 className="h-3.5 w-3.5" />
|
|
136
|
+
) : (
|
|
137
|
+
<Users className="h-3.5 w-3.5" />
|
|
138
|
+
)}
|
|
139
|
+
{entityType === 'companies' ? 'Toutes les entreprises' : 'Tous les contacts'}
|
|
140
|
+
{activeViewId === null && activeFilters && activeFilters.length > 0 && (
|
|
141
|
+
<span className="ml-1 inline-flex h-4 min-w-4 items-center justify-center rounded-full bg-blue-100 px-1 text-[10px] font-semibold text-blue-700">
|
|
142
|
+
{activeFilters.length}
|
|
143
|
+
</span>
|
|
144
|
+
)}
|
|
145
|
+
</button>
|
|
146
|
+
|
|
147
|
+
{/* Vues épinglées */}
|
|
148
|
+
{pinnedViews.map((view) => (
|
|
149
|
+
<div key={view.id} className="group relative flex shrink-0 items-center">
|
|
150
|
+
<button
|
|
151
|
+
onClick={() => onSelectView(view.id)}
|
|
152
|
+
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 transition-colors',
|
|
154
|
+
activeViewId === view.id
|
|
155
|
+
? 'border-blue-600 text-blue-600'
|
|
156
|
+
: 'border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700',
|
|
157
|
+
)}
|
|
158
|
+
>
|
|
159
|
+
{view.isPublic && !isOwner(view) && <Lock className="h-3 w-3 text-gray-400" />}
|
|
160
|
+
{view.isPublic && isOwner(view) && <Globe className="h-3 w-3 text-gray-400" />}
|
|
161
|
+
{view.name}
|
|
162
|
+
{getFilterCount(view) > 0 && (
|
|
163
|
+
<span className="ml-1 inline-flex h-4 min-w-4 items-center justify-center rounded-full bg-gray-100 px-1 text-[10px] font-semibold text-gray-600">
|
|
164
|
+
{getFilterCount(view)}
|
|
165
|
+
</span>
|
|
166
|
+
)}
|
|
167
|
+
{activeViewId === view.id && hasUnsavedChanges && (
|
|
168
|
+
<span className="ml-1 h-1.5 w-1.5 rounded-full bg-orange-400" />
|
|
169
|
+
)}
|
|
170
|
+
</button>
|
|
171
|
+
|
|
172
|
+
{/* Menu contextuel par vue épinglée */}
|
|
173
|
+
{hasContextActions(view) && (
|
|
174
|
+
<div className="relative">
|
|
175
|
+
<button
|
|
176
|
+
onClick={(e) => {
|
|
177
|
+
e.stopPropagation();
|
|
178
|
+
setContextMenu(contextMenu === view.id ? null : view.id);
|
|
179
|
+
}}
|
|
180
|
+
className={cn(
|
|
181
|
+
'cursor-pointer rounded p-0.5 text-gray-400 hover:bg-gray-100 hover:text-gray-600',
|
|
182
|
+
activeViewId === view.id ? 'visible' : 'invisible group-hover:visible',
|
|
183
|
+
)}
|
|
184
|
+
>
|
|
185
|
+
<MoreHorizontal className="h-3.5 w-3.5" />
|
|
186
|
+
</button>
|
|
187
|
+
|
|
188
|
+
{contextMenu === view.id && (
|
|
189
|
+
<div
|
|
190
|
+
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"
|
|
192
|
+
>
|
|
193
|
+
{canEdit(view) && (
|
|
194
|
+
<button
|
|
195
|
+
onClick={() => {
|
|
196
|
+
onRenameView(view);
|
|
197
|
+
setContextMenu(null);
|
|
198
|
+
}}
|
|
199
|
+
className="flex w-full cursor-pointer items-center gap-2 px-3 py-2 text-sm text-gray-700 hover:bg-gray-50"
|
|
200
|
+
>
|
|
201
|
+
<Pencil className="h-3.5 w-3.5" />
|
|
202
|
+
Renommer
|
|
203
|
+
</button>
|
|
204
|
+
)}
|
|
205
|
+
{permissions.canCreate && (
|
|
206
|
+
<button
|
|
207
|
+
onClick={() => {
|
|
208
|
+
onCloneView(view);
|
|
209
|
+
setContextMenu(null);
|
|
210
|
+
}}
|
|
211
|
+
className="flex w-full cursor-pointer items-center gap-2 px-3 py-2 text-sm text-gray-700 hover:bg-gray-50"
|
|
212
|
+
>
|
|
213
|
+
<Copy className="h-3.5 w-3.5" />
|
|
214
|
+
Dupliquer
|
|
215
|
+
</button>
|
|
216
|
+
)}
|
|
217
|
+
{canEdit(view) && permissions.canShare && (
|
|
218
|
+
<button
|
|
219
|
+
onClick={() => {
|
|
220
|
+
onTogglePublic(view);
|
|
221
|
+
setContextMenu(null);
|
|
222
|
+
}}
|
|
223
|
+
className="flex w-full cursor-pointer items-center gap-2 px-3 py-2 text-sm text-gray-700 hover:bg-gray-50"
|
|
224
|
+
>
|
|
225
|
+
{view.isPublic ? (
|
|
226
|
+
<>
|
|
227
|
+
<Lock className="h-3.5 w-3.5" />
|
|
228
|
+
Rendre privée
|
|
229
|
+
</>
|
|
230
|
+
) : (
|
|
231
|
+
<>
|
|
232
|
+
<Globe className="h-3.5 w-3.5" />
|
|
233
|
+
Rendre publique
|
|
234
|
+
</>
|
|
235
|
+
)}
|
|
236
|
+
</button>
|
|
237
|
+
)}
|
|
238
|
+
<button
|
|
239
|
+
onClick={() => {
|
|
240
|
+
onTogglePin(view);
|
|
241
|
+
setContextMenu(null);
|
|
242
|
+
}}
|
|
243
|
+
className="flex w-full cursor-pointer items-center gap-2 px-3 py-2 text-sm text-gray-700 hover:bg-gray-50"
|
|
244
|
+
>
|
|
245
|
+
<PinOff className="h-3.5 w-3.5" />
|
|
246
|
+
Désépingler
|
|
247
|
+
</button>
|
|
248
|
+
{canDelete(view) && (
|
|
249
|
+
<>
|
|
250
|
+
<hr className="my-1 border-gray-100" />
|
|
251
|
+
<button
|
|
252
|
+
onClick={() => {
|
|
253
|
+
onDeleteView(view);
|
|
254
|
+
setContextMenu(null);
|
|
255
|
+
}}
|
|
256
|
+
className="flex w-full cursor-pointer items-center gap-2 px-3 py-2 text-sm text-red-600 hover:bg-red-50"
|
|
257
|
+
>
|
|
258
|
+
<Trash2 className="h-3.5 w-3.5" />
|
|
259
|
+
Supprimer
|
|
260
|
+
</button>
|
|
261
|
+
</>
|
|
262
|
+
)}
|
|
263
|
+
</div>
|
|
264
|
+
)}
|
|
265
|
+
</div>
|
|
266
|
+
)}
|
|
267
|
+
</div>
|
|
268
|
+
))}
|
|
269
|
+
|
|
270
|
+
{/* Dropdown "Toutes les vues" */}
|
|
271
|
+
{unpinnedViews.length > 0 && (
|
|
272
|
+
<div className="relative" ref={allViewsRef}>
|
|
273
|
+
<button
|
|
274
|
+
onClick={() => setShowAllViews(!showAllViews)}
|
|
275
|
+
className={cn(
|
|
276
|
+
'flex shrink-0 cursor-pointer items-center gap-1 border-b-2 px-3 py-2.5 text-sm font-medium whitespace-nowrap transition-colors',
|
|
277
|
+
unpinnedViews.some((v) => v.id === activeViewId)
|
|
278
|
+
? 'border-blue-600 text-blue-600'
|
|
279
|
+
: 'border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700',
|
|
280
|
+
)}
|
|
281
|
+
>
|
|
282
|
+
{unpinnedViews.find((v) => v.id === activeViewId)?.name || 'Toutes les vues'}
|
|
283
|
+
<ChevronDown className="h-3.5 w-3.5" />
|
|
284
|
+
{unpinnedViews.length > 0 && (
|
|
285
|
+
<span className="inline-flex h-4 min-w-4 items-center justify-center rounded-full bg-gray-100 px-1 text-[10px] font-semibold text-gray-500">
|
|
286
|
+
{unpinnedViews.length}
|
|
287
|
+
</span>
|
|
288
|
+
)}
|
|
289
|
+
</button>
|
|
290
|
+
|
|
291
|
+
{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">
|
|
293
|
+
<div className="px-3 py-2 text-xs font-semibold tracking-wide text-gray-400 uppercase">
|
|
294
|
+
Vues enregistrées
|
|
295
|
+
</div>
|
|
296
|
+
{unpinnedViews.map((view) => (
|
|
297
|
+
<div
|
|
298
|
+
key={view.id}
|
|
299
|
+
className="group flex items-center justify-between px-3 py-2 hover:bg-gray-50"
|
|
300
|
+
>
|
|
301
|
+
<button
|
|
302
|
+
onClick={() => {
|
|
303
|
+
onSelectView(view.id);
|
|
304
|
+
setShowAllViews(false);
|
|
305
|
+
}}
|
|
306
|
+
className={cn(
|
|
307
|
+
'flex min-w-0 flex-1 cursor-pointer items-center gap-2 text-left text-sm transition-colors hover:text-gray-900',
|
|
308
|
+
activeViewId === view.id ? 'font-medium text-blue-600' : 'text-gray-700',
|
|
309
|
+
)}
|
|
310
|
+
>
|
|
311
|
+
{view.isPublic ? (
|
|
312
|
+
<Globe className="h-3.5 w-3.5 shrink-0 text-gray-400" />
|
|
313
|
+
) : (
|
|
314
|
+
<Lock className="h-3.5 w-3.5 shrink-0 text-gray-400" />
|
|
315
|
+
)}
|
|
316
|
+
<span className="truncate">{view.name}</span>
|
|
317
|
+
{getFilterCount(view) > 0 && (
|
|
318
|
+
<span className="ml-1 inline-flex h-4 min-w-4 shrink-0 items-center justify-center rounded-full bg-gray-100 px-1 text-[10px] font-semibold text-gray-500">
|
|
319
|
+
{getFilterCount(view)}
|
|
320
|
+
</span>
|
|
321
|
+
)}
|
|
322
|
+
{!isOwner(view) && (
|
|
323
|
+
<span className="shrink-0 text-xs text-gray-400">par {view.user?.name}</span>
|
|
324
|
+
)}
|
|
325
|
+
</button>
|
|
326
|
+
{hasContextActions(view) && (
|
|
327
|
+
<div className="relative shrink-0">
|
|
328
|
+
<button
|
|
329
|
+
onClick={(e) => {
|
|
330
|
+
e.stopPropagation();
|
|
331
|
+
setContextMenu(contextMenu === view.id ? null : view.id);
|
|
332
|
+
}}
|
|
333
|
+
className="cursor-pointer rounded p-1 text-gray-400 opacity-0 transition-opacity group-hover:opacity-100 hover:bg-gray-200 hover:text-gray-600"
|
|
334
|
+
title="Actions"
|
|
335
|
+
>
|
|
336
|
+
<MoreHorizontal className="h-3.5 w-3.5" />
|
|
337
|
+
</button>
|
|
338
|
+
{contextMenu === view.id && (
|
|
339
|
+
<div
|
|
340
|
+
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"
|
|
342
|
+
>
|
|
343
|
+
{canEdit(view) && (
|
|
344
|
+
<button
|
|
345
|
+
onClick={() => {
|
|
346
|
+
onRenameView(view);
|
|
347
|
+
setContextMenu(null);
|
|
348
|
+
setShowAllViews(false);
|
|
349
|
+
}}
|
|
350
|
+
className="flex w-full cursor-pointer items-center gap-2 px-3 py-2 text-sm text-gray-700 hover:bg-gray-50"
|
|
351
|
+
>
|
|
352
|
+
<Pencil className="h-3.5 w-3.5" />
|
|
353
|
+
Renommer
|
|
354
|
+
</button>
|
|
355
|
+
)}
|
|
356
|
+
{permissions.canCreate && (
|
|
357
|
+
<button
|
|
358
|
+
onClick={() => {
|
|
359
|
+
onCloneView(view);
|
|
360
|
+
setContextMenu(null);
|
|
361
|
+
setShowAllViews(false);
|
|
362
|
+
}}
|
|
363
|
+
className="flex w-full cursor-pointer items-center gap-2 px-3 py-2 text-sm text-gray-700 hover:bg-gray-50"
|
|
364
|
+
>
|
|
365
|
+
<Copy className="h-3.5 w-3.5" />
|
|
366
|
+
Dupliquer
|
|
367
|
+
</button>
|
|
368
|
+
)}
|
|
369
|
+
{canEdit(view) && permissions.canShare && (
|
|
370
|
+
<button
|
|
371
|
+
onClick={() => {
|
|
372
|
+
onTogglePublic(view);
|
|
373
|
+
setContextMenu(null);
|
|
374
|
+
setShowAllViews(false);
|
|
375
|
+
}}
|
|
376
|
+
className="flex w-full cursor-pointer items-center gap-2 px-3 py-2 text-sm text-gray-700 hover:bg-gray-50"
|
|
377
|
+
>
|
|
378
|
+
{view.isPublic ? (
|
|
379
|
+
<>
|
|
380
|
+
<Lock className="h-3.5 w-3.5" />
|
|
381
|
+
Rendre privée
|
|
382
|
+
</>
|
|
383
|
+
) : (
|
|
384
|
+
<>
|
|
385
|
+
<Globe className="h-3.5 w-3.5" />
|
|
386
|
+
Rendre publique
|
|
387
|
+
</>
|
|
388
|
+
)}
|
|
389
|
+
</button>
|
|
390
|
+
)}
|
|
391
|
+
<button
|
|
392
|
+
onClick={() => {
|
|
393
|
+
onTogglePin(view);
|
|
394
|
+
setContextMenu(null);
|
|
395
|
+
setShowAllViews(false);
|
|
396
|
+
}}
|
|
397
|
+
className="flex w-full cursor-pointer items-center gap-2 px-3 py-2 text-sm text-gray-700 hover:bg-gray-50"
|
|
398
|
+
>
|
|
399
|
+
<Pin className="h-3.5 w-3.5" />
|
|
400
|
+
Épingler
|
|
401
|
+
</button>
|
|
402
|
+
{canDelete(view) && (
|
|
403
|
+
<>
|
|
404
|
+
<hr className="my-1 border-gray-100" />
|
|
405
|
+
<button
|
|
406
|
+
onClick={() => {
|
|
407
|
+
onDeleteView(view);
|
|
408
|
+
setContextMenu(null);
|
|
409
|
+
setShowAllViews(false);
|
|
410
|
+
}}
|
|
411
|
+
className="flex w-full cursor-pointer items-center gap-2 px-3 py-2 text-sm text-red-600 hover:bg-red-50"
|
|
412
|
+
>
|
|
413
|
+
<Trash2 className="h-3.5 w-3.5" />
|
|
414
|
+
Supprimer
|
|
415
|
+
</button>
|
|
416
|
+
</>
|
|
417
|
+
)}
|
|
418
|
+
</div>
|
|
419
|
+
)}
|
|
420
|
+
</div>
|
|
421
|
+
)}
|
|
422
|
+
</div>
|
|
423
|
+
))}
|
|
424
|
+
</div>
|
|
425
|
+
)}
|
|
426
|
+
</div>
|
|
427
|
+
)}
|
|
428
|
+
|
|
429
|
+
{/* Bouton "+" pour créer */}
|
|
430
|
+
{permissions.canCreate && (
|
|
431
|
+
<button
|
|
432
|
+
onClick={onCreateView}
|
|
433
|
+
className="ml-1 flex shrink-0 cursor-pointer items-center gap-1 rounded-md border border-dashed border-gray-300 px-2.5 py-1.5 text-sm text-gray-500 transition-colors hover:border-gray-400 hover:bg-gray-50 hover:text-gray-700"
|
|
434
|
+
>
|
|
435
|
+
<Plus className="h-3.5 w-3.5" />
|
|
436
|
+
</button>
|
|
437
|
+
)}
|
|
438
|
+
</div>
|
|
439
|
+
);
|
|
440
|
+
}
|