create-crm-tmp 1.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/bin/create-crm-tmp.js +93 -0
- package/package.json +25 -0
- package/template/.prettierignore +33 -0
- package/template/.prettierrc.json +25 -0
- package/template/README.md +173 -0
- package/template/eslint.config.mjs +18 -0
- package/template/exemple-contacts.csv +11 -0
- package/template/next.config.ts +8 -0
- package/template/package.json +64 -0
- package/template/postcss.config.mjs +7 -0
- package/template/prisma/migrations/20251126144728_init/migration.sql +78 -0
- package/template/prisma/migrations/20251126155204_add_user_roles/migration.sql +5 -0
- package/template/prisma/migrations/20251128095126_add_company_info/migration.sql +19 -0
- package/template/prisma/migrations/20251128123321_add_smtp_config/migration.sql +22 -0
- package/template/prisma/migrations/20251128132303_add_status/migration.sql +23 -0
- package/template/prisma/migrations/20251201102207_add_user_active/migration.sql +75 -0
- package/template/prisma/migrations/20251201105507_add_email_signature/migration.sql +2 -0
- package/template/prisma/migrations/20251201151122_add_tasks/migration.sql +45 -0
- package/template/prisma/migrations/20251202111854_add_task_reminder/migration.sql +2 -0
- package/template/prisma/migrations/20251202135859_add_google_meet_integration/migration.sql +27 -0
- package/template/prisma/migrations/20251203103317_add_meta_lead_integration/migration.sql +20 -0
- package/template/prisma/migrations/20251203104002_add_google_ads_integration/migration.sql +18 -0
- package/template/prisma/migrations/20251203112122_add_google_sheet_integration/migration.sql +32 -0
- package/template/prisma/migrations/20251203153853_allow_multiple_integration_configs/migration.sql +20 -0
- package/template/prisma/migrations/20251205141705_update_user_roles/migration.sql +12 -0
- package/template/prisma/migrations/20251205150000_add_commercial_and_telepro_assignment/migration.sql +21 -0
- package/template/prisma/migrations/20251205160000_add_interaction_logging/migration.sql +11 -0
- package/template/prisma/migrations/20251208090314_add_automatic_interaction_types/migration.sql +12 -0
- package/template/prisma/migrations/20251208094843_mg/migration.sql +14 -0
- package/template/prisma/migrations/20251208100000_add_company_support/migration.sql +14 -0
- package/template/prisma/migrations/20251208110000_add_templates/migration.sql +26 -0
- package/template/prisma/migrations/20251208141304_add_video_conference_task_type/migration.sql +2 -0
- package/template/prisma/migrations/20251209104759_add_internal_note_to_task/migration.sql +2 -0
- package/template/prisma/migrations/20251209134803_add_company_field/migration.sql +2 -0
- package/template/prisma/migrations/20251209150000_rename_company_to_company_name/migration.sql +3 -0
- package/template/prisma/migrations/20251209150016_add_email_tracking/migration.sql +21 -0
- package/template/prisma/migrations/20251209155908_add_notify_contact_to_task/migration.sql +2 -0
- package/template/prisma/migrations/20251210110019_add_appointment_types/migration.sql +10 -0
- package/template/prisma/migrations/20251210113928_add_contact_files/migration.sql +26 -0
- package/template/prisma/migrations/20251212132339_add_custom_roles/migration.sql +24 -0
- package/template/prisma/migrations/20251215104448_add_file_interaction_types/migration.sql +11 -0
- package/template/prisma/migrations/20251215145616_add_closing_reasons/migration.sql +12 -0
- package/template/prisma/migrations/20251216140850_add_log_users/migration.sql +25 -0
- package/template/prisma/migrations/20251216151000_rename_perdu_to_ferme/migration.sql +8 -0
- package/template/prisma/migrations/20251216162318_add_column_mappings_to_google_sheet/migration.sql +2 -0
- package/template/prisma/migrations/20251216185127_add_workflows/migration.sql +80 -0
- package/template/prisma/migrations/20251216192237_add_scheduled_workflow_actions/migration.sql +32 -0
- package/template/prisma/migrations/migration_lock.toml +3 -0
- package/template/prisma/schema.prisma +582 -0
- package/template/prisma.config.ts +14 -0
- package/template/src/app/(auth)/invite/[token]/page.tsx +200 -0
- package/template/src/app/(auth)/layout.tsx +3 -0
- package/template/src/app/(auth)/reset-password/complete/page.tsx +213 -0
- package/template/src/app/(auth)/reset-password/page.tsx +146 -0
- package/template/src/app/(auth)/reset-password/verify/page.tsx +183 -0
- package/template/src/app/(auth)/signin/page.tsx +166 -0
- package/template/src/app/(dashboard)/agenda/page.tsx +3051 -0
- package/template/src/app/(dashboard)/automatisation/[id]/page.tsx +24 -0
- package/template/src/app/(dashboard)/automatisation/_components/workflow-editor.tsx +905 -0
- package/template/src/app/(dashboard)/automatisation/new/page.tsx +20 -0
- package/template/src/app/(dashboard)/automatisation/page.tsx +337 -0
- package/template/src/app/(dashboard)/closing/page.tsx +1052 -0
- package/template/src/app/(dashboard)/contacts/[id]/page.tsx +6028 -0
- package/template/src/app/(dashboard)/contacts/page.tsx +3713 -0
- package/template/src/app/(dashboard)/dashboard/page.tsx +186 -0
- package/template/src/app/(dashboard)/layout.tsx +30 -0
- package/template/src/app/(dashboard)/settings/page.tsx +4070 -0
- package/template/src/app/(dashboard)/templates/page.tsx +567 -0
- package/template/src/app/(dashboard)/users/list/page.tsx +507 -0
- package/template/src/app/(dashboard)/users/page.tsx +457 -0
- package/template/src/app/(dashboard)/users/permissions/page.tsx +181 -0
- package/template/src/app/(dashboard)/users/roles/page.tsx +434 -0
- package/template/src/app/api/audit-logs/route.ts +57 -0
- package/template/src/app/api/auth/[...all]/route.ts +4 -0
- package/template/src/app/api/auth/check-active/route.ts +31 -0
- package/template/src/app/api/auth/google/callback/route.ts +94 -0
- package/template/src/app/api/auth/google/disconnect/route.ts +32 -0
- package/template/src/app/api/auth/google/route.ts +34 -0
- package/template/src/app/api/auth/google/status/route.ts +32 -0
- package/template/src/app/api/closing-reasons/route.ts +27 -0
- package/template/src/app/api/contacts/[id]/files/[fileId]/route.ts +94 -0
- package/template/src/app/api/contacts/[id]/files/route.ts +269 -0
- package/template/src/app/api/contacts/[id]/interactions/[interactionId]/route.ts +91 -0
- package/template/src/app/api/contacts/[id]/interactions/route.ts +103 -0
- package/template/src/app/api/contacts/[id]/meet/route.ts +296 -0
- package/template/src/app/api/contacts/[id]/route.ts +322 -0
- package/template/src/app/api/contacts/[id]/send-email/route.ts +254 -0
- package/template/src/app/api/contacts/export/route.ts +270 -0
- package/template/src/app/api/contacts/import/route.ts +381 -0
- package/template/src/app/api/contacts/route.ts +283 -0
- package/template/src/app/api/dashboard/stats/route.ts +299 -0
- package/template/src/app/api/email/track/[id]/route.ts +68 -0
- package/template/src/app/api/integrations/google-sheet/sync/route.ts +526 -0
- package/template/src/app/api/invite/complete/route.ts +88 -0
- package/template/src/app/api/invite/validate/route.ts +55 -0
- package/template/src/app/api/reminders/route.ts +95 -0
- package/template/src/app/api/reset-password/complete/route.ts +73 -0
- package/template/src/app/api/reset-password/request/route.ts +84 -0
- package/template/src/app/api/reset-password/validate/route.ts +49 -0
- package/template/src/app/api/reset-password/verify/route.ts +74 -0
- package/template/src/app/api/roles/[id]/route.ts +183 -0
- package/template/src/app/api/roles/route.ts +140 -0
- package/template/src/app/api/send/route.ts +282 -0
- package/template/src/app/api/settings/change-password/route.ts +95 -0
- package/template/src/app/api/settings/closing-reasons/[id]/route.ts +84 -0
- package/template/src/app/api/settings/closing-reasons/route.ts +74 -0
- package/template/src/app/api/settings/company/route.ts +121 -0
- package/template/src/app/api/settings/google-ads/[id]/route.ts +117 -0
- package/template/src/app/api/settings/google-ads/route.ts +122 -0
- package/template/src/app/api/settings/google-sheet/[id]/route.ts +230 -0
- package/template/src/app/api/settings/google-sheet/auto-map/route.ts +196 -0
- package/template/src/app/api/settings/google-sheet/route.ts +254 -0
- package/template/src/app/api/settings/meta-leads/[id]/route.ts +123 -0
- package/template/src/app/api/settings/meta-leads/route.ts +132 -0
- package/template/src/app/api/settings/profile/route.ts +42 -0
- package/template/src/app/api/settings/smtp/route.ts +130 -0
- package/template/src/app/api/settings/smtp/test/route.ts +121 -0
- package/template/src/app/api/settings/statuses/[id]/route.ts +101 -0
- package/template/src/app/api/settings/statuses/route.ts +83 -0
- package/template/src/app/api/statuses/route.ts +25 -0
- package/template/src/app/api/tasks/[id]/attendees/route.ts +76 -0
- package/template/src/app/api/tasks/[id]/route.ts +728 -0
- package/template/src/app/api/tasks/meet/route.ts +240 -0
- package/template/src/app/api/tasks/route.ts +417 -0
- package/template/src/app/api/templates/[id]/route.ts +140 -0
- package/template/src/app/api/templates/route.ts +91 -0
- package/template/src/app/api/users/[id]/route.ts +168 -0
- package/template/src/app/api/users/list/route.ts +45 -0
- package/template/src/app/api/users/me/route.ts +48 -0
- package/template/src/app/api/users/route.ts +250 -0
- package/template/src/app/api/webhooks/google-ads/route.ts +208 -0
- package/template/src/app/api/webhooks/meta-leads/route.ts +258 -0
- package/template/src/app/api/workflows/[id]/route.ts +192 -0
- package/template/src/app/api/workflows/process/route.ts +293 -0
- package/template/src/app/api/workflows/route.ts +124 -0
- package/template/src/app/favicon.ico +0 -0
- package/template/src/app/globals.css +1416 -0
- package/template/src/app/layout.tsx +31 -0
- package/template/src/app/page.tsx +32 -0
- package/template/src/components/dashboard/activity-chart.tsx +67 -0
- package/template/src/components/dashboard/contacts-chart.tsx +63 -0
- package/template/src/components/dashboard/recent-activity.tsx +164 -0
- package/template/src/components/dashboard/sales-analytics-chart.tsx +81 -0
- package/template/src/components/dashboard/stat-card.tsx +61 -0
- package/template/src/components/dashboard/status-distribution-chart.tsx +45 -0
- package/template/src/components/dashboard/tasks-pie-chart.tsx +88 -0
- package/template/src/components/dashboard/top-contacts-list.tsx +129 -0
- package/template/src/components/dashboard/upcoming-tasks-list.tsx +126 -0
- package/template/src/components/editor.tsx +856 -0
- package/template/src/components/email-template.tsx +35 -0
- package/template/src/components/header.tsx +320 -0
- package/template/src/components/invitation-email-template.tsx +79 -0
- package/template/src/components/meet-cancellation-email-template.tsx +120 -0
- package/template/src/components/meet-confirmation-email-template.tsx +156 -0
- package/template/src/components/meet-update-email-template.tsx +209 -0
- package/template/src/components/page-header.tsx +61 -0
- package/template/src/components/reset-password-email-template.tsx +79 -0
- package/template/src/components/sidebar.tsx +294 -0
- package/template/src/components/skeleton.tsx +380 -0
- package/template/src/components/ui/commands.tsx +396 -0
- package/template/src/components/ui/components.tsx +150 -0
- package/template/src/components/ui/theme.tsx +5 -0
- package/template/src/components/view-as-banner.tsx +45 -0
- package/template/src/components/view-as-modal.tsx +186 -0
- package/template/src/contexts/mobile-menu-context.tsx +31 -0
- package/template/src/contexts/sidebar-context.tsx +107 -0
- package/template/src/contexts/task-reminder-context.tsx +239 -0
- package/template/src/contexts/view-as-context.tsx +84 -0
- package/template/src/hooks/use-user-role.ts +82 -0
- package/template/src/lib/audit-log.ts +45 -0
- package/template/src/lib/auth-client.ts +16 -0
- package/template/src/lib/auth.ts +35 -0
- package/template/src/lib/check-permission.ts +193 -0
- package/template/src/lib/contact-duplicate.ts +112 -0
- package/template/src/lib/contact-interactions.ts +371 -0
- package/template/src/lib/encryption.ts +99 -0
- package/template/src/lib/google-calendar.ts +300 -0
- package/template/src/lib/google-drive.ts +372 -0
- package/template/src/lib/permissions.ts +412 -0
- package/template/src/lib/prisma.ts +32 -0
- package/template/src/lib/roles.ts +120 -0
- package/template/src/lib/template-variables.ts +76 -0
- package/template/src/lib/utils.ts +46 -0
- package/template/src/lib/workflow-executor.ts +482 -0
- package/template/src/proxy.ts +91 -0
- package/template/tsconfig.json +34 -0
- package/template/vercel.json +8 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { prisma } from '@/lib/prisma';
|
|
3
|
+
import { requireAdmin } from '@/lib/roles';
|
|
4
|
+
|
|
5
|
+
// PUT /api/settings/google-ads/[id] - Mettre à jour une configuration (admin uniquement)
|
|
6
|
+
export async function PUT(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
|
7
|
+
try {
|
|
8
|
+
await requireAdmin(request.headers);
|
|
9
|
+
|
|
10
|
+
const { id } = await params;
|
|
11
|
+
const body = await request.json();
|
|
12
|
+
const { name, webhookKey, active, defaultStatusId, defaultAssignedUserId } = body;
|
|
13
|
+
|
|
14
|
+
const client = prisma as any;
|
|
15
|
+
|
|
16
|
+
const existingConfig = await client.googleAdsLeadConfig.findUnique({
|
|
17
|
+
where: { id },
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
if (!existingConfig) {
|
|
21
|
+
return NextResponse.json({ error: 'Configuration non trouvée' }, { status: 404 });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const updateData: any = {};
|
|
25
|
+
if (name !== undefined) updateData.name = name;
|
|
26
|
+
if (webhookKey !== undefined) updateData.webhookKey = webhookKey;
|
|
27
|
+
if (active !== undefined) updateData.active = !!active;
|
|
28
|
+
if (defaultStatusId !== undefined) updateData.defaultStatusId = defaultStatusId || null;
|
|
29
|
+
if (defaultAssignedUserId !== undefined) {
|
|
30
|
+
if (!defaultAssignedUserId) {
|
|
31
|
+
return NextResponse.json(
|
|
32
|
+
{ error: "L'utilisateur assigné par défaut est obligatoire." },
|
|
33
|
+
{ status: 400 },
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
updateData.defaultAssignedUserId = defaultAssignedUserId;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const config = await client.googleAdsLeadConfig.update({
|
|
40
|
+
where: { id },
|
|
41
|
+
data: updateData,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
return NextResponse.json({
|
|
45
|
+
success: true,
|
|
46
|
+
config: {
|
|
47
|
+
id: config.id,
|
|
48
|
+
name: config.name,
|
|
49
|
+
webhookKey: config.webhookKey,
|
|
50
|
+
active: config.active,
|
|
51
|
+
defaultStatusId: config.defaultStatusId,
|
|
52
|
+
defaultAssignedUserId: config.defaultAssignedUserId,
|
|
53
|
+
},
|
|
54
|
+
message: 'Configuration Google Ads Lead Forms mise à jour avec succès.',
|
|
55
|
+
});
|
|
56
|
+
} catch (error: any) {
|
|
57
|
+
console.error(
|
|
58
|
+
'Erreur lors de la mise à jour de la configuration Google Ads Lead Forms:',
|
|
59
|
+
error,
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
if (error.message === 'Non authentifié') {
|
|
63
|
+
return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (error.message === 'Permissions insuffisantes') {
|
|
67
|
+
return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return NextResponse.json({ error: error.message || 'Erreur serveur' }, { status: 500 });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// DELETE /api/settings/google-ads/[id] - Supprimer une configuration (admin uniquement)
|
|
75
|
+
export async function DELETE(
|
|
76
|
+
request: NextRequest,
|
|
77
|
+
{ params }: { params: Promise<{ id: string }> },
|
|
78
|
+
) {
|
|
79
|
+
try {
|
|
80
|
+
await requireAdmin(request.headers);
|
|
81
|
+
|
|
82
|
+
const { id } = await params;
|
|
83
|
+
const client = prisma as any;
|
|
84
|
+
|
|
85
|
+
const existingConfig = await client.googleAdsLeadConfig.findUnique({
|
|
86
|
+
where: { id },
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
if (!existingConfig) {
|
|
90
|
+
return NextResponse.json({ error: 'Configuration non trouvée' }, { status: 404 });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
await client.googleAdsLeadConfig.delete({
|
|
94
|
+
where: { id },
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
return NextResponse.json({
|
|
98
|
+
success: true,
|
|
99
|
+
message: 'Configuration Google Ads Lead Forms supprimée avec succès.',
|
|
100
|
+
});
|
|
101
|
+
} catch (error: any) {
|
|
102
|
+
console.error(
|
|
103
|
+
'Erreur lors de la suppression de la configuration Google Ads Lead Forms:',
|
|
104
|
+
error,
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
if (error.message === 'Non authentifié') {
|
|
108
|
+
return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (error.message === 'Permissions insuffisantes') {
|
|
112
|
+
return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return NextResponse.json({ error: error.message || 'Erreur serveur' }, { status: 500 });
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { prisma } from '@/lib/prisma';
|
|
3
|
+
import { requireAdmin } from '@/lib/roles';
|
|
4
|
+
|
|
5
|
+
// GET /api/settings/google-ads - Récupérer toutes les configurations Google Ads Leads (admin uniquement)
|
|
6
|
+
export async function GET(request: NextRequest) {
|
|
7
|
+
try {
|
|
8
|
+
await requireAdmin(request.headers);
|
|
9
|
+
|
|
10
|
+
const client = prisma as any;
|
|
11
|
+
|
|
12
|
+
const configs = await client.googleAdsLeadConfig.findMany({
|
|
13
|
+
include: {
|
|
14
|
+
defaultStatus: true,
|
|
15
|
+
defaultAssignedUser: {
|
|
16
|
+
select: { id: true, name: true, email: true },
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
orderBy: {
|
|
20
|
+
createdAt: 'desc',
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
return NextResponse.json(
|
|
25
|
+
configs.map((config: any) => ({
|
|
26
|
+
id: config.id,
|
|
27
|
+
name: config.name,
|
|
28
|
+
webhookKey: config.webhookKey,
|
|
29
|
+
active: config.active,
|
|
30
|
+
defaultStatusId: config.defaultStatusId,
|
|
31
|
+
defaultAssignedUserId: config.defaultAssignedUserId,
|
|
32
|
+
defaultStatus: config.defaultStatus
|
|
33
|
+
? {
|
|
34
|
+
id: config.defaultStatus.id,
|
|
35
|
+
name: config.defaultStatus.name,
|
|
36
|
+
color: config.defaultStatus.color,
|
|
37
|
+
}
|
|
38
|
+
: null,
|
|
39
|
+
defaultAssignedUser: config.defaultAssignedUser || null,
|
|
40
|
+
})),
|
|
41
|
+
);
|
|
42
|
+
} catch (error: any) {
|
|
43
|
+
console.error(
|
|
44
|
+
'Erreur lors de la récupération des configurations Google Ads Lead Forms:',
|
|
45
|
+
error,
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
if (error.message === 'Non authentifié') {
|
|
49
|
+
return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (error.message === 'Permissions insuffisantes') {
|
|
53
|
+
return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// POST /api/settings/google-ads - Créer une nouvelle configuration (admin uniquement)
|
|
61
|
+
export async function POST(request: NextRequest) {
|
|
62
|
+
try {
|
|
63
|
+
await requireAdmin(request.headers);
|
|
64
|
+
|
|
65
|
+
const body = await request.json();
|
|
66
|
+
const { name, webhookKey, active = true, defaultStatusId, defaultAssignedUserId } = body;
|
|
67
|
+
|
|
68
|
+
if (!name || !webhookKey) {
|
|
69
|
+
return NextResponse.json(
|
|
70
|
+
{
|
|
71
|
+
error:
|
|
72
|
+
'Les champs nom et webhookKey sont obligatoires pour activer l’intégration Google Ads (clé secrète partagée).',
|
|
73
|
+
},
|
|
74
|
+
{ status: 400 },
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (!defaultAssignedUserId) {
|
|
79
|
+
return NextResponse.json(
|
|
80
|
+
{ error: "L'utilisateur assigné par défaut est obligatoire." },
|
|
81
|
+
{ status: 400 },
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const client = prisma as any;
|
|
86
|
+
|
|
87
|
+
const config = await client.googleAdsLeadConfig.create({
|
|
88
|
+
data: {
|
|
89
|
+
name,
|
|
90
|
+
webhookKey,
|
|
91
|
+
active: !!active,
|
|
92
|
+
defaultStatusId: defaultStatusId || null,
|
|
93
|
+
defaultAssignedUserId,
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
return NextResponse.json({
|
|
98
|
+
success: true,
|
|
99
|
+
config: {
|
|
100
|
+
id: config.id,
|
|
101
|
+
name: config.name,
|
|
102
|
+
webhookKey: config.webhookKey,
|
|
103
|
+
active: config.active,
|
|
104
|
+
defaultStatusId: config.defaultStatusId,
|
|
105
|
+
defaultAssignedUserId: config.defaultAssignedUserId,
|
|
106
|
+
},
|
|
107
|
+
message: 'Configuration Google Ads Lead Forms créée avec succès.',
|
|
108
|
+
});
|
|
109
|
+
} catch (error: any) {
|
|
110
|
+
console.error('Erreur lors de la création de la configuration Google Ads Lead Forms:', error);
|
|
111
|
+
|
|
112
|
+
if (error.message === 'Non authentifié') {
|
|
113
|
+
return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (error.message === 'Permissions insuffisantes') {
|
|
117
|
+
return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return NextResponse.json({ error: error.message || 'Erreur serveur' }, { status: 500 });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { prisma } from '@/lib/prisma';
|
|
3
|
+
import { requireAdmin } from '@/lib/roles';
|
|
4
|
+
|
|
5
|
+
function extractSpreadsheetId(sheetUrlOrId: string): string {
|
|
6
|
+
if (!sheetUrlOrId) return sheetUrlOrId;
|
|
7
|
+
|
|
8
|
+
if (!sheetUrlOrId.includes('https://')) {
|
|
9
|
+
return sheetUrlOrId;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const match = sheetUrlOrId.match(/spreadsheets\/d\/([a-zA-Z0-9-_]+)/);
|
|
13
|
+
if (match && match[1]) {
|
|
14
|
+
return match[1];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return sheetUrlOrId;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// PUT /api/settings/google-sheet/[id] - Mettre à jour une configuration (admin uniquement)
|
|
21
|
+
export async function PUT(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
|
22
|
+
try {
|
|
23
|
+
await requireAdmin(request.headers);
|
|
24
|
+
|
|
25
|
+
const { id } = await params;
|
|
26
|
+
const body = await request.json();
|
|
27
|
+
const {
|
|
28
|
+
name,
|
|
29
|
+
sheetUrl,
|
|
30
|
+
sheetName,
|
|
31
|
+
headerRow,
|
|
32
|
+
phoneColumn,
|
|
33
|
+
firstNameColumn,
|
|
34
|
+
lastNameColumn,
|
|
35
|
+
emailColumn,
|
|
36
|
+
cityColumn,
|
|
37
|
+
postalCodeColumn,
|
|
38
|
+
originColumn,
|
|
39
|
+
columnMappings,
|
|
40
|
+
active,
|
|
41
|
+
defaultStatusId,
|
|
42
|
+
defaultAssignedUserId,
|
|
43
|
+
} = body;
|
|
44
|
+
|
|
45
|
+
const client = prisma as any;
|
|
46
|
+
|
|
47
|
+
const existingConfig = await client.googleSheetSyncConfig.findUnique({
|
|
48
|
+
where: { id },
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
if (!existingConfig) {
|
|
52
|
+
return NextResponse.json({ error: 'Configuration non trouvée' }, { status: 404 });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const updateData: any = {};
|
|
56
|
+
if (name !== undefined) updateData.name = name;
|
|
57
|
+
if (sheetUrl !== undefined) updateData.spreadsheetId = extractSpreadsheetId(sheetUrl);
|
|
58
|
+
if (sheetName !== undefined) updateData.sheetName = sheetName;
|
|
59
|
+
if (headerRow !== undefined) {
|
|
60
|
+
const headerRowNumber = Number(headerRow);
|
|
61
|
+
if (!headerRowNumber || headerRowNumber < 1) {
|
|
62
|
+
return NextResponse.json(
|
|
63
|
+
{ error: 'La ligne des en-têtes doit être un nombre positif (ex: 1, 2...).' },
|
|
64
|
+
{ status: 400 },
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
updateData.headerRow = headerRowNumber;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Gérer les mappings (nouveau format)
|
|
71
|
+
if (columnMappings !== undefined && Array.isArray(columnMappings)) {
|
|
72
|
+
// Vérifier qu'au moins un mapping téléphone est configuré
|
|
73
|
+
const phoneMapping = columnMappings.find(
|
|
74
|
+
(m: any) => m.action === 'map' && m.crmField === 'phone' && m.columnName?.trim() !== '',
|
|
75
|
+
);
|
|
76
|
+
if (!phoneMapping) {
|
|
77
|
+
return NextResponse.json(
|
|
78
|
+
{
|
|
79
|
+
error: 'Le mapping du téléphone est obligatoire.',
|
|
80
|
+
},
|
|
81
|
+
{ status: 400 },
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
updateData.columnMappings = JSON.parse(JSON.stringify(columnMappings));
|
|
86
|
+
|
|
87
|
+
// Convertir aussi vers l'ancien format pour compatibilité
|
|
88
|
+
let phoneColumnValue = phoneColumn || '';
|
|
89
|
+
let firstNameColumnValue = firstNameColumn || null;
|
|
90
|
+
let lastNameColumnValue = lastNameColumn || null;
|
|
91
|
+
let emailColumnValue = emailColumn || null;
|
|
92
|
+
let cityColumnValue = cityColumn || null;
|
|
93
|
+
let postalCodeColumnValue = postalCodeColumn || null;
|
|
94
|
+
let originColumnValue = originColumn || null;
|
|
95
|
+
|
|
96
|
+
columnMappings.forEach((mapping: any) => {
|
|
97
|
+
if (mapping.action === 'map' && mapping.crmField && mapping.columnName) {
|
|
98
|
+
switch (mapping.crmField) {
|
|
99
|
+
case 'phone':
|
|
100
|
+
phoneColumnValue = mapping.columnName;
|
|
101
|
+
break;
|
|
102
|
+
case 'firstName':
|
|
103
|
+
firstNameColumnValue = mapping.columnName;
|
|
104
|
+
break;
|
|
105
|
+
case 'lastName':
|
|
106
|
+
lastNameColumnValue = mapping.columnName;
|
|
107
|
+
break;
|
|
108
|
+
case 'email':
|
|
109
|
+
emailColumnValue = mapping.columnName;
|
|
110
|
+
break;
|
|
111
|
+
case 'city':
|
|
112
|
+
cityColumnValue = mapping.columnName;
|
|
113
|
+
break;
|
|
114
|
+
case 'postalCode':
|
|
115
|
+
postalCodeColumnValue = mapping.columnName;
|
|
116
|
+
break;
|
|
117
|
+
case 'origin':
|
|
118
|
+
originColumnValue = mapping.columnName;
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
updateData.phoneColumn = phoneColumnValue;
|
|
125
|
+
updateData.firstNameColumn = firstNameColumnValue;
|
|
126
|
+
updateData.lastNameColumn = lastNameColumnValue;
|
|
127
|
+
updateData.emailColumn = emailColumnValue;
|
|
128
|
+
updateData.cityColumn = cityColumnValue;
|
|
129
|
+
updateData.postalCodeColumn = postalCodeColumnValue;
|
|
130
|
+
updateData.originColumn = originColumnValue;
|
|
131
|
+
} else {
|
|
132
|
+
// Ancien format (compatibilité)
|
|
133
|
+
if (phoneColumn !== undefined) updateData.phoneColumn = phoneColumn;
|
|
134
|
+
if (firstNameColumn !== undefined) updateData.firstNameColumn = firstNameColumn || null;
|
|
135
|
+
if (lastNameColumn !== undefined) updateData.lastNameColumn = lastNameColumn || null;
|
|
136
|
+
if (emailColumn !== undefined) updateData.emailColumn = emailColumn || null;
|
|
137
|
+
if (cityColumn !== undefined) updateData.cityColumn = cityColumn || null;
|
|
138
|
+
if (postalCodeColumn !== undefined) updateData.postalCodeColumn = postalCodeColumn || null;
|
|
139
|
+
if (originColumn !== undefined) updateData.originColumn = originColumn || null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (active !== undefined) updateData.active = !!active;
|
|
143
|
+
if (defaultStatusId !== undefined) updateData.defaultStatusId = defaultStatusId || null;
|
|
144
|
+
if (defaultAssignedUserId !== undefined) {
|
|
145
|
+
// Permettre de supprimer l'utilisateur par défaut (null)
|
|
146
|
+
updateData.defaultAssignedUserId = defaultAssignedUserId || null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const config = await client.googleSheetSyncConfig.update({
|
|
150
|
+
where: { id },
|
|
151
|
+
data: updateData,
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
return NextResponse.json({
|
|
155
|
+
success: true,
|
|
156
|
+
config: {
|
|
157
|
+
id: config.id,
|
|
158
|
+
name: config.name,
|
|
159
|
+
spreadsheetId: config.spreadsheetId,
|
|
160
|
+
sheetName: config.sheetName,
|
|
161
|
+
headerRow: config.headerRow,
|
|
162
|
+
phoneColumn: config.phoneColumn,
|
|
163
|
+
firstNameColumn: config.firstNameColumn,
|
|
164
|
+
lastNameColumn: config.lastNameColumn,
|
|
165
|
+
emailColumn: config.emailColumn,
|
|
166
|
+
cityColumn: config.cityColumn,
|
|
167
|
+
postalCodeColumn: config.postalCodeColumn,
|
|
168
|
+
originColumn: config.originColumn,
|
|
169
|
+
active: config.active,
|
|
170
|
+
defaultStatusId: config.defaultStatusId,
|
|
171
|
+
defaultAssignedUserId: config.defaultAssignedUserId,
|
|
172
|
+
},
|
|
173
|
+
message: 'Configuration Google Sheets mise à jour avec succès.',
|
|
174
|
+
});
|
|
175
|
+
} catch (error: any) {
|
|
176
|
+
console.error('Erreur lors de la mise à jour de la configuration Google Sheets:', error);
|
|
177
|
+
|
|
178
|
+
if (error.message === 'Non authentifié') {
|
|
179
|
+
return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (error.message === 'Permissions insuffisantes') {
|
|
183
|
+
return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return NextResponse.json({ error: error.message || 'Erreur serveur' }, { status: 500 });
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// DELETE /api/settings/google-sheet/[id] - Supprimer une configuration (admin uniquement)
|
|
191
|
+
export async function DELETE(
|
|
192
|
+
request: NextRequest,
|
|
193
|
+
{ params }: { params: Promise<{ id: string }> },
|
|
194
|
+
) {
|
|
195
|
+
try {
|
|
196
|
+
await requireAdmin(request.headers);
|
|
197
|
+
|
|
198
|
+
const { id } = await params;
|
|
199
|
+
const client = prisma as any;
|
|
200
|
+
|
|
201
|
+
const existingConfig = await client.googleSheetSyncConfig.findUnique({
|
|
202
|
+
where: { id },
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
if (!existingConfig) {
|
|
206
|
+
return NextResponse.json({ error: 'Configuration non trouvée' }, { status: 404 });
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
await client.googleSheetSyncConfig.delete({
|
|
210
|
+
where: { id },
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
return NextResponse.json({
|
|
214
|
+
success: true,
|
|
215
|
+
message: 'Configuration Google Sheets supprimée avec succès.',
|
|
216
|
+
});
|
|
217
|
+
} catch (error: any) {
|
|
218
|
+
console.error('Erreur lors de la suppression de la configuration Google Sheets:', error);
|
|
219
|
+
|
|
220
|
+
if (error.message === 'Non authentifié') {
|
|
221
|
+
return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (error.message === 'Permissions insuffisantes') {
|
|
225
|
+
return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return NextResponse.json({ error: error.message || 'Erreur serveur' }, { status: 500 });
|
|
229
|
+
}
|
|
230
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { prisma } from '@/lib/prisma';
|
|
3
|
+
import { requireAdmin } from '@/lib/roles';
|
|
4
|
+
import { getValidAccessToken } from '@/lib/google-calendar';
|
|
5
|
+
|
|
6
|
+
function extractSpreadsheetId(sheetUrlOrId: string): string {
|
|
7
|
+
if (!sheetUrlOrId) return sheetUrlOrId;
|
|
8
|
+
|
|
9
|
+
// Si c'est déjà un ID simple, on le renvoie
|
|
10
|
+
if (!sheetUrlOrId.includes('https://')) {
|
|
11
|
+
return sheetUrlOrId;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const match = sheetUrlOrId.match(/spreadsheets\/d\/([a-zA-Z0-9-_]+)/);
|
|
15
|
+
if (match && match[1]) {
|
|
16
|
+
return match[1];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return sheetUrlOrId;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function indexToColumn(index: number): string {
|
|
23
|
+
let col = '';
|
|
24
|
+
let n = index + 1;
|
|
25
|
+
while (n > 0) {
|
|
26
|
+
const rem = (n - 1) % 26;
|
|
27
|
+
col = String.fromCharCode(65 + rem) + col;
|
|
28
|
+
n = Math.floor((n - 1) / 26);
|
|
29
|
+
}
|
|
30
|
+
return col;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// POST /api/settings/google-sheet/auto-map - Proposer un mapping automatique des colonnes
|
|
34
|
+
export async function POST(request: NextRequest) {
|
|
35
|
+
try {
|
|
36
|
+
const session = await requireAdmin(request.headers);
|
|
37
|
+
|
|
38
|
+
const body = await request.json();
|
|
39
|
+
const { sheetUrl, sheetName, headerRow } = body || {};
|
|
40
|
+
|
|
41
|
+
if (!sheetUrl || !sheetName || !headerRow) {
|
|
42
|
+
return NextResponse.json(
|
|
43
|
+
{
|
|
44
|
+
error:
|
|
45
|
+
'Les champs lien du Google Sheet, nom de l’onglet et ligne des en-têtes sont obligatoires pour le mapping automatique.',
|
|
46
|
+
},
|
|
47
|
+
{ status: 400 },
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const spreadsheetId = extractSpreadsheetId(sheetUrl);
|
|
52
|
+
const headerRowNumber = Number(headerRow);
|
|
53
|
+
|
|
54
|
+
if (!headerRowNumber || headerRowNumber < 1) {
|
|
55
|
+
return NextResponse.json(
|
|
56
|
+
{ error: 'La ligne des en-têtes doit être un nombre positif (ex: 1, 2...).' },
|
|
57
|
+
{ status: 400 },
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Récupérer le compte Google de l'utilisateur admin courant
|
|
62
|
+
const googleAccount = await prisma.userGoogleAccount.findUnique({
|
|
63
|
+
where: { userId: session.user.id },
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
if (!googleAccount) {
|
|
67
|
+
return NextResponse.json(
|
|
68
|
+
{
|
|
69
|
+
error:
|
|
70
|
+
'Aucun compte Google connecté. Veuillez connecter votre compte Google dans les paramètres avant de configurer Google Sheets.',
|
|
71
|
+
},
|
|
72
|
+
{ status: 400 },
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const accessToken = await getValidAccessToken(
|
|
77
|
+
googleAccount.accessToken,
|
|
78
|
+
googleAccount.refreshToken,
|
|
79
|
+
googleAccount.tokenExpiresAt,
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
const range = encodeURIComponent(sheetName);
|
|
83
|
+
const response = await fetch(
|
|
84
|
+
`https://sheets.googleapis.com/v4/spreadsheets/${spreadsheetId}/values/${range}`,
|
|
85
|
+
{
|
|
86
|
+
headers: {
|
|
87
|
+
Authorization: `Bearer ${accessToken}`,
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
if (!response.ok) {
|
|
93
|
+
const errorText = await response.text();
|
|
94
|
+
console.error('Erreur lors de la lecture du Google Sheet pour auto-map:', errorText);
|
|
95
|
+
return NextResponse.json(
|
|
96
|
+
{ error: 'Impossible de lire les données depuis Google Sheets.' },
|
|
97
|
+
{ status: 400 },
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const data = await response.json();
|
|
102
|
+
const values: string[][] = data.values || [];
|
|
103
|
+
|
|
104
|
+
if (!values.length) {
|
|
105
|
+
return NextResponse.json(
|
|
106
|
+
{ error: 'Le Google Sheet ne contient aucune donnée.' },
|
|
107
|
+
{ status: 400 },
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const headerRowIndex = headerRowNumber - 1;
|
|
112
|
+
if (headerRowIndex < 0 || headerRowIndex >= values.length) {
|
|
113
|
+
return NextResponse.json(
|
|
114
|
+
{ error: 'La ligne des en-têtes spécifiée est en dehors de la plage du fichier.' },
|
|
115
|
+
{ status: 400 },
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const headerValues = values[headerRowIndex] || [];
|
|
120
|
+
|
|
121
|
+
const fieldMappings: Record<string, string[]> = {
|
|
122
|
+
phoneColumn: ['téléphone', 'telephone', 'phone', 'tel', 'tél', 'mobile', 'portable'],
|
|
123
|
+
firstNameColumn: ['prénom', 'prenom', 'firstname', 'first name', 'first_name', 'prenom'],
|
|
124
|
+
lastNameColumn: ['nom', 'lastname', 'last name', 'last_name', 'nom de famille', 'surname'],
|
|
125
|
+
emailColumn: ['email', 'e-mail', 'mail', 'courriel', "mél", "mel"],
|
|
126
|
+
cityColumn: ['ville', 'city', 'localité', 'locality'],
|
|
127
|
+
postalCodeColumn: ['code postal', 'postal code', 'cp', 'zip', 'zipcode', 'code_postal'],
|
|
128
|
+
originColumn: ['origine', 'origin', 'source', 'campagne', 'campaign', 'origine de la campagne'],
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const mapping: Record<string, string> = {};
|
|
132
|
+
|
|
133
|
+
headerValues.forEach((header, index) => {
|
|
134
|
+
if (!header) return;
|
|
135
|
+
const normalized = header.toString().toLowerCase().trim();
|
|
136
|
+
|
|
137
|
+
for (const [fieldKey, aliases] of Object.entries(fieldMappings)) {
|
|
138
|
+
if (mapping[fieldKey]) continue; // déjà mappé
|
|
139
|
+
if (
|
|
140
|
+
aliases.some(
|
|
141
|
+
(alias) =>
|
|
142
|
+
normalized.includes(alias) ||
|
|
143
|
+
alias.includes(normalized) ||
|
|
144
|
+
normalized === alias.replace(/_/g, ' '),
|
|
145
|
+
)
|
|
146
|
+
) {
|
|
147
|
+
mapping[fieldKey] = indexToColumn(index);
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// Récupérer les 5 premières lignes de données (après la ligne d'en-tête)
|
|
154
|
+
const previewRows: string[][] = [];
|
|
155
|
+
const startDataRow = headerRowIndex + 1;
|
|
156
|
+
const endDataRow = Math.min(startDataRow + 5, values.length);
|
|
157
|
+
|
|
158
|
+
for (let i = startDataRow; i < endDataRow; i++) {
|
|
159
|
+
if (values[i]) {
|
|
160
|
+
previewRows.push(values[i]);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Convertir les lignes en objets avec les headers comme clés
|
|
165
|
+
const previewData = previewRows.map((row) => {
|
|
166
|
+
const rowObj: Record<string, string> = {};
|
|
167
|
+
headerValues.forEach((header, index) => {
|
|
168
|
+
rowObj[header || `Colonne ${index + 1}`] = row[index] || '';
|
|
169
|
+
});
|
|
170
|
+
return rowObj;
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
return NextResponse.json({
|
|
174
|
+
headers: headerValues,
|
|
175
|
+
mapping,
|
|
176
|
+
preview: previewData,
|
|
177
|
+
});
|
|
178
|
+
} catch (error: any) {
|
|
179
|
+
console.error('Erreur lors du mapping automatique Google Sheets:', error);
|
|
180
|
+
|
|
181
|
+
if (error.message === 'Non authentifié') {
|
|
182
|
+
return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (error.message === 'Permissions insuffisantes') {
|
|
186
|
+
return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return NextResponse.json(
|
|
190
|
+
{ error: error.message || 'Erreur serveur lors du mapping automatique' },
|
|
191
|
+
{ status: 500 },
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
|