create-crm-tmp 2.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/create-crm-tmp.js +56 -35
- package/package.json +1 -1
- package/template/README.md +230 -115
- package/template/eslint.config.mjs +13 -0
- package/template/next.config.ts +14 -0
- package/template/package.json +15 -2
- package/template/prisma/migrations/20260318095700_init_db/migration.sql +978 -0
- package/template/prisma/migrations/migration_lock.toml +3 -0
- package/template/prisma/schema.prisma +132 -637
- package/template/src/app/(auth)/invite/[token]/page.tsx +10 -8
- package/template/src/app/(auth)/layout.tsx +1 -1
- package/template/src/app/(auth)/reset-password/complete/page.tsx +11 -8
- package/template/src/app/(auth)/reset-password/page.tsx +4 -4
- package/template/src/app/(auth)/reset-password/verify/page.tsx +4 -4
- package/template/src/app/(auth)/signin/page.tsx +14 -6
- package/template/src/app/(dashboard)/agenda/page.tsx +2243 -988
- package/template/src/app/(dashboard)/automatisation/_components/workflow-editor.tsx +18 -104
- package/template/src/app/(dashboard)/automatisation/page.tsx +10 -26
- package/template/src/app/(dashboard)/closing/page.tsx +78 -62
- package/template/src/app/(dashboard)/contacts/[id]/page.tsx +2082 -1080
- package/template/src/app/(dashboard)/contacts/companies/[id]/page.tsx +46 -47
- package/template/src/app/(dashboard)/contacts/page.tsx +1062 -780
- package/template/src/app/(dashboard)/dashboard/page.tsx +533 -37
- package/template/src/app/(dashboard)/dev/page.tsx +1291 -0
- package/template/src/app/(dashboard)/layout.tsx +6 -2
- package/template/src/app/(dashboard)/settings/page.tsx +797 -2582
- package/template/src/app/(dashboard)/templates/page.tsx +55 -54
- package/template/src/app/(dashboard)/users/list/page.tsx +51 -48
- package/template/src/app/(dashboard)/users/page.tsx +1 -1
- package/template/src/app/(dashboard)/users/permissions/page.tsx +2 -2
- package/template/src/app/(dashboard)/users/roles/page.tsx +7 -5
- package/template/src/app/api/agenda/google-events/route.ts +92 -0
- package/template/src/app/api/auth/check-active/route.ts +3 -2
- package/template/src/app/api/auth/google/route.ts +2 -1
- package/template/src/app/api/auth/google/status/route.ts +7 -31
- package/template/src/app/api/companies/[id]/activities/route.ts +1 -3
- package/template/src/app/api/companies/[id]/route.ts +1 -2
- package/template/src/app/api/companies/route.ts +42 -12
- package/template/src/app/api/contacts/[id]/files/[fileId]/preview/route.ts +9 -31
- package/template/src/app/api/contacts/[id]/files/[fileId]/route.ts +14 -32
- package/template/src/app/api/contacts/[id]/files/route.ts +112 -212
- package/template/src/app/api/contacts/[id]/interactions/[interactionId]/route.ts +27 -1
- package/template/src/app/api/contacts/[id]/interactions/route.ts +16 -16
- package/template/src/app/api/contacts/[id]/kyc/route.ts +21 -11
- package/template/src/app/api/contacts/[id]/meet/route.ts +19 -2
- package/template/src/app/api/contacts/[id]/route.ts +106 -34
- package/template/src/app/api/contacts/[id]/send-email/route.ts +27 -11
- package/template/src/app/api/contacts/[id]/workflows/run/route.ts +6 -0
- package/template/src/app/api/contacts/export/route.ts +9 -13
- package/template/src/app/api/contacts/import/route.ts +55 -25
- package/template/src/app/api/contacts/import-preview/route.ts +1 -1
- package/template/src/app/api/contacts/origins/route.ts +63 -0
- package/template/src/app/api/contacts/route.ts +153 -41
- package/template/src/app/api/cron/cleanup-editor-images/route.ts +166 -0
- package/template/src/app/api/dashboard/widgets/[id]/route.ts +44 -0
- package/template/src/app/api/dashboard/widgets/route.ts +181 -0
- package/template/src/app/api/dev/reminders/test/route.ts +114 -0
- package/template/src/app/api/editor/upload-image/route.ts +61 -0
- package/template/src/app/api/integrations/google-sheet/jobs/[jobId]/route.ts +47 -0
- package/template/src/app/api/integrations/google-sheet/jobs/usage/route.ts +50 -0
- package/template/src/app/api/integrations/google-sheet/sync/route.ts +24 -556
- package/template/src/app/api/jobs/google-sheet/process/route.ts +84 -0
- package/template/src/app/api/jobs/google-sheet/schedule/route.ts +50 -0
- package/template/src/app/api/reminders/clear/route.ts +120 -0
- package/template/src/app/api/reminders/clear/undo/route.ts +112 -0
- package/template/src/app/api/reminders/route.ts +164 -39
- package/template/src/app/api/reminders/state/route.ts +164 -0
- package/template/src/app/api/reset-password/request/route.ts +1 -1
- package/template/src/app/api/reset-password/verify/route.ts +1 -1
- package/template/src/app/api/send/route.ts +16 -4
- package/template/src/app/api/settings/google-ads/route.ts +14 -0
- package/template/src/app/api/settings/google-calendar/calendars/route.ts +97 -0
- package/template/src/app/api/settings/google-calendar/route.ts +124 -0
- package/template/src/app/api/settings/google-sheet/[id]/route.ts +28 -0
- package/template/src/app/api/settings/google-sheet/auto-map/route.ts +37 -4
- package/template/src/app/api/settings/google-sheet/preview/route.ts +9 -3
- package/template/src/app/api/settings/google-sheet/route.ts +14 -0
- package/template/src/app/api/settings/integrations/logs/route.ts +93 -0
- package/template/src/app/api/settings/integrations/notifications/route.ts +67 -0
- package/template/src/app/api/settings/meta-leads/[id]/route.ts +0 -1
- package/template/src/app/api/settings/meta-leads/route.ts +14 -2
- package/template/src/app/api/settings/smtp/route.ts +53 -6
- package/template/src/app/api/tasks/[id]/attendees/route.ts +24 -8
- package/template/src/app/api/tasks/[id]/route.ts +234 -58
- package/template/src/app/api/tasks/meet/route.ts +27 -19
- package/template/src/app/api/tasks/route.ts +62 -17
- package/template/src/app/api/users/[id]/route.ts +20 -14
- package/template/src/app/api/users/list/route.ts +57 -19
- package/template/src/app/api/webhooks/google-ads/route.ts +34 -14
- package/template/src/app/api/webhooks/meta-leads/route.ts +32 -12
- package/template/src/app/api/workflows/[id]/route.ts +0 -4
- package/template/src/app/api/workflows/process/route.ts +22 -51
- package/template/src/app/api/workflows/route.ts +0 -4
- package/template/src/app/globals.css +342 -4
- package/template/src/app/layout.tsx +11 -3
- package/template/src/app/page.tsx +1 -1
- package/template/src/components/address-autocomplete.tsx +7 -6
- package/template/src/components/config-error-alert.tsx +46 -0
- package/template/src/components/contacts/filter-bar.tsx +12 -3
- package/template/src/components/contacts/filter-builder.tsx +28 -43
- package/template/src/components/contacts/save-view-dialog.tsx +1 -1
- package/template/src/components/contacts/views-tab-bar.tsx +15 -6
- package/template/src/components/dashboard/activity-chart.tsx +41 -28
- package/template/src/components/dashboard/add-widget-dialog.tsx +157 -0
- package/template/src/components/dashboard/color-picker.tsx +64 -0
- package/template/src/components/dashboard/contacts-chart.tsx +69 -0
- package/template/src/components/dashboard/interactions-by-type-chart.tsx +121 -0
- package/template/src/components/dashboard/recent-activity.tsx +154 -0
- package/template/src/components/dashboard/stat-card.tsx +40 -40
- package/template/src/components/dashboard/status-distribution-chart.tsx +81 -0
- package/template/src/components/dashboard/tasks-pie-chart.tsx +37 -34
- package/template/src/components/dashboard/top-contacts-list.tsx +113 -0
- package/template/src/components/dashboard/upcoming-tasks-list.tsx +72 -81
- package/template/src/components/dashboard/widget-wrapper.tsx +36 -0
- package/template/src/components/date-picker.tsx +9 -6
- package/template/src/components/editor/upload-editor-image.ts +42 -0
- package/template/src/components/editor.tsx +161 -22
- package/template/src/components/email-template.tsx +2 -2
- package/template/src/components/global-search.tsx +30 -28
- package/template/src/components/header.tsx +178 -80
- package/template/src/components/inactive-account-guard.tsx +58 -0
- package/template/src/components/integration-notifications-listener.tsx +12 -0
- package/template/src/components/invitation-email-template.tsx +2 -2
- package/template/src/components/meet-cancellation-email-template.tsx +3 -3
- package/template/src/components/meet-confirmation-email-template.tsx +3 -3
- package/template/src/components/meet-update-email-template.tsx +3 -3
- package/template/src/components/page-header.tsx +5 -5
- package/template/src/components/protected-page.tsx +1 -1
- package/template/src/components/reset-password-email-template.tsx +2 -2
- package/template/src/components/settings/integrations/GoogleAdsIntegration.tsx +428 -0
- package/template/src/components/settings/integrations/GoogleSheetConfigMonitoringModal.tsx +680 -0
- package/template/src/components/settings/integrations/GoogleSheetIntegration.tsx +809 -0
- package/template/src/components/settings/integrations/ImportResultDialog.tsx +124 -0
- package/template/src/components/settings/integrations/IntegrationLogPanel.tsx +57 -0
- package/template/src/components/settings/integrations/IntegrationLogsTable.tsx +186 -0
- package/template/src/components/settings/integrations/MetaLeadIntegration.tsx +451 -0
- package/template/src/components/sidebar.tsx +45 -26
- package/template/src/components/skeleton.tsx +40 -43
- package/template/src/components/ui/accordion.tsx +2 -2
- package/template/src/components/ui/alert-dialog.tsx +1 -1
- package/template/src/components/ui/button.tsx +20 -9
- package/template/src/components/ui/components.tsx +1 -1
- package/template/src/components/ui/date-picker.tsx +422 -0
- package/template/src/components/ui/datetime-picker.tsx +338 -0
- package/template/src/components/ui/status-select.tsx +271 -0
- package/template/src/components/ui/tooltip.tsx +37 -0
- package/template/src/components/view-as-modal.tsx +13 -7
- package/template/src/contexts/app-toast-context.tsx +245 -57
- package/template/src/contexts/dashboard-theme-context.tsx +53 -0
- package/template/src/contexts/sidebar-context.tsx +22 -17
- package/template/src/contexts/task-reminder-context.tsx +134 -160
- package/template/src/contexts/view-as-context.tsx +33 -6
- package/template/src/hooks/use-focus-trap.ts +2 -2
- package/template/src/hooks/useIntegrationNotifications.ts +49 -0
- package/template/src/lib/auth.ts +8 -1
- package/template/src/lib/config-links.ts +14 -0
- package/template/src/lib/contact-duplicate.ts +79 -61
- package/template/src/lib/contact-interactions.ts +21 -21
- package/template/src/lib/contact-view-filters.ts +24 -64
- package/template/src/lib/contacts-list-url.ts +190 -0
- package/template/src/lib/dashboard-stats.ts +65 -7
- package/template/src/lib/dashboard-themes.ts +135 -0
- package/template/src/lib/date-utils.ts +127 -0
- package/template/src/lib/default-widgets.ts +12 -0
- package/template/src/lib/editor-html-image-dimensions.ts +172 -0
- package/template/src/lib/editor-image-limits.ts +19 -0
- package/template/src/lib/email-html-sanitize.ts +19 -0
- package/template/src/lib/encryption.ts +9 -6
- package/template/src/lib/fr-geography.ts +192 -0
- package/template/src/lib/google-calendar-agenda.ts +201 -0
- package/template/src/lib/google-calendar.ts +255 -5
- package/template/src/lib/google-sheet-sync-jobs.ts +96 -0
- package/template/src/lib/google-sheet-sync-runner.ts +514 -0
- package/template/src/lib/integration-import-log.ts +21 -0
- package/template/src/lib/permissions.ts +40 -10
- package/template/src/lib/prisma.ts +4 -1
- package/template/src/lib/qstash.ts +65 -0
- package/template/src/lib/reminder-state-server.ts +80 -0
- package/template/src/lib/reminder-state.ts +29 -0
- package/template/src/lib/supabase-storage.ts +113 -0
- package/template/src/lib/template-variables.ts +164 -23
- package/template/src/lib/utils.ts +45 -0
- package/template/src/lib/widget-registry.ts +173 -0
- package/template/src/lib/workflow-executor.ts +16 -70
- package/template/src/proxy.ts +1 -0
- package/template/vercel.json +3 -10
- package/template/skills-lock.json +0 -25
- package/template/src/components/dashboard/dashboard-content.tsx +0 -79
- package/template/src/lib/google-drive.ts +0 -1101
- package/template/src/types/yousign.ts +0 -52
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { prisma } from '@/lib/prisma';
|
|
2
|
+
import { formatFrenchAppointmentDateTime } from '@/lib/date-utils';
|
|
2
3
|
import { InteractionType } from '../../generated/prisma/client';
|
|
3
4
|
|
|
4
5
|
interface CreateInteractionParams {
|
|
@@ -30,6 +31,23 @@ export async function createInteraction(params: CreateInteractionParams) {
|
|
|
30
31
|
});
|
|
31
32
|
}
|
|
32
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Supprime les interactions liées à une tâche (activité « Tâche », rendez-vous créé/modifié).
|
|
36
|
+
* Ne supprime pas APPOINTMENT_DELETED (trace d'annulation si créée après).
|
|
37
|
+
*/
|
|
38
|
+
export async function deleteInteractionsLinkedToTask(contactId: string, taskId: string) {
|
|
39
|
+
return prisma.interaction.deleteMany({
|
|
40
|
+
where: {
|
|
41
|
+
contactId,
|
|
42
|
+
type: { in: ['TASK', 'APPOINTMENT_CREATED', 'APPOINTMENT_CHANGED'] },
|
|
43
|
+
metadata: {
|
|
44
|
+
path: ['taskId'],
|
|
45
|
+
equals: taskId,
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
33
51
|
/**
|
|
34
52
|
* Crée une interaction pour un changement de statut
|
|
35
53
|
*/
|
|
@@ -181,13 +199,7 @@ export async function logAppointmentCreated(
|
|
|
181
199
|
title: string | null,
|
|
182
200
|
userId: string,
|
|
183
201
|
) {
|
|
184
|
-
const formattedDate = scheduledAt
|
|
185
|
-
day: 'numeric',
|
|
186
|
-
month: 'long',
|
|
187
|
-
year: 'numeric',
|
|
188
|
-
hour: '2-digit',
|
|
189
|
-
minute: '2-digit',
|
|
190
|
-
});
|
|
202
|
+
const formattedDate = formatFrenchAppointmentDateTime(scheduledAt);
|
|
191
203
|
|
|
192
204
|
return await createInteraction({
|
|
193
205
|
contactId,
|
|
@@ -214,13 +226,7 @@ export async function logAppointmentCancelled(
|
|
|
214
226
|
userId: string,
|
|
215
227
|
isGoogleMeet: boolean = false,
|
|
216
228
|
) {
|
|
217
|
-
const formattedDate = scheduledAt
|
|
218
|
-
day: 'numeric',
|
|
219
|
-
month: 'long',
|
|
220
|
-
year: 'numeric',
|
|
221
|
-
hour: '2-digit',
|
|
222
|
-
minute: '2-digit',
|
|
223
|
-
});
|
|
229
|
+
const formattedDate = formatFrenchAppointmentDateTime(scheduledAt);
|
|
224
230
|
|
|
225
231
|
const appointmentType = isGoogleMeet ? 'Google Meet' : 'Rendez-vous';
|
|
226
232
|
|
|
@@ -251,13 +257,7 @@ export async function logAppointmentChanged(
|
|
|
251
257
|
userId: string,
|
|
252
258
|
isGoogleMeet: boolean = false,
|
|
253
259
|
) {
|
|
254
|
-
const formattedDate = scheduledAt
|
|
255
|
-
day: 'numeric',
|
|
256
|
-
month: 'long',
|
|
257
|
-
year: 'numeric',
|
|
258
|
-
hour: '2-digit',
|
|
259
|
-
minute: '2-digit',
|
|
260
|
-
});
|
|
260
|
+
const formattedDate = formatFrenchAppointmentDateTime(scheduledAt);
|
|
261
261
|
|
|
262
262
|
const appointmentType = isGoogleMeet ? 'Google Meet' : 'Rendez-vous';
|
|
263
263
|
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import type { ViewFilter, DatePreset } from '@/types/contact-views';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
expandRegionCodesToDepartmentCodes,
|
|
4
|
+
prismaPostalMatchesDepartmentsCondition,
|
|
5
|
+
} from '@/lib/fr-geography';
|
|
3
6
|
|
|
4
7
|
function startOfDay(date: Date): Date {
|
|
5
8
|
const d = new Date(date);
|
|
@@ -146,76 +149,33 @@ export function resolveDatePreset(preset: DatePreset): { gte?: Date; lte?: Date
|
|
|
146
149
|
}
|
|
147
150
|
}
|
|
148
151
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
const list = departmentsByRegion.get(dept.regionCode) ?? [];
|
|
152
|
-
list.push(dept);
|
|
153
|
-
departmentsByRegion.set(dept.regionCode, list);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
function buildPostalCodeConditions(deptCodes: string[], negate: boolean): any | null {
|
|
157
|
-
if (deptCodes.length === 0) return null;
|
|
158
|
-
|
|
159
|
-
const startsWithConditions: any[] = [];
|
|
160
|
-
const corsicaCodes: string[] = [];
|
|
152
|
+
function buildFieldCondition(filter: ViewFilter): any | null {
|
|
153
|
+
const { field, operator, value, preset } = filter;
|
|
161
154
|
|
|
162
|
-
|
|
163
|
-
if (
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
startsWithConditions.push({ postalCode: { startsWith: code } });
|
|
155
|
+
if (field === 'department') {
|
|
156
|
+
if (!Array.isArray(value) || value.length === 0) return null;
|
|
157
|
+
if (operator === 'is_any_of') {
|
|
158
|
+
return prismaPostalMatchesDepartmentsCondition(value as string[]);
|
|
167
159
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
if (corsicaCodes.includes('2A') && corsicaCodes.includes('2B')) {
|
|
172
|
-
startsWithConditions.push({ postalCode: { startsWith: '20' } });
|
|
173
|
-
} else if (corsicaCodes.includes('2A')) {
|
|
174
|
-
startsWithConditions.push({
|
|
175
|
-
AND: [{ postalCode: { startsWith: '20' } }, { postalCode: { lt: '20200' } }],
|
|
176
|
-
});
|
|
177
|
-
} else {
|
|
178
|
-
startsWithConditions.push({
|
|
179
|
-
AND: [{ postalCode: { startsWith: '20' } }, { postalCode: { gte: '20200' } }],
|
|
180
|
-
});
|
|
160
|
+
if (operator === 'is_none_of') {
|
|
161
|
+
const inner = prismaPostalMatchesDepartmentsCondition(value as string[]);
|
|
162
|
+
return inner ? { NOT: inner } : null;
|
|
181
163
|
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
if (startsWithConditions.length === 0) return null;
|
|
185
|
-
|
|
186
|
-
const orCondition =
|
|
187
|
-
startsWithConditions.length === 1 ? startsWithConditions[0] : { OR: startsWithConditions };
|
|
188
|
-
|
|
189
|
-
return negate ? { NOT: orCondition } : orCondition;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
function buildGeoCondition(filter: ViewFilter): any | null {
|
|
193
|
-
const { field, operator, value } = filter;
|
|
194
|
-
if (!Array.isArray(value) || value.length === 0) return null;
|
|
195
|
-
|
|
196
|
-
if (field === 'department') {
|
|
197
|
-
return buildPostalCodeConditions(value, operator === 'is_none_of');
|
|
164
|
+
return null;
|
|
198
165
|
}
|
|
199
166
|
|
|
200
167
|
if (field === 'region') {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
}
|
|
168
|
+
if (!Array.isArray(value) || value.length === 0) return null;
|
|
169
|
+
const deptCodes = expandRegionCodesToDepartmentCodes(value as string[]);
|
|
170
|
+
if (deptCodes.length === 0) return null;
|
|
171
|
+
if (operator === 'is_any_of') {
|
|
172
|
+
return prismaPostalMatchesDepartmentsCondition(deptCodes);
|
|
207
173
|
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
function buildFieldCondition(filter: ViewFilter): any | null {
|
|
215
|
-
const { field, operator, value, preset } = filter;
|
|
216
|
-
|
|
217
|
-
if (field === 'region' || field === 'department') {
|
|
218
|
-
return buildGeoCondition(filter);
|
|
174
|
+
if (operator === 'is_none_of') {
|
|
175
|
+
const inner = prismaPostalMatchesDepartmentsCondition(deptCodes);
|
|
176
|
+
return inner ? { NOT: inner } : null;
|
|
177
|
+
}
|
|
178
|
+
return null;
|
|
219
179
|
}
|
|
220
180
|
|
|
221
181
|
switch (operator) {
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import type { ReadonlyURLSearchParams } from 'next/navigation';
|
|
2
|
+
|
|
3
|
+
const VALID_LIMITS = [25, 50, 100] as const;
|
|
4
|
+
|
|
5
|
+
/** Tri par défaut liste contacts : pas de paramètres d’URL (parse → createdAt desc). */
|
|
6
|
+
export const CONTACTS_LIST_DEFAULT_SORT_FIELD = 'createdAt';
|
|
7
|
+
export const CONTACTS_LIST_DEFAULT_SORT_ORDER = 'desc' as const;
|
|
8
|
+
|
|
9
|
+
export function isContactsListDefaultSort(
|
|
10
|
+
sortField: string,
|
|
11
|
+
sortOrder: 'asc' | 'desc',
|
|
12
|
+
): boolean {
|
|
13
|
+
return (
|
|
14
|
+
sortField === CONTACTS_LIST_DEFAULT_SORT_FIELD &&
|
|
15
|
+
sortOrder === CONTACTS_LIST_DEFAULT_SORT_ORDER
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Chevrons / filtre « tri actif » sur une colonne — masqué pour le tri implicite création ↓. */
|
|
20
|
+
export function contactsListColumnShowsActiveSort(
|
|
21
|
+
columnId: string,
|
|
22
|
+
sortField: string,
|
|
23
|
+
sortOrder: 'asc' | 'desc',
|
|
24
|
+
): boolean {
|
|
25
|
+
if (sortField !== columnId) return false;
|
|
26
|
+
if (
|
|
27
|
+
columnId === CONTACTS_LIST_DEFAULT_SORT_FIELD &&
|
|
28
|
+
isContactsListDefaultSort(sortField, sortOrder)
|
|
29
|
+
) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type ParsedContactsListUrl = {
|
|
36
|
+
viewEntity: 'contacts' | 'companies';
|
|
37
|
+
currentPage: number;
|
|
38
|
+
limit: number;
|
|
39
|
+
viewMode: 'table' | 'cards';
|
|
40
|
+
search: string;
|
|
41
|
+
statusIds: string[];
|
|
42
|
+
origins: string[];
|
|
43
|
+
assignedCommercialIds: string[];
|
|
44
|
+
assignedTeleproIds: string[];
|
|
45
|
+
createdAtStart: string;
|
|
46
|
+
createdAtEnd: string;
|
|
47
|
+
updatedAtStart: string;
|
|
48
|
+
updatedAtEnd: string;
|
|
49
|
+
regionCodes: string[];
|
|
50
|
+
departmentCodes: string[];
|
|
51
|
+
sortField: string;
|
|
52
|
+
sortOrder: 'asc' | 'desc';
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
function splitCsv(v: string | null): string[] {
|
|
56
|
+
if (!v) return [];
|
|
57
|
+
return v
|
|
58
|
+
.split(',')
|
|
59
|
+
.map((s) => s.trim())
|
|
60
|
+
.filter(Boolean);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Source de vérité côté client au premier rendu / après popstate : `window.location`
|
|
65
|
+
* peut être à jour avant `useSearchParams()` (retour arrière), sinon on retombe sur le hook.
|
|
66
|
+
*/
|
|
67
|
+
export function getContactsListUrlSearchParams(
|
|
68
|
+
sp: URLSearchParams | ReadonlyURLSearchParams,
|
|
69
|
+
): URLSearchParams {
|
|
70
|
+
if (typeof globalThis.window !== 'undefined') {
|
|
71
|
+
return new URLSearchParams(globalThis.window.location.search);
|
|
72
|
+
}
|
|
73
|
+
return new URLSearchParams(sp.toString());
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function readContactsListFromLocationOrParams(
|
|
77
|
+
sp: URLSearchParams | ReadonlyURLSearchParams,
|
|
78
|
+
): ParsedContactsListUrl {
|
|
79
|
+
return parseContactsListFromSearchParams(getContactsListUrlSearchParams(sp));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function parseContactsListFromSearchParams(
|
|
83
|
+
sp: URLSearchParams | ReadonlyURLSearchParams,
|
|
84
|
+
): ParsedContactsListUrl {
|
|
85
|
+
const page = Number.parseInt(sp.get('page') || '1', 10);
|
|
86
|
+
const lim = Number.parseInt(sp.get('limit') || '25', 10);
|
|
87
|
+
return {
|
|
88
|
+
viewEntity: sp.get('entity') === 'companies' ? 'companies' : 'contacts',
|
|
89
|
+
currentPage: Number.isFinite(page) && page >= 1 ? page : 1,
|
|
90
|
+
limit: VALID_LIMITS.includes(lim as (typeof VALID_LIMITS)[number]) ? lim : 25,
|
|
91
|
+
viewMode: sp.get('view') === 'cards' ? 'cards' : 'table',
|
|
92
|
+
search: sp.get('search') || '',
|
|
93
|
+
statusIds: splitCsv(sp.get('statusIds')),
|
|
94
|
+
origins: splitCsv(sp.get('origins')),
|
|
95
|
+
assignedCommercialIds: splitCsv(sp.get('assignedCommercialIds')),
|
|
96
|
+
assignedTeleproIds: splitCsv(sp.get('assignedTeleproIds')),
|
|
97
|
+
createdAtStart: sp.get('createdAtStart') || '',
|
|
98
|
+
createdAtEnd: sp.get('createdAtEnd') || '',
|
|
99
|
+
updatedAtStart: sp.get('updatedAtStart') || '',
|
|
100
|
+
updatedAtEnd: sp.get('updatedAtEnd') || '',
|
|
101
|
+
regionCodes: splitCsv(sp.get('regionCodes')),
|
|
102
|
+
departmentCodes: splitCsv(sp.get('departmentCodes')),
|
|
103
|
+
sortField: sp.get('sortField') || CONTACTS_LIST_DEFAULT_SORT_FIELD,
|
|
104
|
+
sortOrder:
|
|
105
|
+
sp.get('sortOrder') === 'asc'
|
|
106
|
+
? 'asc'
|
|
107
|
+
: sp.get('sortOrder') === 'desc'
|
|
108
|
+
? 'desc'
|
|
109
|
+
: CONTACTS_LIST_DEFAULT_SORT_ORDER,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export type ContactsListUrlInput = {
|
|
114
|
+
viewEntity: 'contacts' | 'companies';
|
|
115
|
+
currentPage: number;
|
|
116
|
+
limit: number;
|
|
117
|
+
viewMode: 'table' | 'cards';
|
|
118
|
+
search: string;
|
|
119
|
+
statusFilter: Set<string>;
|
|
120
|
+
originFilter: Set<string>;
|
|
121
|
+
assignedCommercialFilter: Set<string>;
|
|
122
|
+
assignedTeleproFilter: Set<string>;
|
|
123
|
+
createdAtStart: string;
|
|
124
|
+
createdAtEnd: string;
|
|
125
|
+
updatedAtStart: string;
|
|
126
|
+
updatedAtEnd: string;
|
|
127
|
+
regionFilter: Set<string>;
|
|
128
|
+
departmentFilter: Set<string>;
|
|
129
|
+
sortField: string;
|
|
130
|
+
sortOrder: 'asc' | 'desc';
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
export function buildContactsListSearchParams(input: ContactsListUrlInput): URLSearchParams {
|
|
134
|
+
const p = new URLSearchParams();
|
|
135
|
+
if (input.viewEntity === 'companies') {
|
|
136
|
+
p.set('entity', 'companies');
|
|
137
|
+
}
|
|
138
|
+
p.set('page', String(input.currentPage));
|
|
139
|
+
p.set('limit', String(input.limit));
|
|
140
|
+
p.set('view', input.viewMode);
|
|
141
|
+
if (input.search.trim()) {
|
|
142
|
+
p.set('search', input.search.trim());
|
|
143
|
+
}
|
|
144
|
+
if (input.statusFilter.size > 0) {
|
|
145
|
+
p.set('statusIds', [...input.statusFilter].join(','));
|
|
146
|
+
}
|
|
147
|
+
if (input.originFilter.size > 0) {
|
|
148
|
+
p.set('origins', [...input.originFilter].join(','));
|
|
149
|
+
}
|
|
150
|
+
if (input.assignedCommercialFilter.size > 0) {
|
|
151
|
+
p.set('assignedCommercialIds', [...input.assignedCommercialFilter].join(','));
|
|
152
|
+
}
|
|
153
|
+
if (input.assignedTeleproFilter.size > 0) {
|
|
154
|
+
p.set('assignedTeleproIds', [...input.assignedTeleproFilter].join(','));
|
|
155
|
+
}
|
|
156
|
+
if (input.createdAtStart) p.set('createdAtStart', input.createdAtStart);
|
|
157
|
+
if (input.createdAtEnd) p.set('createdAtEnd', input.createdAtEnd);
|
|
158
|
+
if (input.updatedAtStart) p.set('updatedAtStart', input.updatedAtStart);
|
|
159
|
+
if (input.updatedAtEnd) p.set('updatedAtEnd', input.updatedAtEnd);
|
|
160
|
+
if (input.regionFilter.size > 0) {
|
|
161
|
+
p.set('regionCodes', [...input.regionFilter].join(','));
|
|
162
|
+
}
|
|
163
|
+
if (input.departmentFilter.size > 0) {
|
|
164
|
+
p.set('departmentCodes', [...input.departmentFilter].join(','));
|
|
165
|
+
}
|
|
166
|
+
if (
|
|
167
|
+
input.sortField &&
|
|
168
|
+
!isContactsListDefaultSort(input.sortField, input.sortOrder)
|
|
169
|
+
) {
|
|
170
|
+
p.set('sortField', input.sortField);
|
|
171
|
+
p.set('sortOrder', input.sortOrder);
|
|
172
|
+
}
|
|
173
|
+
return p;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Comparaison stable pour éviter replace / hydrate inutiles */
|
|
177
|
+
export function canonicalSearchParamsString(params: URLSearchParams): string {
|
|
178
|
+
return [...params.entries()]
|
|
179
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
180
|
+
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
|
|
181
|
+
.join('&');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function setsEqualToIds(prev: Set<string>, ids: string[]): boolean {
|
|
185
|
+
if (prev.size !== ids.length) return false;
|
|
186
|
+
for (const id of ids) {
|
|
187
|
+
if (!prev.has(id)) return false;
|
|
188
|
+
}
|
|
189
|
+
return true;
|
|
190
|
+
}
|
|
@@ -6,9 +6,9 @@ export interface DashboardStats {
|
|
|
6
6
|
totalContacts: number;
|
|
7
7
|
contactsThisMonth: number;
|
|
8
8
|
contactsGrowth: number;
|
|
9
|
-
|
|
10
|
-
monthlyContacts: Array<{ month: string; count: number }>;
|
|
9
|
+
monthsData: Array<{ month: string; count: number }>;
|
|
11
10
|
};
|
|
11
|
+
statusDistribution: Array<{ name: string; value: number }>;
|
|
12
12
|
tasks: {
|
|
13
13
|
total: number;
|
|
14
14
|
completed: number;
|
|
@@ -24,12 +24,29 @@ export interface DashboardStats {
|
|
|
24
24
|
byType: Array<{ type: string; count: number }>;
|
|
25
25
|
};
|
|
26
26
|
interactions: {
|
|
27
|
-
recent:
|
|
27
|
+
recent: Array<{
|
|
28
|
+
id: string;
|
|
29
|
+
type: string;
|
|
30
|
+
title: string | null;
|
|
31
|
+
content: string;
|
|
32
|
+
date: string;
|
|
33
|
+
contact: { id: string; name: string };
|
|
34
|
+
}>;
|
|
28
35
|
byType: Array<{ type: string; count: number }>;
|
|
29
36
|
};
|
|
30
37
|
activity: {
|
|
31
38
|
last7Days: Array<{ date: string; interactions: number; tasks: number }>;
|
|
32
39
|
};
|
|
40
|
+
topContacts: Array<{
|
|
41
|
+
id: string;
|
|
42
|
+
name: string;
|
|
43
|
+
phone: string;
|
|
44
|
+
email: string | null;
|
|
45
|
+
status: string;
|
|
46
|
+
interactionsCount: number;
|
|
47
|
+
assignedCommercial?: string;
|
|
48
|
+
assignedTelepro?: string;
|
|
49
|
+
}>;
|
|
33
50
|
}
|
|
34
51
|
|
|
35
52
|
export async function getDashboardStats(
|
|
@@ -80,6 +97,7 @@ export async function getDashboardStats(
|
|
|
80
97
|
contactsLast12Months,
|
|
81
98
|
interactionsLast7Days,
|
|
82
99
|
tasksLast7Days,
|
|
100
|
+
recentContacts,
|
|
83
101
|
] = await Promise.all([
|
|
84
102
|
prisma.contact.count({ where: contactFilter }),
|
|
85
103
|
prisma.contact.count({
|
|
@@ -114,7 +132,7 @@ export async function getDashboardStats(
|
|
|
114
132
|
user: { select: { id: true, name: true } },
|
|
115
133
|
},
|
|
116
134
|
orderBy: { createdAt: 'desc' },
|
|
117
|
-
take:
|
|
135
|
+
take: 10,
|
|
118
136
|
}),
|
|
119
137
|
prisma.task.count({ where: { assignedUserId: userId } }),
|
|
120
138
|
prisma.task.count({ where: { assignedUserId: userId, completed: true } }),
|
|
@@ -140,6 +158,22 @@ export async function getDashboardStats(
|
|
|
140
158
|
where: { assignedUserId: userId, createdAt: { gte: sevenDaysAgo } },
|
|
141
159
|
select: { createdAt: true },
|
|
142
160
|
}),
|
|
161
|
+
prisma.contact.findMany({
|
|
162
|
+
where: contactFilter,
|
|
163
|
+
select: {
|
|
164
|
+
id: true,
|
|
165
|
+
firstName: true,
|
|
166
|
+
lastName: true,
|
|
167
|
+
phone: true,
|
|
168
|
+
email: true,
|
|
169
|
+
status: { select: { name: true } },
|
|
170
|
+
assignedCommercial: { select: { name: true } },
|
|
171
|
+
assignedTelepro: { select: { name: true } },
|
|
172
|
+
_count: { select: { interactions: true } },
|
|
173
|
+
},
|
|
174
|
+
orderBy: { createdAt: 'desc' },
|
|
175
|
+
take: 8,
|
|
176
|
+
}),
|
|
143
177
|
]);
|
|
144
178
|
|
|
145
179
|
const contactsGrowth =
|
|
@@ -184,14 +218,37 @@ export async function getDashboardStats(
|
|
|
184
218
|
});
|
|
185
219
|
}
|
|
186
220
|
|
|
221
|
+
const formattedRecentInteractions = recentInteractions.map((i) => ({
|
|
222
|
+
id: i.id,
|
|
223
|
+
type: i.type,
|
|
224
|
+
title: null,
|
|
225
|
+
content: i.content,
|
|
226
|
+
date: i.createdAt.toISOString(),
|
|
227
|
+
contact: {
|
|
228
|
+
id: i.contact.id,
|
|
229
|
+
name: `${i.contact.firstName || ''} ${i.contact.lastName || ''}`.trim() || i.contact.phone,
|
|
230
|
+
},
|
|
231
|
+
}));
|
|
232
|
+
|
|
233
|
+
const topContacts = recentContacts.map((c) => ({
|
|
234
|
+
id: c.id,
|
|
235
|
+
name: `${c.firstName || ''} ${c.lastName || ''}`.trim() || c.phone,
|
|
236
|
+
phone: c.phone,
|
|
237
|
+
email: c.email,
|
|
238
|
+
status: c.status?.name || 'N/A',
|
|
239
|
+
interactionsCount: c._count.interactions,
|
|
240
|
+
assignedCommercial: c.assignedCommercial?.name,
|
|
241
|
+
assignedTelepro: c.assignedTelepro?.name,
|
|
242
|
+
}));
|
|
243
|
+
|
|
187
244
|
return {
|
|
188
245
|
overview: {
|
|
189
246
|
totalContacts,
|
|
190
247
|
contactsThisMonth,
|
|
191
248
|
contactsGrowth: Math.round(contactsGrowth * 10) / 10,
|
|
192
|
-
|
|
193
|
-
monthlyContacts: monthsData,
|
|
249
|
+
monthsData,
|
|
194
250
|
},
|
|
251
|
+
statusDistribution: statusData,
|
|
195
252
|
tasks: {
|
|
196
253
|
total: totalTasks,
|
|
197
254
|
completed: completedTasks,
|
|
@@ -214,11 +271,12 @@ export async function getDashboardStats(
|
|
|
214
271
|
byType: tasksByType,
|
|
215
272
|
},
|
|
216
273
|
interactions: {
|
|
217
|
-
recent:
|
|
274
|
+
recent: formattedRecentInteractions,
|
|
218
275
|
byType: interactionsByType,
|
|
219
276
|
},
|
|
220
277
|
activity: {
|
|
221
278
|
last7Days,
|
|
222
279
|
},
|
|
280
|
+
topContacts,
|
|
223
281
|
};
|
|
224
282
|
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
export interface DashboardTheme {
|
|
2
|
+
key: string;
|
|
3
|
+
label: string;
|
|
4
|
+
hex: {
|
|
5
|
+
50: string;
|
|
6
|
+
100: string;
|
|
7
|
+
200: string;
|
|
8
|
+
300: string;
|
|
9
|
+
400: string;
|
|
10
|
+
500: string;
|
|
11
|
+
600: string;
|
|
12
|
+
700: string;
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const DASHBOARD_THEMES: DashboardTheme[] = [
|
|
17
|
+
{
|
|
18
|
+
key: 'orange',
|
|
19
|
+
label: 'Orange',
|
|
20
|
+
hex: {
|
|
21
|
+
50: '#fff7ed',
|
|
22
|
+
100: '#ffedd5',
|
|
23
|
+
200: '#fed7aa',
|
|
24
|
+
300: '#fdba74',
|
|
25
|
+
400: '#fb923c',
|
|
26
|
+
500: '#f97316',
|
|
27
|
+
600: '#ea580c',
|
|
28
|
+
700: '#c2410c',
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
key: 'blue',
|
|
33
|
+
label: 'Bleu',
|
|
34
|
+
hex: {
|
|
35
|
+
50: '#eff6ff',
|
|
36
|
+
100: '#dbeafe',
|
|
37
|
+
200: '#bfdbfe',
|
|
38
|
+
300: '#93c5fd',
|
|
39
|
+
400: '#60a5fa',
|
|
40
|
+
500: '#3b82f6',
|
|
41
|
+
600: '#2563eb',
|
|
42
|
+
700: '#1d4ed8',
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
key: 'violet',
|
|
47
|
+
label: 'Violet',
|
|
48
|
+
hex: {
|
|
49
|
+
50: '#f5f3ff',
|
|
50
|
+
100: '#ede9fe',
|
|
51
|
+
200: '#ddd6fe',
|
|
52
|
+
300: '#c4b5fd',
|
|
53
|
+
400: '#a78bfa',
|
|
54
|
+
500: '#8b5cf6',
|
|
55
|
+
600: '#7c3aed',
|
|
56
|
+
700: '#6d28d9',
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
key: 'emerald',
|
|
61
|
+
label: 'Émeraude',
|
|
62
|
+
hex: {
|
|
63
|
+
50: '#ecfdf5',
|
|
64
|
+
100: '#d1fae5',
|
|
65
|
+
200: '#a7f3d0',
|
|
66
|
+
300: '#6ee7b7',
|
|
67
|
+
400: '#34d399',
|
|
68
|
+
500: '#10b981',
|
|
69
|
+
600: '#059669',
|
|
70
|
+
700: '#047857',
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
key: 'rose',
|
|
75
|
+
label: 'Rose',
|
|
76
|
+
hex: {
|
|
77
|
+
50: '#fff1f2',
|
|
78
|
+
100: '#ffe4e6',
|
|
79
|
+
200: '#fecdd3',
|
|
80
|
+
300: '#fda4af',
|
|
81
|
+
400: '#fb7185',
|
|
82
|
+
500: '#f43f5e',
|
|
83
|
+
600: '#e11d48',
|
|
84
|
+
700: '#be123c',
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
key: 'cyan',
|
|
89
|
+
label: 'Cyan',
|
|
90
|
+
hex: {
|
|
91
|
+
50: '#ecfeff',
|
|
92
|
+
100: '#cffafe',
|
|
93
|
+
200: '#a5f3fc',
|
|
94
|
+
300: '#67e8f9',
|
|
95
|
+
400: '#22d3ee',
|
|
96
|
+
500: '#06b6d4',
|
|
97
|
+
600: '#0891b2',
|
|
98
|
+
700: '#0e7490',
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
key: 'amber',
|
|
103
|
+
label: 'Ambre',
|
|
104
|
+
hex: {
|
|
105
|
+
50: '#fffbeb',
|
|
106
|
+
100: '#fef3c7',
|
|
107
|
+
200: '#fde68a',
|
|
108
|
+
300: '#fcd34d',
|
|
109
|
+
400: '#fbbf24',
|
|
110
|
+
500: '#f59e0b',
|
|
111
|
+
600: '#d97706',
|
|
112
|
+
700: '#b45309',
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
key: 'indigo',
|
|
117
|
+
label: 'Indigo',
|
|
118
|
+
hex: {
|
|
119
|
+
50: '#eef2ff',
|
|
120
|
+
100: '#e0e7ff',
|
|
121
|
+
200: '#c7d2fe',
|
|
122
|
+
300: '#a5b4fc',
|
|
123
|
+
400: '#818cf8',
|
|
124
|
+
500: '#6366f1',
|
|
125
|
+
600: '#4f46e5',
|
|
126
|
+
700: '#4338ca',
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
];
|
|
130
|
+
|
|
131
|
+
export const DEFAULT_THEME_KEY = 'orange';
|
|
132
|
+
|
|
133
|
+
export function getThemeByKey(key: string): DashboardTheme {
|
|
134
|
+
return DASHBOARD_THEMES.find((t) => t.key === key) || DASHBOARD_THEMES[0];
|
|
135
|
+
}
|