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
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { auth } from '@/lib/auth';
|
|
3
|
+
import { prisma } from '@/lib/prisma';
|
|
4
|
+
import { checkPermission } from '@/lib/check-permission';
|
|
5
|
+
|
|
6
|
+
const DAILY_SOFT_LIMIT = 800;
|
|
7
|
+
const DAILY_HARD_TARGET = 950;
|
|
8
|
+
|
|
9
|
+
export async function GET(request: NextRequest) {
|
|
10
|
+
try {
|
|
11
|
+
const session = await auth.api.getSession({ headers: request.headers });
|
|
12
|
+
if (!session) {
|
|
13
|
+
return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
|
|
14
|
+
}
|
|
15
|
+
const canManageIntegrations = await checkPermission('integrations.google_sheets.manage');
|
|
16
|
+
if (!canManageIntegrations) {
|
|
17
|
+
return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const since = new Date(Date.now() - 24 * 60 * 60 * 1000);
|
|
21
|
+
const grouped = await prisma.googleSheetSyncJob.groupBy({
|
|
22
|
+
by: ['triggerType'],
|
|
23
|
+
where: { createdAt: { gte: since } },
|
|
24
|
+
_count: { _all: true },
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const scheduled = grouped.find((g) => g.triggerType === 'SCHEDULED')?._count._all ?? 0;
|
|
28
|
+
const manual = grouped.find((g) => g.triggerType === 'MANUAL')?._count._all ?? 0;
|
|
29
|
+
const total = scheduled + manual;
|
|
30
|
+
|
|
31
|
+
if (total > DAILY_SOFT_LIMIT) {
|
|
32
|
+
console.warn(
|
|
33
|
+
`[GoogleSheetSync] usage 24h élevé: total=${total}, scheduled=${scheduled}, manual=${manual}, target=${DAILY_HARD_TARGET}`,
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return NextResponse.json({
|
|
38
|
+
windowHours: 24,
|
|
39
|
+
total,
|
|
40
|
+
scheduled,
|
|
41
|
+
manual,
|
|
42
|
+
softLimit: DAILY_SOFT_LIMIT,
|
|
43
|
+
hardTarget: DAILY_HARD_TARGET,
|
|
44
|
+
remainingBeforeHardTarget: Math.max(0, DAILY_HARD_TARGET - total),
|
|
45
|
+
});
|
|
46
|
+
} catch (error) {
|
|
47
|
+
console.error('Erreur usage jobs Google Sheet:', error);
|
|
48
|
+
return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -1,555 +1,41 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { handleContactDuplicate } from '@/lib/contact-duplicate';
|
|
2
|
+
import { auth } from '@/lib/auth';
|
|
3
|
+
import { enqueueGoogleSheetSyncJob, SyncJobRateLimitError } from '@/lib/google-sheet-sync-jobs';
|
|
5
4
|
|
|
6
|
-
// POST /api/integrations/google-sheet/sync
|
|
5
|
+
// POST /api/integrations/google-sheet/sync — tout utilisateur connecté peut lancer une synchro.
|
|
6
|
+
// La permission integrations.google_sheets.manage reste requise côté UI / routes settings pour configurer l’intégration.
|
|
7
7
|
export async function POST(request: NextRequest) {
|
|
8
8
|
try {
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const configs = await client.googleSheetSyncConfig.findMany({
|
|
13
|
-
where: { active: true },
|
|
14
|
-
include: {
|
|
15
|
-
ownerUser: true,
|
|
16
|
-
},
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
if (!configs || configs.length === 0) {
|
|
20
|
-
return NextResponse.json({
|
|
21
|
-
totalImported: 0,
|
|
22
|
-
totalUpdated: 0,
|
|
23
|
-
totalSkipped: 0,
|
|
24
|
-
results: [],
|
|
25
|
-
message: "Aucune configuration Google Sheets active n'a été trouvée.",
|
|
26
|
-
});
|
|
9
|
+
const session = await auth.api.getSession({ headers: request.headers });
|
|
10
|
+
if (!session) {
|
|
11
|
+
return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
|
|
27
12
|
}
|
|
28
13
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
skipped: number;
|
|
35
|
-
error?: string;
|
|
36
|
-
}> = [];
|
|
37
|
-
|
|
38
|
-
let totalImported = 0;
|
|
39
|
-
let totalUpdated = 0;
|
|
40
|
-
let totalSkipped = 0;
|
|
41
|
-
|
|
42
|
-
// Synchroniser chaque configuration
|
|
43
|
-
for (const config of configs) {
|
|
44
|
-
try {
|
|
45
|
-
// Récupérer le compte Google de l'utilisateur propriétaire
|
|
46
|
-
const googleAccount = await client.userGoogleAccount.findUnique({
|
|
47
|
-
where: { userId: config.ownerUserId },
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
if (!googleAccount) {
|
|
51
|
-
results.push({
|
|
52
|
-
configId: config.id,
|
|
53
|
-
configName: config.name,
|
|
54
|
-
imported: 0,
|
|
55
|
-
updated: 0,
|
|
56
|
-
skipped: 0,
|
|
57
|
-
error:
|
|
58
|
-
'Aucun compte Google connecté pour l’utilisateur propriétaire. Veuillez connecter votre compte Google dans les paramètres.',
|
|
59
|
-
});
|
|
60
|
-
continue;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
let accessToken: string;
|
|
64
|
-
try {
|
|
65
|
-
accessToken = await getValidAccessToken(
|
|
66
|
-
googleAccount.accessToken,
|
|
67
|
-
googleAccount.refreshToken,
|
|
68
|
-
googleAccount.tokenExpiresAt,
|
|
69
|
-
);
|
|
70
|
-
|
|
71
|
-
// Mettre à jour le token si nécessaire
|
|
72
|
-
if (accessToken !== googleAccount.accessToken) {
|
|
73
|
-
const tokenExpiresAt = new Date();
|
|
74
|
-
tokenExpiresAt.setSeconds(tokenExpiresAt.getSeconds() + 3600);
|
|
75
|
-
await client.userGoogleAccount.update({
|
|
76
|
-
where: { userId: config.ownerUserId },
|
|
77
|
-
data: {
|
|
78
|
-
accessToken,
|
|
79
|
-
tokenExpiresAt,
|
|
80
|
-
},
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
} catch (error: any) {
|
|
84
|
-
// Gérer spécifiquement les erreurs de token expiré/révoqué
|
|
85
|
-
if (error instanceof GoogleTokenError && error.isRevoked) {
|
|
86
|
-
results.push({
|
|
87
|
-
configId: config.id,
|
|
88
|
-
configName: config.name,
|
|
89
|
-
imported: 0,
|
|
90
|
-
updated: 0,
|
|
91
|
-
skipped: 0,
|
|
92
|
-
error: error.message,
|
|
93
|
-
});
|
|
94
|
-
continue;
|
|
95
|
-
}
|
|
96
|
-
// Relancer l'erreur si ce n'est pas une erreur de token
|
|
97
|
-
throw error;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const range = encodeURIComponent(config.sheetName);
|
|
101
|
-
const response = await fetch(
|
|
102
|
-
`https://sheets.googleapis.com/v4/spreadsheets/${config.spreadsheetId}/values/${range}`,
|
|
103
|
-
{
|
|
104
|
-
headers: {
|
|
105
|
-
Authorization: `Bearer ${accessToken}`,
|
|
106
|
-
},
|
|
107
|
-
},
|
|
108
|
-
);
|
|
109
|
-
|
|
110
|
-
if (!response.ok) {
|
|
111
|
-
const errorText = await response.text();
|
|
112
|
-
console.error(`Erreur lors de la lecture du Google Sheet ${config.name}:`, errorText);
|
|
113
|
-
results.push({
|
|
114
|
-
configId: config.id,
|
|
115
|
-
configName: config.name,
|
|
116
|
-
imported: 0,
|
|
117
|
-
updated: 0,
|
|
118
|
-
skipped: 0,
|
|
119
|
-
error: 'Impossible de lire les données depuis Google Sheets.',
|
|
120
|
-
});
|
|
121
|
-
continue;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const data = await response.json();
|
|
125
|
-
const values: string[][] = data.values || [];
|
|
126
|
-
|
|
127
|
-
if (!values.length) {
|
|
128
|
-
results.push({
|
|
129
|
-
configId: config.id,
|
|
130
|
-
configName: config.name,
|
|
131
|
-
imported: 0,
|
|
132
|
-
updated: 0,
|
|
133
|
-
skipped: 0,
|
|
134
|
-
});
|
|
135
|
-
continue;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
const headerRowIndex = config.headerRow - 1;
|
|
139
|
-
const startRowIndex = Math.max(
|
|
140
|
-
headerRowIndex + 1,
|
|
141
|
-
(config.lastSyncedRow || headerRowIndex) + 1,
|
|
142
|
-
);
|
|
143
|
-
|
|
144
|
-
// Si aucune nouvelle ligne à traiter, skip
|
|
145
|
-
if (startRowIndex >= values.length) {
|
|
146
|
-
results.push({
|
|
147
|
-
configId: config.id,
|
|
148
|
-
configName: config.name,
|
|
149
|
-
imported: 0,
|
|
150
|
-
updated: 0,
|
|
151
|
-
skipped: 0,
|
|
152
|
-
});
|
|
153
|
-
continue;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// Réserver atomiquement les lignes pour éviter les imports en double
|
|
157
|
-
// (protection contre les synchronisations concurrentes, ex: React Strict Mode, multi-pages)
|
|
158
|
-
const claimedMaxRow = values.length - 1;
|
|
159
|
-
const claimCondition: Record<string, unknown> = { id: config.id };
|
|
160
|
-
if (config.lastSyncedRow !== null && config.lastSyncedRow !== undefined) {
|
|
161
|
-
claimCondition.lastSyncedRow = config.lastSyncedRow;
|
|
162
|
-
} else {
|
|
163
|
-
claimCondition.lastSyncedRow = null;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
const claimResult = await client.googleSheetSyncConfig.updateMany({
|
|
167
|
-
where: claimCondition,
|
|
168
|
-
data: {
|
|
169
|
-
lastSyncedRow: claimedMaxRow,
|
|
170
|
-
},
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
if (claimResult.count === 0) {
|
|
174
|
-
// Une autre synchronisation concurrente a déjà réservé ces lignes
|
|
175
|
-
results.push({
|
|
176
|
-
configId: config.id,
|
|
177
|
-
configName: config.name,
|
|
178
|
-
imported: 0,
|
|
179
|
-
updated: 0,
|
|
180
|
-
skipped: 0,
|
|
181
|
-
});
|
|
182
|
-
continue;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// Récupérer les headers
|
|
186
|
-
const headerRow = values[headerRowIndex] || [];
|
|
187
|
-
|
|
188
|
-
// Utiliser le nouveau format columnMappings
|
|
189
|
-
let columnMappings: Record<string, number> = {}; // crmField -> index
|
|
190
|
-
let noteFields: Array<{ name: string; index: number }> = [];
|
|
191
|
-
|
|
192
|
-
if (!config.columnMappings) {
|
|
193
|
-
results.push({
|
|
194
|
-
configId: config.id,
|
|
195
|
-
configName: config.name,
|
|
196
|
-
imported: 0,
|
|
197
|
-
updated: 0,
|
|
198
|
-
skipped: 0,
|
|
199
|
-
error:
|
|
200
|
-
"La configuration n'utilise pas le nouveau format de mapping. Veuillez reconfigurer cette intégration.",
|
|
201
|
-
});
|
|
202
|
-
continue;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// Parser les mappings
|
|
206
|
-
const mappings =
|
|
207
|
-
typeof config.columnMappings === 'string'
|
|
208
|
-
? JSON.parse(config.columnMappings)
|
|
209
|
-
: config.columnMappings;
|
|
210
|
-
|
|
211
|
-
if (!Array.isArray(mappings)) {
|
|
212
|
-
results.push({
|
|
213
|
-
configId: config.id,
|
|
214
|
-
configName: config.name,
|
|
215
|
-
imported: 0,
|
|
216
|
-
updated: 0,
|
|
217
|
-
skipped: 0,
|
|
218
|
-
error: 'Format de mapping invalide.',
|
|
219
|
-
});
|
|
220
|
-
continue;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
mappings.forEach((mapping: any) => {
|
|
224
|
-
if (mapping.action === 'map' && mapping.crmField && mapping.columnName) {
|
|
225
|
-
// Trouver l'index de la colonne par son nom
|
|
226
|
-
const columnIndex = headerRow.findIndex(
|
|
227
|
-
(h: string) =>
|
|
228
|
-
h && h.trim().toLowerCase() === mapping.columnName.trim().toLowerCase(),
|
|
229
|
-
);
|
|
230
|
-
if (columnIndex !== -1) {
|
|
231
|
-
columnMappings[mapping.crmField] = columnIndex;
|
|
232
|
-
}
|
|
233
|
-
} else if (mapping.action === 'note' && mapping.columnName) {
|
|
234
|
-
const columnIndex = headerRow.findIndex(
|
|
235
|
-
(h: string) =>
|
|
236
|
-
h && h.trim().toLowerCase() === mapping.columnName.trim().toLowerCase(),
|
|
237
|
-
);
|
|
238
|
-
if (columnIndex !== -1) {
|
|
239
|
-
noteFields.push({ name: mapping.columnName, index: columnIndex });
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
// Vérifier que le téléphone est mappé (obligatoire)
|
|
245
|
-
if (columnMappings['phone'] === undefined) {
|
|
246
|
-
results.push({
|
|
247
|
-
configId: config.id,
|
|
248
|
-
configName: config.name,
|
|
249
|
-
imported: 0,
|
|
250
|
-
updated: 0,
|
|
251
|
-
skipped: 0,
|
|
252
|
-
error: "La colonne téléphone n'est pas correctement mappée.",
|
|
253
|
-
});
|
|
254
|
-
continue;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
const phoneIdx = columnMappings['phone'];
|
|
258
|
-
|
|
259
|
-
// Déterminer le statut par défaut à utiliser (configuré ou "Nouveau")
|
|
260
|
-
let effectiveDefaultStatusId = config.defaultStatusId || null;
|
|
261
|
-
if (!effectiveDefaultStatusId) {
|
|
262
|
-
const fallbackStatus = await client.status.findFirst({
|
|
263
|
-
where: { name: 'Nouveau' },
|
|
264
|
-
});
|
|
265
|
-
if (fallbackStatus) {
|
|
266
|
-
effectiveDefaultStatusId = fallbackStatus.id;
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
let imported = 0;
|
|
271
|
-
let updated = 0;
|
|
272
|
-
let skipped = 0;
|
|
273
|
-
|
|
274
|
-
for (let rowIndex = startRowIndex; rowIndex < values.length; rowIndex++) {
|
|
275
|
-
const row = values[rowIndex];
|
|
276
|
-
if (!row) continue;
|
|
277
|
-
|
|
278
|
-
const phone = row[phoneIdx]?.trim();
|
|
279
|
-
if (!phone) {
|
|
280
|
-
skipped++;
|
|
281
|
-
continue;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
const firstName =
|
|
285
|
-
columnMappings['firstName'] !== undefined
|
|
286
|
-
? row[columnMappings['firstName']]?.trim() || undefined
|
|
287
|
-
: undefined;
|
|
288
|
-
const lastName =
|
|
289
|
-
columnMappings['lastName'] !== undefined
|
|
290
|
-
? row[columnMappings['lastName']]?.trim() || undefined
|
|
291
|
-
: undefined;
|
|
292
|
-
const email =
|
|
293
|
-
columnMappings['email'] !== undefined
|
|
294
|
-
? row[columnMappings['email']]?.trim() || undefined
|
|
295
|
-
: undefined;
|
|
296
|
-
const city =
|
|
297
|
-
columnMappings['city'] !== undefined
|
|
298
|
-
? row[columnMappings['city']]?.trim() || undefined
|
|
299
|
-
: undefined;
|
|
300
|
-
const postalCode =
|
|
301
|
-
columnMappings['postalCode'] !== undefined
|
|
302
|
-
? row[columnMappings['postalCode']]?.trim() || undefined
|
|
303
|
-
: undefined;
|
|
304
|
-
const origin =
|
|
305
|
-
columnMappings['origin'] !== undefined
|
|
306
|
-
? row[columnMappings['origin']]?.trim() || 'Google Sheets'
|
|
307
|
-
: 'Google Sheets';
|
|
308
|
-
|
|
309
|
-
// Collecter les notes si des colonnes sont configurées comme "note"
|
|
310
|
-
const noteContents: Array<{ label: string; value: string }> = [];
|
|
311
|
-
if (noteFields.length > 0) {
|
|
312
|
-
noteFields.forEach(({ name, index }) => {
|
|
313
|
-
if (row[index]) {
|
|
314
|
-
noteContents.push({
|
|
315
|
-
label: name,
|
|
316
|
-
value: row[index].trim(),
|
|
317
|
-
});
|
|
318
|
-
}
|
|
319
|
-
});
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
// Fonction pour échapper le HTML
|
|
323
|
-
const escapeHtml = (text: string): string => {
|
|
324
|
-
const map: { [key: string]: string } = {
|
|
325
|
-
'&': '&',
|
|
326
|
-
'<': '<',
|
|
327
|
-
'>': '>',
|
|
328
|
-
'"': '"',
|
|
329
|
-
"'": ''',
|
|
330
|
-
};
|
|
331
|
-
return text.replace(/[&<>"']/g, (m) => map[m]);
|
|
332
|
-
};
|
|
333
|
-
|
|
334
|
-
// Fonction pour formater le contenu de la note en HTML
|
|
335
|
-
const formatNoteContent = (noteItems: Array<{ label: string; value: string }>) => {
|
|
336
|
-
const escapedConfigName = escapeHtml(config.name);
|
|
337
|
-
|
|
338
|
-
if (noteItems.length === 0) {
|
|
339
|
-
return `<p style="font-size: 14px; line-height: 1.5; color: #374151; margin-bottom: 0;">Ce contact a été importé automatiquement depuis Google Sheets (${escapedConfigName}).</p>`;
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
let html = `<p style="font-size: 14px; line-height: 1.5; color: #374151; margin-bottom: 16px;">Ce contact a été importé automatiquement depuis Google Sheets (${escapedConfigName}).</p>`;
|
|
343
|
-
|
|
344
|
-
html +=
|
|
345
|
-
'<div style="display: flex; flex-direction: column; gap: 12px; margin-top: 16px;">';
|
|
346
|
-
|
|
347
|
-
noteItems.forEach((item) => {
|
|
348
|
-
// Formater les valeurs qui sont des tableaux JSON
|
|
349
|
-
let formattedValue = item.value;
|
|
350
|
-
try {
|
|
351
|
-
const parsed = JSON.parse(item.value);
|
|
352
|
-
if (Array.isArray(parsed)) {
|
|
353
|
-
formattedValue = parsed.map((v) => String(v)).join(', ');
|
|
354
|
-
}
|
|
355
|
-
} catch {
|
|
356
|
-
// Ce n'est pas du JSON, on garde la valeur telle quelle
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
// Capitaliser le label pour le rendre plus humain
|
|
360
|
-
const humanLabel = item.label
|
|
361
|
-
.split(/(?=[A-Z])/)
|
|
362
|
-
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
|
363
|
-
.join(' ');
|
|
364
|
-
|
|
365
|
-
// Échapper le HTML pour éviter les injections XSS
|
|
366
|
-
const escapedLabel = escapeHtml(humanLabel);
|
|
367
|
-
const escapedValue = escapeHtml(String(formattedValue)).replace(/\n/g, '<br>');
|
|
368
|
-
|
|
369
|
-
html += `
|
|
370
|
-
<div style="padding: 12px 14px; background: linear-gradient(to right, #f8fafc 0%, #f1f5f9 100%); border-radius: 10px; border-left: 4px solid #6366f1; box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.05); transition: all 0.2s ease;">
|
|
371
|
-
<div style="font-size: 11px; font-weight: 700; color: #64748b; text-transform: uppercase; letter-spacing: 0.8px; margin-bottom: 6px;">
|
|
372
|
-
${escapedLabel}
|
|
373
|
-
</div>
|
|
374
|
-
<div style="font-size: 14px; line-height: 1.5; color: #0f172a; font-weight: 500;">
|
|
375
|
-
${escapedValue}
|
|
376
|
-
</div>
|
|
377
|
-
</div>
|
|
378
|
-
`;
|
|
379
|
-
});
|
|
380
|
-
|
|
381
|
-
html += '</div>';
|
|
382
|
-
return html;
|
|
383
|
-
};
|
|
384
|
-
|
|
385
|
-
// Déterminer l'assignation selon le rôle de l'utilisateur par défaut
|
|
386
|
-
let assignedCommercialId: string | null = null;
|
|
387
|
-
let assignedTeleproId: string | null = null;
|
|
388
|
-
|
|
389
|
-
if (config.defaultAssignedUserId) {
|
|
390
|
-
const defaultUser = await client.user.findUnique({
|
|
391
|
-
where: { id: config.defaultAssignedUserId },
|
|
392
|
-
select: { role: true },
|
|
393
|
-
});
|
|
394
|
-
|
|
395
|
-
if (defaultUser) {
|
|
396
|
-
if (
|
|
397
|
-
defaultUser.role === 'COMMERCIAL' ||
|
|
398
|
-
defaultUser.role === 'ADMIN' ||
|
|
399
|
-
defaultUser.role === 'MANAGER'
|
|
400
|
-
) {
|
|
401
|
-
assignedCommercialId = config.defaultAssignedUserId;
|
|
402
|
-
} else if (defaultUser.role === 'TELEPRO') {
|
|
403
|
-
assignedTeleproId = config.defaultAssignedUserId;
|
|
404
|
-
}
|
|
405
|
-
// Sinon, on ne assigne pas (null pour les deux)
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
// Vérifier si c'est un doublon (nom, prénom ET email)
|
|
410
|
-
const duplicateContactId = await handleContactDuplicate(
|
|
411
|
-
firstName,
|
|
412
|
-
lastName,
|
|
413
|
-
email,
|
|
414
|
-
origin,
|
|
415
|
-
config.defaultAssignedUserId || config.ownerUserId,
|
|
416
|
-
);
|
|
417
|
-
|
|
418
|
-
let contact;
|
|
419
|
-
let isNewContact = false;
|
|
420
|
-
|
|
421
|
-
if (duplicateContactId) {
|
|
422
|
-
// C'est un doublon, récupérer le contact existant
|
|
423
|
-
contact = await client.contact.findUnique({
|
|
424
|
-
where: { id: duplicateContactId },
|
|
425
|
-
});
|
|
426
|
-
updated++;
|
|
427
|
-
} else {
|
|
428
|
-
// Chercher un contact existant (par téléphone uniquement)
|
|
429
|
-
contact =
|
|
430
|
-
(email &&
|
|
431
|
-
(await client.contact.findFirst({
|
|
432
|
-
where: {
|
|
433
|
-
OR: [{ email: email.toLowerCase() }, { phone }],
|
|
434
|
-
},
|
|
435
|
-
}))) ||
|
|
436
|
-
(await client.contact.findFirst({
|
|
437
|
-
where: { phone },
|
|
438
|
-
}));
|
|
439
|
-
|
|
440
|
-
if (!contact) {
|
|
441
|
-
// Préparer les interactions à créer
|
|
442
|
-
const formattedContent = formatNoteContent(noteContents);
|
|
443
|
-
const interactionsToCreate: any[] = [
|
|
444
|
-
{
|
|
445
|
-
type: 'NOTE',
|
|
446
|
-
title: `Contact importé depuis Google Sheets: ${config.name}`,
|
|
447
|
-
content: formattedContent,
|
|
448
|
-
userId: config.defaultAssignedUserId || config.ownerUserId,
|
|
449
|
-
date: new Date(),
|
|
450
|
-
metadata: {
|
|
451
|
-
htmlContent: formattedContent,
|
|
452
|
-
isGoogleSheetsImport: true,
|
|
453
|
-
},
|
|
454
|
-
},
|
|
455
|
-
];
|
|
456
|
-
|
|
457
|
-
contact = await client.contact.create({
|
|
458
|
-
data: {
|
|
459
|
-
firstName: firstName || null,
|
|
460
|
-
lastName: lastName || null,
|
|
461
|
-
email: email ? email.toLowerCase() : null,
|
|
462
|
-
phone,
|
|
463
|
-
city: city || null,
|
|
464
|
-
postalCode: postalCode || null,
|
|
465
|
-
origin,
|
|
466
|
-
statusId: effectiveDefaultStatusId,
|
|
467
|
-
assignedCommercialId: assignedCommercialId,
|
|
468
|
-
assignedTeleproId: assignedTeleproId,
|
|
469
|
-
createdById: config.defaultAssignedUserId || config.ownerUserId,
|
|
470
|
-
interactions: {
|
|
471
|
-
create: interactionsToCreate,
|
|
472
|
-
},
|
|
473
|
-
},
|
|
474
|
-
});
|
|
475
|
-
isNewContact = true;
|
|
476
|
-
imported++;
|
|
477
|
-
} else {
|
|
478
|
-
await client.contact.update({
|
|
479
|
-
where: { id: contact.id },
|
|
480
|
-
data: {
|
|
481
|
-
firstName: contact.firstName || firstName || null,
|
|
482
|
-
lastName: contact.lastName || lastName || null,
|
|
483
|
-
email: contact.email || (email ? email.toLowerCase() : null),
|
|
484
|
-
city: contact.city || city || null,
|
|
485
|
-
postalCode: contact.postalCode || postalCode || null,
|
|
486
|
-
origin: contact.origin || origin,
|
|
487
|
-
statusId: contact.statusId || effectiveDefaultStatusId,
|
|
488
|
-
// Ne pas écraser les assignations existantes
|
|
489
|
-
assignedCommercialId: contact.assignedCommercialId || assignedCommercialId,
|
|
490
|
-
assignedTeleproId: contact.assignedTeleproId || assignedTeleproId,
|
|
491
|
-
},
|
|
492
|
-
});
|
|
493
|
-
updated++;
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
// Créer une interaction de log uniquement pour les contacts mis à jour (pas les nouveaux qui ont déjà leur interaction)
|
|
498
|
-
if (contact && !isNewContact) {
|
|
499
|
-
// Contact mis à jour, créer l'interaction si nécessaire
|
|
500
|
-
const formattedContent = formatNoteContent(noteContents);
|
|
501
|
-
|
|
502
|
-
await client.interaction.create({
|
|
503
|
-
data: {
|
|
504
|
-
contactId: contact.id,
|
|
505
|
-
type: 'NOTE',
|
|
506
|
-
title: `Contact importé depuis Google Sheets: ${config.name}`,
|
|
507
|
-
content: formattedContent,
|
|
508
|
-
userId: config.defaultAssignedUserId || config.ownerUserId,
|
|
509
|
-
date: new Date(),
|
|
510
|
-
metadata: {
|
|
511
|
-
htmlContent: formattedContent,
|
|
512
|
-
isGoogleSheetsImport: true,
|
|
513
|
-
},
|
|
514
|
-
},
|
|
515
|
-
});
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
// Pas besoin de mettre à jour lastSyncedRow ici,
|
|
520
|
-
// car il a été réservé atomiquement au début du traitement (claimedMaxRow).
|
|
521
|
-
|
|
522
|
-
totalImported += imported;
|
|
523
|
-
totalUpdated += updated;
|
|
524
|
-
totalSkipped += skipped;
|
|
525
|
-
|
|
526
|
-
results.push({
|
|
527
|
-
configId: config.id,
|
|
528
|
-
configName: config.name,
|
|
529
|
-
imported,
|
|
530
|
-
updated,
|
|
531
|
-
skipped,
|
|
532
|
-
});
|
|
533
|
-
} catch (error: any) {
|
|
534
|
-
console.error(`Erreur lors de la synchronisation de ${config.name}:`, error);
|
|
535
|
-
results.push({
|
|
536
|
-
configId: config.id,
|
|
537
|
-
configName: config.name,
|
|
538
|
-
imported: 0,
|
|
539
|
-
updated: 0,
|
|
540
|
-
skipped: 0,
|
|
541
|
-
error: error.message || 'Erreur lors de la synchronisation',
|
|
542
|
-
});
|
|
543
|
-
}
|
|
14
|
+
let body: { configId?: string } = {};
|
|
15
|
+
try {
|
|
16
|
+
body = await request.json().catch(() => ({}));
|
|
17
|
+
} catch {
|
|
18
|
+
// body optionnel
|
|
544
19
|
}
|
|
20
|
+
const { configId: requestedConfigId } = body;
|
|
545
21
|
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
results,
|
|
22
|
+
const job = await enqueueGoogleSheetSyncJob({
|
|
23
|
+
requestedByUserId: session.user.id,
|
|
24
|
+
configId: requestedConfigId ?? null,
|
|
25
|
+
triggerType: 'MANUAL',
|
|
551
26
|
});
|
|
27
|
+
|
|
28
|
+
return NextResponse.json(
|
|
29
|
+
{
|
|
30
|
+
jobId: job.id,
|
|
31
|
+
status: job.status,
|
|
32
|
+
},
|
|
33
|
+
{ status: 202 },
|
|
34
|
+
);
|
|
552
35
|
} catch (error: any) {
|
|
36
|
+
if (error instanceof SyncJobRateLimitError) {
|
|
37
|
+
return NextResponse.json({ error: error.message }, { status: 429 });
|
|
38
|
+
}
|
|
553
39
|
console.error('Erreur lors de la synchronisation Google Sheets:', error);
|
|
554
40
|
return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 });
|
|
555
41
|
}
|
|
@@ -53,29 +53,26 @@ export async function POST(request: NextRequest) {
|
|
|
53
53
|
|
|
54
54
|
const newPassword = await hashPassword(password);
|
|
55
55
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
emailVerified: true,
|
|
77
|
-
},
|
|
78
|
-
});
|
|
56
|
+
await prisma.$transaction([
|
|
57
|
+
prisma.account.create({
|
|
58
|
+
data: {
|
|
59
|
+
id: crypto.randomUUID(),
|
|
60
|
+
accountId: user.id,
|
|
61
|
+
providerId: 'credential',
|
|
62
|
+
userId: user.id,
|
|
63
|
+
password: newPassword,
|
|
64
|
+
},
|
|
65
|
+
}),
|
|
66
|
+
prisma.verification.delete({
|
|
67
|
+
where: { id: verification.id },
|
|
68
|
+
}),
|
|
69
|
+
prisma.user.update({
|
|
70
|
+
where: { id: user.id },
|
|
71
|
+
data: {
|
|
72
|
+
emailVerified: true,
|
|
73
|
+
},
|
|
74
|
+
}),
|
|
75
|
+
]);
|
|
79
76
|
|
|
80
77
|
return NextResponse.json({
|
|
81
78
|
success: true,
|