create-crm-tmp 2.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/create-crm-tmp.js +56 -35
- package/package.json +1 -1
- package/template/README.md +230 -115
- package/template/eslint.config.mjs +13 -0
- package/template/next.config.ts +14 -0
- package/template/package.json +15 -2
- package/template/prisma/migrations/20260318095700_init_db/migration.sql +978 -0
- package/template/prisma/migrations/migration_lock.toml +3 -0
- package/template/prisma/schema.prisma +132 -637
- package/template/src/app/(auth)/invite/[token]/page.tsx +10 -8
- package/template/src/app/(auth)/layout.tsx +1 -1
- package/template/src/app/(auth)/reset-password/complete/page.tsx +11 -8
- package/template/src/app/(auth)/reset-password/page.tsx +4 -4
- package/template/src/app/(auth)/reset-password/verify/page.tsx +4 -4
- package/template/src/app/(auth)/signin/page.tsx +14 -6
- package/template/src/app/(dashboard)/agenda/page.tsx +2243 -988
- package/template/src/app/(dashboard)/automatisation/_components/workflow-editor.tsx +18 -104
- package/template/src/app/(dashboard)/automatisation/page.tsx +10 -26
- package/template/src/app/(dashboard)/closing/page.tsx +78 -62
- package/template/src/app/(dashboard)/contacts/[id]/page.tsx +2082 -1080
- package/template/src/app/(dashboard)/contacts/companies/[id]/page.tsx +46 -47
- package/template/src/app/(dashboard)/contacts/page.tsx +1062 -780
- package/template/src/app/(dashboard)/dashboard/page.tsx +533 -37
- package/template/src/app/(dashboard)/dev/page.tsx +1291 -0
- package/template/src/app/(dashboard)/layout.tsx +6 -2
- package/template/src/app/(dashboard)/settings/page.tsx +797 -2582
- package/template/src/app/(dashboard)/templates/page.tsx +55 -54
- package/template/src/app/(dashboard)/users/list/page.tsx +51 -48
- package/template/src/app/(dashboard)/users/page.tsx +1 -1
- package/template/src/app/(dashboard)/users/permissions/page.tsx +2 -2
- package/template/src/app/(dashboard)/users/roles/page.tsx +7 -5
- package/template/src/app/api/agenda/google-events/route.ts +92 -0
- package/template/src/app/api/auth/check-active/route.ts +3 -2
- package/template/src/app/api/auth/google/route.ts +2 -1
- package/template/src/app/api/auth/google/status/route.ts +7 -31
- package/template/src/app/api/companies/[id]/activities/route.ts +1 -3
- package/template/src/app/api/companies/[id]/route.ts +1 -2
- package/template/src/app/api/companies/route.ts +42 -12
- package/template/src/app/api/contacts/[id]/files/[fileId]/preview/route.ts +9 -31
- package/template/src/app/api/contacts/[id]/files/[fileId]/route.ts +14 -32
- package/template/src/app/api/contacts/[id]/files/route.ts +112 -212
- package/template/src/app/api/contacts/[id]/interactions/[interactionId]/route.ts +27 -1
- package/template/src/app/api/contacts/[id]/interactions/route.ts +16 -16
- package/template/src/app/api/contacts/[id]/kyc/route.ts +21 -11
- package/template/src/app/api/contacts/[id]/meet/route.ts +19 -2
- package/template/src/app/api/contacts/[id]/route.ts +106 -34
- package/template/src/app/api/contacts/[id]/send-email/route.ts +27 -11
- package/template/src/app/api/contacts/[id]/workflows/run/route.ts +6 -0
- package/template/src/app/api/contacts/export/route.ts +9 -13
- package/template/src/app/api/contacts/import/route.ts +55 -25
- package/template/src/app/api/contacts/import-preview/route.ts +1 -1
- package/template/src/app/api/contacts/origins/route.ts +63 -0
- package/template/src/app/api/contacts/route.ts +153 -41
- package/template/src/app/api/cron/cleanup-editor-images/route.ts +166 -0
- package/template/src/app/api/dashboard/widgets/[id]/route.ts +44 -0
- package/template/src/app/api/dashboard/widgets/route.ts +181 -0
- package/template/src/app/api/dev/reminders/test/route.ts +114 -0
- package/template/src/app/api/editor/upload-image/route.ts +61 -0
- package/template/src/app/api/integrations/google-sheet/jobs/[jobId]/route.ts +47 -0
- package/template/src/app/api/integrations/google-sheet/jobs/usage/route.ts +50 -0
- package/template/src/app/api/integrations/google-sheet/sync/route.ts +24 -556
- package/template/src/app/api/jobs/google-sheet/process/route.ts +84 -0
- package/template/src/app/api/jobs/google-sheet/schedule/route.ts +50 -0
- package/template/src/app/api/reminders/clear/route.ts +120 -0
- package/template/src/app/api/reminders/clear/undo/route.ts +112 -0
- package/template/src/app/api/reminders/route.ts +164 -39
- package/template/src/app/api/reminders/state/route.ts +164 -0
- package/template/src/app/api/reset-password/request/route.ts +1 -1
- package/template/src/app/api/reset-password/verify/route.ts +1 -1
- package/template/src/app/api/send/route.ts +16 -4
- package/template/src/app/api/settings/google-ads/route.ts +14 -0
- package/template/src/app/api/settings/google-calendar/calendars/route.ts +97 -0
- package/template/src/app/api/settings/google-calendar/route.ts +124 -0
- package/template/src/app/api/settings/google-sheet/[id]/route.ts +28 -0
- package/template/src/app/api/settings/google-sheet/auto-map/route.ts +37 -4
- package/template/src/app/api/settings/google-sheet/preview/route.ts +9 -3
- package/template/src/app/api/settings/google-sheet/route.ts +14 -0
- package/template/src/app/api/settings/integrations/logs/route.ts +93 -0
- package/template/src/app/api/settings/integrations/notifications/route.ts +67 -0
- package/template/src/app/api/settings/meta-leads/[id]/route.ts +0 -1
- package/template/src/app/api/settings/meta-leads/route.ts +14 -2
- package/template/src/app/api/settings/smtp/route.ts +53 -6
- package/template/src/app/api/tasks/[id]/attendees/route.ts +24 -8
- package/template/src/app/api/tasks/[id]/route.ts +234 -58
- package/template/src/app/api/tasks/meet/route.ts +27 -19
- package/template/src/app/api/tasks/route.ts +62 -17
- package/template/src/app/api/users/[id]/route.ts +20 -14
- package/template/src/app/api/users/list/route.ts +57 -19
- package/template/src/app/api/webhooks/google-ads/route.ts +34 -14
- package/template/src/app/api/webhooks/meta-leads/route.ts +32 -12
- package/template/src/app/api/workflows/[id]/route.ts +0 -4
- package/template/src/app/api/workflows/process/route.ts +22 -51
- package/template/src/app/api/workflows/route.ts +0 -4
- package/template/src/app/globals.css +342 -4
- package/template/src/app/layout.tsx +11 -3
- package/template/src/app/page.tsx +1 -1
- package/template/src/components/address-autocomplete.tsx +7 -6
- package/template/src/components/config-error-alert.tsx +46 -0
- package/template/src/components/contacts/filter-bar.tsx +12 -3
- package/template/src/components/contacts/filter-builder.tsx +28 -43
- package/template/src/components/contacts/save-view-dialog.tsx +1 -1
- package/template/src/components/contacts/views-tab-bar.tsx +15 -6
- package/template/src/components/dashboard/activity-chart.tsx +41 -28
- package/template/src/components/dashboard/add-widget-dialog.tsx +157 -0
- package/template/src/components/dashboard/color-picker.tsx +64 -0
- package/template/src/components/dashboard/contacts-chart.tsx +69 -0
- package/template/src/components/dashboard/interactions-by-type-chart.tsx +121 -0
- package/template/src/components/dashboard/recent-activity.tsx +154 -0
- package/template/src/components/dashboard/stat-card.tsx +40 -40
- package/template/src/components/dashboard/status-distribution-chart.tsx +81 -0
- package/template/src/components/dashboard/tasks-pie-chart.tsx +37 -34
- package/template/src/components/dashboard/top-contacts-list.tsx +113 -0
- package/template/src/components/dashboard/upcoming-tasks-list.tsx +72 -81
- package/template/src/components/dashboard/widget-wrapper.tsx +36 -0
- package/template/src/components/date-picker.tsx +9 -6
- package/template/src/components/editor/upload-editor-image.ts +42 -0
- package/template/src/components/editor.tsx +161 -22
- package/template/src/components/email-template.tsx +2 -2
- package/template/src/components/global-search.tsx +30 -28
- package/template/src/components/header.tsx +178 -80
- package/template/src/components/inactive-account-guard.tsx +58 -0
- package/template/src/components/integration-notifications-listener.tsx +12 -0
- package/template/src/components/invitation-email-template.tsx +2 -2
- package/template/src/components/meet-cancellation-email-template.tsx +3 -3
- package/template/src/components/meet-confirmation-email-template.tsx +3 -3
- package/template/src/components/meet-update-email-template.tsx +3 -3
- package/template/src/components/page-header.tsx +5 -5
- package/template/src/components/protected-page.tsx +1 -1
- package/template/src/components/reset-password-email-template.tsx +2 -2
- package/template/src/components/settings/integrations/GoogleAdsIntegration.tsx +428 -0
- package/template/src/components/settings/integrations/GoogleSheetConfigMonitoringModal.tsx +680 -0
- package/template/src/components/settings/integrations/GoogleSheetIntegration.tsx +809 -0
- package/template/src/components/settings/integrations/ImportResultDialog.tsx +124 -0
- package/template/src/components/settings/integrations/IntegrationLogPanel.tsx +57 -0
- package/template/src/components/settings/integrations/IntegrationLogsTable.tsx +186 -0
- package/template/src/components/settings/integrations/MetaLeadIntegration.tsx +451 -0
- package/template/src/components/sidebar.tsx +45 -26
- package/template/src/components/skeleton.tsx +40 -43
- package/template/src/components/ui/accordion.tsx +2 -2
- package/template/src/components/ui/alert-dialog.tsx +1 -1
- package/template/src/components/ui/button.tsx +20 -9
- package/template/src/components/ui/components.tsx +1 -1
- package/template/src/components/ui/date-picker.tsx +422 -0
- package/template/src/components/ui/datetime-picker.tsx +338 -0
- package/template/src/components/ui/status-select.tsx +271 -0
- package/template/src/components/ui/tooltip.tsx +37 -0
- package/template/src/components/view-as-modal.tsx +13 -7
- package/template/src/contexts/app-toast-context.tsx +245 -57
- package/template/src/contexts/dashboard-theme-context.tsx +53 -0
- package/template/src/contexts/sidebar-context.tsx +22 -17
- package/template/src/contexts/task-reminder-context.tsx +134 -160
- package/template/src/contexts/view-as-context.tsx +33 -6
- package/template/src/hooks/use-focus-trap.ts +2 -2
- package/template/src/hooks/useIntegrationNotifications.ts +49 -0
- package/template/src/lib/auth.ts +8 -1
- package/template/src/lib/config-links.ts +14 -0
- package/template/src/lib/contact-duplicate.ts +79 -61
- package/template/src/lib/contact-interactions.ts +21 -21
- package/template/src/lib/contact-view-filters.ts +24 -64
- package/template/src/lib/contacts-list-url.ts +190 -0
- package/template/src/lib/dashboard-stats.ts +65 -7
- package/template/src/lib/dashboard-themes.ts +135 -0
- package/template/src/lib/date-utils.ts +127 -0
- package/template/src/lib/default-widgets.ts +12 -0
- package/template/src/lib/editor-html-image-dimensions.ts +172 -0
- package/template/src/lib/editor-image-limits.ts +19 -0
- package/template/src/lib/email-html-sanitize.ts +19 -0
- package/template/src/lib/encryption.ts +9 -6
- package/template/src/lib/fr-geography.ts +192 -0
- package/template/src/lib/google-calendar-agenda.ts +201 -0
- package/template/src/lib/google-calendar.ts +255 -5
- package/template/src/lib/google-sheet-sync-jobs.ts +96 -0
- package/template/src/lib/google-sheet-sync-runner.ts +514 -0
- package/template/src/lib/integration-import-log.ts +21 -0
- package/template/src/lib/permissions.ts +40 -10
- package/template/src/lib/prisma.ts +4 -1
- package/template/src/lib/qstash.ts +65 -0
- package/template/src/lib/reminder-state-server.ts +80 -0
- package/template/src/lib/reminder-state.ts +29 -0
- package/template/src/lib/supabase-storage.ts +113 -0
- package/template/src/lib/template-variables.ts +164 -23
- package/template/src/lib/utils.ts +45 -0
- package/template/src/lib/widget-registry.ts +173 -0
- package/template/src/lib/workflow-executor.ts +16 -70
- package/template/src/proxy.ts +1 -0
- package/template/vercel.json +3 -10
- package/template/skills-lock.json +0 -25
- package/template/src/components/dashboard/dashboard-content.tsx +0 -79
- package/template/src/lib/google-drive.ts +0 -1101
- package/template/src/types/yousign.ts +0 -52
|
@@ -3,6 +3,7 @@ import { z } from 'zod';
|
|
|
3
3
|
import { auth } from '@/lib/auth';
|
|
4
4
|
import { prisma } from '@/lib/prisma';
|
|
5
5
|
import { checkPermission } from '@/lib/check-permission';
|
|
6
|
+
import { normalizePhoneNumber } from '@/lib/utils';
|
|
6
7
|
|
|
7
8
|
const createCompanySchema = z.object({
|
|
8
9
|
name: z.string().trim().min(1, 'Le nom est obligatoire'),
|
|
@@ -58,13 +59,35 @@ export async function GET(request: NextRequest) {
|
|
|
58
59
|
}
|
|
59
60
|
|
|
60
61
|
if (search) {
|
|
62
|
+
const trimmedSearch = search.trim();
|
|
63
|
+
const searchTerms = trimmedSearch.split(/\s+/).filter(Boolean);
|
|
64
|
+
const firstTerm = searchTerms[0] || '';
|
|
65
|
+
const remainingTerms = searchTerms.slice(1).join(' ');
|
|
66
|
+
|
|
67
|
+
const digitsOnly = trimmedSearch.replace(/\D/g, '');
|
|
68
|
+
const looksLikePhone = digitsOnly.length >= 4;
|
|
69
|
+
const normalizedPhone = looksLikePhone ? normalizePhoneNumber(trimmedSearch) : null;
|
|
70
|
+
|
|
61
71
|
where.AND = where.AND || [];
|
|
62
72
|
where.AND.push({
|
|
63
73
|
OR: [
|
|
64
|
-
{ name: { contains:
|
|
65
|
-
{ email: { contains:
|
|
66
|
-
{ phone: { contains:
|
|
67
|
-
{ siret: { contains:
|
|
74
|
+
{ name: { contains: trimmedSearch, mode: 'insensitive' } },
|
|
75
|
+
{ email: { contains: trimmedSearch, mode: 'insensitive' } },
|
|
76
|
+
{ phone: { contains: trimmedSearch, mode: 'insensitive' } },
|
|
77
|
+
{ siret: { contains: trimmedSearch, mode: 'insensitive' } },
|
|
78
|
+
...(normalizedPhone
|
|
79
|
+
? [{ phone: { contains: normalizedPhone, mode: 'insensitive' as const } }]
|
|
80
|
+
: []),
|
|
81
|
+
...(remainingTerms
|
|
82
|
+
? [
|
|
83
|
+
{
|
|
84
|
+
AND: [
|
|
85
|
+
{ name: { contains: firstTerm, mode: 'insensitive' } },
|
|
86
|
+
{ name: { contains: remainingTerms, mode: 'insensitive' } },
|
|
87
|
+
],
|
|
88
|
+
},
|
|
89
|
+
]
|
|
90
|
+
: []),
|
|
68
91
|
],
|
|
69
92
|
});
|
|
70
93
|
}
|
|
@@ -94,15 +117,22 @@ export async function GET(request: NextRequest) {
|
|
|
94
117
|
prisma.company.count({ where }),
|
|
95
118
|
]);
|
|
96
119
|
|
|
97
|
-
return NextResponse.json(
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
120
|
+
return NextResponse.json(
|
|
121
|
+
{
|
|
122
|
+
companies,
|
|
123
|
+
pagination: {
|
|
124
|
+
total,
|
|
125
|
+
page,
|
|
126
|
+
limit,
|
|
127
|
+
totalPages: Math.ceil(total / limit),
|
|
128
|
+
},
|
|
104
129
|
},
|
|
105
|
-
|
|
130
|
+
{
|
|
131
|
+
headers: {
|
|
132
|
+
'Cache-Control': 'private, no-store, max-age=0, must-revalidate',
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
);
|
|
106
136
|
} catch (error) {
|
|
107
137
|
console.error('Erreur lors de la récupération des entreprises:', error);
|
|
108
138
|
return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 });
|
|
@@ -2,18 +2,15 @@ import { NextRequest, NextResponse } from 'next/server';
|
|
|
2
2
|
import { auth } from '@/lib/auth';
|
|
3
3
|
import { prisma } from '@/lib/prisma';
|
|
4
4
|
import { checkPermission } from '@/lib/check-permission';
|
|
5
|
-
import {
|
|
5
|
+
import { BUCKETS, createSignedDownloadUrl } from '@/lib/supabase-storage';
|
|
6
6
|
|
|
7
|
-
// GET /api/contacts/[id]/files/[fileId]/preview
|
|
7
|
+
// GET /api/contacts/[id]/files/[fileId]/preview — redirect to signed URL
|
|
8
8
|
export async function GET(
|
|
9
9
|
request: NextRequest,
|
|
10
10
|
{ params }: { params: Promise<{ id: string; fileId: string }> },
|
|
11
11
|
) {
|
|
12
12
|
try {
|
|
13
|
-
const session = await auth.api.getSession({
|
|
14
|
-
headers: request.headers,
|
|
15
|
-
});
|
|
16
|
-
|
|
13
|
+
const session = await auth.api.getSession({ headers: request.headers });
|
|
17
14
|
if (!session) {
|
|
18
15
|
return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
|
|
19
16
|
}
|
|
@@ -29,22 +26,14 @@ export async function GET(
|
|
|
29
26
|
where: { id: contactId },
|
|
30
27
|
select: { id: true },
|
|
31
28
|
});
|
|
32
|
-
|
|
33
29
|
if (!contact) {
|
|
34
30
|
return NextResponse.json({ error: 'Contact non trouvé' }, { status: 404 });
|
|
35
31
|
}
|
|
36
32
|
|
|
37
33
|
const file = await prisma.contactFile.findUnique({
|
|
38
34
|
where: { id: fileId },
|
|
39
|
-
select: {
|
|
40
|
-
id: true,
|
|
41
|
-
contactId: true,
|
|
42
|
-
fileName: true,
|
|
43
|
-
mimeType: true,
|
|
44
|
-
googleDriveFileId: true,
|
|
45
|
-
},
|
|
35
|
+
select: { id: true, contactId: true, fileName: true, mimeType: true, storagePath: true },
|
|
46
36
|
});
|
|
47
|
-
|
|
48
37
|
if (!file) {
|
|
49
38
|
return NextResponse.json({ error: 'Fichier non trouvé' }, { status: 404 });
|
|
50
39
|
}
|
|
@@ -53,24 +42,13 @@ export async function GET(
|
|
|
53
42
|
return NextResponse.json({ error: 'Fichier non associé à ce contact' }, { status: 403 });
|
|
54
43
|
}
|
|
55
44
|
|
|
56
|
-
|
|
57
|
-
return NextResponse.json({ error: 'Prévisualisation disponible uniquement pour les images' }, { status: 404 });
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const downloaded = await downloadFileFromDrive(session.user.id, file.googleDriveFileId);
|
|
45
|
+
const signedUrl = await createSignedDownloadUrl(BUCKETS.CONTACTS, file.storagePath, 300);
|
|
61
46
|
|
|
62
|
-
return
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
'Content-Type': file.mimeType,
|
|
66
|
-
'Cache-Control': 'private, max-age=300',
|
|
67
|
-
'Content-Disposition': `inline; filename="${encodeURIComponent(file.fileName)}"`,
|
|
68
|
-
},
|
|
69
|
-
});
|
|
70
|
-
} catch (error: any) {
|
|
71
|
-
console.error("Erreur lors de la prévisualisation d'image:", error);
|
|
47
|
+
return NextResponse.redirect(signedUrl);
|
|
48
|
+
} catch (error: unknown) {
|
|
49
|
+
console.error('Erreur lors de la prévisualisation:', error);
|
|
72
50
|
return NextResponse.json(
|
|
73
|
-
{ error: error.message
|
|
51
|
+
{ error: error instanceof Error ? error.message : 'Erreur lors de la prévisualisation' },
|
|
74
52
|
{ status: 500 },
|
|
75
53
|
);
|
|
76
54
|
}
|
|
@@ -2,19 +2,16 @@ import { NextRequest, NextResponse } from 'next/server';
|
|
|
2
2
|
import { auth } from '@/lib/auth';
|
|
3
3
|
import { prisma } from '@/lib/prisma';
|
|
4
4
|
import { checkPermission } from '@/lib/check-permission';
|
|
5
|
-
import {
|
|
5
|
+
import { BUCKETS, deleteFile } from '@/lib/supabase-storage';
|
|
6
6
|
import { logFileDeleted } from '@/lib/contact-interactions';
|
|
7
7
|
|
|
8
|
-
// DELETE /api/contacts/[id]/files/[fileId]
|
|
8
|
+
// DELETE /api/contacts/[id]/files/[fileId]
|
|
9
9
|
export async function DELETE(
|
|
10
10
|
request: NextRequest,
|
|
11
11
|
{ params }: { params: Promise<{ id: string; fileId: string }> },
|
|
12
12
|
) {
|
|
13
13
|
try {
|
|
14
|
-
const session = await auth.api.getSession({
|
|
15
|
-
headers: request.headers,
|
|
16
|
-
});
|
|
17
|
-
|
|
14
|
+
const session = await auth.api.getSession({ headers: request.headers });
|
|
18
15
|
if (!session) {
|
|
19
16
|
return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
|
|
20
17
|
}
|
|
@@ -26,18 +23,12 @@ export async function DELETE(
|
|
|
26
23
|
|
|
27
24
|
const { id: contactId, fileId } = await params;
|
|
28
25
|
|
|
29
|
-
const contact = await prisma.contact.findUnique({
|
|
30
|
-
where: { id: contactId },
|
|
31
|
-
});
|
|
32
|
-
|
|
26
|
+
const contact = await prisma.contact.findUnique({ where: { id: contactId } });
|
|
33
27
|
if (!contact) {
|
|
34
28
|
return NextResponse.json({ error: 'Contact non trouvé' }, { status: 404 });
|
|
35
29
|
}
|
|
36
30
|
|
|
37
|
-
const file = await prisma.contactFile.findUnique({
|
|
38
|
-
where: { id: fileId },
|
|
39
|
-
});
|
|
40
|
-
|
|
31
|
+
const file = await prisma.contactFile.findUnique({ where: { id: fileId } });
|
|
41
32
|
if (!file) {
|
|
42
33
|
return NextResponse.json({ error: 'Fichier non trouvé' }, { status: 404 });
|
|
43
34
|
}
|
|
@@ -46,39 +37,30 @@ export async function DELETE(
|
|
|
46
37
|
return NextResponse.json({ error: 'Fichier non associé à ce contact' }, { status: 403 });
|
|
47
38
|
}
|
|
48
39
|
|
|
49
|
-
// Sauvegarder les informations du fichier avant suppression pour l'interaction
|
|
50
40
|
const fileName = file.fileName;
|
|
51
41
|
const fileSize = file.fileSize;
|
|
52
42
|
|
|
53
|
-
// Supprimer le fichier de Google Drive
|
|
54
43
|
try {
|
|
55
|
-
await
|
|
44
|
+
await deleteFile(BUCKETS.CONTACTS, file.storagePath);
|
|
56
45
|
} catch (error) {
|
|
57
|
-
console.error('Erreur lors de la suppression du fichier
|
|
58
|
-
// On continue quand même pour supprimer l'enregistrement en base
|
|
46
|
+
console.error('Erreur lors de la suppression du fichier du stockage:', error);
|
|
59
47
|
}
|
|
60
48
|
|
|
61
|
-
|
|
62
|
-
await prisma.contactFile.delete({
|
|
63
|
-
where: { id: fileId },
|
|
64
|
-
});
|
|
49
|
+
await prisma.contactFile.delete({ where: { id: fileId } });
|
|
65
50
|
|
|
66
|
-
// Créer une interaction pour la suppression du fichier
|
|
67
51
|
try {
|
|
68
52
|
await logFileDeleted(contactId, fileName, fileSize, session.user.id);
|
|
69
|
-
} catch (
|
|
70
|
-
console.error(
|
|
71
|
-
"Erreur lors de la création de l'interaction de suppression:",
|
|
72
|
-
interactionError,
|
|
73
|
-
);
|
|
74
|
-
// On continue même si l'interaction échoue
|
|
53
|
+
} catch (e) {
|
|
54
|
+
console.error('Erreur interaction suppression:', e);
|
|
75
55
|
}
|
|
76
56
|
|
|
77
57
|
return NextResponse.json({ success: true });
|
|
78
|
-
} catch (error:
|
|
58
|
+
} catch (error: unknown) {
|
|
79
59
|
console.error('Erreur lors de la suppression du fichier:', error);
|
|
80
60
|
return NextResponse.json(
|
|
81
|
-
{
|
|
61
|
+
{
|
|
62
|
+
error: error instanceof Error ? error.message : 'Erreur lors de la suppression du fichier',
|
|
63
|
+
},
|
|
82
64
|
{ status: 500 },
|
|
83
65
|
);
|
|
84
66
|
}
|
|
@@ -2,16 +2,20 @@ import { NextRequest, NextResponse } from 'next/server';
|
|
|
2
2
|
import { auth } from '@/lib/auth';
|
|
3
3
|
import { prisma } from '@/lib/prisma';
|
|
4
4
|
import { checkPermission } from '@/lib/check-permission';
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
BUCKETS,
|
|
7
|
+
buildContactFilePath,
|
|
8
|
+
createSignedUploadUrl,
|
|
9
|
+
createSignedDownloadUrl,
|
|
10
|
+
} from '@/lib/supabase-storage';
|
|
6
11
|
import { logFileUploaded, logFileReplaced } from '@/lib/contact-interactions';
|
|
7
12
|
|
|
8
|
-
|
|
13
|
+
const MAX_FILE_SIZE = 100 * 1024 * 1024; // 100 MB
|
|
14
|
+
|
|
15
|
+
// POST /api/contacts/[id]/files — create-upload-url | finalize
|
|
9
16
|
export async function POST(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
|
10
17
|
try {
|
|
11
|
-
const session = await auth.api.getSession({
|
|
12
|
-
headers: request.headers,
|
|
13
|
-
});
|
|
14
|
-
|
|
18
|
+
const session = await auth.api.getSession({ headers: request.headers });
|
|
15
19
|
if (!session) {
|
|
16
20
|
return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
|
|
17
21
|
}
|
|
@@ -23,210 +27,132 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
|
|
23
27
|
|
|
24
28
|
const { id: contactId } = await params;
|
|
25
29
|
|
|
26
|
-
// Vérifier que le contact existe
|
|
27
30
|
const contact = await prisma.contact.findUnique({
|
|
28
31
|
where: { id: contactId },
|
|
29
|
-
select: {
|
|
30
|
-
id: true,
|
|
31
|
-
firstName: true,
|
|
32
|
-
lastName: true,
|
|
33
|
-
},
|
|
32
|
+
select: { id: true },
|
|
34
33
|
});
|
|
35
34
|
|
|
36
35
|
if (!contact) {
|
|
37
36
|
return NextResponse.json({ error: 'Contact non trouvé' }, { status: 404 });
|
|
38
37
|
}
|
|
39
38
|
|
|
40
|
-
|
|
41
|
-
const { searchParams } = new URL(request.url);
|
|
42
|
-
const isForTransaction = searchParams.get('transaction') === 'true';
|
|
43
|
-
const isIdentityDocument = searchParams.get('isIdentityDocument') === 'true';
|
|
39
|
+
const body = await request.json();
|
|
44
40
|
|
|
45
|
-
//
|
|
46
|
-
|
|
41
|
+
// ── Step 1 : generate a signed upload URL ─────────────────────────
|
|
42
|
+
if (body.action === 'create-upload-url') {
|
|
43
|
+
const { fileName, fileSize, mimeType } = body;
|
|
47
44
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
45
|
+
if (!fileName || typeof fileSize !== 'number' || !mimeType) {
|
|
46
|
+
return NextResponse.json(
|
|
47
|
+
{ error: 'Champs requis: fileName, fileSize (number), mimeType' },
|
|
48
|
+
{ status: 400 },
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
51
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
52
|
+
if (fileSize > MAX_FILE_SIZE) {
|
|
53
|
+
return NextResponse.json(
|
|
54
|
+
{ error: 'Le fichier est trop volumineux. Taille maximale: 100 MB' },
|
|
55
|
+
{ status: 400 },
|
|
56
|
+
);
|
|
57
|
+
}
|
|
55
58
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
return NextResponse.json(
|
|
60
|
-
{ error: 'Le fichier est trop volumineux. Taille maximale: 100MB' },
|
|
61
|
-
{ status: 400 },
|
|
62
|
-
);
|
|
59
|
+
const storagePath = buildContactFilePath(contactId, fileName);
|
|
60
|
+
const result = await createSignedUploadUrl(BUCKETS.CONTACTS, storagePath);
|
|
61
|
+
return NextResponse.json(result);
|
|
63
62
|
}
|
|
64
63
|
|
|
65
|
-
//
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
// Vérifier si un fichier avec le même nom existe déjà pour ce contact
|
|
70
|
-
const existingFile = await prisma.contactFile.findFirst({
|
|
71
|
-
where: {
|
|
72
|
-
contactId,
|
|
73
|
-
fileName: file.name,
|
|
74
|
-
},
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
let contactFile;
|
|
64
|
+
// ── Step 2 : finalize after client PUT ────────────────────────────
|
|
65
|
+
if (body.action === 'finalize') {
|
|
66
|
+
const { storagePath, fileName, fileSize, mimeType } = body;
|
|
78
67
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
} catch (error) {
|
|
85
|
-
console.error("Erreur lors de la suppression de l'ancien fichier:", error);
|
|
86
|
-
// On continue même si la suppression échoue
|
|
68
|
+
if (!storagePath || !fileName || typeof fileSize !== 'number' || !mimeType) {
|
|
69
|
+
return NextResponse.json(
|
|
70
|
+
{ error: 'Champs requis: storagePath, fileName, fileSize (number), mimeType' },
|
|
71
|
+
{ status: 400 },
|
|
72
|
+
);
|
|
87
73
|
}
|
|
88
74
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
contactId,
|
|
93
|
-
contactName,
|
|
94
|
-
file,
|
|
95
|
-
isForTransaction,
|
|
96
|
-
);
|
|
75
|
+
if (!storagePath.startsWith(`files/${contactId}/`)) {
|
|
76
|
+
return NextResponse.json({ error: 'Chemin de stockage invalide' }, { status: 400 });
|
|
77
|
+
}
|
|
97
78
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
where: { id: existingFile.id },
|
|
101
|
-
data: {
|
|
102
|
-
fileSize: file.size,
|
|
103
|
-
mimeType: file.type || 'application/octet-stream',
|
|
104
|
-
googleDriveFileId: fileId,
|
|
105
|
-
isIdentityDocument,
|
|
106
|
-
uploadedById: session.user.id,
|
|
107
|
-
updatedAt: new Date(),
|
|
108
|
-
},
|
|
109
|
-
include: {
|
|
110
|
-
uploadedBy: {
|
|
111
|
-
select: {
|
|
112
|
-
id: true,
|
|
113
|
-
name: true,
|
|
114
|
-
email: true,
|
|
115
|
-
},
|
|
116
|
-
},
|
|
117
|
-
},
|
|
79
|
+
const existingFile = await prisma.contactFile.findFirst({
|
|
80
|
+
where: { contactId, fileName },
|
|
118
81
|
});
|
|
119
82
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
file,
|
|
137
|
-
isForTransaction,
|
|
138
|
-
);
|
|
83
|
+
let contactFile;
|
|
84
|
+
|
|
85
|
+
if (existingFile) {
|
|
86
|
+
contactFile = await prisma.contactFile.update({
|
|
87
|
+
where: { id: existingFile.id },
|
|
88
|
+
data: {
|
|
89
|
+
fileSize,
|
|
90
|
+
mimeType,
|
|
91
|
+
storagePath,
|
|
92
|
+
uploadedById: session.user.id,
|
|
93
|
+
updatedAt: new Date(),
|
|
94
|
+
},
|
|
95
|
+
include: {
|
|
96
|
+
uploadedBy: { select: { id: true, name: true, email: true } },
|
|
97
|
+
},
|
|
98
|
+
});
|
|
139
99
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
id: true,
|
|
155
|
-
name: true,
|
|
156
|
-
email: true,
|
|
157
|
-
},
|
|
100
|
+
try {
|
|
101
|
+
await logFileReplaced(contactId, contactFile.id, fileName, fileSize, session.user.id);
|
|
102
|
+
} catch (e) {
|
|
103
|
+
console.error('Erreur interaction remplacement:', e);
|
|
104
|
+
}
|
|
105
|
+
} else {
|
|
106
|
+
contactFile = await prisma.contactFile.create({
|
|
107
|
+
data: {
|
|
108
|
+
contactId,
|
|
109
|
+
fileName,
|
|
110
|
+
fileSize,
|
|
111
|
+
mimeType,
|
|
112
|
+
storagePath,
|
|
113
|
+
uploadedById: session.user.id,
|
|
158
114
|
},
|
|
159
|
-
|
|
160
|
-
|
|
115
|
+
include: {
|
|
116
|
+
uploadedBy: { select: { id: true, name: true, email: true } },
|
|
117
|
+
},
|
|
118
|
+
});
|
|
161
119
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
// On continue même si l'interaction échoue
|
|
120
|
+
try {
|
|
121
|
+
await logFileUploaded(contactId, contactFile.id, fileName, fileSize, session.user.id);
|
|
122
|
+
} catch (e) {
|
|
123
|
+
console.error('Erreur interaction upload:', e);
|
|
124
|
+
}
|
|
168
125
|
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// Récupérer le webViewLink pour la réponse
|
|
172
|
-
const fileInfo = await getFileInfo(session.user.id, contactFile.googleDriveFileId);
|
|
173
|
-
const webViewLink = fileInfo.webViewLink;
|
|
174
126
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
127
|
+
return NextResponse.json({
|
|
128
|
+
id: contactFile.id,
|
|
129
|
+
fileName: contactFile.fileName,
|
|
130
|
+
fileSize: contactFile.fileSize,
|
|
131
|
+
mimeType: contactFile.mimeType,
|
|
132
|
+
storagePath: contactFile.storagePath,
|
|
133
|
+
uploadedBy: contactFile.uploadedBy,
|
|
134
|
+
createdAt: contactFile.createdAt,
|
|
135
|
+
updatedAt: contactFile.updatedAt,
|
|
182
136
|
});
|
|
183
137
|
}
|
|
184
138
|
|
|
185
|
-
return NextResponse.json(
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
isIdentityDocument: contactFile.isIdentityDocument,
|
|
191
|
-
webViewLink,
|
|
192
|
-
uploadedBy: contactFile.uploadedBy,
|
|
193
|
-
createdAt: contactFile.createdAt,
|
|
194
|
-
updatedAt: contactFile.updatedAt,
|
|
195
|
-
});
|
|
196
|
-
} catch (error: any) {
|
|
139
|
+
return NextResponse.json(
|
|
140
|
+
{ error: 'Action invalide. Utilisez "create-upload-url" ou "finalize".' },
|
|
141
|
+
{ status: 400 },
|
|
142
|
+
);
|
|
143
|
+
} catch (error: unknown) {
|
|
197
144
|
console.error("Erreur lors de l'upload du fichier:", error);
|
|
198
|
-
|
|
199
|
-
// Détecter les erreurs d'authentification Google (token expiré/révoqué)
|
|
200
|
-
if (
|
|
201
|
-
error.message?.includes('401') ||
|
|
202
|
-
error.message?.includes('UNAUTHENTICATED') ||
|
|
203
|
-
error.message?.includes('Invalid Credentials') ||
|
|
204
|
-
error.message?.includes('invalid_grant')
|
|
205
|
-
) {
|
|
206
|
-
return NextResponse.json(
|
|
207
|
-
{
|
|
208
|
-
error:
|
|
209
|
-
'🔒 La connexion Google Drive a expiré. Veuillez demander à un administrateur de reconnecter son compte Google dans Paramètres > Intégrations > Google Drive.',
|
|
210
|
-
},
|
|
211
|
-
{ status: 401 },
|
|
212
|
-
);
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
// Autres erreurs
|
|
216
145
|
return NextResponse.json(
|
|
217
|
-
{ error: error.message
|
|
146
|
+
{ error: error instanceof Error ? error.message : "Erreur lors de l'upload du fichier" },
|
|
218
147
|
{ status: 500 },
|
|
219
148
|
);
|
|
220
149
|
}
|
|
221
150
|
}
|
|
222
151
|
|
|
223
|
-
// GET /api/contacts/[id]/files
|
|
152
|
+
// GET /api/contacts/[id]/files — lister les fichiers d'un contact
|
|
224
153
|
export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
|
225
154
|
try {
|
|
226
|
-
const session = await auth.api.getSession({
|
|
227
|
-
headers: request.headers,
|
|
228
|
-
});
|
|
229
|
-
|
|
155
|
+
const session = await auth.api.getSession({ headers: request.headers });
|
|
230
156
|
if (!session) {
|
|
231
157
|
return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
|
|
232
158
|
}
|
|
@@ -238,54 +164,26 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
|
|
|
238
164
|
|
|
239
165
|
const { id: contactId } = await params;
|
|
240
166
|
|
|
241
|
-
|
|
242
|
-
const contact = await prisma.contact.findUnique({
|
|
243
|
-
where: { id: contactId },
|
|
244
|
-
});
|
|
245
|
-
|
|
167
|
+
const contact = await prisma.contact.findUnique({ where: { id: contactId } });
|
|
246
168
|
if (!contact) {
|
|
247
169
|
return NextResponse.json({ error: 'Contact non trouvé' }, { status: 404 });
|
|
248
170
|
}
|
|
249
171
|
|
|
250
|
-
// Récupérer tous les fichiers du contact
|
|
251
172
|
const files = await prisma.contactFile.findMany({
|
|
252
173
|
where: { contactId },
|
|
253
174
|
include: {
|
|
254
|
-
uploadedBy: {
|
|
255
|
-
select: {
|
|
256
|
-
id: true,
|
|
257
|
-
name: true,
|
|
258
|
-
email: true,
|
|
259
|
-
},
|
|
260
|
-
},
|
|
261
|
-
},
|
|
262
|
-
orderBy: {
|
|
263
|
-
createdAt: 'desc',
|
|
175
|
+
uploadedBy: { select: { id: true, name: true, email: true } },
|
|
264
176
|
},
|
|
177
|
+
orderBy: { createdAt: 'desc' },
|
|
265
178
|
});
|
|
266
179
|
|
|
267
|
-
// Pour chaque fichier, récupérer le lien de visualisation depuis Google Drive
|
|
268
180
|
const filesWithLinks = await Promise.all(
|
|
269
181
|
files.map(async (file) => {
|
|
182
|
+
let downloadUrl: string | null = null;
|
|
270
183
|
try {
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
id: file.id,
|
|
275
|
-
fileName: file.fileName,
|
|
276
|
-
fileSize: file.fileSize,
|
|
277
|
-
mimeType: file.mimeType,
|
|
278
|
-
isIdentityDocument: file.isIdentityDocument,
|
|
279
|
-
webViewLink: fileInfo.webViewLink,
|
|
280
|
-
uploadedBy: file.uploadedBy,
|
|
281
|
-
createdAt: file.createdAt,
|
|
282
|
-
updatedAt: file.updatedAt,
|
|
283
|
-
};
|
|
284
|
-
} catch (error) {
|
|
285
|
-
console.error(
|
|
286
|
-
`Erreur lors de la récupération du lien pour le fichier ${file.id}:`,
|
|
287
|
-
error,
|
|
288
|
-
);
|
|
184
|
+
downloadUrl = await createSignedDownloadUrl(BUCKETS.CONTACTS, file.storagePath);
|
|
185
|
+
} catch (err) {
|
|
186
|
+
console.error(`Erreur signed URL pour fichier ${file.id}:`, err);
|
|
289
187
|
}
|
|
290
188
|
|
|
291
189
|
return {
|
|
@@ -293,8 +191,7 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
|
|
|
293
191
|
fileName: file.fileName,
|
|
294
192
|
fileSize: file.fileSize,
|
|
295
193
|
mimeType: file.mimeType,
|
|
296
|
-
|
|
297
|
-
webViewLink: `https://drive.google.com/file/d/${file.googleDriveFileId}/view`,
|
|
194
|
+
downloadUrl,
|
|
298
195
|
uploadedBy: file.uploadedBy,
|
|
299
196
|
createdAt: file.createdAt,
|
|
300
197
|
updatedAt: file.updatedAt,
|
|
@@ -303,10 +200,13 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
|
|
|
303
200
|
);
|
|
304
201
|
|
|
305
202
|
return NextResponse.json(filesWithLinks);
|
|
306
|
-
} catch (error:
|
|
203
|
+
} catch (error: unknown) {
|
|
307
204
|
console.error('Erreur lors de la récupération des fichiers:', error);
|
|
308
205
|
return NextResponse.json(
|
|
309
|
-
{
|
|
206
|
+
{
|
|
207
|
+
error:
|
|
208
|
+
error instanceof Error ? error.message : 'Erreur lors de la récupération des fichiers',
|
|
209
|
+
},
|
|
310
210
|
{ status: 500 },
|
|
311
211
|
);
|
|
312
212
|
}
|