create-crm-tmp 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/create-crm-tmp.js +93 -0
- package/package.json +25 -0
- package/template/.prettierignore +33 -0
- package/template/.prettierrc.json +25 -0
- package/template/README.md +173 -0
- package/template/eslint.config.mjs +18 -0
- package/template/exemple-contacts.csv +11 -0
- package/template/next.config.ts +8 -0
- package/template/package.json +64 -0
- package/template/postcss.config.mjs +7 -0
- package/template/prisma/migrations/20251126144728_init/migration.sql +78 -0
- package/template/prisma/migrations/20251126155204_add_user_roles/migration.sql +5 -0
- package/template/prisma/migrations/20251128095126_add_company_info/migration.sql +19 -0
- package/template/prisma/migrations/20251128123321_add_smtp_config/migration.sql +22 -0
- package/template/prisma/migrations/20251128132303_add_status/migration.sql +23 -0
- package/template/prisma/migrations/20251201102207_add_user_active/migration.sql +75 -0
- package/template/prisma/migrations/20251201105507_add_email_signature/migration.sql +2 -0
- package/template/prisma/migrations/20251201151122_add_tasks/migration.sql +45 -0
- package/template/prisma/migrations/20251202111854_add_task_reminder/migration.sql +2 -0
- package/template/prisma/migrations/20251202135859_add_google_meet_integration/migration.sql +27 -0
- package/template/prisma/migrations/20251203103317_add_meta_lead_integration/migration.sql +20 -0
- package/template/prisma/migrations/20251203104002_add_google_ads_integration/migration.sql +18 -0
- package/template/prisma/migrations/20251203112122_add_google_sheet_integration/migration.sql +32 -0
- package/template/prisma/migrations/20251203153853_allow_multiple_integration_configs/migration.sql +20 -0
- package/template/prisma/migrations/20251205141705_update_user_roles/migration.sql +12 -0
- package/template/prisma/migrations/20251205150000_add_commercial_and_telepro_assignment/migration.sql +21 -0
- package/template/prisma/migrations/20251205160000_add_interaction_logging/migration.sql +11 -0
- package/template/prisma/migrations/20251208090314_add_automatic_interaction_types/migration.sql +12 -0
- package/template/prisma/migrations/20251208094843_mg/migration.sql +14 -0
- package/template/prisma/migrations/20251208100000_add_company_support/migration.sql +14 -0
- package/template/prisma/migrations/20251208110000_add_templates/migration.sql +26 -0
- package/template/prisma/migrations/20251208141304_add_video_conference_task_type/migration.sql +2 -0
- package/template/prisma/migrations/20251209104759_add_internal_note_to_task/migration.sql +2 -0
- package/template/prisma/migrations/20251209134803_add_company_field/migration.sql +2 -0
- package/template/prisma/migrations/20251209150000_rename_company_to_company_name/migration.sql +3 -0
- package/template/prisma/migrations/20251209150016_add_email_tracking/migration.sql +21 -0
- package/template/prisma/migrations/20251209155908_add_notify_contact_to_task/migration.sql +2 -0
- package/template/prisma/migrations/20251210110019_add_appointment_types/migration.sql +10 -0
- package/template/prisma/migrations/20251210113928_add_contact_files/migration.sql +26 -0
- package/template/prisma/migrations/20251212132339_add_custom_roles/migration.sql +24 -0
- package/template/prisma/migrations/20251215104448_add_file_interaction_types/migration.sql +11 -0
- package/template/prisma/migrations/20251215145616_add_closing_reasons/migration.sql +12 -0
- package/template/prisma/migrations/20251216140850_add_log_users/migration.sql +25 -0
- package/template/prisma/migrations/20251216151000_rename_perdu_to_ferme/migration.sql +8 -0
- package/template/prisma/migrations/20251216162318_add_column_mappings_to_google_sheet/migration.sql +2 -0
- package/template/prisma/migrations/20251216185127_add_workflows/migration.sql +80 -0
- package/template/prisma/migrations/20251216192237_add_scheduled_workflow_actions/migration.sql +32 -0
- package/template/prisma/migrations/migration_lock.toml +3 -0
- package/template/prisma/schema.prisma +582 -0
- package/template/prisma.config.ts +14 -0
- package/template/src/app/(auth)/invite/[token]/page.tsx +200 -0
- package/template/src/app/(auth)/layout.tsx +3 -0
- package/template/src/app/(auth)/reset-password/complete/page.tsx +213 -0
- package/template/src/app/(auth)/reset-password/page.tsx +146 -0
- package/template/src/app/(auth)/reset-password/verify/page.tsx +183 -0
- package/template/src/app/(auth)/signin/page.tsx +166 -0
- package/template/src/app/(dashboard)/agenda/page.tsx +3051 -0
- package/template/src/app/(dashboard)/automatisation/[id]/page.tsx +24 -0
- package/template/src/app/(dashboard)/automatisation/_components/workflow-editor.tsx +905 -0
- package/template/src/app/(dashboard)/automatisation/new/page.tsx +20 -0
- package/template/src/app/(dashboard)/automatisation/page.tsx +337 -0
- package/template/src/app/(dashboard)/closing/page.tsx +1052 -0
- package/template/src/app/(dashboard)/contacts/[id]/page.tsx +6028 -0
- package/template/src/app/(dashboard)/contacts/page.tsx +3713 -0
- package/template/src/app/(dashboard)/dashboard/page.tsx +186 -0
- package/template/src/app/(dashboard)/layout.tsx +30 -0
- package/template/src/app/(dashboard)/settings/page.tsx +4070 -0
- package/template/src/app/(dashboard)/templates/page.tsx +567 -0
- package/template/src/app/(dashboard)/users/list/page.tsx +507 -0
- package/template/src/app/(dashboard)/users/page.tsx +457 -0
- package/template/src/app/(dashboard)/users/permissions/page.tsx +181 -0
- package/template/src/app/(dashboard)/users/roles/page.tsx +434 -0
- package/template/src/app/api/audit-logs/route.ts +57 -0
- package/template/src/app/api/auth/[...all]/route.ts +4 -0
- package/template/src/app/api/auth/check-active/route.ts +31 -0
- package/template/src/app/api/auth/google/callback/route.ts +94 -0
- package/template/src/app/api/auth/google/disconnect/route.ts +32 -0
- package/template/src/app/api/auth/google/route.ts +34 -0
- package/template/src/app/api/auth/google/status/route.ts +32 -0
- package/template/src/app/api/closing-reasons/route.ts +27 -0
- package/template/src/app/api/contacts/[id]/files/[fileId]/route.ts +94 -0
- package/template/src/app/api/contacts/[id]/files/route.ts +269 -0
- package/template/src/app/api/contacts/[id]/interactions/[interactionId]/route.ts +91 -0
- package/template/src/app/api/contacts/[id]/interactions/route.ts +103 -0
- package/template/src/app/api/contacts/[id]/meet/route.ts +296 -0
- package/template/src/app/api/contacts/[id]/route.ts +322 -0
- package/template/src/app/api/contacts/[id]/send-email/route.ts +254 -0
- package/template/src/app/api/contacts/export/route.ts +270 -0
- package/template/src/app/api/contacts/import/route.ts +381 -0
- package/template/src/app/api/contacts/route.ts +283 -0
- package/template/src/app/api/dashboard/stats/route.ts +299 -0
- package/template/src/app/api/email/track/[id]/route.ts +68 -0
- package/template/src/app/api/integrations/google-sheet/sync/route.ts +526 -0
- package/template/src/app/api/invite/complete/route.ts +88 -0
- package/template/src/app/api/invite/validate/route.ts +55 -0
- package/template/src/app/api/reminders/route.ts +95 -0
- package/template/src/app/api/reset-password/complete/route.ts +73 -0
- package/template/src/app/api/reset-password/request/route.ts +84 -0
- package/template/src/app/api/reset-password/validate/route.ts +49 -0
- package/template/src/app/api/reset-password/verify/route.ts +74 -0
- package/template/src/app/api/roles/[id]/route.ts +183 -0
- package/template/src/app/api/roles/route.ts +140 -0
- package/template/src/app/api/send/route.ts +282 -0
- package/template/src/app/api/settings/change-password/route.ts +95 -0
- package/template/src/app/api/settings/closing-reasons/[id]/route.ts +84 -0
- package/template/src/app/api/settings/closing-reasons/route.ts +74 -0
- package/template/src/app/api/settings/company/route.ts +121 -0
- package/template/src/app/api/settings/google-ads/[id]/route.ts +117 -0
- package/template/src/app/api/settings/google-ads/route.ts +122 -0
- package/template/src/app/api/settings/google-sheet/[id]/route.ts +230 -0
- package/template/src/app/api/settings/google-sheet/auto-map/route.ts +196 -0
- package/template/src/app/api/settings/google-sheet/route.ts +254 -0
- package/template/src/app/api/settings/meta-leads/[id]/route.ts +123 -0
- package/template/src/app/api/settings/meta-leads/route.ts +132 -0
- package/template/src/app/api/settings/profile/route.ts +42 -0
- package/template/src/app/api/settings/smtp/route.ts +130 -0
- package/template/src/app/api/settings/smtp/test/route.ts +121 -0
- package/template/src/app/api/settings/statuses/[id]/route.ts +101 -0
- package/template/src/app/api/settings/statuses/route.ts +83 -0
- package/template/src/app/api/statuses/route.ts +25 -0
- package/template/src/app/api/tasks/[id]/attendees/route.ts +76 -0
- package/template/src/app/api/tasks/[id]/route.ts +728 -0
- package/template/src/app/api/tasks/meet/route.ts +240 -0
- package/template/src/app/api/tasks/route.ts +417 -0
- package/template/src/app/api/templates/[id]/route.ts +140 -0
- package/template/src/app/api/templates/route.ts +91 -0
- package/template/src/app/api/users/[id]/route.ts +168 -0
- package/template/src/app/api/users/list/route.ts +45 -0
- package/template/src/app/api/users/me/route.ts +48 -0
- package/template/src/app/api/users/route.ts +250 -0
- package/template/src/app/api/webhooks/google-ads/route.ts +208 -0
- package/template/src/app/api/webhooks/meta-leads/route.ts +258 -0
- package/template/src/app/api/workflows/[id]/route.ts +192 -0
- package/template/src/app/api/workflows/process/route.ts +293 -0
- package/template/src/app/api/workflows/route.ts +124 -0
- package/template/src/app/favicon.ico +0 -0
- package/template/src/app/globals.css +1416 -0
- package/template/src/app/layout.tsx +31 -0
- package/template/src/app/page.tsx +32 -0
- package/template/src/components/dashboard/activity-chart.tsx +67 -0
- package/template/src/components/dashboard/contacts-chart.tsx +63 -0
- package/template/src/components/dashboard/recent-activity.tsx +164 -0
- package/template/src/components/dashboard/sales-analytics-chart.tsx +81 -0
- package/template/src/components/dashboard/stat-card.tsx +61 -0
- package/template/src/components/dashboard/status-distribution-chart.tsx +45 -0
- package/template/src/components/dashboard/tasks-pie-chart.tsx +88 -0
- package/template/src/components/dashboard/top-contacts-list.tsx +129 -0
- package/template/src/components/dashboard/upcoming-tasks-list.tsx +126 -0
- package/template/src/components/editor.tsx +856 -0
- package/template/src/components/email-template.tsx +35 -0
- package/template/src/components/header.tsx +320 -0
- package/template/src/components/invitation-email-template.tsx +79 -0
- package/template/src/components/meet-cancellation-email-template.tsx +120 -0
- package/template/src/components/meet-confirmation-email-template.tsx +156 -0
- package/template/src/components/meet-update-email-template.tsx +209 -0
- package/template/src/components/page-header.tsx +61 -0
- package/template/src/components/reset-password-email-template.tsx +79 -0
- package/template/src/components/sidebar.tsx +294 -0
- package/template/src/components/skeleton.tsx +380 -0
- package/template/src/components/ui/commands.tsx +396 -0
- package/template/src/components/ui/components.tsx +150 -0
- package/template/src/components/ui/theme.tsx +5 -0
- package/template/src/components/view-as-banner.tsx +45 -0
- package/template/src/components/view-as-modal.tsx +186 -0
- package/template/src/contexts/mobile-menu-context.tsx +31 -0
- package/template/src/contexts/sidebar-context.tsx +107 -0
- package/template/src/contexts/task-reminder-context.tsx +239 -0
- package/template/src/contexts/view-as-context.tsx +84 -0
- package/template/src/hooks/use-user-role.ts +82 -0
- package/template/src/lib/audit-log.ts +45 -0
- package/template/src/lib/auth-client.ts +16 -0
- package/template/src/lib/auth.ts +35 -0
- package/template/src/lib/check-permission.ts +193 -0
- package/template/src/lib/contact-duplicate.ts +112 -0
- package/template/src/lib/contact-interactions.ts +371 -0
- package/template/src/lib/encryption.ts +99 -0
- package/template/src/lib/google-calendar.ts +300 -0
- package/template/src/lib/google-drive.ts +372 -0
- package/template/src/lib/permissions.ts +412 -0
- package/template/src/lib/prisma.ts +32 -0
- package/template/src/lib/roles.ts +120 -0
- package/template/src/lib/template-variables.ts +76 -0
- package/template/src/lib/utils.ts +46 -0
- package/template/src/lib/workflow-executor.ts +482 -0
- package/template/src/proxy.ts +91 -0
- package/template/tsconfig.json +34 -0
- package/template/vercel.json +8 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
interface EmailTemplateProps {
|
|
2
|
+
firstName: string;
|
|
3
|
+
signature?: string | null;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function EmailTemplate({ firstName, signature }: EmailTemplateProps) {
|
|
7
|
+
return (
|
|
8
|
+
<div
|
|
9
|
+
style={{
|
|
10
|
+
fontFamily: 'Arial, sans-serif',
|
|
11
|
+
padding: '20px',
|
|
12
|
+
maxWidth: '600px',
|
|
13
|
+
margin: '0 auto',
|
|
14
|
+
}}
|
|
15
|
+
>
|
|
16
|
+
<h1 style={{ color: '#1F2937', fontSize: '24px', marginBottom: '16px' }}>
|
|
17
|
+
Welcome, {firstName}!
|
|
18
|
+
</h1>
|
|
19
|
+
|
|
20
|
+
{signature && (
|
|
21
|
+
<div
|
|
22
|
+
style={{
|
|
23
|
+
marginTop: '24px',
|
|
24
|
+
paddingTop: '16px',
|
|
25
|
+
borderTop: '1px solid #E5E7EB',
|
|
26
|
+
color: '#4B5563',
|
|
27
|
+
fontSize: '14px',
|
|
28
|
+
lineHeight: '1.6',
|
|
29
|
+
}}
|
|
30
|
+
dangerouslySetInnerHTML={{ __html: signature }}
|
|
31
|
+
/>
|
|
32
|
+
)}
|
|
33
|
+
</div>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useSession, signOut } from '@/lib/auth-client';
|
|
4
|
+
import { Bell, ChevronDown, LogOut, Calendar } from 'lucide-react';
|
|
5
|
+
import { useState, useEffect, useRef } from 'react';
|
|
6
|
+
import { useRouter } from 'next/navigation';
|
|
7
|
+
import Link from 'next/link';
|
|
8
|
+
import { cn } from '@/lib/utils';
|
|
9
|
+
|
|
10
|
+
interface Reminder {
|
|
11
|
+
id: string;
|
|
12
|
+
taskId: string;
|
|
13
|
+
type: string;
|
|
14
|
+
title: string | null;
|
|
15
|
+
description: string;
|
|
16
|
+
priority: string;
|
|
17
|
+
scheduledAt: string;
|
|
18
|
+
reminderTime: string;
|
|
19
|
+
reminderMinutesBefore: number | null;
|
|
20
|
+
contact: {
|
|
21
|
+
id: string;
|
|
22
|
+
firstName: string | null;
|
|
23
|
+
lastName: string | null;
|
|
24
|
+
} | null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const TASK_TYPE_LABELS: Record<string, string> = {
|
|
28
|
+
CALL: 'Appel téléphonique',
|
|
29
|
+
MEETING: 'Rendez-vous',
|
|
30
|
+
EMAIL: 'Email',
|
|
31
|
+
VIDEO_CONFERENCE: 'Google Meet',
|
|
32
|
+
OTHER: 'Autre',
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const PRIORITY_COLORS: Record<string, string> = {
|
|
36
|
+
LOW: 'bg-gray-100 text-gray-700',
|
|
37
|
+
MEDIUM: 'bg-yellow-100 text-yellow-700',
|
|
38
|
+
HIGH: 'bg-orange-100 text-orange-700',
|
|
39
|
+
URGENT: 'bg-red-100 text-red-700',
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
function formatDateTime(dateString: string) {
|
|
43
|
+
const date = new Date(dateString);
|
|
44
|
+
return {
|
|
45
|
+
date: date.toLocaleDateString('fr-FR', {
|
|
46
|
+
day: 'numeric',
|
|
47
|
+
month: 'short',
|
|
48
|
+
}),
|
|
49
|
+
time: date.toLocaleTimeString('fr-FR', {
|
|
50
|
+
hour: '2-digit',
|
|
51
|
+
minute: '2-digit',
|
|
52
|
+
}),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function Header() {
|
|
57
|
+
const { data: session } = useSession();
|
|
58
|
+
const router = useRouter();
|
|
59
|
+
const [showRemindersDropdown, setShowRemindersDropdown] = useState(false);
|
|
60
|
+
const [showUserDropdown, setShowUserDropdown] = useState(false);
|
|
61
|
+
const [reminders, setReminders] = useState<Reminder[]>([]);
|
|
62
|
+
const [readReminders, setReadReminders] = useState<Set<string>>(new Set());
|
|
63
|
+
const [loading, setLoading] = useState(false);
|
|
64
|
+
const remindersRef = useRef<HTMLDivElement>(null);
|
|
65
|
+
const userRef = useRef<HTMLDivElement>(null);
|
|
66
|
+
|
|
67
|
+
const userName = session?.user?.name || 'Utilisateur';
|
|
68
|
+
const userEmail = session?.user?.email || '';
|
|
69
|
+
const userInitial = userName?.[0]?.toUpperCase() || 'U';
|
|
70
|
+
|
|
71
|
+
// Charger les rappels lus depuis localStorage
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
if (typeof window !== 'undefined') {
|
|
74
|
+
const stored = localStorage.getItem('readReminders');
|
|
75
|
+
if (stored) {
|
|
76
|
+
try {
|
|
77
|
+
setReadReminders(new Set(JSON.parse(stored)));
|
|
78
|
+
} catch (e) {
|
|
79
|
+
console.error('Erreur lors du chargement des rappels lus:', e);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}, []);
|
|
84
|
+
|
|
85
|
+
// Sauvegarder les rappels lus dans localStorage
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
if (typeof window !== 'undefined' && readReminders.size > 0) {
|
|
88
|
+
localStorage.setItem('readReminders', JSON.stringify(Array.from(readReminders)));
|
|
89
|
+
}
|
|
90
|
+
}, [readReminders]);
|
|
91
|
+
|
|
92
|
+
// Charger les rappels
|
|
93
|
+
useEffect(() => {
|
|
94
|
+
if (!session) return;
|
|
95
|
+
|
|
96
|
+
const fetchReminders = async () => {
|
|
97
|
+
try {
|
|
98
|
+
setLoading(true);
|
|
99
|
+
const response = await fetch('/api/reminders');
|
|
100
|
+
if (response.ok) {
|
|
101
|
+
const data = await response.json();
|
|
102
|
+
setReminders(data);
|
|
103
|
+
}
|
|
104
|
+
} catch (error) {
|
|
105
|
+
console.error('Erreur lors du chargement des rappels:', error);
|
|
106
|
+
} finally {
|
|
107
|
+
setLoading(false);
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
fetchReminders();
|
|
112
|
+
const interval = setInterval(fetchReminders, 60 * 1000); // Rafraîchir toutes les minutes
|
|
113
|
+
return () => clearInterval(interval);
|
|
114
|
+
}, [session]);
|
|
115
|
+
|
|
116
|
+
// Fermer les dropdowns en cliquant à l'extérieur
|
|
117
|
+
useEffect(() => {
|
|
118
|
+
const handleClickOutside = (event: MouseEvent) => {
|
|
119
|
+
if (remindersRef.current && !remindersRef.current.contains(event.target as Node)) {
|
|
120
|
+
setShowRemindersDropdown(false);
|
|
121
|
+
}
|
|
122
|
+
if (userRef.current && !userRef.current.contains(event.target as Node)) {
|
|
123
|
+
setShowUserDropdown(false);
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
128
|
+
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
129
|
+
}, []);
|
|
130
|
+
|
|
131
|
+
const unreadCount = reminders.filter((r) => !readReminders.has(r.id)).length;
|
|
132
|
+
|
|
133
|
+
const handleMarkAsRead = (reminderId: string) => {
|
|
134
|
+
setReadReminders((prev) => new Set([...prev, reminderId]));
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const handleMarkAllAsRead = () => {
|
|
138
|
+
const allIds = new Set(reminders.map((r) => r.id));
|
|
139
|
+
setReadReminders((prev) => new Set([...prev, ...allIds]));
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const handleSignOut = async () => {
|
|
143
|
+
await signOut();
|
|
144
|
+
router.push('/signin');
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
return (
|
|
148
|
+
<header className="sticky top-0 z-30 border-b border-gray-200 bg-white px-4 py-3 sm:px-6 lg:px-8">
|
|
149
|
+
<div className="flex items-center justify-between gap-2 sm:gap-4">
|
|
150
|
+
{/* Left: Logo + Greeting */}
|
|
151
|
+
<div className="flex items-center gap-2 sm:gap-3">
|
|
152
|
+
<div className="flex items-center gap-1.5 sm:gap-2">
|
|
153
|
+
{/* <Rocket className="h-5 w-5 text-indigo-600 sm:h-6 sm:w-6" /> */}
|
|
154
|
+
<span className="text-base font-bold text-gray-900 sm:text-lg">CRM Template</span>
|
|
155
|
+
</div>
|
|
156
|
+
<span className="hidden text-sm text-gray-600 sm:inline">👋 Salut {userName} !</span>
|
|
157
|
+
</div>
|
|
158
|
+
|
|
159
|
+
{/* Right: Notifications + User Avatar */}
|
|
160
|
+
<div className="flex items-center gap-2 sm:gap-3">
|
|
161
|
+
{/* Notifications Dropdown */}
|
|
162
|
+
<div className="relative" ref={remindersRef}>
|
|
163
|
+
<button
|
|
164
|
+
onClick={() => setShowRemindersDropdown(!showRemindersDropdown)}
|
|
165
|
+
className="relative cursor-pointer rounded-lg p-2 text-gray-600 transition-colors hover:bg-gray-100"
|
|
166
|
+
aria-label="Notifications"
|
|
167
|
+
>
|
|
168
|
+
<Bell className="h-5 w-5" />
|
|
169
|
+
{unreadCount > 0 && (
|
|
170
|
+
<span className="absolute top-1 right-1 flex h-4 w-4 items-center justify-center rounded-full bg-blue-600 text-[10px] font-semibold text-white">
|
|
171
|
+
{unreadCount > 9 ? '9+' : unreadCount}
|
|
172
|
+
</span>
|
|
173
|
+
)}
|
|
174
|
+
</button>
|
|
175
|
+
|
|
176
|
+
{/* Dropdown des rappels */}
|
|
177
|
+
{showRemindersDropdown && (
|
|
178
|
+
<div className="absolute right-0 mt-2 w-80 max-w-[calc(100vw-2rem)] rounded-lg border border-gray-200 bg-white shadow-xl">
|
|
179
|
+
<div className="border-b border-gray-200 px-4 py-3">
|
|
180
|
+
<div className="flex items-center justify-between">
|
|
181
|
+
<h3 className="text-sm font-semibold text-gray-900">Rappels</h3>
|
|
182
|
+
{unreadCount > 0 && (
|
|
183
|
+
<button
|
|
184
|
+
onClick={handleMarkAllAsRead}
|
|
185
|
+
className="cursor-pointer text-xs text-indigo-600 hover:text-indigo-700"
|
|
186
|
+
>
|
|
187
|
+
Tout marquer comme lu
|
|
188
|
+
</button>
|
|
189
|
+
)}
|
|
190
|
+
</div>
|
|
191
|
+
</div>
|
|
192
|
+
<div className="max-h-96 overflow-y-auto">
|
|
193
|
+
{loading ? (
|
|
194
|
+
<div className="px-4 py-8 text-center text-sm text-gray-500">
|
|
195
|
+
Chargement...
|
|
196
|
+
</div>
|
|
197
|
+
) : reminders.length === 0 ? (
|
|
198
|
+
<div className="px-4 py-8 text-center text-sm text-gray-500">
|
|
199
|
+
Aucun rappel
|
|
200
|
+
</div>
|
|
201
|
+
) : (
|
|
202
|
+
<div className="divide-y divide-gray-100">
|
|
203
|
+
{reminders.map((reminder) => {
|
|
204
|
+
const isRead = readReminders.has(reminder.id);
|
|
205
|
+
const { date, time } = formatDateTime(reminder.scheduledAt);
|
|
206
|
+
const contactName = reminder.contact
|
|
207
|
+
? `${reminder.contact.firstName || ''} ${reminder.contact.lastName || ''}`.trim() ||
|
|
208
|
+
'Contact sans nom'
|
|
209
|
+
: null;
|
|
210
|
+
|
|
211
|
+
return (
|
|
212
|
+
<div
|
|
213
|
+
key={reminder.id}
|
|
214
|
+
className={cn(
|
|
215
|
+
'px-4 py-3 transition-colors hover:bg-gray-50',
|
|
216
|
+
!isRead && 'bg-blue-50/50',
|
|
217
|
+
)}
|
|
218
|
+
onClick={() => handleMarkAsRead(reminder.id)}
|
|
219
|
+
>
|
|
220
|
+
<div className="flex items-start gap-3">
|
|
221
|
+
<div className="mt-0.5 shrink-0">
|
|
222
|
+
<Calendar className="h-4 w-4 text-indigo-600" />
|
|
223
|
+
</div>
|
|
224
|
+
<div className="min-w-0 flex-1">
|
|
225
|
+
<div className="flex items-start justify-between gap-2">
|
|
226
|
+
<div className="min-w-0 flex-1">
|
|
227
|
+
<p
|
|
228
|
+
className={cn(
|
|
229
|
+
'text-sm font-medium',
|
|
230
|
+
!isRead ? 'text-gray-900' : 'text-gray-700',
|
|
231
|
+
)}
|
|
232
|
+
>
|
|
233
|
+
{reminder.title || TASK_TYPE_LABELS[reminder.type] || 'Tâche'}
|
|
234
|
+
</p>
|
|
235
|
+
{contactName && (
|
|
236
|
+
<p className="mt-0.5 text-xs text-gray-500">{contactName}</p>
|
|
237
|
+
)}
|
|
238
|
+
<div className="mt-1 flex items-center gap-2">
|
|
239
|
+
<span className="text-xs text-gray-500">{date}</span>
|
|
240
|
+
<span className="text-xs text-gray-400">•</span>
|
|
241
|
+
<span className="text-xs text-gray-500">{time}</span>
|
|
242
|
+
<span
|
|
243
|
+
className={cn(
|
|
244
|
+
'rounded-full px-1.5 py-0.5 text-[10px] font-medium',
|
|
245
|
+
PRIORITY_COLORS[reminder.priority] ||
|
|
246
|
+
PRIORITY_COLORS.MEDIUM,
|
|
247
|
+
)}
|
|
248
|
+
>
|
|
249
|
+
{reminder.priority === 'URGENT'
|
|
250
|
+
? 'Urgente'
|
|
251
|
+
: reminder.priority === 'HIGH'
|
|
252
|
+
? 'Haute'
|
|
253
|
+
: reminder.priority === 'MEDIUM'
|
|
254
|
+
? 'Moyenne'
|
|
255
|
+
: 'Faible'}
|
|
256
|
+
</span>
|
|
257
|
+
</div>
|
|
258
|
+
</div>
|
|
259
|
+
{!isRead && (
|
|
260
|
+
<div className="h-2 w-2 shrink-0 rounded-full bg-blue-600" />
|
|
261
|
+
)}
|
|
262
|
+
</div>
|
|
263
|
+
{reminder.contact && (
|
|
264
|
+
<Link
|
|
265
|
+
href={`/contacts/${reminder.contact.id}`}
|
|
266
|
+
onClick={(e) => e.stopPropagation()}
|
|
267
|
+
className="mt-2 inline-block text-xs font-medium text-indigo-600 hover:text-indigo-700"
|
|
268
|
+
>
|
|
269
|
+
Voir le contact
|
|
270
|
+
</Link>
|
|
271
|
+
)}
|
|
272
|
+
</div>
|
|
273
|
+
</div>
|
|
274
|
+
</div>
|
|
275
|
+
);
|
|
276
|
+
})}
|
|
277
|
+
</div>
|
|
278
|
+
)}
|
|
279
|
+
</div>
|
|
280
|
+
</div>
|
|
281
|
+
)}
|
|
282
|
+
</div>
|
|
283
|
+
|
|
284
|
+
{/* User Avatar Dropdown */}
|
|
285
|
+
<div className="relative" ref={userRef}>
|
|
286
|
+
<button
|
|
287
|
+
onClick={() => setShowUserDropdown(!showUserDropdown)}
|
|
288
|
+
className="flex items-center gap-1.5 sm:gap-2 cursor-pointer"
|
|
289
|
+
aria-label="Menu utilisateur"
|
|
290
|
+
>
|
|
291
|
+
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-indigo-100 text-xs font-semibold text-indigo-600 sm:h-9 sm:w-9 sm:text-sm">
|
|
292
|
+
{userInitial}
|
|
293
|
+
</div>
|
|
294
|
+
<ChevronDown className="hidden h-4 w-4 text-gray-400 transition-colors hover:text-gray-600 sm:block" />
|
|
295
|
+
</button>
|
|
296
|
+
|
|
297
|
+
{/* Dropdown utilisateur */}
|
|
298
|
+
{showUserDropdown && (
|
|
299
|
+
<div className="absolute right-0 mt-2 w-56 rounded-lg border border-gray-200 bg-white shadow-xl">
|
|
300
|
+
<div className="px-4 py-3 border-b border-gray-200">
|
|
301
|
+
<p className="text-sm font-medium text-gray-900">{userName}</p>
|
|
302
|
+
<p className="mt-0.5 text-xs text-gray-500">{userEmail}</p>
|
|
303
|
+
</div>
|
|
304
|
+
<div className="py-1">
|
|
305
|
+
<button
|
|
306
|
+
onClick={handleSignOut}
|
|
307
|
+
className="flex w-full cursor-pointer items-center gap-2 px-4 py-2 text-sm text-gray-700 transition-colors hover:bg-gray-50"
|
|
308
|
+
>
|
|
309
|
+
<LogOut className="h-4 w-4" />
|
|
310
|
+
<span>Déconnexion</span>
|
|
311
|
+
</button>
|
|
312
|
+
</div>
|
|
313
|
+
</div>
|
|
314
|
+
)}
|
|
315
|
+
</div>
|
|
316
|
+
</div>
|
|
317
|
+
</div>
|
|
318
|
+
</header>
|
|
319
|
+
);
|
|
320
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
interface InvitationEmailProps {
|
|
2
|
+
name: string;
|
|
3
|
+
invitationUrl: string;
|
|
4
|
+
signature?: string | null;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function InvitationEmailTemplate({ name, invitationUrl, signature }: InvitationEmailProps) {
|
|
8
|
+
return (
|
|
9
|
+
<div
|
|
10
|
+
style={{
|
|
11
|
+
fontFamily: 'Arial, sans-serif',
|
|
12
|
+
padding: '20px',
|
|
13
|
+
maxWidth: '600px',
|
|
14
|
+
margin: '0 auto',
|
|
15
|
+
}}
|
|
16
|
+
>
|
|
17
|
+
<h1 style={{ color: '#1F2937', fontSize: '24px', marginBottom: '16px' }}>
|
|
18
|
+
Bienvenue, {name} !
|
|
19
|
+
</h1>
|
|
20
|
+
<p style={{ color: '#4B5563', fontSize: '16px', lineHeight: '1.6', marginBottom: '16px' }}>
|
|
21
|
+
Vous avez été invité à rejoindre notre plateforme CRM.
|
|
22
|
+
</p>
|
|
23
|
+
<p style={{ color: '#4B5563', fontSize: '16px', lineHeight: '1.6', marginBottom: '24px' }}>
|
|
24
|
+
Cliquez sur le bouton ci-dessous pour définir votre mot de passe et activer votre compte :
|
|
25
|
+
</p>
|
|
26
|
+
<div style={{ textAlign: 'center', marginBottom: '32px' }}>
|
|
27
|
+
<a
|
|
28
|
+
href={invitationUrl}
|
|
29
|
+
style={{
|
|
30
|
+
display: 'inline-block',
|
|
31
|
+
padding: '12px 24px',
|
|
32
|
+
backgroundColor: '#4F46E5',
|
|
33
|
+
color: 'white',
|
|
34
|
+
textDecoration: 'none',
|
|
35
|
+
borderRadius: '6px',
|
|
36
|
+
fontWeight: '600',
|
|
37
|
+
fontSize: '16px',
|
|
38
|
+
}}
|
|
39
|
+
>
|
|
40
|
+
Définir mon mot de passe
|
|
41
|
+
</a>
|
|
42
|
+
</div>
|
|
43
|
+
<p style={{ color: '#6B7280', fontSize: '14px', lineHeight: '1.6', marginBottom: '8px' }}>
|
|
44
|
+
Si le bouton ne fonctionne pas, copiez et collez ce lien dans votre navigateur :
|
|
45
|
+
</p>
|
|
46
|
+
<p
|
|
47
|
+
style={{ color: '#4F46E5', fontSize: '12px', wordBreak: 'break-all', marginBottom: '24px' }}
|
|
48
|
+
>
|
|
49
|
+
{invitationUrl}
|
|
50
|
+
</p>
|
|
51
|
+
<p
|
|
52
|
+
style={{
|
|
53
|
+
color: '#9CA3AF',
|
|
54
|
+
fontSize: '12px',
|
|
55
|
+
marginTop: '32px',
|
|
56
|
+
paddingTop: '16px',
|
|
57
|
+
borderTop: '1px solid #E5E7EB',
|
|
58
|
+
}}
|
|
59
|
+
>
|
|
60
|
+
Ce lien est valide pendant 24 heures. Si vous n'avez pas demandé cette invitation, vous
|
|
61
|
+
pouvez ignorer cet email.
|
|
62
|
+
</p>
|
|
63
|
+
|
|
64
|
+
{signature && (
|
|
65
|
+
<div
|
|
66
|
+
style={{
|
|
67
|
+
marginTop: '24px',
|
|
68
|
+
paddingTop: '16px',
|
|
69
|
+
borderTop: '1px solid #E5E7EB',
|
|
70
|
+
color: '#4B5563',
|
|
71
|
+
fontSize: '14px',
|
|
72
|
+
lineHeight: '1.6',
|
|
73
|
+
}}
|
|
74
|
+
dangerouslySetInnerHTML={{ __html: signature }}
|
|
75
|
+
/>
|
|
76
|
+
)}
|
|
77
|
+
</div>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
interface MeetCancellationEmailTemplateProps {
|
|
2
|
+
contactName: string;
|
|
3
|
+
title: string;
|
|
4
|
+
scheduledAt: string;
|
|
5
|
+
durationMinutes?: number;
|
|
6
|
+
meetLink?: string;
|
|
7
|
+
description?: string;
|
|
8
|
+
organizerName: string;
|
|
9
|
+
signature?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function MeetCancellationEmailTemplate({
|
|
13
|
+
contactName,
|
|
14
|
+
title,
|
|
15
|
+
scheduledAt,
|
|
16
|
+
durationMinutes,
|
|
17
|
+
meetLink,
|
|
18
|
+
description,
|
|
19
|
+
organizerName,
|
|
20
|
+
signature,
|
|
21
|
+
}: MeetCancellationEmailTemplateProps) {
|
|
22
|
+
const formatDate = (dateString: string) => {
|
|
23
|
+
const date = new Date(dateString);
|
|
24
|
+
return date.toLocaleDateString('fr-FR', {
|
|
25
|
+
weekday: 'long',
|
|
26
|
+
year: 'numeric',
|
|
27
|
+
month: 'long',
|
|
28
|
+
day: 'numeric',
|
|
29
|
+
});
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const formatTime = (dateString: string) => {
|
|
33
|
+
const date = new Date(dateString);
|
|
34
|
+
return date.toLocaleTimeString('fr-FR', {
|
|
35
|
+
hour: '2-digit',
|
|
36
|
+
minute: '2-digit',
|
|
37
|
+
});
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const formatDuration = (minutes: number) => {
|
|
41
|
+
if (minutes < 60) {
|
|
42
|
+
return `${minutes} minutes`;
|
|
43
|
+
}
|
|
44
|
+
const hours = Math.floor(minutes / 60);
|
|
45
|
+
const mins = minutes % 60;
|
|
46
|
+
if (mins === 0) {
|
|
47
|
+
return `${hours} heure${hours > 1 ? 's' : ''}`;
|
|
48
|
+
}
|
|
49
|
+
return `${hours} heure${hours > 1 ? 's' : ''} ${mins} minute${mins > 1 ? 's' : ''}`;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<div style={{ fontFamily: 'Arial, sans-serif', lineHeight: '1.6', color: '#333' }}>
|
|
54
|
+
<div style={{ maxWidth: '600px', margin: '0 auto', padding: '20px' }}>
|
|
55
|
+
<h1 style={{ color: '#EF4444', fontSize: '24px', marginBottom: '20px' }}>
|
|
56
|
+
Annulation de rendez-vous
|
|
57
|
+
</h1>
|
|
58
|
+
|
|
59
|
+
<p style={{ fontSize: '16px', marginBottom: '20px' }}>Bonjour {contactName},</p>
|
|
60
|
+
|
|
61
|
+
<p style={{ fontSize: '16px', marginBottom: '20px' }}>
|
|
62
|
+
Nous vous informons que votre rendez-vous a été annulé.
|
|
63
|
+
</p>
|
|
64
|
+
|
|
65
|
+
<div
|
|
66
|
+
style={{
|
|
67
|
+
backgroundColor: '#FEF2F2',
|
|
68
|
+
borderLeft: '4px solid #EF4444',
|
|
69
|
+
padding: '20px',
|
|
70
|
+
borderRadius: '8px',
|
|
71
|
+
marginBottom: '20px',
|
|
72
|
+
}}
|
|
73
|
+
>
|
|
74
|
+
<h2 style={{ color: '#1a1a1a', fontSize: '20px', marginBottom: '15px' }}>{title}</h2>
|
|
75
|
+
|
|
76
|
+
<div style={{ marginBottom: '10px' }}>
|
|
77
|
+
<strong>Date :</strong> {formatDate(scheduledAt)}
|
|
78
|
+
</div>
|
|
79
|
+
<div style={{ marginBottom: '10px' }}>
|
|
80
|
+
<strong>Heure :</strong> {formatTime(scheduledAt)}
|
|
81
|
+
</div>
|
|
82
|
+
{meetLink && durationMinutes && (
|
|
83
|
+
<div style={{ marginBottom: '10px' }}>
|
|
84
|
+
<strong>Durée :</strong> {formatDuration(durationMinutes)}
|
|
85
|
+
</div>
|
|
86
|
+
)}
|
|
87
|
+
<div style={{ marginBottom: '10px' }}>
|
|
88
|
+
<strong>Organisateur :</strong> {organizerName}
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
{description && (
|
|
92
|
+
<div style={{ marginTop: '15px', paddingTop: '15px', borderTop: '1px solid #ddd' }}>
|
|
93
|
+
<strong>Description :</strong>
|
|
94
|
+
<div
|
|
95
|
+
style={{ marginTop: '10px' }}
|
|
96
|
+
dangerouslySetInnerHTML={{ __html: description }}
|
|
97
|
+
/>
|
|
98
|
+
</div>
|
|
99
|
+
)}
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
<p style={{ fontSize: '16px', marginBottom: '20px', color: '#666' }}>
|
|
103
|
+
Si vous souhaitez reprogrammer ce rendez-vous, n'hésitez pas à nous contacter.
|
|
104
|
+
</p>
|
|
105
|
+
|
|
106
|
+
{signature && (
|
|
107
|
+
<div
|
|
108
|
+
style={{
|
|
109
|
+
marginTop: '30px',
|
|
110
|
+
paddingTop: '20px',
|
|
111
|
+
borderTop: '1px solid #ddd',
|
|
112
|
+
fontSize: '14px',
|
|
113
|
+
}}
|
|
114
|
+
dangerouslySetInnerHTML={{ __html: signature }}
|
|
115
|
+
/>
|
|
116
|
+
)}
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
);
|
|
120
|
+
}
|