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
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import Link from 'next/link';
|
|
4
|
+
import { Users } from 'lucide-react';
|
|
5
|
+
|
|
6
|
+
interface Contact {
|
|
7
|
+
id: string;
|
|
8
|
+
name: string | null;
|
|
9
|
+
phone: string | null;
|
|
10
|
+
email: string | null;
|
|
11
|
+
status: string;
|
|
12
|
+
interactionsCount: number;
|
|
13
|
+
assignedCommercial?: string;
|
|
14
|
+
assignedTelepro?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface TopContactsListProps {
|
|
18
|
+
readonly contacts: Contact[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function TopContactsList({ contacts }: TopContactsListProps) {
|
|
22
|
+
if (contacts.length === 0) {
|
|
23
|
+
return (
|
|
24
|
+
<div className="flex h-full flex-col rounded-2xl border border-gray-100 bg-white p-5 shadow-sm">
|
|
25
|
+
<div className="flex items-center justify-between">
|
|
26
|
+
<div>
|
|
27
|
+
<h3 className="text-base font-semibold text-gray-900">Derniers Prospects</h3>
|
|
28
|
+
<p className="mt-0.5 text-xs text-gray-400">Contacts récemment ajoutés</p>
|
|
29
|
+
</div>
|
|
30
|
+
<Link href="/contacts" className="dash-link cursor-pointer text-xs font-medium">
|
|
31
|
+
Voir tout →
|
|
32
|
+
</Link>
|
|
33
|
+
</div>
|
|
34
|
+
<div className="flex flex-1 items-center justify-center">
|
|
35
|
+
<div className="text-center">
|
|
36
|
+
<Users className="mx-auto h-10 w-10 text-gray-200" />
|
|
37
|
+
<p className="mt-2 text-sm text-gray-400">Aucun contact</p>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<div className="flex h-full flex-col rounded-2xl border border-gray-100 bg-white p-5 shadow-sm">
|
|
46
|
+
<div className="mb-4 flex items-center justify-between">
|
|
47
|
+
<div>
|
|
48
|
+
<h3 className="text-base font-semibold text-gray-900">Derniers Prospects</h3>
|
|
49
|
+
<p className="mt-0.5 text-xs text-gray-400">Contacts récemment ajoutés</p>
|
|
50
|
+
</div>
|
|
51
|
+
<Link href="/contacts" className="dash-link cursor-pointer text-xs font-medium">
|
|
52
|
+
Voir tout →
|
|
53
|
+
</Link>
|
|
54
|
+
</div>
|
|
55
|
+
<div className="flex-1 overflow-auto">
|
|
56
|
+
<table className="w-full">
|
|
57
|
+
<thead>
|
|
58
|
+
<tr className="border-b border-gray-100">
|
|
59
|
+
<th className="pb-2.5 text-left text-[11px] font-medium tracking-wider text-gray-400 uppercase">
|
|
60
|
+
Prospect
|
|
61
|
+
</th>
|
|
62
|
+
<th className="pb-2.5 text-left text-[11px] font-medium tracking-wider text-gray-400 uppercase">
|
|
63
|
+
Email
|
|
64
|
+
</th>
|
|
65
|
+
<th className="pb-2.5 text-left text-[11px] font-medium tracking-wider text-gray-400 uppercase">
|
|
66
|
+
Télépro
|
|
67
|
+
</th>
|
|
68
|
+
<th className="pb-2.5 text-left text-[11px] font-medium tracking-wider text-gray-400 uppercase">
|
|
69
|
+
Statut
|
|
70
|
+
</th>
|
|
71
|
+
</tr>
|
|
72
|
+
</thead>
|
|
73
|
+
<tbody>
|
|
74
|
+
{contacts.map((contact) => {
|
|
75
|
+
const nameParts = (contact.name ?? '').split(' ');
|
|
76
|
+
const firstName = nameParts[0] || '';
|
|
77
|
+
const lastName = nameParts.slice(1).join(' ') || '';
|
|
78
|
+
const initials = `${firstName[0] || ''}${lastName[0] || ''}`.toUpperCase();
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<tr
|
|
82
|
+
key={contact.id}
|
|
83
|
+
className="dash-hover-bg border-b border-gray-50 transition-colors"
|
|
84
|
+
>
|
|
85
|
+
<td className="py-3">
|
|
86
|
+
<div className="flex items-center gap-2.5">
|
|
87
|
+
<div className="dash-avatar flex h-7 w-7 shrink-0 items-center justify-center rounded-full text-[10px] font-semibold">
|
|
88
|
+
{initials || '?'}
|
|
89
|
+
</div>
|
|
90
|
+
<Link
|
|
91
|
+
href={`/contacts/${contact.id}`}
|
|
92
|
+
className="dash-hover-text cursor-pointer text-sm font-medium text-gray-900 transition-colors"
|
|
93
|
+
>
|
|
94
|
+
{contact.name || 'Sans nom'}
|
|
95
|
+
</Link>
|
|
96
|
+
</div>
|
|
97
|
+
</td>
|
|
98
|
+
<td className="py-3 text-xs text-gray-500">{contact.email || '-'}</td>
|
|
99
|
+
<td className="py-3 text-xs text-gray-500">{contact.assignedTelepro || '-'}</td>
|
|
100
|
+
<td className="py-3">
|
|
101
|
+
<span className="inline-flex rounded-full bg-gray-100 px-2 py-0.5 text-[10px] font-medium text-gray-700">
|
|
102
|
+
{contact.status}
|
|
103
|
+
</span>
|
|
104
|
+
</td>
|
|
105
|
+
</tr>
|
|
106
|
+
);
|
|
107
|
+
})}
|
|
108
|
+
</tbody>
|
|
109
|
+
</table>
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import Link from 'next/link';
|
|
4
|
-
import { Calendar, Phone, Video, Mail, CheckCircle2, Clock } from 'lucide-react';
|
|
4
|
+
import { Calendar, Phone, Video, Mail, CheckCircle2, Clock, ListTodo } from 'lucide-react';
|
|
5
|
+
import { cn } from '@/lib/utils';
|
|
5
6
|
|
|
6
7
|
interface Task {
|
|
7
8
|
id: string;
|
|
@@ -13,25 +14,26 @@ interface Task {
|
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
interface UpcomingTasksListProps {
|
|
16
|
-
tasks: Task[];
|
|
17
|
+
readonly tasks: Task[];
|
|
17
18
|
}
|
|
18
19
|
|
|
19
|
-
const taskIcons = {
|
|
20
|
+
const taskIcons: Record<string, typeof Phone> = {
|
|
20
21
|
CALL: Phone,
|
|
21
22
|
MEETING: Calendar,
|
|
22
23
|
EMAIL: Mail,
|
|
23
24
|
VIDEO_CONFERENCE: Video,
|
|
25
|
+
TASK: ListTodo,
|
|
24
26
|
OTHER: CheckCircle2,
|
|
25
27
|
};
|
|
26
28
|
|
|
27
|
-
const
|
|
28
|
-
LOW: '
|
|
29
|
-
MEDIUM: '
|
|
30
|
-
HIGH: '
|
|
31
|
-
URGENT: '
|
|
29
|
+
const priorityStyles: Record<string, string> = {
|
|
30
|
+
LOW: 'bg-gray-100 text-gray-600',
|
|
31
|
+
MEDIUM: 'bg-blue-50 text-blue-600',
|
|
32
|
+
HIGH: 'bg-orange-50 text-orange-600',
|
|
33
|
+
URGENT: 'bg-red-50 text-red-600',
|
|
32
34
|
};
|
|
33
35
|
|
|
34
|
-
const priorityLabels = {
|
|
36
|
+
const priorityLabels: Record<string, string> = {
|
|
35
37
|
LOW: 'Basse',
|
|
36
38
|
MEDIUM: 'Moyenne',
|
|
37
39
|
HIGH: 'Haute',
|
|
@@ -39,87 +41,76 @@ const priorityLabels = {
|
|
|
39
41
|
};
|
|
40
42
|
|
|
41
43
|
export function UpcomingTasksList({ tasks }: UpcomingTasksListProps) {
|
|
42
|
-
if (tasks.length === 0) {
|
|
43
|
-
return (
|
|
44
|
-
<div className="rounded-lg border border-gray-200 bg-white p-6 shadow-sm">
|
|
45
|
-
<div className="flex items-center justify-between">
|
|
46
|
-
<h3 className="text-lg font-semibold text-gray-900">Tâches à Venir</h3>
|
|
47
|
-
<Link
|
|
48
|
-
href="/agenda"
|
|
49
|
-
className="text-sm font-medium text-blue-600 hover:text-blue-700"
|
|
50
|
-
>
|
|
51
|
-
Voir tout
|
|
52
|
-
</Link>
|
|
53
|
-
</div>
|
|
54
|
-
<div className="mt-6 text-center text-sm text-gray-500">
|
|
55
|
-
<Clock className="mx-auto h-12 w-12 text-gray-400" />
|
|
56
|
-
<p className="mt-2">Aucune tâche à venir</p>
|
|
57
|
-
</div>
|
|
58
|
-
</div>
|
|
59
|
-
);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
44
|
return (
|
|
63
|
-
<div className="rounded-
|
|
45
|
+
<div className="flex h-full flex-col rounded-2xl border border-gray-100 bg-white p-5 shadow-sm">
|
|
64
46
|
<div className="mb-4 flex items-center justify-between">
|
|
65
|
-
<
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
>
|
|
47
|
+
<div>
|
|
48
|
+
<h3 className="text-base font-semibold text-gray-900">Tâches à Venir</h3>
|
|
49
|
+
<p className="mt-0.5 text-xs text-gray-400">Prochains rendez-vous et tâches</p>
|
|
50
|
+
</div>
|
|
51
|
+
<Link href="/agenda" className="dash-link cursor-pointer text-xs font-medium">
|
|
70
52
|
Voir tout →
|
|
71
53
|
</Link>
|
|
72
54
|
</div>
|
|
73
|
-
<div className="space-y-3">
|
|
74
|
-
{tasks.map((task) => {
|
|
75
|
-
const Icon = taskIcons[task.type as keyof typeof taskIcons] || CheckCircle2;
|
|
76
|
-
const scheduledDate = new Date(task.scheduledAt);
|
|
77
55
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
>
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
56
|
+
{tasks.length === 0 ? (
|
|
57
|
+
<div className="flex flex-1 items-center justify-center">
|
|
58
|
+
<div className="text-center">
|
|
59
|
+
<Clock className="mx-auto h-10 w-10 text-gray-200" />
|
|
60
|
+
<p className="mt-2 text-sm text-gray-400">Aucune tâche à venir</p>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
) : (
|
|
64
|
+
<div className="flex-1 space-y-2 overflow-auto">
|
|
65
|
+
{tasks.map((task) => {
|
|
66
|
+
const Icon = taskIcons[task.type] || CheckCircle2;
|
|
67
|
+
const scheduledDate = new Date(task.scheduledAt);
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<div
|
|
71
|
+
key={task.id}
|
|
72
|
+
className="group dash-hover-border-light dash-hover-bg flex items-start gap-3 rounded-xl border border-gray-50 bg-gray-50/50 p-3 transition-colors duration-150"
|
|
73
|
+
>
|
|
74
|
+
<div className="dash-icon-box flex h-8 w-8 shrink-0 items-center justify-center rounded-lg">
|
|
75
|
+
<Icon className="dash-icon-color h-3.5 w-3.5" />
|
|
76
|
+
</div>
|
|
77
|
+
<div className="min-w-0 flex-1">
|
|
78
|
+
<div className="flex items-start justify-between gap-2">
|
|
79
|
+
<p className="truncate text-sm font-medium text-gray-900">{task.title}</p>
|
|
80
|
+
<span
|
|
81
|
+
className={cn(
|
|
82
|
+
'shrink-0 rounded-full px-2 py-0.5 text-[10px] font-medium',
|
|
83
|
+
priorityStyles[task.priority] || 'bg-gray-100 text-gray-600',
|
|
84
|
+
)}
|
|
85
|
+
>
|
|
86
|
+
{priorityLabels[task.priority] || task.priority}
|
|
87
|
+
</span>
|
|
102
88
|
</div>
|
|
103
|
-
|
|
104
|
-
<
|
|
105
|
-
|
|
89
|
+
{task.contact && (
|
|
90
|
+
<Link
|
|
91
|
+
href={`/contacts/${task.contact.id}`}
|
|
92
|
+
className="dash-hover-text cursor-pointer text-xs text-gray-500 transition-colors"
|
|
93
|
+
>
|
|
94
|
+
{task.contact.name}
|
|
95
|
+
</Link>
|
|
96
|
+
)}
|
|
97
|
+
<p className="mt-1 text-[11px] text-gray-400">
|
|
98
|
+
{scheduledDate.toLocaleDateString('fr-FR', {
|
|
99
|
+
day: 'numeric',
|
|
100
|
+
month: 'long',
|
|
101
|
+
})}{' '}
|
|
102
|
+
à{' '}
|
|
103
|
+
{scheduledDate.toLocaleTimeString('fr-FR', {
|
|
104
|
+
hour: '2-digit',
|
|
105
|
+
minute: '2-digit',
|
|
106
|
+
})}
|
|
107
|
+
</p>
|
|
106
108
|
</div>
|
|
107
|
-
<p className="mt-1 text-xs text-gray-500">
|
|
108
|
-
{scheduledDate.toLocaleDateString('fr-FR', {
|
|
109
|
-
day: 'numeric',
|
|
110
|
-
month: 'long',
|
|
111
|
-
})}{' '}
|
|
112
|
-
à{' '}
|
|
113
|
-
{scheduledDate.toLocaleTimeString('fr-FR', {
|
|
114
|
-
hour: '2-digit',
|
|
115
|
-
minute: '2-digit',
|
|
116
|
-
})}
|
|
117
|
-
</p>
|
|
118
109
|
</div>
|
|
119
|
-
|
|
120
|
-
)
|
|
121
|
-
|
|
122
|
-
|
|
110
|
+
);
|
|
111
|
+
})}
|
|
112
|
+
</div>
|
|
113
|
+
)}
|
|
123
114
|
</div>
|
|
124
115
|
);
|
|
125
116
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { X, GripVertical } from 'lucide-react';
|
|
4
|
+
import { cn } from '@/lib/utils';
|
|
5
|
+
|
|
6
|
+
interface WidgetWrapperProps {
|
|
7
|
+
readonly children: React.ReactNode;
|
|
8
|
+
readonly onRemove?: () => void;
|
|
9
|
+
readonly className?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function WidgetWrapper({ children, onRemove, className }: WidgetWrapperProps) {
|
|
13
|
+
return (
|
|
14
|
+
<div className={cn('group/widget relative h-full w-full', className)}>
|
|
15
|
+
{onRemove && (
|
|
16
|
+
<div className="absolute top-0 right-0 z-10 flex items-center gap-1 opacity-0 transition-[opacity,transform] duration-(--duration-normal) ease-(--ease-standard) group-hover/widget:opacity-100">
|
|
17
|
+
<div className="drag-handle flex h-7 w-7 cursor-grab items-center justify-center rounded-lg bg-white/90 shadow-sm ring-1 ring-gray-200/60 backdrop-blur-sm transition-colors hover:bg-gray-50 active:cursor-grabbing">
|
|
18
|
+
<GripVertical className="h-3.5 w-3.5 text-gray-400" />
|
|
19
|
+
</div>
|
|
20
|
+
<button
|
|
21
|
+
type="button"
|
|
22
|
+
onClick={(e) => {
|
|
23
|
+
e.stopPropagation();
|
|
24
|
+
onRemove();
|
|
25
|
+
}}
|
|
26
|
+
aria-label="Supprimer le widget"
|
|
27
|
+
className="flex h-7 w-7 cursor-pointer items-center justify-center rounded-lg bg-white/90 shadow-sm ring-1 ring-gray-200/60 backdrop-blur-sm transition-colors hover:bg-red-50 hover:text-red-600"
|
|
28
|
+
>
|
|
29
|
+
<X className="h-3.5 w-3.5" />
|
|
30
|
+
</button>
|
|
31
|
+
</div>
|
|
32
|
+
)}
|
|
33
|
+
{children}
|
|
34
|
+
</div>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
@@ -235,7 +235,7 @@ export const DatePicker: React.FC<DatePickerProps> = ({
|
|
|
235
235
|
<div
|
|
236
236
|
ref={dropdownRef}
|
|
237
237
|
className={cn(
|
|
238
|
-
'fixed z-[9999] transition-
|
|
238
|
+
'fixed z-[9999] transition-opacity duration-200',
|
|
239
239
|
'left-1/2 -translate-x-1/2',
|
|
240
240
|
isDesktopView && 'top-1/2 -translate-y-1/2',
|
|
241
241
|
)}
|
|
@@ -272,9 +272,10 @@ export const DatePicker: React.FC<DatePickerProps> = ({
|
|
|
272
272
|
</div>
|
|
273
273
|
<button
|
|
274
274
|
onClick={handleClose}
|
|
275
|
+
aria-label="Fermer"
|
|
275
276
|
className="ml-2 shrink-0 cursor-pointer rounded-lg p-1 text-gray-400 transition-colors hover:bg-gray-100 hover:text-gray-600"
|
|
276
277
|
>
|
|
277
|
-
<X className="h-4 w-4" />
|
|
278
|
+
<X aria-hidden="true" className="h-4 w-4" />
|
|
278
279
|
</button>
|
|
279
280
|
</div>
|
|
280
281
|
|
|
@@ -283,24 +284,26 @@ export const DatePicker: React.FC<DatePickerProps> = ({
|
|
|
283
284
|
<div className="mb-2 flex items-center justify-between">
|
|
284
285
|
<button
|
|
285
286
|
type="button"
|
|
287
|
+
aria-label="Mois precedent"
|
|
286
288
|
onClick={() =>
|
|
287
289
|
setCurrentMonth(new Date(currentMonth.setMonth(currentMonth.getMonth() - 1)))
|
|
288
290
|
}
|
|
289
291
|
className="shrink-0 cursor-pointer rounded-lg p-1.5 text-gray-600 transition-colors hover:bg-gray-100"
|
|
290
292
|
>
|
|
291
|
-
<ChevronLeft className="h-4 w-4" />
|
|
293
|
+
<ChevronLeft aria-hidden="true" className="h-4 w-4" />
|
|
292
294
|
</button>
|
|
293
295
|
<h3 className="min-w-0 flex-1 truncate text-center text-xs font-semibold text-gray-900 sm:text-sm">
|
|
294
296
|
{currentMonth.toLocaleDateString('fr-FR', { month: 'long', year: 'numeric' })}
|
|
295
297
|
</h3>
|
|
296
298
|
<button
|
|
297
299
|
type="button"
|
|
300
|
+
aria-label="Mois suivant"
|
|
298
301
|
onClick={() =>
|
|
299
302
|
setCurrentMonth(new Date(currentMonth.setMonth(currentMonth.getMonth() + 1)))
|
|
300
303
|
}
|
|
301
304
|
className="shrink-0 cursor-pointer rounded-lg p-1.5 text-gray-600 transition-colors hover:bg-gray-100"
|
|
302
305
|
>
|
|
303
|
-
<ChevronRight className="h-4 w-4" />
|
|
306
|
+
<ChevronRight aria-hidden="true" className="h-4 w-4" />
|
|
304
307
|
</button>
|
|
305
308
|
</div>
|
|
306
309
|
|
|
@@ -329,7 +332,7 @@ export const DatePicker: React.FC<DatePickerProps> = ({
|
|
|
329
332
|
type="button"
|
|
330
333
|
onClick={() => handleDateClick(date)}
|
|
331
334
|
className={cn(
|
|
332
|
-
'aspect-square cursor-pointer rounded-md text-[10px] font-medium transition-
|
|
335
|
+
'aspect-square cursor-pointer rounded-md text-[10px] font-medium transition-colors sm:text-xs',
|
|
333
336
|
selected &&
|
|
334
337
|
'z-10 bg-blue-600 text-white shadow-md shadow-blue-500/30 hover:bg-blue-700',
|
|
335
338
|
inRange && !selected && 'bg-blue-50 text-blue-700 hover:bg-blue-100',
|
|
@@ -383,7 +386,7 @@ export const DatePicker: React.FC<DatePickerProps> = ({
|
|
|
383
386
|
ref={buttonRef}
|
|
384
387
|
type="button"
|
|
385
388
|
onClick={() => setIsOpen(!isOpen)}
|
|
386
|
-
className="flex w-full cursor-pointer items-center justify-between rounded-xl border border-gray-300 bg-white px-4 py-2.5 text-left text-sm shadow-sm transition-colors hover:border-blue-400 focus:
|
|
389
|
+
className="flex w-full cursor-pointer items-center justify-between rounded-xl border border-gray-300 bg-white px-4 py-2.5 text-left text-sm shadow-sm transition-colors hover:border-blue-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
|
|
387
390
|
>
|
|
388
391
|
<div className="flex items-center gap-2">
|
|
389
392
|
<Calendar className="h-4 w-4 text-gray-400" />
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Upload une image vers Supabase Storage via l'API /api/editor/upload-image.
|
|
3
|
+
* Retourne l'URL publique persistante de l'image.
|
|
4
|
+
*/
|
|
5
|
+
export async function uploadEditorImage(file: File | Blob, fileName?: string): Promise<string> {
|
|
6
|
+
const name = fileName || (file instanceof File ? file.name : 'image.png');
|
|
7
|
+
const mimeType = file.type || 'image/png';
|
|
8
|
+
|
|
9
|
+
// 1. Obtenir une URL d'upload signée
|
|
10
|
+
const res = await fetch('/api/editor/upload-image', {
|
|
11
|
+
method: 'POST',
|
|
12
|
+
headers: { 'Content-Type': 'application/json' },
|
|
13
|
+
body: JSON.stringify({
|
|
14
|
+
fileName: name,
|
|
15
|
+
fileSize: file.size,
|
|
16
|
+
mimeType,
|
|
17
|
+
}),
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
if (!res.ok) {
|
|
21
|
+
const err = await res.json().catch(() => ({ error: 'Erreur inconnue' }));
|
|
22
|
+
throw new Error(err.error || "Erreur lors de la préparation de l'upload");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const { signedUrl, token, publicUrl } = await res.json();
|
|
26
|
+
|
|
27
|
+
// 2. Upload direct vers Supabase Storage
|
|
28
|
+
const uploadRes = await fetch(signedUrl, {
|
|
29
|
+
method: 'PUT',
|
|
30
|
+
headers: {
|
|
31
|
+
'Content-Type': mimeType,
|
|
32
|
+
Authorization: `Bearer ${token}`,
|
|
33
|
+
},
|
|
34
|
+
body: file,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
if (!uploadRes.ok) {
|
|
38
|
+
throw new Error("Erreur lors de l'upload de l'image");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return publicUrl;
|
|
42
|
+
}
|