create-crm-tmp 1.1.2 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/template/.prettierignore +2 -0
- package/template/README.md +53 -67
- package/template/components.json +22 -0
- package/template/exemple-contacts.csv +54 -0
- package/template/next.config.ts +27 -1
- package/template/package.json +64 -27
- package/template/prisma/schema.prisma +821 -72
- package/template/skills-lock.json +25 -0
- package/template/src/app/(auth)/invite/[token]/page.tsx +21 -24
- package/template/src/app/(auth)/reset-password/complete/page.tsx +12 -21
- package/template/src/app/(auth)/reset-password/page.tsx +12 -8
- package/template/src/app/(auth)/reset-password/verify/page.tsx +12 -8
- package/template/src/app/(auth)/signin/page.tsx +20 -17
- package/template/src/app/(dashboard)/agenda/page.tsx +2231 -2188
- package/template/src/app/(dashboard)/automatisation/[id]/page.tsx +10 -7
- package/template/src/app/(dashboard)/automatisation/_components/workflow-editor.tsx +680 -323
- package/template/src/app/(dashboard)/automatisation/new/page.tsx +11 -8
- package/template/src/app/(dashboard)/automatisation/page.tsx +473 -180
- package/template/src/app/(dashboard)/closing/page.tsx +500 -468
- package/template/src/app/(dashboard)/contacts/[id]/page.tsx +5035 -4126
- package/template/src/app/(dashboard)/contacts/companies/[id]/page.tsx +1703 -0
- package/template/src/app/(dashboard)/contacts/loading.tsx +13 -0
- package/template/src/app/(dashboard)/contacts/page.tsx +3776 -2064
- package/template/src/app/(dashboard)/dashboard/page.tsx +37 -519
- package/template/src/app/(dashboard)/error.tsx +37 -0
- package/template/src/app/(dashboard)/layout.tsx +1 -1
- 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 +2685 -2489
- package/template/src/app/(dashboard)/templates/page.tsx +500 -300
- package/template/src/app/(dashboard)/users/list/page.tsx +356 -350
- 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 +164 -137
- package/template/src/app/api/audit-logs/route.ts +1 -1
- 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/companies/[id]/activities/route.ts +131 -0
- package/template/src/app/api/companies/[id]/route.ts +195 -0
- package/template/src/app/api/companies/export/route.ts +206 -0
- package/template/src/app/api/companies/route.ts +166 -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 +77 -0
- package/template/src/app/api/contacts/[id]/files/[fileId]/route.ts +7 -17
- package/template/src/app/api/contacts/[id]/files/route.ts +83 -44
- package/template/src/app/api/contacts/[id]/interactions/route.ts +37 -0
- package/template/src/app/api/contacts/[id]/kyc/route.ts +71 -0
- package/template/src/app/api/contacts/[id]/meet/route.ts +38 -29
- package/template/src/app/api/contacts/[id]/route.ts +111 -20
- package/template/src/app/api/contacts/[id]/send-email/route.ts +6 -0
- package/template/src/app/api/contacts/[id]/workflows/run/route.ts +61 -0
- package/template/src/app/api/contacts/export/route.ts +12 -17
- package/template/src/app/api/contacts/import/route.ts +22 -19
- package/template/src/app/api/contacts/import-preview/route.ts +139 -0
- package/template/src/app/api/contacts/route.ts +202 -49
- package/template/src/app/api/dashboard/stats/route.ts +9 -292
- package/template/src/app/api/integrations/google-sheet/sync/route.ts +203 -185
- package/template/src/app/api/invite/complete/route.ts +20 -23
- package/template/src/app/api/reminders/route.ts +1 -0
- package/template/src/app/api/reset-password/complete/route.ts +11 -13
- package/template/src/app/api/send/route.ts +9 -85
- 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 +20 -23
- package/template/src/app/api/settings/google-sheet/[id]/route.ts +20 -23
- package/template/src/app/api/settings/google-sheet/auto-map/route.ts +23 -32
- package/template/src/app/api/settings/google-sheet/preview/route.ts +104 -0
- package/template/src/app/api/settings/google-sheet/route.ts +20 -23
- package/template/src/app/api/settings/meta-leads/[id]/route.ts +20 -23
- package/template/src/app/api/settings/meta-leads/route.ts +20 -23
- package/template/src/app/api/settings/statuses/[id]/route.ts +33 -23
- 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 +14 -7
- package/template/src/app/api/tasks/[id]/route.ts +161 -137
- package/template/src/app/api/tasks/meet/route.ts +11 -8
- package/template/src/app/api/tasks/route.ts +155 -95
- 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 +16 -1
- 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/route.ts +94 -55
- package/template/src/app/api/webhooks/google-ads/route.ts +20 -1
- package/template/src/app/api/webhooks/meta-leads/route.ts +18 -1
- package/template/src/app/api/workflows/[id]/route.ts +33 -6
- package/template/src/app/api/workflows/process/route.ts +509 -146
- package/template/src/app/api/workflows/route.ts +46 -4
- package/template/src/app/globals.css +210 -101
- package/template/src/app/layout.tsx +19 -8
- package/template/src/app/page.tsx +37 -7
- package/template/src/components/address-autocomplete.tsx +232 -0
- package/template/src/components/contacts/filter-bar.tsx +181 -0
- package/template/src/components/contacts/filter-builder.tsx +589 -0
- package/template/src/components/contacts/save-view-dialog.tsx +160 -0
- package/template/src/components/contacts/views-tab-bar.tsx +440 -0
- package/template/src/components/dashboard/activity-chart.tsx +31 -39
- package/template/src/components/dashboard/dashboard-content.tsx +79 -0
- package/template/src/components/dashboard/stat-card.tsx +40 -42
- package/template/src/components/dashboard/tasks-pie-chart.tsx +34 -37
- package/template/src/components/dashboard/upcoming-tasks-list.tsx +78 -72
- package/template/src/components/date-picker.tsx +396 -0
- package/template/src/components/editor.tsx +27 -13
- package/template/src/components/email-template.tsx +4 -2
- package/template/src/components/global-search.tsx +358 -0
- package/template/src/components/header.tsx +57 -62
- 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/sidebar.tsx +92 -94
- package/template/src/components/skeleton.tsx +128 -42
- 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 +60 -0
- package/template/src/components/view-as-banner.tsx +1 -1
- package/template/src/components/view-as-modal.tsx +21 -16
- package/template/src/config/nav-pages.ts +108 -0
- package/template/src/contexts/app-toast-context.tsx +174 -0
- package/template/src/contexts/sidebar-context.tsx +16 -47
- package/template/src/contexts/task-reminder-context.tsx +6 -6
- package/template/src/contexts/view-as-context.tsx +11 -16
- 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/lib/address-api.ts +155 -0
- package/template/src/lib/cache.ts +73 -0
- package/template/src/lib/check-permission.ts +12 -177
- package/template/src/lib/contact-interactions.ts +3 -1
- package/template/src/lib/contact-view-filters.ts +341 -0
- package/template/src/lib/dashboard-stats.ts +224 -0
- package/template/src/lib/date-utils.ts +49 -0
- package/template/src/lib/get-auth-user.ts +25 -0
- package/template/src/lib/google-calendar.ts +54 -12
- package/template/src/lib/google-drive.ts +796 -75
- package/template/src/lib/google-fetch.ts +63 -0
- package/template/src/lib/local-storage.ts +34 -0
- package/template/src/lib/permissions.ts +245 -47
- package/template/src/lib/prisma.ts +11 -11
- package/template/src/lib/roles.ts +14 -39
- package/template/src/lib/template-variables.ts +67 -33
- package/template/src/lib/utils.ts +26 -2
- package/template/src/lib/workflow-executor.ts +445 -229
- package/template/src/proxy.ts +34 -73
- package/template/src/types/contact-views.ts +351 -0
- package/template/src/types/yousign.ts +52 -0
- package/template/vercel.json +12 -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/migration_lock.toml +0 -3
- package/template/src/app/(dashboard)/users/layout.tsx +0 -30
- package/template/src/app/api/dashboard/widgets/[id]/route.ts +0 -47
- package/template/src/app/api/dashboard/widgets/route.ts +0 -181
- package/template/src/components/dashboard/add-widget-dialog.tsx +0 -161
- package/template/src/components/dashboard/color-picker.tsx +0 -65
- package/template/src/components/dashboard/contacts-chart.tsx +0 -69
- package/template/src/components/dashboard/interactions-by-type-chart.tsx +0 -121
- package/template/src/components/dashboard/recent-activity.tsx +0 -157
- package/template/src/components/dashboard/sales-analytics-chart.tsx +0 -77
- package/template/src/components/dashboard/status-distribution-chart.tsx +0 -82
- package/template/src/components/dashboard/top-contacts-list.tsx +0 -119
- package/template/src/components/dashboard/widget-wrapper.tsx +0 -39
- package/template/src/contexts/dashboard-theme-context.tsx +0 -58
- package/template/src/lib/dashboard-themes.ts +0 -140
- package/template/src/lib/default-widgets.ts +0 -14
- package/template/src/lib/widget-registry.ts +0 -177
|
@@ -2,11 +2,18 @@ import { NextRequest, NextResponse } from 'next/server';
|
|
|
2
2
|
import { prisma } from '@/lib/prisma';
|
|
3
3
|
import { getValidAccessToken, GoogleTokenError } from '@/lib/google-calendar';
|
|
4
4
|
import { handleContactDuplicate } from '@/lib/contact-duplicate';
|
|
5
|
+
import { googleFetch } from '@/lib/google-fetch';
|
|
6
|
+
import { encrypt, decrypt } from '@/lib/encryption';
|
|
7
|
+
import { auth } from '@/lib/auth';
|
|
5
8
|
|
|
6
9
|
// POST /api/integrations/google-sheet/sync - Synchroniser toutes les configurations actives
|
|
7
10
|
export async function POST(request: NextRequest) {
|
|
8
11
|
try {
|
|
9
12
|
const client = prisma as any;
|
|
13
|
+
const session = await auth.api.getSession({ headers: request.headers });
|
|
14
|
+
if (!session) {
|
|
15
|
+
return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
|
|
16
|
+
}
|
|
10
17
|
|
|
11
18
|
// Récupérer toutes les configurations actives
|
|
12
19
|
const configs = await client.googleSheetSyncConfig.findMany({
|
|
@@ -41,13 +48,14 @@ export async function POST(request: NextRequest) {
|
|
|
41
48
|
|
|
42
49
|
// Synchroniser chaque configuration
|
|
43
50
|
for (const config of configs) {
|
|
51
|
+
const lockKey = `google_sheet_sync:${config.id}`;
|
|
52
|
+
let lockAcquired = false;
|
|
44
53
|
try {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
if (!googleAccount) {
|
|
54
|
+
const lockResult = await client.$queryRaw<
|
|
55
|
+
Array<{ locked: boolean }>
|
|
56
|
+
>`SELECT pg_try_advisory_lock(hashtext(${lockKey}), 4261) AS locked`;
|
|
57
|
+
lockAcquired = lockResult?.[0]?.locked === true;
|
|
58
|
+
if (!lockAcquired) {
|
|
51
59
|
results.push({
|
|
52
60
|
configId: config.id,
|
|
53
61
|
configName: config.name,
|
|
@@ -55,73 +63,100 @@ export async function POST(request: NextRequest) {
|
|
|
55
63
|
updated: 0,
|
|
56
64
|
skipped: 0,
|
|
57
65
|
error:
|
|
58
|
-
'
|
|
66
|
+
'Une synchronisation est déjà en cours pour cette configuration. Veuillez patienter quelques secondes puis relancer.',
|
|
59
67
|
});
|
|
60
68
|
continue;
|
|
61
69
|
}
|
|
62
70
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
)
|
|
71
|
+
const range = encodeURIComponent(config.sheetName);
|
|
72
|
+
const candidateUserIds = Array.from(
|
|
73
|
+
new Set(
|
|
74
|
+
[config.ownerUserId, config.defaultAssignedUserId, session.user.id].filter(
|
|
75
|
+
(value): value is string => Boolean(value),
|
|
76
|
+
),
|
|
77
|
+
),
|
|
78
|
+
);
|
|
70
79
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
});
|
|
80
|
+
let selectedSheetsResponse: Response | null = null;
|
|
81
|
+
let lastAuthError =
|
|
82
|
+
'Aucun compte Google compatible trouvé pour cette configuration. Reconnectez Google puis partagez le Sheet avec ce compte.';
|
|
83
|
+
|
|
84
|
+
for (const candidateUserId of candidateUserIds) {
|
|
85
|
+
const googleAccount = await client.userGoogleAccount.findUnique({
|
|
86
|
+
where: { userId: candidateUserId },
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
if (!googleAccount) {
|
|
90
|
+
lastAuthError = 'Aucun compte Google connecté parmi les utilisateurs autorisés.';
|
|
94
91
|
continue;
|
|
95
92
|
}
|
|
96
|
-
// Relancer l'erreur si ce n'est pas une erreur de token
|
|
97
|
-
throw error;
|
|
98
|
-
}
|
|
99
93
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
94
|
+
try {
|
|
95
|
+
const decryptedAccess = decrypt(googleAccount.accessToken);
|
|
96
|
+
const decryptedRefresh = decrypt(googleAccount.refreshToken);
|
|
97
|
+
const accessToken = await getValidAccessToken(
|
|
98
|
+
decryptedAccess,
|
|
99
|
+
decryptedRefresh,
|
|
100
|
+
googleAccount.tokenExpiresAt,
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
if (accessToken !== decryptedAccess) {
|
|
104
|
+
const tokenExpiresAt = new Date();
|
|
105
|
+
tokenExpiresAt.setSeconds(tokenExpiresAt.getSeconds() + 3600);
|
|
106
|
+
await client.userGoogleAccount.update({
|
|
107
|
+
where: { userId: candidateUserId },
|
|
108
|
+
data: {
|
|
109
|
+
accessToken: encrypt(accessToken),
|
|
110
|
+
tokenExpiresAt,
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const candidateResponse = await googleFetch(
|
|
116
|
+
`https://sheets.googleapis.com/v4/spreadsheets/${config.spreadsheetId}/values/${range}`,
|
|
117
|
+
{
|
|
118
|
+
headers: {
|
|
119
|
+
Authorization: `Bearer ${accessToken}`,
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
if (candidateResponse.ok) {
|
|
125
|
+
selectedSheetsResponse = candidateResponse;
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
109
128
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
129
|
+
const errorText = await candidateResponse.text();
|
|
130
|
+
lastAuthError =
|
|
131
|
+
candidateResponse.status === 401 || candidateResponse.status === 403
|
|
132
|
+
? 'Le compte Google connecté n’a pas accès à ce Sheet. Partagez le document avec ce compte ou reconnectez Google.'
|
|
133
|
+
: 'Impossible de lire les données depuis Google Sheets.';
|
|
134
|
+
console.error(
|
|
135
|
+
`Erreur lors de la lecture du Google Sheet ${config.name} (user ${candidateUserId}):`,
|
|
136
|
+
errorText,
|
|
137
|
+
);
|
|
138
|
+
} catch (error: any) {
|
|
139
|
+
if (error instanceof GoogleTokenError && error.isRevoked) {
|
|
140
|
+
lastAuthError = error.message;
|
|
141
|
+
} else {
|
|
142
|
+
lastAuthError = error?.message || 'Erreur lors de la validation du compte Google.';
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (!selectedSheetsResponse) {
|
|
113
148
|
results.push({
|
|
114
149
|
configId: config.id,
|
|
115
150
|
configName: config.name,
|
|
116
151
|
imported: 0,
|
|
117
152
|
updated: 0,
|
|
118
153
|
skipped: 0,
|
|
119
|
-
error:
|
|
154
|
+
error: lastAuthError,
|
|
120
155
|
});
|
|
121
156
|
continue;
|
|
122
157
|
}
|
|
123
158
|
|
|
124
|
-
const data = await
|
|
159
|
+
const data = await selectedSheetsResponse.json();
|
|
125
160
|
const values: string[][] = data.values || [];
|
|
126
161
|
|
|
127
162
|
if (!values.length) {
|
|
@@ -141,47 +176,6 @@ export async function POST(request: NextRequest) {
|
|
|
141
176
|
(config.lastSyncedRow || headerRowIndex) + 1,
|
|
142
177
|
);
|
|
143
178
|
|
|
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
179
|
// Récupérer les headers
|
|
186
180
|
const headerRow = values[headerRowIndex] || [];
|
|
187
181
|
|
|
@@ -267,9 +261,92 @@ export async function POST(request: NextRequest) {
|
|
|
267
261
|
}
|
|
268
262
|
}
|
|
269
263
|
|
|
264
|
+
// Pré-charger le rôle de l'utilisateur par défaut (constant pour toute la config)
|
|
265
|
+
let assignedCommercialId: string | null = null;
|
|
266
|
+
let assignedTeleproId: string | null = null;
|
|
267
|
+
|
|
268
|
+
if (config.defaultAssignedUserId) {
|
|
269
|
+
const defaultUser = await client.user.findUnique({
|
|
270
|
+
where: { id: config.defaultAssignedUserId },
|
|
271
|
+
select: { role: true },
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
if (defaultUser) {
|
|
275
|
+
if (
|
|
276
|
+
defaultUser.role === 'COMMERCIAL' ||
|
|
277
|
+
defaultUser.role === 'ADMIN' ||
|
|
278
|
+
defaultUser.role === 'MANAGER'
|
|
279
|
+
) {
|
|
280
|
+
assignedCommercialId = config.defaultAssignedUserId;
|
|
281
|
+
} else if (defaultUser.role === 'TELEPRO') {
|
|
282
|
+
assignedTeleproId = config.defaultAssignedUserId;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const escapeHtml = (text: string): string => {
|
|
288
|
+
const map: { [key: string]: string } = {
|
|
289
|
+
'&': '&',
|
|
290
|
+
'<': '<',
|
|
291
|
+
'>': '>',
|
|
292
|
+
'"': '"',
|
|
293
|
+
"'": ''',
|
|
294
|
+
};
|
|
295
|
+
return text.replace(/[&<>"']/g, (m) => map[m]);
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
const formatNoteContent = (noteItems: Array<{ label: string; value: string }>) => {
|
|
299
|
+
const escapedConfigName = escapeHtml(config.name);
|
|
300
|
+
|
|
301
|
+
if (noteItems.length === 0) {
|
|
302
|
+
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>`;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
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>`;
|
|
306
|
+
|
|
307
|
+
html +=
|
|
308
|
+
'<div style="display: flex; flex-direction: column; gap: 12px; margin-top: 16px;">';
|
|
309
|
+
|
|
310
|
+
noteItems.forEach((item) => {
|
|
311
|
+
let formattedValue = item.value;
|
|
312
|
+
try {
|
|
313
|
+
const parsed = JSON.parse(item.value);
|
|
314
|
+
if (Array.isArray(parsed)) {
|
|
315
|
+
formattedValue = parsed.map((v) => String(v)).join(', ');
|
|
316
|
+
}
|
|
317
|
+
} catch {
|
|
318
|
+
// Ce n'est pas du JSON, on garde la valeur telle quelle
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const humanLabel = item.label
|
|
322
|
+
.split(/(?=[A-Z])/)
|
|
323
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
|
324
|
+
.join(' ');
|
|
325
|
+
|
|
326
|
+
const escapedLabel = escapeHtml(humanLabel);
|
|
327
|
+
const escapedValue = escapeHtml(String(formattedValue)).replace(/\n/g, '<br>');
|
|
328
|
+
|
|
329
|
+
html += `
|
|
330
|
+
<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;">
|
|
331
|
+
<div style="font-size: 11px; font-weight: 700; color: #64748b; text-transform: uppercase; letter-spacing: 0.8px; margin-bottom: 6px;">
|
|
332
|
+
${escapedLabel}
|
|
333
|
+
</div>
|
|
334
|
+
<div style="font-size: 14px; line-height: 1.5; color: #0f172a; font-weight: 500;">
|
|
335
|
+
${escapedValue}
|
|
336
|
+
</div>
|
|
337
|
+
</div>
|
|
338
|
+
`;
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
html += '</div>';
|
|
342
|
+
return html;
|
|
343
|
+
};
|
|
344
|
+
|
|
270
345
|
let imported = 0;
|
|
271
346
|
let updated = 0;
|
|
272
347
|
let skipped = 0;
|
|
348
|
+
let maxProcessedRow = config.lastSyncedRow || headerRowIndex;
|
|
349
|
+
const seenRowKeys = new Set<string>();
|
|
273
350
|
|
|
274
351
|
for (let rowIndex = startRowIndex; rowIndex < values.length; rowIndex++) {
|
|
275
352
|
const row = values[rowIndex];
|
|
@@ -305,8 +382,22 @@ export async function POST(request: NextRequest) {
|
|
|
305
382
|
columnMappings['origin'] !== undefined
|
|
306
383
|
? row[columnMappings['origin']]?.trim() || 'Google Sheets'
|
|
307
384
|
: 'Google Sheets';
|
|
385
|
+
const normalizedEmail = email ? email.trim().toLowerCase() : '';
|
|
386
|
+
const normalizedPhone = phone.replace(/\D/g, '');
|
|
387
|
+
const normalizedFirstName = firstName ? firstName.trim().toLowerCase() : '';
|
|
388
|
+
const normalizedLastName = lastName ? lastName.trim().toLowerCase() : '';
|
|
389
|
+
const rowKey = normalizedEmail
|
|
390
|
+
? `email:${normalizedEmail}`
|
|
391
|
+
: normalizedPhone
|
|
392
|
+
? `phone:${normalizedPhone}`
|
|
393
|
+
: `name:${normalizedFirstName}|${normalizedLastName}`;
|
|
394
|
+
|
|
395
|
+
if (seenRowKeys.has(rowKey)) {
|
|
396
|
+
skipped++;
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
seenRowKeys.add(rowKey);
|
|
308
400
|
|
|
309
|
-
// Collecter les notes si des colonnes sont configurées comme "note"
|
|
310
401
|
const noteContents: Array<{ label: string; value: string }> = [];
|
|
311
402
|
if (noteFields.length > 0) {
|
|
312
403
|
noteFields.forEach(({ name, index }) => {
|
|
@@ -319,93 +410,6 @@ export async function POST(request: NextRequest) {
|
|
|
319
410
|
});
|
|
320
411
|
}
|
|
321
412
|
|
|
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
413
|
// Vérifier si c'est un doublon (nom, prénom ET email)
|
|
410
414
|
const duplicateContactId = await handleContactDuplicate(
|
|
411
415
|
firstName,
|
|
@@ -514,10 +518,20 @@ export async function POST(request: NextRequest) {
|
|
|
514
518
|
},
|
|
515
519
|
});
|
|
516
520
|
}
|
|
521
|
+
|
|
522
|
+
if (rowIndex > maxProcessedRow) {
|
|
523
|
+
maxProcessedRow = rowIndex;
|
|
524
|
+
}
|
|
517
525
|
}
|
|
518
526
|
|
|
519
|
-
|
|
520
|
-
|
|
527
|
+
if (maxProcessedRow > (config.lastSyncedRow || headerRowIndex)) {
|
|
528
|
+
await client.googleSheetSyncConfig.update({
|
|
529
|
+
where: { id: config.id },
|
|
530
|
+
data: {
|
|
531
|
+
lastSyncedRow: maxProcessedRow,
|
|
532
|
+
},
|
|
533
|
+
});
|
|
534
|
+
}
|
|
521
535
|
|
|
522
536
|
totalImported += imported;
|
|
523
537
|
totalUpdated += updated;
|
|
@@ -540,6 +554,10 @@ export async function POST(request: NextRequest) {
|
|
|
540
554
|
skipped: 0,
|
|
541
555
|
error: error.message || 'Erreur lors de la synchronisation',
|
|
542
556
|
});
|
|
557
|
+
} finally {
|
|
558
|
+
if (lockAcquired) {
|
|
559
|
+
await client.$queryRaw`SELECT pg_advisory_unlock(hashtext(${lockKey}), 4261)`;
|
|
560
|
+
}
|
|
543
561
|
}
|
|
544
562
|
}
|
|
545
563
|
|
|
@@ -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,
|
|
@@ -46,21 +46,19 @@ export async function POST(request: NextRequest) {
|
|
|
46
46
|
return NextResponse.json({ error: 'Utilisateur non trouvé' }, { status: 404 });
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
// Hasher le nouveau mot de passe
|
|
50
49
|
const hashedPassword = await hashPassword(password);
|
|
51
50
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
});
|
|
51
|
+
await prisma.$transaction([
|
|
52
|
+
prisma.account.update({
|
|
53
|
+
where: { id: user.accounts[0].id },
|
|
54
|
+
data: {
|
|
55
|
+
password: hashedPassword,
|
|
56
|
+
},
|
|
57
|
+
}),
|
|
58
|
+
prisma.verification.delete({
|
|
59
|
+
where: { id: verification.id },
|
|
60
|
+
}),
|
|
61
|
+
]);
|
|
64
62
|
|
|
65
63
|
return NextResponse.json({
|
|
66
64
|
success: true,
|