create-crm-tmp 1.1.3 → 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/.prettierignore +2 -0
- package/template/README.md +230 -115
- package/template/components.json +22 -0
- package/template/eslint.config.mjs +13 -0
- package/template/exemple-contacts.csv +54 -0
- package/template/next.config.ts +41 -1
- package/template/package.json +63 -15
- package/template/prisma/migrations/20260318095700_init_db/migration.sql +978 -0
- package/template/prisma/schema.prisma +311 -67
- package/template/src/app/(auth)/invite/[token]/page.tsx +28 -29
- package/template/src/app/(auth)/layout.tsx +1 -1
- package/template/src/app/(auth)/reset-password/complete/page.tsx +21 -27
- package/template/src/app/(auth)/reset-password/page.tsx +14 -10
- package/template/src/app/(auth)/reset-password/verify/page.tsx +14 -10
- package/template/src/app/(auth)/signin/page.tsx +34 -23
- package/template/src/app/(dashboard)/agenda/page.tsx +3655 -2357
- package/template/src/app/(dashboard)/automatisation/[id]/page.tsx +10 -7
- package/template/src/app/(dashboard)/automatisation/_components/workflow-editor.tsx +609 -338
- package/template/src/app/(dashboard)/automatisation/new/page.tsx +11 -8
- package/template/src/app/(dashboard)/automatisation/page.tsx +463 -186
- package/template/src/app/(dashboard)/closing/page.tsx +517 -469
- package/template/src/app/(dashboard)/contacts/[id]/page.tsx +6151 -4210
- package/template/src/app/(dashboard)/contacts/companies/[id]/page.tsx +1702 -0
- package/template/src/app/(dashboard)/contacts/loading.tsx +13 -0
- package/template/src/app/(dashboard)/contacts/page.tsx +4124 -2130
- package/template/src/app/(dashboard)/dashboard/page.tsx +119 -105
- package/template/src/app/(dashboard)/dev/page.tsx +1291 -0
- package/template/src/app/(dashboard)/error.tsx +37 -0
- package/template/src/app/(dashboard)/layout.tsx +6 -2
- package/template/src/app/(dashboard)/loading.tsx +5 -0
- package/template/src/app/(dashboard)/settings/loading.tsx +19 -0
- package/template/src/app/(dashboard)/settings/page.tsx +1773 -3362
- package/template/src/app/(dashboard)/templates/page.tsx +504 -303
- package/template/src/app/(dashboard)/users/list/page.tsx +364 -355
- package/template/src/app/(dashboard)/users/page.tsx +279 -310
- package/template/src/app/(dashboard)/users/permissions/page.tsx +104 -99
- package/template/src/app/(dashboard)/users/roles/page.tsx +169 -140
- package/template/src/app/api/agenda/google-events/route.ts +92 -0
- package/template/src/app/api/audit-logs/route.ts +1 -1
- package/template/src/app/api/auth/check-active/route.ts +3 -2
- package/template/src/app/api/auth/google/callback/route.ts +8 -5
- package/template/src/app/api/auth/google/disconnect/route.ts +2 -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 +129 -0
- package/template/src/app/api/companies/[id]/route.ts +194 -0
- package/template/src/app/api/companies/export/route.ts +206 -0
- package/template/src/app/api/companies/route.ts +196 -0
- package/template/src/app/api/contact-views/[id]/pin/route.ts +69 -0
- package/template/src/app/api/contact-views/[id]/route.ts +197 -0
- package/template/src/app/api/contact-views/route.ts +146 -0
- package/template/src/app/api/contacts/[id]/files/[fileId]/preview/route.ts +55 -0
- package/template/src/app/api/contacts/[id]/files/[fileId]/route.ts +20 -48
- package/template/src/app/api/contacts/[id]/files/route.ts +125 -186
- package/template/src/app/api/contacts/[id]/interactions/[interactionId]/route.ts +27 -1
- package/template/src/app/api/contacts/[id]/interactions/route.ts +45 -8
- package/template/src/app/api/contacts/[id]/kyc/route.ts +81 -0
- package/template/src/app/api/contacts/[id]/meet/route.ts +55 -29
- package/template/src/app/api/contacts/[id]/route.ts +184 -21
- package/template/src/app/api/contacts/[id]/send-email/route.ts +33 -11
- package/template/src/app/api/contacts/[id]/workflows/run/route.ts +67 -0
- package/template/src/app/api/contacts/export/route.ts +22 -31
- package/template/src/app/api/contacts/import/route.ts +77 -44
- package/template/src/app/api/contacts/import-preview/route.ts +139 -0
- package/template/src/app/api/contacts/origins/route.ts +63 -0
- package/template/src/app/api/contacts/route.ts +322 -57
- package/template/src/app/api/cron/cleanup-editor-images/route.ts +166 -0
- package/template/src/app/api/dashboard/stats/route.ts +9 -292
- package/template/src/app/api/dashboard/widgets/[id]/route.ts +0 -3
- package/template/src/app/api/dashboard/widgets/route.ts +19 -19
- 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 +28 -542
- package/template/src/app/api/invite/complete/route.ts +20 -23
- 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 +165 -39
- package/template/src/app/api/reminders/state/route.ts +164 -0
- package/template/src/app/api/reset-password/complete/route.ts +11 -13
- 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 +25 -47
- package/template/src/app/api/settings/closing-reasons/[id]/route.ts +10 -21
- package/template/src/app/api/settings/closing-reasons/route.ts +10 -21
- package/template/src/app/api/settings/company/route.ts +19 -26
- package/template/src/app/api/settings/google-ads/[id]/route.ts +20 -23
- package/template/src/app/api/settings/google-ads/route.ts +34 -23
- 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 +48 -23
- package/template/src/app/api/settings/google-sheet/auto-map/route.ts +56 -32
- package/template/src/app/api/settings/google-sheet/preview/route.ts +110 -0
- package/template/src/app/api/settings/google-sheet/route.ts +34 -23
- 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 +20 -24
- package/template/src/app/api/settings/meta-leads/route.ts +34 -25
- package/template/src/app/api/settings/smtp/route.ts +53 -6
- package/template/src/app/api/settings/statuses/[id]/route.ts +29 -32
- package/template/src/app/api/settings/statuses/route.ts +24 -22
- package/template/src/app/api/statuses/route.ts +2 -5
- package/template/src/app/api/tasks/[id]/attendees/route.ts +36 -13
- package/template/src/app/api/tasks/[id]/route.ts +357 -145
- package/template/src/app/api/tasks/meet/route.ts +37 -26
- package/template/src/app/api/tasks/route.ts +201 -96
- package/template/src/app/api/templates/[id]/route.ts +22 -13
- package/template/src/app/api/templates/route.ts +22 -5
- package/template/src/app/api/users/[id]/resend-invite/route.ts +95 -0
- package/template/src/app/api/users/[id]/route.ts +22 -16
- package/template/src/app/api/users/commercials/route.ts +38 -0
- package/template/src/app/api/users/for-agenda/route.ts +1 -2
- package/template/src/app/api/users/list/route.ts +57 -19
- package/template/src/app/api/users/route.ts +89 -34
- package/template/src/app/api/webhooks/google-ads/route.ts +40 -1
- package/template/src/app/api/webhooks/meta-leads/route.ts +38 -1
- package/template/src/app/api/workflows/[id]/route.ts +29 -6
- package/template/src/app/api/workflows/process/route.ts +505 -170
- package/template/src/app/api/workflows/route.ts +42 -4
- package/template/src/app/globals.css +512 -32
- package/template/src/app/layout.tsx +28 -9
- package/template/src/app/page.tsx +37 -7
- package/template/src/components/address-autocomplete.tsx +233 -0
- package/template/src/components/config-error-alert.tsx +46 -0
- package/template/src/components/contacts/filter-bar.tsx +190 -0
- package/template/src/components/contacts/filter-builder.tsx +574 -0
- package/template/src/components/contacts/save-view-dialog.tsx +160 -0
- package/template/src/components/contacts/views-tab-bar.tsx +449 -0
- package/template/src/components/dashboard/activity-chart.tsx +6 -1
- package/template/src/components/dashboard/add-widget-dialog.tsx +13 -17
- package/template/src/components/dashboard/color-picker.tsx +7 -8
- package/template/src/components/dashboard/recent-activity.tsx +2 -5
- package/template/src/components/dashboard/stat-card.tsx +1 -3
- package/template/src/components/dashboard/status-distribution-chart.tsx +0 -1
- package/template/src/components/dashboard/top-contacts-list.tsx +7 -13
- package/template/src/components/dashboard/upcoming-tasks-list.tsx +2 -5
- package/template/src/components/dashboard/widget-wrapper.tsx +3 -6
- package/template/src/components/date-picker.tsx +399 -0
- package/template/src/components/editor/upload-editor-image.ts +42 -0
- package/template/src/components/editor.tsx +188 -35
- package/template/src/components/email-template.tsx +4 -2
- package/template/src/components/global-search.tsx +360 -0
- package/template/src/components/header.tsx +200 -107
- 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 +4 -2
- package/template/src/components/lazy-editor.tsx +11 -0
- package/template/src/components/meet-cancellation-email-template.tsx +11 -3
- package/template/src/components/meet-confirmation-email-template.tsx +10 -3
- package/template/src/components/meet-update-email-template.tsx +10 -3
- package/template/src/components/page-header.tsx +19 -15
- package/template/src/components/protected-page.tsx +94 -0
- package/template/src/components/reset-password-email-template.tsx +4 -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 +117 -100
- package/template/src/components/skeleton.tsx +128 -45
- package/template/src/components/ui/accordion.tsx +64 -0
- package/template/src/components/ui/alert-dialog.tsx +139 -0
- package/template/src/components/ui/button.tsx +71 -0
- 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-banner.tsx +1 -1
- package/template/src/components/view-as-modal.tsx +30 -19
- package/template/src/config/nav-pages.ts +108 -0
- package/template/src/contexts/app-toast-context.tsx +362 -0
- package/template/src/contexts/dashboard-theme-context.tsx +2 -7
- package/template/src/contexts/sidebar-context.tsx +27 -53
- package/template/src/contexts/task-reminder-context.tsx +134 -160
- package/template/src/contexts/view-as-context.tsx +32 -10
- package/template/src/hooks/use-alert.tsx +65 -0
- package/template/src/hooks/use-confirm.tsx +87 -0
- package/template/src/hooks/use-contact-views.ts +140 -0
- package/template/src/hooks/use-contacts.ts +69 -0
- package/template/src/hooks/use-fetch.ts +17 -0
- package/template/src/hooks/use-focus-trap.ts +73 -0
- package/template/src/hooks/use-statuses.ts +22 -0
- package/template/src/hooks/useIntegrationNotifications.ts +49 -0
- package/template/src/lib/address-api.ts +155 -0
- package/template/src/lib/auth.ts +8 -1
- package/template/src/lib/cache.ts +73 -0
- package/template/src/lib/check-permission.ts +12 -177
- 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 +24 -22
- package/template/src/lib/contact-view-filters.ts +301 -0
- package/template/src/lib/contacts-list-url.ts +190 -0
- package/template/src/lib/dashboard-stats.ts +282 -0
- package/template/src/lib/dashboard-themes.ts +0 -5
- package/template/src/lib/date-utils.ts +176 -0
- package/template/src/lib/default-widgets.ts +0 -2
- 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/get-auth-user.ts +25 -0
- package/template/src/lib/google-calendar-agenda.ts +201 -0
- package/template/src/lib/google-calendar.ts +309 -17
- package/template/src/lib/google-fetch.ts +63 -0
- 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/local-storage.ts +34 -0
- package/template/src/lib/permissions.ts +268 -40
- package/template/src/lib/prisma.ts +15 -12
- 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/roles.ts +12 -15
- package/template/src/lib/supabase-storage.ts +113 -0
- package/template/src/lib/template-variables.ts +204 -29
- package/template/src/lib/utils.ts +71 -11
- package/template/src/lib/widget-registry.ts +0 -4
- package/template/src/lib/workflow-executor.ts +391 -228
- package/template/src/proxy.ts +35 -73
- package/template/src/types/contact-views.ts +351 -0
- package/template/vercel.json +5 -0
- package/template/WORKFLOWS_CRON.md +0 -185
- package/template/prisma/migrations/20251126144728_init/migration.sql +0 -78
- package/template/prisma/migrations/20251126155204_add_user_roles/migration.sql +0 -5
- package/template/prisma/migrations/20251128095126_add_company_info/migration.sql +0 -19
- package/template/prisma/migrations/20251128123321_add_smtp_config/migration.sql +0 -22
- package/template/prisma/migrations/20251128132303_add_status/migration.sql +0 -23
- package/template/prisma/migrations/20251201102207_add_user_active/migration.sql +0 -75
- package/template/prisma/migrations/20251201105507_add_email_signature/migration.sql +0 -2
- package/template/prisma/migrations/20251201151122_add_tasks/migration.sql +0 -45
- package/template/prisma/migrations/20251202111854_add_task_reminder/migration.sql +0 -2
- package/template/prisma/migrations/20251202135859_add_google_meet_integration/migration.sql +0 -27
- package/template/prisma/migrations/20251203103317_add_meta_lead_integration/migration.sql +0 -20
- package/template/prisma/migrations/20251203104002_add_google_ads_integration/migration.sql +0 -18
- package/template/prisma/migrations/20251203112122_add_google_sheet_integration/migration.sql +0 -32
- package/template/prisma/migrations/20251203153853_allow_multiple_integration_configs/migration.sql +0 -20
- package/template/prisma/migrations/20251205141705_update_user_roles/migration.sql +0 -12
- package/template/prisma/migrations/20251205150000_add_commercial_and_telepro_assignment/migration.sql +0 -21
- package/template/prisma/migrations/20251205160000_add_interaction_logging/migration.sql +0 -11
- package/template/prisma/migrations/20251208090314_add_automatic_interaction_types/migration.sql +0 -12
- package/template/prisma/migrations/20251208094843_mg/migration.sql +0 -14
- package/template/prisma/migrations/20251208100000_add_company_support/migration.sql +0 -14
- package/template/prisma/migrations/20251208110000_add_templates/migration.sql +0 -26
- package/template/prisma/migrations/20251208141304_add_video_conference_task_type/migration.sql +0 -2
- package/template/prisma/migrations/20251209104759_add_internal_note_to_task/migration.sql +0 -2
- package/template/prisma/migrations/20251209134803_add_company_field/migration.sql +0 -2
- package/template/prisma/migrations/20251209150000_rename_company_to_company_name/migration.sql +0 -3
- package/template/prisma/migrations/20251209150016_add_email_tracking/migration.sql +0 -21
- package/template/prisma/migrations/20251209155908_add_notify_contact_to_task/migration.sql +0 -2
- package/template/prisma/migrations/20251210110019_add_appointment_types/migration.sql +0 -10
- package/template/prisma/migrations/20251210113928_add_contact_files/migration.sql +0 -26
- package/template/prisma/migrations/20251212132339_add_custom_roles/migration.sql +0 -24
- package/template/prisma/migrations/20251215104448_add_file_interaction_types/migration.sql +0 -11
- package/template/prisma/migrations/20251215145616_add_closing_reasons/migration.sql +0 -12
- package/template/prisma/migrations/20251216140850_add_log_users/migration.sql +0 -25
- package/template/prisma/migrations/20251216151000_rename_perdu_to_ferme/migration.sql +0 -8
- package/template/prisma/migrations/20251216162318_add_column_mappings_to_google_sheet/migration.sql +0 -2
- package/template/prisma/migrations/20251216185127_add_workflows/migration.sql +0 -80
- package/template/prisma/migrations/20251216192237_add_scheduled_workflow_actions/migration.sql +0 -32
- package/template/prisma/migrations/20251220000000_add_task_interaction_type/migration.sql +0 -4
- package/template/prisma/migrations/20251221000000_add_task_type/migration.sql +0 -3
- package/template/prisma/migrations/20251221000001_add_event_color/migration.sql +0 -23
- package/template/prisma/migrations/20260210114913_add_dashboard_widget/migration.sql +0 -20
- package/template/prisma/migrations/20260226093949_fix_cascade_on_user_delete/migration.sql +0 -69
- package/template/src/app/(dashboard)/users/layout.tsx +0 -30
- package/template/src/lib/google-drive.ts +0 -380
|
@@ -1,9 +1,77 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { z } from 'zod';
|
|
2
3
|
import { auth } from '@/lib/auth';
|
|
3
|
-
import { prisma } from '@/lib/prisma';
|
|
4
|
+
import { prisma, Prisma } from '@/lib/prisma';
|
|
5
|
+
import { checkPermission } from '@/lib/check-permission';
|
|
4
6
|
import { handleContactDuplicate } from '@/lib/contact-duplicate';
|
|
5
7
|
import { executeWorkflowsOnContactCreated } from '@/lib/workflow-executor';
|
|
6
8
|
import { normalizePhoneNumber } from '@/lib/utils';
|
|
9
|
+
import { buildPrismaWhereFromFilters } from '@/lib/contact-view-filters';
|
|
10
|
+
import {
|
|
11
|
+
expandRegionCodesToDepartmentCodes,
|
|
12
|
+
prismaPostalMatchesDepartmentsCondition,
|
|
13
|
+
} from '@/lib/fr-geography';
|
|
14
|
+
import type { ViewFilter, ViewSortConfig } from '@/types/contact-views';
|
|
15
|
+
|
|
16
|
+
const socialNetworkSchema = z.object({
|
|
17
|
+
platform: z.string().trim().min(1),
|
|
18
|
+
url: z.string().trim().url(),
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const createContactSchema = z.object({
|
|
22
|
+
civility: z.enum(['M', 'MME', 'MLLE']).optional().nullable(),
|
|
23
|
+
firstName: z.string().trim().min(1).optional().nullable(),
|
|
24
|
+
lastName: z.string().trim().min(1).optional().nullable(),
|
|
25
|
+
phone: z.string().trim().min(3, 'Le téléphone est obligatoire'),
|
|
26
|
+
secondaryPhone: z.string().trim().optional().nullable(),
|
|
27
|
+
email: z.string().email().optional().nullable(),
|
|
28
|
+
address: z.string().optional().nullable(),
|
|
29
|
+
city: z.string().optional().nullable(),
|
|
30
|
+
postalCode: z.string().optional().nullable(),
|
|
31
|
+
origin: z.string().optional().nullable(),
|
|
32
|
+
companyName: z.string().trim().optional().nullable(),
|
|
33
|
+
companyId: z.string().optional().nullable(),
|
|
34
|
+
jobTitle: z.string().trim().optional().nullable(),
|
|
35
|
+
website: z.string().trim().url().optional().nullable().or(z.literal('')),
|
|
36
|
+
socialNetworks: z.array(socialNetworkSchema).optional().nullable(),
|
|
37
|
+
statusId: z.string().optional().nullable(),
|
|
38
|
+
closingReason: z.string().optional().nullable(),
|
|
39
|
+
assignedCommercialId: z.string().optional().nullable(),
|
|
40
|
+
assignedTeleproId: z.string().optional().nullable(),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
function parseContactsListSortDirection(
|
|
44
|
+
sortDir: string | null,
|
|
45
|
+
sortOrder: string | null,
|
|
46
|
+
): 'asc' | 'desc' | null {
|
|
47
|
+
const raw = sortDir ?? sortOrder;
|
|
48
|
+
return raw === 'asc' || raw === 'desc' ? raw : null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Identifiants UI liste → orderBy Prisma (whitelist). */
|
|
52
|
+
function contactListPrismaOrderBy(
|
|
53
|
+
uiField: string,
|
|
54
|
+
direction: 'asc' | 'desc',
|
|
55
|
+
): Prisma.ContactOrderByWithRelationInput | null {
|
|
56
|
+
switch (uiField) {
|
|
57
|
+
case 'createdAt':
|
|
58
|
+
return { createdAt: direction };
|
|
59
|
+
case 'updatedAt':
|
|
60
|
+
return { updatedAt: direction };
|
|
61
|
+
case 'postalCode':
|
|
62
|
+
return { postalCode: direction };
|
|
63
|
+
case 'status':
|
|
64
|
+
return { status: { name: direction } };
|
|
65
|
+
case 'commercial':
|
|
66
|
+
return { assignedCommercial: { name: direction } };
|
|
67
|
+
case 'telepro':
|
|
68
|
+
return { assignedTelepro: { name: direction } };
|
|
69
|
+
case 'origin':
|
|
70
|
+
return { origin: direction };
|
|
71
|
+
default:
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
7
75
|
|
|
8
76
|
// GET /api/contacts - Récupérer tous les contacts avec filtres
|
|
9
77
|
export async function GET(request: NextRequest) {
|
|
@@ -16,8 +84,26 @@ export async function GET(request: NextRequest) {
|
|
|
16
84
|
return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
|
|
17
85
|
}
|
|
18
86
|
|
|
87
|
+
const [canViewAll, canViewOwn, canViewUnassigned] = await Promise.all([
|
|
88
|
+
checkPermission('contacts.view_all'),
|
|
89
|
+
checkPermission('contacts.view_own'),
|
|
90
|
+
checkPermission('contacts.view_unassigned'),
|
|
91
|
+
]);
|
|
92
|
+
|
|
93
|
+
if (!canViewAll && !canViewOwn) {
|
|
94
|
+
return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
|
|
95
|
+
}
|
|
96
|
+
|
|
19
97
|
const { searchParams } = new URL(request.url);
|
|
20
98
|
const search = searchParams.get('search') || '';
|
|
99
|
+
const viewId = searchParams.get('viewId');
|
|
100
|
+
const filtersParam = searchParams.get('filters');
|
|
101
|
+
const statusIds = searchParams.get('statusIds');
|
|
102
|
+
const assignedCommercialIds = searchParams.get('assignedCommercialIds');
|
|
103
|
+
const assignedTeleproIds = searchParams.get('assignedTeleproIds');
|
|
104
|
+
const origins = searchParams.get('origins');
|
|
105
|
+
const departmentCodesParam = searchParams.get('departmentCodes');
|
|
106
|
+
const regionCodesParam = searchParams.get('regionCodes');
|
|
21
107
|
const statusId = searchParams.get('statusId');
|
|
22
108
|
const assignedCommercialId = searchParams.get('assignedCommercialId');
|
|
23
109
|
const assignedTeleproId = searchParams.get('assignedTeleproId');
|
|
@@ -26,40 +112,182 @@ export async function GET(request: NextRequest) {
|
|
|
26
112
|
const createdAtEnd = searchParams.get('createdAtEnd');
|
|
27
113
|
const updatedAtStart = searchParams.get('updatedAtStart');
|
|
28
114
|
const updatedAtEnd = searchParams.get('updatedAtEnd');
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
const
|
|
115
|
+
const sortFieldParam = searchParams.get('sortField');
|
|
116
|
+
const sortDirParam = searchParams.get('sortDir');
|
|
117
|
+
const sortOrderParam = searchParams.get('sortOrder');
|
|
118
|
+
const page = Number.parseInt(searchParams.get('page') || '1');
|
|
119
|
+
const limit = Math.min(Number.parseInt(searchParams.get('limit') || '50'), 200);
|
|
32
120
|
const skip = (page - 1) * limit;
|
|
33
121
|
|
|
34
|
-
// Construire les filtres
|
|
35
122
|
const where: any = {};
|
|
123
|
+
let viewSortConfig: ViewSortConfig | null = null;
|
|
36
124
|
|
|
37
|
-
if
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
125
|
+
// Resolve view filters if viewId or inline filters are provided
|
|
126
|
+
if (viewId) {
|
|
127
|
+
const view = await prisma.contactView.findUnique({ where: { id: viewId } });
|
|
128
|
+
if (view && (view.userId === session.user.id || view.isPublic)) {
|
|
129
|
+
const viewFilters = (view.filters ?? []) as unknown as ViewFilter[];
|
|
130
|
+
const viewWhere = buildPrismaWhereFromFilters(viewFilters);
|
|
131
|
+
if (Object.keys(viewWhere).length > 0) {
|
|
132
|
+
where.AND = where.AND || [];
|
|
133
|
+
where.AND.push(viewWhere);
|
|
134
|
+
}
|
|
135
|
+
if (view.sortConfig) {
|
|
136
|
+
viewSortConfig = view.sortConfig as unknown as ViewSortConfig;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
} else if (filtersParam) {
|
|
140
|
+
try {
|
|
141
|
+
const parsedFilters = JSON.parse(filtersParam) as ViewFilter[];
|
|
142
|
+
if (Array.isArray(parsedFilters) && parsedFilters.length > 0) {
|
|
143
|
+
const filtersWhere = buildPrismaWhereFromFilters(parsedFilters);
|
|
144
|
+
if (Object.keys(filtersWhere).length > 0) {
|
|
145
|
+
where.AND = where.AND || [];
|
|
146
|
+
where.AND.push(filtersWhere);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
} catch {
|
|
150
|
+
// Invalid JSON, ignore
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (!canViewAll && canViewOwn) {
|
|
155
|
+
const ownershipConditions: any[] = [
|
|
156
|
+
{ assignedCommercialId: session.user.id },
|
|
157
|
+
{ assignedTeleproId: session.user.id },
|
|
158
|
+
{ createdById: session.user.id },
|
|
43
159
|
];
|
|
160
|
+
if (canViewUnassigned) {
|
|
161
|
+
ownershipConditions.push({
|
|
162
|
+
AND: [{ assignedCommercialId: null }, { assignedTeleproId: null }],
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
where.AND = where.AND || [];
|
|
166
|
+
where.AND.push({ OR: ownershipConditions });
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (search) {
|
|
170
|
+
const trimmedSearch = search.trim();
|
|
171
|
+
const searchTerms = trimmedSearch.split(/\s+/).filter(Boolean);
|
|
172
|
+
const firstTerm = searchTerms[0] || '';
|
|
173
|
+
const remainingTerms = searchTerms.slice(1).join(' ');
|
|
174
|
+
|
|
175
|
+
// Normaliser le numéro si la recherche ressemble à un téléphone
|
|
176
|
+
const digitsOnly = trimmedSearch.replace(/\D/g, '');
|
|
177
|
+
const looksLikePhone = digitsOnly.length >= 4;
|
|
178
|
+
const normalizedPhone = looksLikePhone ? normalizePhoneNumber(trimmedSearch) : null;
|
|
179
|
+
|
|
180
|
+
where.AND = where.AND || [];
|
|
181
|
+
where.AND.push({
|
|
182
|
+
OR: [
|
|
183
|
+
{ firstName: { contains: trimmedSearch, mode: 'insensitive' } },
|
|
184
|
+
{ lastName: { contains: trimmedSearch, mode: 'insensitive' } },
|
|
185
|
+
{ email: { contains: trimmedSearch, mode: 'insensitive' } },
|
|
186
|
+
{ phone: { contains: trimmedSearch, mode: 'insensitive' } },
|
|
187
|
+
{ city: { contains: trimmedSearch, mode: 'insensitive' } },
|
|
188
|
+
{ postalCode: { contains: trimmedSearch, mode: 'insensitive' } },
|
|
189
|
+
{ address: { contains: trimmedSearch, mode: 'insensitive' } },
|
|
190
|
+
{ secondaryPhone: { contains: trimmedSearch, mode: 'insensitive' } },
|
|
191
|
+
{ origin: { contains: trimmedSearch, mode: 'insensitive' } },
|
|
192
|
+
{ jobTitle: { contains: trimmedSearch, mode: 'insensitive' } },
|
|
193
|
+
{ companyName: { contains: trimmedSearch, mode: 'insensitive' } },
|
|
194
|
+
{ company: { name: { contains: trimmedSearch, mode: 'insensitive' } } },
|
|
195
|
+
...(normalizedPhone
|
|
196
|
+
? [
|
|
197
|
+
{ phone: { contains: normalizedPhone, mode: 'insensitive' as const } },
|
|
198
|
+
{ secondaryPhone: { contains: normalizedPhone, mode: 'insensitive' as const } },
|
|
199
|
+
]
|
|
200
|
+
: []),
|
|
201
|
+
...(remainingTerms
|
|
202
|
+
? [
|
|
203
|
+
{
|
|
204
|
+
AND: [
|
|
205
|
+
{ firstName: { contains: firstTerm, mode: 'insensitive' } },
|
|
206
|
+
{ lastName: { contains: remainingTerms, mode: 'insensitive' } },
|
|
207
|
+
],
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
AND: [
|
|
211
|
+
{ firstName: { contains: remainingTerms, mode: 'insensitive' } },
|
|
212
|
+
{ lastName: { contains: firstTerm, mode: 'insensitive' } },
|
|
213
|
+
],
|
|
214
|
+
},
|
|
215
|
+
]
|
|
216
|
+
: []),
|
|
217
|
+
],
|
|
218
|
+
});
|
|
44
219
|
}
|
|
45
220
|
|
|
46
|
-
|
|
221
|
+
// Legacy query-param filters (rétrocompatibilité)
|
|
222
|
+
if (statusIds) {
|
|
223
|
+
const ids = statusIds.split(',').filter(Boolean);
|
|
224
|
+
where.statusId = ids.length === 1 ? ids[0] : { in: ids };
|
|
225
|
+
} else if (statusId) {
|
|
47
226
|
where.statusId = statusId;
|
|
48
227
|
}
|
|
49
228
|
|
|
50
|
-
if (
|
|
229
|
+
if (assignedCommercialIds) {
|
|
230
|
+
const ids = assignedCommercialIds.split(',').filter(Boolean);
|
|
231
|
+
const hasUnassigned = ids.includes('UNASSIGNED');
|
|
232
|
+
const realIds = ids.filter((id) => id !== 'UNASSIGNED');
|
|
233
|
+
if (hasUnassigned && realIds.length > 0) {
|
|
234
|
+
where.AND = where.AND || [];
|
|
235
|
+
where.AND.push({
|
|
236
|
+
OR: [{ assignedCommercialId: null }, { assignedCommercialId: { in: realIds } }],
|
|
237
|
+
});
|
|
238
|
+
} else if (hasUnassigned) {
|
|
239
|
+
where.assignedCommercialId = null;
|
|
240
|
+
} else {
|
|
241
|
+
where.assignedCommercialId = realIds.length === 1 ? realIds[0] : { in: realIds };
|
|
242
|
+
}
|
|
243
|
+
} else if (assignedCommercialId) {
|
|
51
244
|
where.assignedCommercialId = assignedCommercialId;
|
|
52
245
|
}
|
|
53
246
|
|
|
54
|
-
if (
|
|
247
|
+
if (assignedTeleproIds) {
|
|
248
|
+
const ids = assignedTeleproIds.split(',').filter(Boolean);
|
|
249
|
+
const hasUnassigned = ids.includes('UNASSIGNED');
|
|
250
|
+
const realIds = ids.filter((id) => id !== 'UNASSIGNED');
|
|
251
|
+
if (hasUnassigned && realIds.length > 0) {
|
|
252
|
+
where.AND = where.AND || [];
|
|
253
|
+
where.AND.push({
|
|
254
|
+
OR: [{ assignedTeleproId: null }, { assignedTeleproId: { in: realIds } }],
|
|
255
|
+
});
|
|
256
|
+
} else if (hasUnassigned) {
|
|
257
|
+
where.assignedTeleproId = null;
|
|
258
|
+
} else {
|
|
259
|
+
where.assignedTeleproId = realIds.length === 1 ? realIds[0] : { in: realIds };
|
|
260
|
+
}
|
|
261
|
+
} else if (assignedTeleproId) {
|
|
55
262
|
where.assignedTeleproId = assignedTeleproId;
|
|
56
263
|
}
|
|
57
264
|
|
|
58
|
-
if (
|
|
265
|
+
if (origins) {
|
|
266
|
+
const vals = origins.split(',').filter(Boolean);
|
|
267
|
+
where.origin = vals.length === 1 ? vals[0] : { in: vals };
|
|
268
|
+
} else if (origin) {
|
|
59
269
|
where.origin = origin;
|
|
60
270
|
}
|
|
61
271
|
|
|
62
|
-
|
|
272
|
+
if (departmentCodesParam) {
|
|
273
|
+
const codes = departmentCodesParam.split(',').filter(Boolean);
|
|
274
|
+
const geoCond = prismaPostalMatchesDepartmentsCondition(codes);
|
|
275
|
+
if (geoCond) {
|
|
276
|
+
where.AND = where.AND || [];
|
|
277
|
+
where.AND.push(geoCond);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (regionCodesParam) {
|
|
282
|
+
const codes = regionCodesParam.split(',').filter(Boolean);
|
|
283
|
+
const depts = expandRegionCodesToDepartmentCodes(codes);
|
|
284
|
+
const geoCond = prismaPostalMatchesDepartmentsCondition(depts);
|
|
285
|
+
if (geoCond) {
|
|
286
|
+
where.AND = where.AND || [];
|
|
287
|
+
where.AND.push(geoCond);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
63
291
|
if (createdAtStart || createdAtEnd) {
|
|
64
292
|
where.createdAt = {};
|
|
65
293
|
if (createdAtStart) {
|
|
@@ -69,7 +297,6 @@ export async function GET(request: NextRequest) {
|
|
|
69
297
|
}
|
|
70
298
|
}
|
|
71
299
|
if (createdAtEnd) {
|
|
72
|
-
// Ajouter 23h59m59s pour inclure toute la journée
|
|
73
300
|
const endDate = new Date(createdAtEnd);
|
|
74
301
|
if (!isNaN(endDate.getTime())) {
|
|
75
302
|
endDate.setHours(23, 59, 59, 999);
|
|
@@ -78,7 +305,6 @@ export async function GET(request: NextRequest) {
|
|
|
78
305
|
}
|
|
79
306
|
}
|
|
80
307
|
|
|
81
|
-
// Filtres de date pour updatedAt
|
|
82
308
|
if (updatedAtStart || updatedAtEnd) {
|
|
83
309
|
where.updatedAt = {};
|
|
84
310
|
if (updatedAtStart) {
|
|
@@ -88,7 +314,6 @@ export async function GET(request: NextRequest) {
|
|
|
88
314
|
}
|
|
89
315
|
}
|
|
90
316
|
if (updatedAtEnd) {
|
|
91
|
-
// Ajouter 23h59m59s pour inclure toute la journée
|
|
92
317
|
const endDate = new Date(updatedAtEnd);
|
|
93
318
|
if (!isNaN(endDate.getTime())) {
|
|
94
319
|
endDate.setHours(23, 59, 59, 999);
|
|
@@ -97,21 +322,33 @@ export async function GET(request: NextRequest) {
|
|
|
97
322
|
}
|
|
98
323
|
}
|
|
99
324
|
|
|
100
|
-
//
|
|
101
|
-
|
|
102
|
-
|
|
325
|
+
// Determine sort order: explicit param > view config > default
|
|
326
|
+
let orderBy: Prisma.ContactOrderByWithRelationInput = { createdAt: 'desc' };
|
|
327
|
+
const explicitDirection = parseContactsListSortDirection(sortDirParam, sortOrderParam);
|
|
328
|
+
if (sortFieldParam && explicitDirection) {
|
|
329
|
+
const mapped = contactListPrismaOrderBy(sortFieldParam, explicitDirection);
|
|
330
|
+
if (mapped) {
|
|
331
|
+
orderBy = mapped;
|
|
332
|
+
}
|
|
333
|
+
} else if (viewSortConfig?.field) {
|
|
334
|
+
const dir =
|
|
335
|
+
viewSortConfig.direction === 'asc' || viewSortConfig.direction === 'desc'
|
|
336
|
+
? viewSortConfig.direction
|
|
337
|
+
: null;
|
|
338
|
+
if (dir) {
|
|
339
|
+
const mapped = contactListPrismaOrderBy(viewSortConfig.field, dir);
|
|
340
|
+
if (mapped) {
|
|
341
|
+
orderBy = mapped;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
103
345
|
|
|
104
346
|
const [contacts, total] = await Promise.all([
|
|
105
347
|
prisma.contact.findMany({
|
|
106
|
-
where
|
|
107
|
-
...where,
|
|
108
|
-
isCompany: false,
|
|
109
|
-
},
|
|
348
|
+
where,
|
|
110
349
|
include: {
|
|
111
350
|
status: true,
|
|
112
|
-
|
|
113
|
-
select: { id: true, firstName: true, lastName: true, isCompany: true },
|
|
114
|
-
},
|
|
351
|
+
company: { select: { id: true, name: true } },
|
|
115
352
|
assignedCommercial: {
|
|
116
353
|
select: { id: true, name: true, email: true },
|
|
117
354
|
},
|
|
@@ -122,22 +359,29 @@ export async function GET(request: NextRequest) {
|
|
|
122
359
|
select: { id: true, name: true, email: true },
|
|
123
360
|
},
|
|
124
361
|
},
|
|
125
|
-
orderBy
|
|
362
|
+
orderBy,
|
|
126
363
|
skip,
|
|
127
364
|
take: limit,
|
|
128
365
|
}),
|
|
129
|
-
prisma.contact.count({ where
|
|
366
|
+
prisma.contact.count({ where }),
|
|
130
367
|
]);
|
|
131
368
|
|
|
132
|
-
return NextResponse.json(
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
369
|
+
return NextResponse.json(
|
|
370
|
+
{
|
|
371
|
+
contacts,
|
|
372
|
+
pagination: {
|
|
373
|
+
page,
|
|
374
|
+
limit,
|
|
375
|
+
total,
|
|
376
|
+
totalPages: Math.ceil(total / limit),
|
|
377
|
+
},
|
|
139
378
|
},
|
|
140
|
-
|
|
379
|
+
{
|
|
380
|
+
headers: {
|
|
381
|
+
'Cache-Control': 'private, no-store, max-age=0, must-revalidate',
|
|
382
|
+
},
|
|
383
|
+
},
|
|
384
|
+
);
|
|
141
385
|
} catch (error: any) {
|
|
142
386
|
console.error('Erreur lors de la récupération des contacts:', error);
|
|
143
387
|
return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 });
|
|
@@ -155,7 +399,24 @@ export async function POST(request: NextRequest) {
|
|
|
155
399
|
return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
|
|
156
400
|
}
|
|
157
401
|
|
|
158
|
-
const
|
|
402
|
+
const canCreate = await checkPermission('contacts.create');
|
|
403
|
+
if (!canCreate) {
|
|
404
|
+
return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const json = await request.json();
|
|
408
|
+
const parseResult = createContactSchema.safeParse(json);
|
|
409
|
+
|
|
410
|
+
if (!parseResult.success) {
|
|
411
|
+
return NextResponse.json(
|
|
412
|
+
{
|
|
413
|
+
error: 'Données invalides',
|
|
414
|
+
details: parseResult.error.flatten(),
|
|
415
|
+
},
|
|
416
|
+
{ status: 400 },
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
|
|
159
420
|
const {
|
|
160
421
|
civility,
|
|
161
422
|
firstName,
|
|
@@ -168,18 +429,15 @@ export async function POST(request: NextRequest) {
|
|
|
168
429
|
postalCode,
|
|
169
430
|
origin,
|
|
170
431
|
companyName,
|
|
171
|
-
isCompany,
|
|
172
432
|
companyId,
|
|
433
|
+
jobTitle,
|
|
434
|
+
website,
|
|
435
|
+
socialNetworks,
|
|
173
436
|
statusId,
|
|
174
437
|
closingReason,
|
|
175
438
|
assignedCommercialId,
|
|
176
439
|
assignedTeleproId,
|
|
177
|
-
} =
|
|
178
|
-
|
|
179
|
-
// Validation
|
|
180
|
-
if (!phone) {
|
|
181
|
-
return NextResponse.json({ error: 'Le téléphone est obligatoire' }, { status: 400 });
|
|
182
|
-
}
|
|
440
|
+
} = parseResult.data;
|
|
183
441
|
|
|
184
442
|
// Vérifier si c'est un doublon (nom, prénom ET email)
|
|
185
443
|
const duplicateContactId = await handleContactDuplicate(
|
|
@@ -196,9 +454,7 @@ export async function POST(request: NextRequest) {
|
|
|
196
454
|
where: { id: duplicateContactId },
|
|
197
455
|
include: {
|
|
198
456
|
status: true,
|
|
199
|
-
|
|
200
|
-
select: { id: true, firstName: true, lastName: true, isCompany: true },
|
|
201
|
-
},
|
|
457
|
+
company: { select: { id: true, name: true } },
|
|
202
458
|
assignedCommercial: {
|
|
203
459
|
select: { id: true, name: true, email: true },
|
|
204
460
|
},
|
|
@@ -216,7 +472,7 @@ export async function POST(request: NextRequest) {
|
|
|
216
472
|
// Sinon, créer un nouveau contact
|
|
217
473
|
const contact = await prisma.contact.create({
|
|
218
474
|
data: {
|
|
219
|
-
civility: civility
|
|
475
|
+
civility: civility ?? null,
|
|
220
476
|
firstName: firstName || null,
|
|
221
477
|
lastName: lastName || null,
|
|
222
478
|
phone: normalizePhoneNumber(phone),
|
|
@@ -226,9 +482,12 @@ export async function POST(request: NextRequest) {
|
|
|
226
482
|
city: city || null,
|
|
227
483
|
postalCode: postalCode || null,
|
|
228
484
|
origin: origin || null,
|
|
229
|
-
companyName: companyName
|
|
230
|
-
isCompany: isCompany === true,
|
|
485
|
+
companyName: companyName && !companyId ? companyName : null,
|
|
231
486
|
companyId: companyId || null,
|
|
487
|
+
jobTitle: jobTitle || null,
|
|
488
|
+
website: website && website.trim() ? website : null,
|
|
489
|
+
socialNetworks:
|
|
490
|
+
socialNetworks && socialNetworks.length > 0 ? socialNetworks : Prisma.JsonNull,
|
|
232
491
|
statusId: statusId || null,
|
|
233
492
|
closingReason: closingReason || null,
|
|
234
493
|
assignedCommercialId: assignedCommercialId || null,
|
|
@@ -252,9 +511,7 @@ export async function POST(request: NextRequest) {
|
|
|
252
511
|
},
|
|
253
512
|
include: {
|
|
254
513
|
status: true,
|
|
255
|
-
|
|
256
|
-
select: { id: true, firstName: true, lastName: true, isCompany: true },
|
|
257
|
-
},
|
|
514
|
+
company: { select: { id: true, name: true } },
|
|
258
515
|
assignedCommercial: {
|
|
259
516
|
select: { id: true, name: true, email: true },
|
|
260
517
|
},
|
|
@@ -278,6 +535,14 @@ export async function POST(request: NextRequest) {
|
|
|
278
535
|
return NextResponse.json(contact, { status: 201 });
|
|
279
536
|
} catch (error: any) {
|
|
280
537
|
console.error('Erreur lors de la création du contact:', error);
|
|
281
|
-
return NextResponse.json(
|
|
538
|
+
return NextResponse.json(
|
|
539
|
+
{
|
|
540
|
+
error:
|
|
541
|
+
process.env.NODE_ENV === 'development'
|
|
542
|
+
? error.message || 'Erreur serveur'
|
|
543
|
+
: 'Erreur serveur',
|
|
544
|
+
},
|
|
545
|
+
{ status: 500 },
|
|
546
|
+
);
|
|
282
547
|
}
|
|
283
548
|
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { createClient } from '@supabase/supabase-js';
|
|
3
|
+
import { prisma } from '@/lib/prisma';
|
|
4
|
+
import { BUCKETS } from '@/lib/supabase-storage';
|
|
5
|
+
|
|
6
|
+
const BUCKET = BUCKETS.EDITOR_IMAGES;
|
|
7
|
+
const FOLDER = 'images';
|
|
8
|
+
// Ne supprimer que les images de plus de 24h (laisse le temps de sauvegarder)
|
|
9
|
+
const MIN_AGE_MS = 24 * 60 * 60 * 1000;
|
|
10
|
+
const LIST_LIMIT = 1000;
|
|
11
|
+
|
|
12
|
+
function getAdminClient() {
|
|
13
|
+
return createClient(
|
|
14
|
+
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
|
15
|
+
process.env.SUPABASE_SERVICE_ROLE_KEY!,
|
|
16
|
+
{ auth: { autoRefreshToken: false, persistSession: false } },
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Collecte toutes les URLs d'images éditeur référencées en BDD.
|
|
22
|
+
*/
|
|
23
|
+
async function getReferencedImageUrls(): Promise<Set<string>> {
|
|
24
|
+
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
|
25
|
+
const prefix = `${supabaseUrl}/storage/v1/object/public/${BUCKET}/`;
|
|
26
|
+
|
|
27
|
+
const urls = new Set<string>();
|
|
28
|
+
|
|
29
|
+
function extractUrls(text: string | null) {
|
|
30
|
+
if (!text) return;
|
|
31
|
+
const regex = new RegExp(
|
|
32
|
+
prefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '[^"\'\\s<>]+',
|
|
33
|
+
'g',
|
|
34
|
+
);
|
|
35
|
+
for (const match of text.matchAll(regex)) {
|
|
36
|
+
urls.add(match[0]);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Templates
|
|
41
|
+
const templates = await prisma.template.findMany({ select: { content: true } });
|
|
42
|
+
for (const t of templates) extractUrls(t.content);
|
|
43
|
+
|
|
44
|
+
// Interactions (notes, emails)
|
|
45
|
+
const interactions = await prisma.interaction.findMany({
|
|
46
|
+
where: { content: { contains: BUCKET } },
|
|
47
|
+
select: { content: true },
|
|
48
|
+
});
|
|
49
|
+
for (const i of interactions) extractUrls(i.content);
|
|
50
|
+
|
|
51
|
+
// Tasks
|
|
52
|
+
const tasks = await prisma.task.findMany({
|
|
53
|
+
where: { description: { contains: BUCKET } },
|
|
54
|
+
select: { description: true },
|
|
55
|
+
});
|
|
56
|
+
for (const t of tasks) extractUrls(t.description);
|
|
57
|
+
|
|
58
|
+
// SMTP signatures
|
|
59
|
+
const smtpConfigs = await prisma.smtpConfig.findMany({
|
|
60
|
+
where: { signature: { not: null } },
|
|
61
|
+
select: { signature: true },
|
|
62
|
+
});
|
|
63
|
+
for (const s of smtpConfigs) extractUrls(s.signature);
|
|
64
|
+
|
|
65
|
+
// Workflow actions
|
|
66
|
+
const actions = await prisma.workflowAction.findMany({
|
|
67
|
+
where: {
|
|
68
|
+
OR: [
|
|
69
|
+
{ taskDescription: { contains: BUCKET } },
|
|
70
|
+
{ smsMessage: { contains: BUCKET } },
|
|
71
|
+
{ noteContent: { contains: BUCKET } },
|
|
72
|
+
],
|
|
73
|
+
},
|
|
74
|
+
select: { taskDescription: true, smsMessage: true, noteContent: true },
|
|
75
|
+
});
|
|
76
|
+
for (const a of actions) {
|
|
77
|
+
extractUrls(a.taskDescription);
|
|
78
|
+
extractUrls(a.smsMessage);
|
|
79
|
+
extractUrls(a.noteContent);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return urls;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// GET /api/cron/cleanup-editor-images
|
|
86
|
+
export async function GET(request: NextRequest) {
|
|
87
|
+
try {
|
|
88
|
+
const authHeader = request.headers.get('authorization');
|
|
89
|
+
if (authHeader !== `Bearer ${process.env.CRON_SECRET}`) {
|
|
90
|
+
return NextResponse.json({ error: 'Non autorisé' }, { status: 401 });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const client = getAdminClient();
|
|
94
|
+
const now = Date.now();
|
|
95
|
+
|
|
96
|
+
// 1. Lister toutes les images du bucket
|
|
97
|
+
const allFiles: { name: string; created_at: string }[] = [];
|
|
98
|
+
let offset = 0;
|
|
99
|
+
while (true) {
|
|
100
|
+
const { data, error } = await client.storage.from(BUCKET).list(FOLDER, {
|
|
101
|
+
limit: LIST_LIMIT,
|
|
102
|
+
offset,
|
|
103
|
+
sortBy: { column: 'created_at', order: 'asc' },
|
|
104
|
+
});
|
|
105
|
+
if (error) throw new Error(`Erreur listing bucket: ${error.message}`);
|
|
106
|
+
if (!data || data.length === 0) break;
|
|
107
|
+
allFiles.push(...data.map((f) => ({ name: f.name, created_at: f.created_at ?? '' })));
|
|
108
|
+
if (data.length < LIST_LIMIT) break;
|
|
109
|
+
offset += LIST_LIMIT;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (allFiles.length === 0) {
|
|
113
|
+
return NextResponse.json({ deleted: 0, total: 0 });
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// 2. Filtrer : ne garder que les images > 24h
|
|
117
|
+
const oldFiles = allFiles.filter((f) => {
|
|
118
|
+
const createdAt = new Date(f.created_at).getTime();
|
|
119
|
+
return now - createdAt > MIN_AGE_MS;
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
if (oldFiles.length === 0) {
|
|
123
|
+
return NextResponse.json({ deleted: 0, total: allFiles.length, message: 'Aucune image ancienne' });
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// 3. Récupérer les URLs référencées en BDD
|
|
127
|
+
const referencedUrls = await getReferencedImageUrls();
|
|
128
|
+
|
|
129
|
+
// 4. Identifier les orphelines
|
|
130
|
+
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
|
131
|
+
const orphanPaths: string[] = [];
|
|
132
|
+
for (const file of oldFiles) {
|
|
133
|
+
const fullUrl = `${supabaseUrl}/storage/v1/object/public/${BUCKET}/${FOLDER}/${file.name}`;
|
|
134
|
+
if (!referencedUrls.has(fullUrl)) {
|
|
135
|
+
orphanPaths.push(`${FOLDER}/${file.name}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// 5. Supprimer par batch de 100
|
|
140
|
+
let deleted = 0;
|
|
141
|
+
for (let i = 0; i < orphanPaths.length; i += 100) {
|
|
142
|
+
const batch = orphanPaths.slice(i, i + 100);
|
|
143
|
+
const { error } = await client.storage.from(BUCKET).remove(batch);
|
|
144
|
+
if (error) {
|
|
145
|
+
console.error(`Erreur suppression batch ${i}:`, error.message);
|
|
146
|
+
} else {
|
|
147
|
+
deleted += batch.length;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
console.log(
|
|
152
|
+
`[cleanup-editor-images] ${deleted} orphelines supprimées sur ${allFiles.length} total (${referencedUrls.size} référencées)`,
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
return NextResponse.json({
|
|
156
|
+
deleted,
|
|
157
|
+
total: allFiles.length,
|
|
158
|
+
referenced: referencedUrls.size,
|
|
159
|
+
orphans: orphanPaths.length,
|
|
160
|
+
});
|
|
161
|
+
} catch (error: unknown) {
|
|
162
|
+
console.error('[cleanup-editor-images] Erreur:', error);
|
|
163
|
+
const message = error instanceof Error ? error.message : 'Erreur serveur';
|
|
164
|
+
return NextResponse.json({ error: message }, { status: 500 });
|
|
165
|
+
}
|
|
166
|
+
}
|