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,482 @@
|
|
|
1
|
+
import { prisma } from '@/lib/prisma';
|
|
2
|
+
import nodemailer from 'nodemailer';
|
|
3
|
+
import { decrypt } from '@/lib/encryption';
|
|
4
|
+
import { replaceTemplateVariables } from '@/lib/template-variables';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Exécute les workflows déclenchés par la création d'un contact
|
|
8
|
+
*/
|
|
9
|
+
export async function executeWorkflowsOnContactCreated(contactId: string) {
|
|
10
|
+
try {
|
|
11
|
+
// Récupérer tous les workflows actifs avec le déclencheur CONTACT_CREATED
|
|
12
|
+
const workflows = await prisma.workflow.findMany({
|
|
13
|
+
where: {
|
|
14
|
+
active: true,
|
|
15
|
+
triggerType: 'CONTACT_CREATED',
|
|
16
|
+
},
|
|
17
|
+
include: {
|
|
18
|
+
actions: {
|
|
19
|
+
orderBy: { order: 'asc' },
|
|
20
|
+
include: {
|
|
21
|
+
emailTemplate: true,
|
|
22
|
+
newStatus: true,
|
|
23
|
+
conditionStatus: true,
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
user: {
|
|
27
|
+
include: {
|
|
28
|
+
smtpConfig: true,
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const contact = await prisma.contact.findUnique({
|
|
35
|
+
where: { id: contactId },
|
|
36
|
+
include: {
|
|
37
|
+
status: true,
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
if (!contact) return;
|
|
42
|
+
|
|
43
|
+
for (const workflow of workflows) {
|
|
44
|
+
await executeWorkflowActions(workflow, contactId, contact);
|
|
45
|
+
}
|
|
46
|
+
} catch (error) {
|
|
47
|
+
console.error('Erreur lors de l\'exécution des workflows pour contact créé:', error);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Exécute les workflows déclenchés par un changement de statut
|
|
53
|
+
*/
|
|
54
|
+
export async function executeWorkflowsOnStatusChanged(
|
|
55
|
+
contactId: string,
|
|
56
|
+
oldStatusId: string | null,
|
|
57
|
+
newStatusId: string | null,
|
|
58
|
+
) {
|
|
59
|
+
try {
|
|
60
|
+
// Récupérer tous les workflows actifs avec le déclencheur STATUS_CHANGED
|
|
61
|
+
const workflows = await prisma.workflow.findMany({
|
|
62
|
+
where: {
|
|
63
|
+
active: true,
|
|
64
|
+
triggerType: 'STATUS_CHANGED',
|
|
65
|
+
},
|
|
66
|
+
include: {
|
|
67
|
+
actions: {
|
|
68
|
+
orderBy: { order: 'asc' },
|
|
69
|
+
include: {
|
|
70
|
+
emailTemplate: true,
|
|
71
|
+
newStatus: true,
|
|
72
|
+
conditionStatus: true,
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
triggerStatus: true,
|
|
76
|
+
triggerToStatus: true,
|
|
77
|
+
user: {
|
|
78
|
+
include: {
|
|
79
|
+
smtpConfig: true,
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const contact = await prisma.contact.findUnique({
|
|
86
|
+
where: { id: contactId },
|
|
87
|
+
include: {
|
|
88
|
+
status: true,
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
if (!contact) return;
|
|
93
|
+
|
|
94
|
+
// Filtrer les workflows qui correspondent aux conditions de déclenchement
|
|
95
|
+
const matchingWorkflows = workflows.filter((workflow) => {
|
|
96
|
+
// Si le workflow spécifie un statut source, vérifier qu'il correspond
|
|
97
|
+
if (workflow.triggerFromStatusId && oldStatusId !== workflow.triggerFromStatusId) {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Si le workflow spécifie un statut cible, vérifier qu'il correspond
|
|
102
|
+
if (workflow.triggerToStatusId && newStatusId !== workflow.triggerToStatusId) {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return true;
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
for (const workflow of matchingWorkflows) {
|
|
110
|
+
await executeWorkflowActions(workflow, contactId, contact);
|
|
111
|
+
}
|
|
112
|
+
} catch (error) {
|
|
113
|
+
console.error('Erreur lors de l\'exécution des workflows pour changement de statut:', error);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Exécute les actions d'un workflow pour un contact
|
|
119
|
+
*/
|
|
120
|
+
async function executeWorkflowActions(workflow: any, contactId: string, contact: any) {
|
|
121
|
+
for (const action of workflow.actions) {
|
|
122
|
+
// Vérifier la condition si elle existe
|
|
123
|
+
if (action.conditionOperator && action.conditionStatusId) {
|
|
124
|
+
const contactStatusId = contact.statusId;
|
|
125
|
+
const conditionMet =
|
|
126
|
+
action.conditionOperator === 'EQUALS'
|
|
127
|
+
? contactStatusId === action.conditionStatusId
|
|
128
|
+
: contactStatusId !== action.conditionStatusId;
|
|
129
|
+
|
|
130
|
+
if (!conditionMet) {
|
|
131
|
+
continue; // Ignorer cette action si la condition n'est pas remplie
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Calculer la date d'exécution avec le délai
|
|
136
|
+
const delayMs =
|
|
137
|
+
(action.delayDays || 0) * 24 * 60 * 60 * 1000 + (action.delayHours || 0) * 60 * 60 * 1000;
|
|
138
|
+
const executeAt = new Date(Date.now() + delayMs);
|
|
139
|
+
|
|
140
|
+
// Exécuter l'action selon son type
|
|
141
|
+
switch (action.actionType) {
|
|
142
|
+
case 'SEND_EMAIL':
|
|
143
|
+
await executeSendEmailAction(action, workflow, contact, executeAt);
|
|
144
|
+
break;
|
|
145
|
+
case 'SEND_SMS':
|
|
146
|
+
await executeSendSMSAction(action, workflow, contact, executeAt);
|
|
147
|
+
break;
|
|
148
|
+
case 'CHANGE_STATUS':
|
|
149
|
+
await executeChangeStatusAction(action, workflow, contactId, executeAt);
|
|
150
|
+
break;
|
|
151
|
+
case 'CREATE_TASK':
|
|
152
|
+
await executeCreateTaskAction(action, workflow, contactId, executeAt);
|
|
153
|
+
break;
|
|
154
|
+
case 'WAIT':
|
|
155
|
+
// L'action "Attendre" est gérée par le délai, rien à faire ici
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Exécute l'action "Envoyer un email"
|
|
163
|
+
*/
|
|
164
|
+
async function executeSendEmailAction(
|
|
165
|
+
action: any,
|
|
166
|
+
workflow: any,
|
|
167
|
+
contact: any,
|
|
168
|
+
executeAt: Date,
|
|
169
|
+
) {
|
|
170
|
+
if (!action.emailTemplateId || !workflow.user.smtpConfig) {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
const template = action.emailTemplate;
|
|
176
|
+
if (!template) return;
|
|
177
|
+
|
|
178
|
+
// Préparer les variables de template (même logique que l'envoi manuel)
|
|
179
|
+
const variables = {
|
|
180
|
+
firstName: contact.firstName || '',
|
|
181
|
+
lastName: contact.lastName || '',
|
|
182
|
+
civility: contact.civility || '',
|
|
183
|
+
email: contact.email || '',
|
|
184
|
+
phone: contact.phone || '',
|
|
185
|
+
secondaryPhone: contact.secondaryPhone || '',
|
|
186
|
+
address: contact.address || '',
|
|
187
|
+
city: contact.city || '',
|
|
188
|
+
postalCode: contact.postalCode || '',
|
|
189
|
+
companyName: contact.companyName || '',
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const subject = replaceTemplateVariables(template.subject || '', variables);
|
|
193
|
+
const content = replaceTemplateVariables(template.content || '', variables);
|
|
194
|
+
|
|
195
|
+
// Si le délai est 0, envoyer immédiatement, sinon planifier
|
|
196
|
+
if (executeAt <= new Date()) {
|
|
197
|
+
await sendEmailImmediate(workflow, contact, template, subject, content);
|
|
198
|
+
} else {
|
|
199
|
+
// Planifier l'action pour exécution différée
|
|
200
|
+
await prisma.scheduledWorkflowAction.create({
|
|
201
|
+
data: {
|
|
202
|
+
workflowId: workflow.id,
|
|
203
|
+
actionId: action.id,
|
|
204
|
+
contactId: contact.id,
|
|
205
|
+
actionType: 'SEND_EMAIL',
|
|
206
|
+
actionData: {
|
|
207
|
+
emailTemplateId: action.emailTemplateId,
|
|
208
|
+
templateSubject: template.subject,
|
|
209
|
+
templateContent: template.content,
|
|
210
|
+
templateName: template.name,
|
|
211
|
+
smtpConfigId: workflow.user.smtpConfig.id,
|
|
212
|
+
userId: workflow.userId,
|
|
213
|
+
workflowName: workflow.name,
|
|
214
|
+
},
|
|
215
|
+
executeAt,
|
|
216
|
+
},
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
} catch (error) {
|
|
220
|
+
console.error('Erreur lors de l\'envoi de l\'email:', error);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Exécute l'action "Envoyer un SMS"
|
|
226
|
+
*/
|
|
227
|
+
async function executeSendSMSAction(action: any, workflow: any, contact: any, executeAt: Date) {
|
|
228
|
+
if (!action.smsMessage || !contact.phone) {
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
try {
|
|
233
|
+
// Remplacer les variables dans le message
|
|
234
|
+
let message = action.smsMessage;
|
|
235
|
+
const variables: Record<string, string> = {
|
|
236
|
+
firstName: contact.firstName || '',
|
|
237
|
+
lastName: contact.lastName || '',
|
|
238
|
+
email: contact.email || '',
|
|
239
|
+
phone: contact.phone || '',
|
|
240
|
+
companyName: contact.companyName || '',
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
for (const [key, value] of Object.entries(variables)) {
|
|
244
|
+
message = message.replace(new RegExp(`{${key}}`, 'g'), value);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Si le délai est 0, envoyer immédiatement
|
|
248
|
+
if (executeAt <= new Date()) {
|
|
249
|
+
await sendSMSImmediate(workflow, contact, message);
|
|
250
|
+
} else {
|
|
251
|
+
// Planifier l'action pour exécution différée
|
|
252
|
+
await prisma.scheduledWorkflowAction.create({
|
|
253
|
+
data: {
|
|
254
|
+
workflowId: workflow.id,
|
|
255
|
+
actionId: action.id,
|
|
256
|
+
contactId: contact.id,
|
|
257
|
+
actionType: 'SEND_SMS',
|
|
258
|
+
actionData: {
|
|
259
|
+
smsMessage: action.smsMessage,
|
|
260
|
+
userId: workflow.userId,
|
|
261
|
+
workflowName: workflow.name,
|
|
262
|
+
},
|
|
263
|
+
executeAt,
|
|
264
|
+
},
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
} catch (error) {
|
|
268
|
+
console.error('Erreur lors de l\'envoi du SMS:', error);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Exécute l'action "Changer le statut"
|
|
274
|
+
*/
|
|
275
|
+
async function executeChangeStatusAction(
|
|
276
|
+
action: any,
|
|
277
|
+
workflow: any,
|
|
278
|
+
contactId: string,
|
|
279
|
+
executeAt: Date,
|
|
280
|
+
) {
|
|
281
|
+
if (!action.newStatusId) {
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
try {
|
|
286
|
+
// Si le délai est 0, changer immédiatement
|
|
287
|
+
if (executeAt <= new Date()) {
|
|
288
|
+
await changeStatusImmediate(contactId, action.newStatusId);
|
|
289
|
+
} else {
|
|
290
|
+
// Planifier l'action pour exécution différée
|
|
291
|
+
await prisma.scheduledWorkflowAction.create({
|
|
292
|
+
data: {
|
|
293
|
+
workflowId: workflow.id,
|
|
294
|
+
actionId: action.id,
|
|
295
|
+
contactId: contactId,
|
|
296
|
+
actionType: 'CHANGE_STATUS',
|
|
297
|
+
actionData: {
|
|
298
|
+
newStatusId: action.newStatusId,
|
|
299
|
+
workflowName: workflow.name,
|
|
300
|
+
},
|
|
301
|
+
executeAt,
|
|
302
|
+
},
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
} catch (error) {
|
|
306
|
+
console.error('Erreur lors du changement de statut:', error);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Exécute l'action "Créer une tâche"
|
|
312
|
+
*/
|
|
313
|
+
async function executeCreateTaskAction(
|
|
314
|
+
action: any,
|
|
315
|
+
workflow: any,
|
|
316
|
+
contactId: string,
|
|
317
|
+
executeAt: Date,
|
|
318
|
+
) {
|
|
319
|
+
if (!action.taskTitle) {
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
try {
|
|
324
|
+
// Si le délai est 0, créer immédiatement
|
|
325
|
+
if (executeAt <= new Date()) {
|
|
326
|
+
await createTaskImmediate(action, workflow, contactId, executeAt);
|
|
327
|
+
} else {
|
|
328
|
+
// Planifier l'action pour exécution différée
|
|
329
|
+
await prisma.scheduledWorkflowAction.create({
|
|
330
|
+
data: {
|
|
331
|
+
workflowId: workflow.id,
|
|
332
|
+
actionId: action.id,
|
|
333
|
+
contactId: contactId,
|
|
334
|
+
actionType: 'CREATE_TASK',
|
|
335
|
+
actionData: {
|
|
336
|
+
taskTitle: action.taskTitle,
|
|
337
|
+
taskDescription: action.taskDescription || '',
|
|
338
|
+
taskPriority: 'MEDIUM',
|
|
339
|
+
userId: workflow.userId,
|
|
340
|
+
workflowName: workflow.name,
|
|
341
|
+
},
|
|
342
|
+
executeAt,
|
|
343
|
+
},
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
} catch (error) {
|
|
347
|
+
console.error('Erreur lors de la création de la tâche:', error);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Fonctions d'exécution immédiate (utilisées aussi par le cron)
|
|
353
|
+
*/
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Envoie un email immédiatement
|
|
357
|
+
*/
|
|
358
|
+
export async function sendEmailImmediate(
|
|
359
|
+
workflow: any,
|
|
360
|
+
contact: any,
|
|
361
|
+
template: any,
|
|
362
|
+
subject: string,
|
|
363
|
+
content: string,
|
|
364
|
+
) {
|
|
365
|
+
try {
|
|
366
|
+
if (!workflow.user.smtpConfig) {
|
|
367
|
+
throw new Error('Configuration SMTP non trouvée');
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Déchiffrer le mot de passe SMTP
|
|
371
|
+
let password: string;
|
|
372
|
+
try {
|
|
373
|
+
password = decrypt(workflow.user.smtpConfig.password);
|
|
374
|
+
} catch (error) {
|
|
375
|
+
password = workflow.user.smtpConfig.password;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Créer le transporteur SMTP
|
|
379
|
+
const transporter = nodemailer.createTransport({
|
|
380
|
+
host: workflow.user.smtpConfig.host,
|
|
381
|
+
port: workflow.user.smtpConfig.port,
|
|
382
|
+
secure: workflow.user.smtpConfig.secure,
|
|
383
|
+
auth: {
|
|
384
|
+
user: workflow.user.smtpConfig.username,
|
|
385
|
+
pass: password,
|
|
386
|
+
},
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
await transporter.sendMail({
|
|
390
|
+
from: workflow.user.smtpConfig.fromName
|
|
391
|
+
? `"${workflow.user.smtpConfig.fromName}" <${workflow.user.smtpConfig.fromEmail}>`
|
|
392
|
+
: workflow.user.smtpConfig.fromEmail,
|
|
393
|
+
to: contact.email,
|
|
394
|
+
subject,
|
|
395
|
+
html: content,
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
// Créer une interaction pour tracer l'email envoyé
|
|
399
|
+
await prisma.interaction.create({
|
|
400
|
+
data: {
|
|
401
|
+
contactId: contact.id,
|
|
402
|
+
type: 'EMAIL',
|
|
403
|
+
title: `Email automatique: ${template.name}`,
|
|
404
|
+
content: `Email envoyé automatiquement via le workflow "${workflow.name}"`,
|
|
405
|
+
userId: workflow.userId,
|
|
406
|
+
date: new Date(),
|
|
407
|
+
},
|
|
408
|
+
});
|
|
409
|
+
} catch (error) {
|
|
410
|
+
console.error('Erreur lors de l\'envoi de l\'email:', error);
|
|
411
|
+
throw error;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Envoie un SMS immédiatement
|
|
417
|
+
*/
|
|
418
|
+
export async function sendSMSImmediate(workflow: any, contact: any, message: string) {
|
|
419
|
+
try {
|
|
420
|
+
// TODO: Implémenter l'envoi de SMS (nécessite une API SMS)
|
|
421
|
+
console.log(`SMS à envoyer à ${contact.phone}: ${message}`);
|
|
422
|
+
|
|
423
|
+
// Créer une interaction pour tracer le SMS
|
|
424
|
+
await prisma.interaction.create({
|
|
425
|
+
data: {
|
|
426
|
+
contactId: contact.id,
|
|
427
|
+
type: 'SMS',
|
|
428
|
+
title: 'SMS automatique',
|
|
429
|
+
content: message,
|
|
430
|
+
userId: workflow.userId,
|
|
431
|
+
date: new Date(),
|
|
432
|
+
},
|
|
433
|
+
});
|
|
434
|
+
} catch (error) {
|
|
435
|
+
console.error('Erreur lors de l\'envoi du SMS:', error);
|
|
436
|
+
throw error;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Change le statut immédiatement
|
|
442
|
+
*/
|
|
443
|
+
export async function changeStatusImmediate(contactId: string, newStatusId: string) {
|
|
444
|
+
try {
|
|
445
|
+
await prisma.contact.update({
|
|
446
|
+
where: { id: contactId },
|
|
447
|
+
data: { statusId: newStatusId },
|
|
448
|
+
});
|
|
449
|
+
} catch (error) {
|
|
450
|
+
console.error('Erreur lors du changement de statut:', error);
|
|
451
|
+
throw error;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Crée une tâche immédiatement
|
|
457
|
+
*/
|
|
458
|
+
export async function createTaskImmediate(
|
|
459
|
+
action: any,
|
|
460
|
+
workflow: any,
|
|
461
|
+
contactId: string,
|
|
462
|
+
scheduledAt: Date,
|
|
463
|
+
) {
|
|
464
|
+
try {
|
|
465
|
+
await prisma.task.create({
|
|
466
|
+
data: {
|
|
467
|
+
type: 'OTHER',
|
|
468
|
+
title: action.taskTitle,
|
|
469
|
+
description: action.taskDescription || '',
|
|
470
|
+
priority: 'MEDIUM',
|
|
471
|
+
scheduledAt,
|
|
472
|
+
contactId: contactId,
|
|
473
|
+
assignedUserId: workflow.userId,
|
|
474
|
+
createdById: workflow.userId,
|
|
475
|
+
},
|
|
476
|
+
});
|
|
477
|
+
} catch (error) {
|
|
478
|
+
console.error('Erreur lors de la création de la tâche:', error);
|
|
479
|
+
throw error;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import type { NextRequest } from 'next/server';
|
|
3
|
+
import { auth } from './lib/auth';
|
|
4
|
+
import { isAdmin } from './lib/roles';
|
|
5
|
+
import { prisma } from './lib/prisma';
|
|
6
|
+
|
|
7
|
+
// Routes qui nécessitent une authentification
|
|
8
|
+
const protectedRoutes = [
|
|
9
|
+
'/dashboard',
|
|
10
|
+
'/contacts',
|
|
11
|
+
'/settings',
|
|
12
|
+
'/users',
|
|
13
|
+
'/agenda',
|
|
14
|
+
'/automatisation',
|
|
15
|
+
'/templates',
|
|
16
|
+
'/closing',
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
// Routes réservées aux admins
|
|
20
|
+
const adminRoutes = ['/users'];
|
|
21
|
+
|
|
22
|
+
// Routes d'authentification
|
|
23
|
+
const authRoutes = ['/signin'];
|
|
24
|
+
|
|
25
|
+
export async function proxy(request: NextRequest) {
|
|
26
|
+
const { pathname } = request.nextUrl;
|
|
27
|
+
|
|
28
|
+
// Vérifier la session en utilisant Better Auth
|
|
29
|
+
const session = await auth.api.getSession({
|
|
30
|
+
headers: request.headers,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
let isAuthenticated = !!session;
|
|
34
|
+
let isActiveUser = true;
|
|
35
|
+
|
|
36
|
+
// Récupérer le rôle depuis la session ou depuis la base de données
|
|
37
|
+
let userRole: string | null = null;
|
|
38
|
+
if (session && session.user?.id) {
|
|
39
|
+
const dbUser = await prisma.user.findUnique({
|
|
40
|
+
where: { id: session.user.id },
|
|
41
|
+
select: { role: true, active: true },
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
userRole = (session.user as any).role || dbUser?.role || null;
|
|
45
|
+
isActiveUser = dbUser?.active ?? true;
|
|
46
|
+
if (!isActiveUser) {
|
|
47
|
+
isAuthenticated = false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Vérifier si la route actuelle est protégée
|
|
52
|
+
const isProtectedRoute = protectedRoutes.some((route) => pathname.startsWith(route));
|
|
53
|
+
|
|
54
|
+
// Vérifier si la route est réservée aux admins
|
|
55
|
+
const isAdminRoute = adminRoutes.some((route) => pathname.startsWith(route));
|
|
56
|
+
|
|
57
|
+
// Vérifier si la route actuelle est une route d'auth
|
|
58
|
+
const isAuthRoute = authRoutes.some((route) => pathname.startsWith(route));
|
|
59
|
+
|
|
60
|
+
// Si l'utilisateur n'est pas connecté et tente d'accéder à une route protégée
|
|
61
|
+
if (!isAuthenticated && isProtectedRoute) {
|
|
62
|
+
const signInUrl = new URL('/signin', request.url);
|
|
63
|
+
signInUrl.searchParams.set('callbackUrl', pathname);
|
|
64
|
+
return NextResponse.redirect(signInUrl);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Si l'utilisateur est connecté mais n'est pas admin et tente d'accéder à une route admin
|
|
68
|
+
if (isAuthenticated && isAdminRoute && !isAdmin(userRole || undefined)) {
|
|
69
|
+
return NextResponse.redirect(new URL('/dashboard', request.url));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Si l'utilisateur est connecté et tente d'accéder aux pages d'auth
|
|
73
|
+
if (isAuthenticated && isAuthRoute) {
|
|
74
|
+
return NextResponse.redirect(new URL('/dashboard', request.url));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return NextResponse.next();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export const config = {
|
|
81
|
+
matcher: [
|
|
82
|
+
/*
|
|
83
|
+
* Match all request paths except for the ones starting with:
|
|
84
|
+
* - api (API routes)
|
|
85
|
+
* - _next/static (static files)
|
|
86
|
+
* - _next/image (image optimization files)
|
|
87
|
+
* - favicon.ico (favicon file)
|
|
88
|
+
*/
|
|
89
|
+
'/((?!api|_next/static|_next/image|favicon.ico).*)',
|
|
90
|
+
],
|
|
91
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2017",
|
|
4
|
+
"lib": ["dom", "dom.iterable", "esnext"],
|
|
5
|
+
"allowJs": true,
|
|
6
|
+
"skipLibCheck": true,
|
|
7
|
+
"strict": true,
|
|
8
|
+
"noEmit": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"module": "esnext",
|
|
11
|
+
"moduleResolution": "bundler",
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"isolatedModules": true,
|
|
14
|
+
"jsx": "react-jsx",
|
|
15
|
+
"incremental": true,
|
|
16
|
+
"plugins": [
|
|
17
|
+
{
|
|
18
|
+
"name": "next"
|
|
19
|
+
}
|
|
20
|
+
],
|
|
21
|
+
"paths": {
|
|
22
|
+
"@/*": ["./src/*"]
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"include": [
|
|
26
|
+
"next-env.d.ts",
|
|
27
|
+
"**/*.ts",
|
|
28
|
+
"**/*.tsx",
|
|
29
|
+
".next/types/**/*.ts",
|
|
30
|
+
".next/dev/types/**/*.ts",
|
|
31
|
+
"**/*.mts"
|
|
32
|
+
],
|
|
33
|
+
"exclude": ["node_modules"]
|
|
34
|
+
}
|