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,371 @@
|
|
|
1
|
+
import { prisma } from '@/lib/prisma';
|
|
2
|
+
import { InteractionType } from '../../generated/prisma/client';
|
|
3
|
+
|
|
4
|
+
interface CreateInteractionParams {
|
|
5
|
+
contactId: string;
|
|
6
|
+
type: InteractionType;
|
|
7
|
+
title?: string | null;
|
|
8
|
+
content: string;
|
|
9
|
+
userId: string;
|
|
10
|
+
date?: Date | null;
|
|
11
|
+
metadata?: Record<string, any>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Crée une interaction pour un contact
|
|
16
|
+
*/
|
|
17
|
+
export async function createInteraction(params: CreateInteractionParams) {
|
|
18
|
+
const { contactId, type, title, content, userId, date, metadata } = params;
|
|
19
|
+
|
|
20
|
+
return await prisma.interaction.create({
|
|
21
|
+
data: {
|
|
22
|
+
contactId,
|
|
23
|
+
type,
|
|
24
|
+
title: title ?? null,
|
|
25
|
+
content,
|
|
26
|
+
userId,
|
|
27
|
+
date: date ?? null,
|
|
28
|
+
metadata: metadata ?? undefined,
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Crée une interaction pour un changement de statut
|
|
35
|
+
*/
|
|
36
|
+
export async function logStatusChange(
|
|
37
|
+
contactId: string,
|
|
38
|
+
oldStatusId: string | null,
|
|
39
|
+
newStatusId: string | null,
|
|
40
|
+
userId: string,
|
|
41
|
+
oldStatusName?: string | null,
|
|
42
|
+
newStatusName?: string | null,
|
|
43
|
+
) {
|
|
44
|
+
const oldStatus = oldStatusName || 'Aucun';
|
|
45
|
+
const newStatus = newStatusName || 'Aucun';
|
|
46
|
+
|
|
47
|
+
return await createInteraction({
|
|
48
|
+
contactId,
|
|
49
|
+
type: 'STATUS_CHANGE',
|
|
50
|
+
title: 'Changement de statut',
|
|
51
|
+
content: `Statut modifié de "${oldStatus}" à "${newStatus}"`,
|
|
52
|
+
userId,
|
|
53
|
+
metadata: {
|
|
54
|
+
oldStatusId,
|
|
55
|
+
newStatusId,
|
|
56
|
+
oldStatusName,
|
|
57
|
+
newStatusName,
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Crée une interaction pour une mise à jour de contact
|
|
64
|
+
*/
|
|
65
|
+
export async function logContactUpdate(
|
|
66
|
+
contactId: string,
|
|
67
|
+
changes: Record<string, { old: any; new: any }>,
|
|
68
|
+
userId: string,
|
|
69
|
+
) {
|
|
70
|
+
const changeDescriptions: string[] = [];
|
|
71
|
+
|
|
72
|
+
for (const [field, { old, new: newValue }] of Object.entries(changes)) {
|
|
73
|
+
const fieldNames: Record<string, string> = {
|
|
74
|
+
firstName: 'Prénom',
|
|
75
|
+
lastName: 'Nom',
|
|
76
|
+
phone: 'Téléphone',
|
|
77
|
+
secondaryPhone: 'Téléphone secondaire',
|
|
78
|
+
email: 'Email',
|
|
79
|
+
address: 'Adresse',
|
|
80
|
+
city: 'Ville',
|
|
81
|
+
postalCode: 'Code postal',
|
|
82
|
+
civility: 'Civilité',
|
|
83
|
+
origin: 'Origine',
|
|
84
|
+
companyName: 'Entreprise',
|
|
85
|
+
closingReason: 'Motif de fermeture',
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const fieldName = fieldNames[field] || field;
|
|
89
|
+
// Normaliser les valeurs : null, undefined et chaînes vides sont considérés comme équivalents
|
|
90
|
+
const normalize = (value: any) => {
|
|
91
|
+
if (value === null || value === undefined) return null;
|
|
92
|
+
if (typeof value === 'string' && value.trim() === '') return null;
|
|
93
|
+
return value;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const oldNorm = normalize(old);
|
|
97
|
+
const newNorm = normalize(newValue);
|
|
98
|
+
|
|
99
|
+
// Si après normalisation il n'y a pas de vraie différence, on ignore ce champ
|
|
100
|
+
if (oldNorm === newNorm) {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const formatValue = (value: any) => {
|
|
105
|
+
if (value === null || value === undefined) return 'Aucun';
|
|
106
|
+
return String(value);
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const oldValueStr = formatValue(oldNorm);
|
|
110
|
+
const newValueStr = formatValue(newNorm);
|
|
111
|
+
|
|
112
|
+
if (oldValueStr !== newValueStr) {
|
|
113
|
+
changeDescriptions.push(`${fieldName}: "${oldValueStr}" → "${newValueStr}"`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (changeDescriptions.length === 0) {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return await createInteraction({
|
|
122
|
+
contactId,
|
|
123
|
+
type: 'CONTACT_UPDATE',
|
|
124
|
+
title: 'Modification de la fiche contact',
|
|
125
|
+
content: changeDescriptions.join('\n'),
|
|
126
|
+
userId,
|
|
127
|
+
metadata: { changes },
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Crée une interaction pour un changement d'assignation
|
|
133
|
+
*/
|
|
134
|
+
export async function logAssignmentChange(
|
|
135
|
+
contactId: string,
|
|
136
|
+
type: 'COMMERCIAL' | 'TELEPRO',
|
|
137
|
+
oldUserId: string | null,
|
|
138
|
+
newUserId: string | null,
|
|
139
|
+
userId: string,
|
|
140
|
+
oldUserName?: string | null,
|
|
141
|
+
newUserName?: string | null,
|
|
142
|
+
) {
|
|
143
|
+
// Normaliser les valeurs pour la comparaison (null, undefined, '' sont considérés comme équivalents)
|
|
144
|
+
const normalizedOldUserId = oldUserId || null;
|
|
145
|
+
const normalizedNewUserId = newUserId || null;
|
|
146
|
+
|
|
147
|
+
// Ne créer l'interaction que si les valeurs ont réellement changé
|
|
148
|
+
if (normalizedOldUserId === normalizedNewUserId) {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const roleName = type === 'COMMERCIAL' ? 'Commercial' : 'Télépro';
|
|
153
|
+
const oldName = oldUserName || 'Non attribué';
|
|
154
|
+
const newName = newUserName || 'Non attribué';
|
|
155
|
+
|
|
156
|
+
return await createInteraction({
|
|
157
|
+
contactId,
|
|
158
|
+
type: 'ASSIGNMENT_CHANGE',
|
|
159
|
+
title: `Changement d'assignation ${roleName}`,
|
|
160
|
+
content: `${roleName} modifié de "${oldName}" à "${newName}"`,
|
|
161
|
+
userId,
|
|
162
|
+
metadata: {
|
|
163
|
+
assignmentType: type,
|
|
164
|
+
oldUserId,
|
|
165
|
+
newUserId,
|
|
166
|
+
oldUserName,
|
|
167
|
+
newUserName,
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Crée une interaction pour la création d'un rendez-vous
|
|
174
|
+
*/
|
|
175
|
+
export async function logAppointmentCreated(
|
|
176
|
+
contactId: string,
|
|
177
|
+
taskId: string,
|
|
178
|
+
scheduledAt: Date,
|
|
179
|
+
title: string | null,
|
|
180
|
+
userId: string,
|
|
181
|
+
) {
|
|
182
|
+
const formattedDate = scheduledAt.toLocaleDateString('fr-FR', {
|
|
183
|
+
day: 'numeric',
|
|
184
|
+
month: 'long',
|
|
185
|
+
year: 'numeric',
|
|
186
|
+
hour: '2-digit',
|
|
187
|
+
minute: '2-digit',
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
return await createInteraction({
|
|
191
|
+
contactId,
|
|
192
|
+
type: 'APPOINTMENT_CREATED',
|
|
193
|
+
title: title ?? null, // Enregistrer seulement le titre
|
|
194
|
+
content: `Rendez-vous programmé le ${formattedDate}`,
|
|
195
|
+
userId,
|
|
196
|
+
date: scheduledAt,
|
|
197
|
+
metadata: {
|
|
198
|
+
taskId,
|
|
199
|
+
scheduledAt: scheduledAt.toISOString(),
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Crée une interaction pour l'annulation d'un rendez-vous
|
|
206
|
+
*/
|
|
207
|
+
export async function logAppointmentCancelled(
|
|
208
|
+
contactId: string,
|
|
209
|
+
taskId: string,
|
|
210
|
+
scheduledAt: Date,
|
|
211
|
+
title: string | null,
|
|
212
|
+
userId: string,
|
|
213
|
+
isGoogleMeet: boolean = false,
|
|
214
|
+
) {
|
|
215
|
+
const formattedDate = scheduledAt.toLocaleDateString('fr-FR', {
|
|
216
|
+
day: 'numeric',
|
|
217
|
+
month: 'long',
|
|
218
|
+
year: 'numeric',
|
|
219
|
+
hour: '2-digit',
|
|
220
|
+
minute: '2-digit',
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
const appointmentType = isGoogleMeet ? 'Google Meet' : 'Rendez-vous';
|
|
224
|
+
|
|
225
|
+
return await createInteraction({
|
|
226
|
+
contactId,
|
|
227
|
+
type: 'APPOINTMENT_DELETED',
|
|
228
|
+
title: title ?? null, // Enregistrer seulement le titre
|
|
229
|
+
content: `${appointmentType} prévu le ${formattedDate} a été annulé.`,
|
|
230
|
+
userId,
|
|
231
|
+
date: scheduledAt,
|
|
232
|
+
metadata: {
|
|
233
|
+
taskId,
|
|
234
|
+
scheduledAt: scheduledAt.toISOString(),
|
|
235
|
+
cancelled: true,
|
|
236
|
+
isGoogleMeet,
|
|
237
|
+
},
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Crée une interaction pour la modification d'un rendez-vous
|
|
243
|
+
*/
|
|
244
|
+
export async function logAppointmentChanged(
|
|
245
|
+
contactId: string,
|
|
246
|
+
taskId: string,
|
|
247
|
+
scheduledAt: Date,
|
|
248
|
+
title: string | null,
|
|
249
|
+
userId: string,
|
|
250
|
+
isGoogleMeet: boolean = false,
|
|
251
|
+
) {
|
|
252
|
+
const formattedDate = scheduledAt.toLocaleDateString('fr-FR', {
|
|
253
|
+
day: 'numeric',
|
|
254
|
+
month: 'long',
|
|
255
|
+
year: 'numeric',
|
|
256
|
+
hour: '2-digit',
|
|
257
|
+
minute: '2-digit',
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
const appointmentType = isGoogleMeet ? 'Google Meet' : 'Rendez-vous';
|
|
261
|
+
|
|
262
|
+
return await createInteraction({
|
|
263
|
+
contactId,
|
|
264
|
+
type: 'APPOINTMENT_CHANGED',
|
|
265
|
+
title: title ?? null, // Enregistrer seulement le titre
|
|
266
|
+
content: `${appointmentType} programmé le ${formattedDate} a été modifié.`,
|
|
267
|
+
userId,
|
|
268
|
+
date: scheduledAt,
|
|
269
|
+
metadata: {
|
|
270
|
+
taskId,
|
|
271
|
+
scheduledAt: scheduledAt.toISOString(),
|
|
272
|
+
isGoogleMeet,
|
|
273
|
+
},
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Crée une interaction pour l'upload d'un fichier
|
|
279
|
+
*/
|
|
280
|
+
export async function logFileUploaded(
|
|
281
|
+
contactId: string,
|
|
282
|
+
fileId: string,
|
|
283
|
+
fileName: string,
|
|
284
|
+
fileSize: number,
|
|
285
|
+
userId: string,
|
|
286
|
+
) {
|
|
287
|
+
const formatFileSize = (bytes: number): string => {
|
|
288
|
+
if (bytes === 0) return '0 Bytes';
|
|
289
|
+
const k = 1024;
|
|
290
|
+
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
|
291
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
292
|
+
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i];
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
return await createInteraction({
|
|
296
|
+
contactId,
|
|
297
|
+
type: 'FILE_UPLOADED',
|
|
298
|
+
title: null,
|
|
299
|
+
content: `Fichier "${fileName}" (${formatFileSize(fileSize)}) ajouté`,
|
|
300
|
+
userId,
|
|
301
|
+
metadata: {
|
|
302
|
+
fileId,
|
|
303
|
+
fileName,
|
|
304
|
+
fileSize,
|
|
305
|
+
},
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Crée une interaction pour le remplacement d'un fichier (doublon)
|
|
311
|
+
*/
|
|
312
|
+
export async function logFileReplaced(
|
|
313
|
+
contactId: string,
|
|
314
|
+
fileId: string,
|
|
315
|
+
fileName: string,
|
|
316
|
+
fileSize: number,
|
|
317
|
+
userId: string,
|
|
318
|
+
) {
|
|
319
|
+
const formatFileSize = (bytes: number): string => {
|
|
320
|
+
if (bytes === 0) return '0 Bytes';
|
|
321
|
+
const k = 1024;
|
|
322
|
+
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
|
323
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
324
|
+
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i];
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
return await createInteraction({
|
|
328
|
+
contactId,
|
|
329
|
+
type: 'FILE_REPLACED',
|
|
330
|
+
title: null,
|
|
331
|
+
content: `Fichier "${fileName}" (${formatFileSize(fileSize)}) remplacé`,
|
|
332
|
+
userId,
|
|
333
|
+
metadata: {
|
|
334
|
+
fileId,
|
|
335
|
+
fileName,
|
|
336
|
+
fileSize,
|
|
337
|
+
replaced: true,
|
|
338
|
+
},
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Crée une interaction pour la suppression d'un fichier
|
|
344
|
+
*/
|
|
345
|
+
export async function logFileDeleted(
|
|
346
|
+
contactId: string,
|
|
347
|
+
fileName: string,
|
|
348
|
+
fileSize: number,
|
|
349
|
+
userId: string,
|
|
350
|
+
) {
|
|
351
|
+
const formatFileSize = (bytes: number): string => {
|
|
352
|
+
if (bytes === 0) return '0 Bytes';
|
|
353
|
+
const k = 1024;
|
|
354
|
+
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
|
355
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
356
|
+
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i];
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
return await createInteraction({
|
|
360
|
+
contactId,
|
|
361
|
+
type: 'FILE_DELETED',
|
|
362
|
+
title: null,
|
|
363
|
+
content: `Fichier "${fileName}" (${formatFileSize(fileSize)}) supprimé`,
|
|
364
|
+
userId,
|
|
365
|
+
metadata: {
|
|
366
|
+
fileName,
|
|
367
|
+
fileSize,
|
|
368
|
+
deleted: true,
|
|
369
|
+
},
|
|
370
|
+
});
|
|
371
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import crypto from 'crypto';
|
|
2
|
+
|
|
3
|
+
const ALGORITHM = 'aes-256-gcm';
|
|
4
|
+
const IV_LENGTH = 16; // 128 bits
|
|
5
|
+
|
|
6
|
+
// Récupérer la clé depuis les variables d'environnement
|
|
7
|
+
function getEncryptionKey(): Buffer | null {
|
|
8
|
+
const key = process.env.ENCRYPTION_KEY;
|
|
9
|
+
if (!key) {
|
|
10
|
+
console.warn("⚠️ ENCRYPTION_KEY n'est pas définie. Le chiffrement ne sera pas utilisé.");
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Si la clé est en hexadécimal, la convertir
|
|
15
|
+
if (key.length === 64) {
|
|
16
|
+
return Buffer.from(key, 'hex');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Sinon, utiliser directement (mais ce n'est pas recommandé)
|
|
20
|
+
// On génère un hash pour avoir exactement 32 bytes
|
|
21
|
+
return crypto.createHash('sha256').update(key).digest();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Chiffre un texte avec AES-256-GCM
|
|
26
|
+
* @param text - Le texte à chiffrer
|
|
27
|
+
* @returns Le texte chiffré au format: iv:tag:encrypted (ou texte en clair si ENCRYPTION_KEY n'est pas définie)
|
|
28
|
+
*/
|
|
29
|
+
export function encrypt(text: string): string {
|
|
30
|
+
if (!text) {
|
|
31
|
+
return text;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const key = getEncryptionKey();
|
|
35
|
+
|
|
36
|
+
// Si la clé n'est pas définie, retourner le texte en clair (avec un avertissement)
|
|
37
|
+
if (!key) {
|
|
38
|
+
console.warn(
|
|
39
|
+
'⚠️ Chiffrement désactivé : ENCRYPTION_KEY non définie. Le mot de passe sera stocké en clair.',
|
|
40
|
+
);
|
|
41
|
+
return text;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const iv = crypto.randomBytes(IV_LENGTH);
|
|
45
|
+
const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
|
|
46
|
+
|
|
47
|
+
let encrypted = cipher.update(text, 'utf8', 'hex');
|
|
48
|
+
encrypted += cipher.final('hex');
|
|
49
|
+
|
|
50
|
+
const tag = cipher.getAuthTag();
|
|
51
|
+
|
|
52
|
+
// Retourner: iv:tag:encrypted
|
|
53
|
+
return iv.toString('hex') + ':' + tag.toString('hex') + ':' + encrypted;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Déchiffre un texte chiffré avec AES-256-GCM
|
|
58
|
+
* @param encryptedData - Le texte chiffré au format: iv:tag:encrypted
|
|
59
|
+
* @returns Le texte déchiffré
|
|
60
|
+
*/
|
|
61
|
+
export function decrypt(encryptedData: string): string {
|
|
62
|
+
if (!encryptedData) {
|
|
63
|
+
return encryptedData;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Si le format ne correspond pas (ancien format non chiffré), retourner tel quel
|
|
67
|
+
const parts = encryptedData.split(':');
|
|
68
|
+
if (parts.length !== 3) {
|
|
69
|
+
// Probablement un ancien mot de passe non chiffré, retourner tel quel
|
|
70
|
+
// (pour la migration des données existantes)
|
|
71
|
+
return encryptedData;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
const key = getEncryptionKey();
|
|
76
|
+
|
|
77
|
+
// Si la clé n'est pas définie et que le texte est chiffré, on ne peut pas le déchiffrer
|
|
78
|
+
if (!key) {
|
|
79
|
+
console.warn('⚠️ Impossible de déchiffrer : ENCRYPTION_KEY non définie.');
|
|
80
|
+
return encryptedData;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const iv = Buffer.from(parts[0], 'hex');
|
|
84
|
+
const tag = Buffer.from(parts[1], 'hex');
|
|
85
|
+
const encrypted = parts[2];
|
|
86
|
+
|
|
87
|
+
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
|
|
88
|
+
decipher.setAuthTag(tag);
|
|
89
|
+
|
|
90
|
+
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
|
|
91
|
+
decrypted += decipher.final('utf8');
|
|
92
|
+
|
|
93
|
+
return decrypted;
|
|
94
|
+
} catch (error) {
|
|
95
|
+
// Si le déchiffrement échoue, retourner tel quel (pour la compatibilité)
|
|
96
|
+
console.error('Erreur lors du déchiffrement:', error);
|
|
97
|
+
return encryptedData;
|
|
98
|
+
}
|
|
99
|
+
}
|