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,6 +2,7 @@ import { NextRequest, NextResponse } from 'next/server';
|
|
|
2
2
|
import { auth } from '@/lib/auth';
|
|
3
3
|
import { prisma } from '@/lib/prisma';
|
|
4
4
|
import { exchangeGoogleCodeForTokens } from '@/lib/google-calendar';
|
|
5
|
+
import { encrypt } from '@/lib/encryption';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* GET /api/auth/google/callback
|
|
@@ -17,6 +18,8 @@ export async function GET(request: NextRequest) {
|
|
|
17
18
|
return NextResponse.redirect(new URL('/signin', request.url));
|
|
18
19
|
}
|
|
19
20
|
|
|
21
|
+
// Tous les utilisateurs peuvent maintenant connecter leur compte Google pour Calendar
|
|
22
|
+
|
|
20
23
|
const { searchParams } = new URL(request.url);
|
|
21
24
|
const code = searchParams.get('code');
|
|
22
25
|
const error = searchParams.get('error');
|
|
@@ -63,19 +66,19 @@ export async function GET(request: NextRequest) {
|
|
|
63
66
|
console.error("Erreur lors de la récupération de l'email Google:", err);
|
|
64
67
|
}
|
|
65
68
|
|
|
66
|
-
// Sauvegarder ou mettre à jour les tokens
|
|
69
|
+
// Sauvegarder ou mettre à jour les tokens (chiffrés si ENCRYPTION_KEY est définie)
|
|
67
70
|
await prisma.userGoogleAccount.upsert({
|
|
68
71
|
where: { userId: session.user.id },
|
|
69
72
|
create: {
|
|
70
73
|
userId: session.user.id,
|
|
71
|
-
accessToken: tokens.access_token,
|
|
72
|
-
refreshToken: tokens.refresh_token
|
|
74
|
+
accessToken: encrypt(tokens.access_token),
|
|
75
|
+
refreshToken: tokens.refresh_token ? encrypt(tokens.refresh_token) : '',
|
|
73
76
|
tokenExpiresAt,
|
|
74
77
|
email: googleEmail,
|
|
75
78
|
},
|
|
76
79
|
update: {
|
|
77
|
-
accessToken: tokens.access_token,
|
|
78
|
-
refreshToken: tokens.refresh_token
|
|
80
|
+
accessToken: encrypt(tokens.access_token),
|
|
81
|
+
refreshToken: tokens.refresh_token ? encrypt(tokens.refresh_token) : undefined,
|
|
79
82
|
tokenExpiresAt,
|
|
80
83
|
email: googleEmail,
|
|
81
84
|
},
|
|
@@ -4,7 +4,7 @@ import { prisma } from '@/lib/prisma';
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* POST /api/auth/google/disconnect
|
|
7
|
-
* Déconnecte le compte Google de l'
|
|
7
|
+
* Déconnecte le compte Google de l'administrateur (seuls les admins peuvent déconnecter)
|
|
8
8
|
*/
|
|
9
9
|
export async function POST(request: NextRequest) {
|
|
10
10
|
try {
|
|
@@ -16,7 +16,7 @@ export async function POST(request: NextRequest) {
|
|
|
16
16
|
return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
//
|
|
19
|
+
// Tous les utilisateurs peuvent déconnecter leur compte Google
|
|
20
20
|
await prisma.userGoogleAccount.deleteMany({
|
|
21
21
|
where: { userId: session.user.id },
|
|
22
22
|
});
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { auth } from '@/lib/auth';
|
|
3
|
+
import { prisma } from '@/lib/prisma';
|
|
4
|
+
import { checkPermission } from '@/lib/check-permission';
|
|
5
|
+
|
|
6
|
+
export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
|
7
|
+
try {
|
|
8
|
+
const session = await auth.api.getSession({ headers: request.headers });
|
|
9
|
+
if (!session) {
|
|
10
|
+
return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const [
|
|
14
|
+
companiesViewAll,
|
|
15
|
+
companiesViewOwn,
|
|
16
|
+
companiesViewActivities,
|
|
17
|
+
contactsViewAll,
|
|
18
|
+
contactsViewOwn,
|
|
19
|
+
] = await Promise.all([
|
|
20
|
+
checkPermission('companies.view_all'),
|
|
21
|
+
checkPermission('companies.view_own'),
|
|
22
|
+
checkPermission('companies.view_activities'),
|
|
23
|
+
checkPermission('contacts.view_all'),
|
|
24
|
+
checkPermission('contacts.view_own'),
|
|
25
|
+
]);
|
|
26
|
+
const canViewAll = companiesViewAll || contactsViewAll;
|
|
27
|
+
const canViewOwn = companiesViewOwn || contactsViewOwn;
|
|
28
|
+
const canViewActivities = companiesViewActivities || canViewAll || canViewOwn;
|
|
29
|
+
|
|
30
|
+
if (!canViewActivities) {
|
|
31
|
+
return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const { id } = await params;
|
|
35
|
+
|
|
36
|
+
const company = await prisma.company.findUnique({
|
|
37
|
+
where: { id },
|
|
38
|
+
select: {
|
|
39
|
+
id: true,
|
|
40
|
+
assignedCommercialId: true,
|
|
41
|
+
assignedTeleproId: true,
|
|
42
|
+
createdById: true,
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
if (!company) {
|
|
47
|
+
return NextResponse.json({ error: 'Entreprise non trouvée' }, { status: 404 });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (!canViewAll && canViewOwn) {
|
|
51
|
+
const isOwner =
|
|
52
|
+
company.assignedCommercialId === session.user.id ||
|
|
53
|
+
company.assignedTeleproId === session.user.id ||
|
|
54
|
+
company.createdById === session.user.id;
|
|
55
|
+
if (!isOwner) {
|
|
56
|
+
return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const activities = await prisma.companyActivity.findMany({
|
|
61
|
+
where: { companyId: id },
|
|
62
|
+
include: {
|
|
63
|
+
user: { select: { id: true, name: true } },
|
|
64
|
+
},
|
|
65
|
+
orderBy: { createdAt: 'desc' },
|
|
66
|
+
take: 100,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
return NextResponse.json(activities);
|
|
70
|
+
} catch (error) {
|
|
71
|
+
console.error('Erreur lors de la récupération des activités entreprise:', error);
|
|
72
|
+
return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function POST(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
|
77
|
+
try {
|
|
78
|
+
const session = await auth.api.getSession({ headers: request.headers });
|
|
79
|
+
if (!session) {
|
|
80
|
+
return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const canAddActivity =
|
|
84
|
+
(await checkPermission('companies.add_activity')) ||
|
|
85
|
+
(await checkPermission('companies.edit')) ||
|
|
86
|
+
(await checkPermission('contacts.edit'));
|
|
87
|
+
if (!canAddActivity) {
|
|
88
|
+
return NextResponse.json({ error: 'Permission insuffisante' }, { status: 403 });
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const { id } = await params;
|
|
92
|
+
const body = await request.json();
|
|
93
|
+
const { type, title, content, metadata } = body as {
|
|
94
|
+
type: string;
|
|
95
|
+
title?: string;
|
|
96
|
+
content: string;
|
|
97
|
+
metadata?: unknown;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
if (!type || !content) {
|
|
101
|
+
return NextResponse.json({ error: 'type et content sont requis' }, { status: 400 });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const company = await prisma.company.findUnique({
|
|
105
|
+
where: { id },
|
|
106
|
+
select: { id: true },
|
|
107
|
+
});
|
|
108
|
+
if (!company) {
|
|
109
|
+
return NextResponse.json({ error: 'Entreprise non trouvée' }, { status: 404 });
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const activity = await prisma.companyActivity.create({
|
|
113
|
+
data: {
|
|
114
|
+
companyId: id,
|
|
115
|
+
type,
|
|
116
|
+
title: title || null,
|
|
117
|
+
content,
|
|
118
|
+
metadata: metadata !== undefined && metadata !== null && metadata,
|
|
119
|
+
userId: session.user.id,
|
|
120
|
+
},
|
|
121
|
+
include: {
|
|
122
|
+
user: { select: { id: true, name: true } },
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
return NextResponse.json(activity, { status: 201 });
|
|
127
|
+
} catch (error) {
|
|
128
|
+
console.error("Erreur lors de la création de l'activité entreprise:", error);
|
|
129
|
+
return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 });
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { auth } from '@/lib/auth';
|
|
3
|
+
import { prisma } from '@/lib/prisma';
|
|
4
|
+
import { checkPermission } from '@/lib/check-permission';
|
|
5
|
+
|
|
6
|
+
export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
|
7
|
+
try {
|
|
8
|
+
const session = await auth.api.getSession({ headers: request.headers });
|
|
9
|
+
if (!session) {
|
|
10
|
+
return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const [companiesViewAll, companiesViewOwn, contactsViewAll, contactsViewOwn] =
|
|
14
|
+
await Promise.all([
|
|
15
|
+
checkPermission('companies.view_all'),
|
|
16
|
+
checkPermission('companies.view_own'),
|
|
17
|
+
checkPermission('contacts.view_all'),
|
|
18
|
+
checkPermission('contacts.view_own'),
|
|
19
|
+
]);
|
|
20
|
+
const canViewAll = companiesViewAll || contactsViewAll;
|
|
21
|
+
const canViewOwn = companiesViewOwn || contactsViewOwn;
|
|
22
|
+
|
|
23
|
+
if (!canViewAll && !canViewOwn) {
|
|
24
|
+
return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const { id } = await params;
|
|
28
|
+
|
|
29
|
+
const company = await prisma.company.findUnique({
|
|
30
|
+
where: { id },
|
|
31
|
+
include: {
|
|
32
|
+
contacts: {
|
|
33
|
+
select: {
|
|
34
|
+
id: true,
|
|
35
|
+
firstName: true,
|
|
36
|
+
lastName: true,
|
|
37
|
+
jobTitle: true,
|
|
38
|
+
phone: true,
|
|
39
|
+
email: true,
|
|
40
|
+
status: { select: { id: true, name: true, color: true } },
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
_count: { select: { contacts: true } },
|
|
44
|
+
assignedCommercial: { select: { id: true, name: true, email: true } },
|
|
45
|
+
assignedTelepro: { select: { id: true, name: true, email: true } },
|
|
46
|
+
createdBy: { select: { id: true, name: true, email: true } },
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
if (!company) {
|
|
51
|
+
return NextResponse.json({ error: 'Entreprise non trouvée' }, { status: 404 });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (!canViewAll && canViewOwn) {
|
|
55
|
+
const isOwner =
|
|
56
|
+
company.assignedCommercialId === session.user.id ||
|
|
57
|
+
company.assignedTeleproId === session.user.id ||
|
|
58
|
+
company.createdById === session.user.id;
|
|
59
|
+
if (!isOwner) {
|
|
60
|
+
return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return NextResponse.json(company);
|
|
65
|
+
} catch (error) {
|
|
66
|
+
console.error("Erreur lors de la récupération de l'entreprise:", error);
|
|
67
|
+
return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function PUT(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
|
72
|
+
try {
|
|
73
|
+
const session = await auth.api.getSession({ headers: request.headers });
|
|
74
|
+
if (!session) {
|
|
75
|
+
return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const canEdit =
|
|
79
|
+
(await checkPermission('companies.edit')) || (await checkPermission('contacts.edit'));
|
|
80
|
+
if (!canEdit) {
|
|
81
|
+
return NextResponse.json({ error: 'Permission insuffisante' }, { status: 403 });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const { id } = await params;
|
|
85
|
+
const body = await request.json();
|
|
86
|
+
|
|
87
|
+
const existing = await prisma.company.findUnique({ where: { id } });
|
|
88
|
+
if (!existing) {
|
|
89
|
+
return NextResponse.json({ error: 'Entreprise non trouvée' }, { status: 404 });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const data: Record<string, unknown> = {};
|
|
93
|
+
const fields = [
|
|
94
|
+
'name',
|
|
95
|
+
'phone',
|
|
96
|
+
'email',
|
|
97
|
+
'address',
|
|
98
|
+
'city',
|
|
99
|
+
'postalCode',
|
|
100
|
+
'website',
|
|
101
|
+
'siret',
|
|
102
|
+
'industry',
|
|
103
|
+
'notes',
|
|
104
|
+
'assignedCommercialId',
|
|
105
|
+
'assignedTeleproId',
|
|
106
|
+
] as const;
|
|
107
|
+
for (const field of fields) {
|
|
108
|
+
if (body[field] !== undefined) {
|
|
109
|
+
(data as any)[field] = body[field] === '' ? null : body[field];
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const company = await prisma.company.update({
|
|
114
|
+
where: { id },
|
|
115
|
+
data,
|
|
116
|
+
include: {
|
|
117
|
+
contacts: {
|
|
118
|
+
select: {
|
|
119
|
+
id: true,
|
|
120
|
+
firstName: true,
|
|
121
|
+
lastName: true,
|
|
122
|
+
jobTitle: true,
|
|
123
|
+
phone: true,
|
|
124
|
+
email: true,
|
|
125
|
+
status: { select: { id: true, name: true, color: true } },
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
_count: { select: { contacts: true } },
|
|
129
|
+
assignedCommercial: { select: { id: true, name: true, email: true } },
|
|
130
|
+
assignedTelepro: { select: { id: true, name: true, email: true } },
|
|
131
|
+
createdBy: { select: { id: true, name: true, email: true } },
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// Enregistrer une activité pour la modification
|
|
136
|
+
const changes: { field: string; old: unknown; new: unknown }[] = [];
|
|
137
|
+
for (const field of fields) {
|
|
138
|
+
if (body[field] === undefined) continue;
|
|
139
|
+
const oldVal = (existing as any)[field];
|
|
140
|
+
const newVal = (company as any)[field];
|
|
141
|
+
if (oldVal !== newVal) {
|
|
142
|
+
changes.push({ field, old: oldVal, new: newVal });
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (changes.length > 0) {
|
|
146
|
+
await prisma.companyActivity.create({
|
|
147
|
+
data: {
|
|
148
|
+
companyId: id,
|
|
149
|
+
type: 'COMPANY_UPDATE',
|
|
150
|
+
title: 'Informations modifiées',
|
|
151
|
+
content: `${changes.length} champ(s) modifié(s) : ${changes.map((c) => c.field).join(', ')}`,
|
|
152
|
+
metadata: JSON.parse(JSON.stringify({ changes })),
|
|
153
|
+
userId: session.user.id,
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return NextResponse.json(company);
|
|
159
|
+
} catch (error) {
|
|
160
|
+
console.error("Erreur lors de la mise à jour de l'entreprise:", error);
|
|
161
|
+
return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export async function DELETE(
|
|
166
|
+
request: NextRequest,
|
|
167
|
+
{ params }: { params: Promise<{ id: string }> },
|
|
168
|
+
) {
|
|
169
|
+
try {
|
|
170
|
+
const session = await auth.api.getSession({ headers: request.headers });
|
|
171
|
+
if (!session) {
|
|
172
|
+
return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const canDelete =
|
|
176
|
+
(await checkPermission('companies.delete')) || (await checkPermission('contacts.delete'));
|
|
177
|
+
if (!canDelete) {
|
|
178
|
+
return NextResponse.json({ error: 'Permission insuffisante' }, { status: 403 });
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const { id } = await params;
|
|
182
|
+
|
|
183
|
+
await prisma.contact.updateMany({
|
|
184
|
+
where: { companyId: id },
|
|
185
|
+
data: { companyId: null },
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
await prisma.company.delete({ where: { id } });
|
|
189
|
+
|
|
190
|
+
return NextResponse.json({ success: true });
|
|
191
|
+
} catch (error) {
|
|
192
|
+
console.error("Erreur lors de la suppression de l'entreprise:", error);
|
|
193
|
+
return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 });
|
|
194
|
+
}
|
|
195
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { auth } from '@/lib/auth';
|
|
3
|
+
import { prisma } from '@/lib/prisma';
|
|
4
|
+
import { checkPermission } from '@/lib/check-permission';
|
|
5
|
+
|
|
6
|
+
export async function POST(request: NextRequest) {
|
|
7
|
+
try {
|
|
8
|
+
const session = await auth.api.getSession({
|
|
9
|
+
headers: request.headers,
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
if (!session) {
|
|
13
|
+
return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const canExport = await checkPermission('contacts.export');
|
|
17
|
+
if (!canExport) {
|
|
18
|
+
return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const body = await request.json();
|
|
22
|
+
const { format } = body;
|
|
23
|
+
|
|
24
|
+
if (!format || (format !== 'csv' && format !== 'excel')) {
|
|
25
|
+
return NextResponse.json(
|
|
26
|
+
{ error: 'Format invalide. Utilisez "csv" ou "excel".' },
|
|
27
|
+
{ status: 400 },
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const MAX_EXPORT = 10_000;
|
|
32
|
+
|
|
33
|
+
const companies = await prisma.company.findMany({
|
|
34
|
+
include: {
|
|
35
|
+
contacts: {
|
|
36
|
+
select: {
|
|
37
|
+
civility: true,
|
|
38
|
+
firstName: true,
|
|
39
|
+
lastName: true,
|
|
40
|
+
jobTitle: true,
|
|
41
|
+
phone: true,
|
|
42
|
+
secondaryPhone: true,
|
|
43
|
+
email: true,
|
|
44
|
+
address: true,
|
|
45
|
+
city: true,
|
|
46
|
+
postalCode: true,
|
|
47
|
+
origin: true,
|
|
48
|
+
status: { select: { name: true } },
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
assignedCommercial: { select: { name: true, email: true } },
|
|
52
|
+
assignedTelepro: { select: { name: true, email: true } },
|
|
53
|
+
createdBy: { select: { name: true, email: true } },
|
|
54
|
+
},
|
|
55
|
+
orderBy: { createdAt: 'desc' },
|
|
56
|
+
take: MAX_EXPORT,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
if (companies.length === 0) {
|
|
60
|
+
return NextResponse.json({ error: 'Aucune entreprise à exporter' }, { status: 400 });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const companyHeaders = [
|
|
64
|
+
'Nom',
|
|
65
|
+
'Téléphone',
|
|
66
|
+
'Email',
|
|
67
|
+
'Adresse',
|
|
68
|
+
'Ville',
|
|
69
|
+
'Code postal',
|
|
70
|
+
'Site web',
|
|
71
|
+
'SIRET',
|
|
72
|
+
'Secteur',
|
|
73
|
+
'Commercial',
|
|
74
|
+
'Email Commercial',
|
|
75
|
+
'Télépro',
|
|
76
|
+
'Email Télépro',
|
|
77
|
+
'Créé par',
|
|
78
|
+
'Créé le',
|
|
79
|
+
'Modifié le',
|
|
80
|
+
'Nb contacts',
|
|
81
|
+
'Notes',
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
const companyRows = companies.map((c) => [
|
|
85
|
+
c.name || '',
|
|
86
|
+
c.phone || '',
|
|
87
|
+
c.email || '',
|
|
88
|
+
c.address || '',
|
|
89
|
+
c.city || '',
|
|
90
|
+
c.postalCode || '',
|
|
91
|
+
c.website || '',
|
|
92
|
+
c.siret || '',
|
|
93
|
+
c.industry || '',
|
|
94
|
+
c.assignedCommercial?.name || '',
|
|
95
|
+
c.assignedCommercial?.email || '',
|
|
96
|
+
c.assignedTelepro?.name || '',
|
|
97
|
+
c.assignedTelepro?.email || '',
|
|
98
|
+
c.createdBy?.name || '',
|
|
99
|
+
c.createdAt ? new Date(c.createdAt).toLocaleString('fr-FR') : '',
|
|
100
|
+
c.updatedAt ? new Date(c.updatedAt).toLocaleString('fr-FR') : '',
|
|
101
|
+
String(c.contacts.length),
|
|
102
|
+
c.notes || '',
|
|
103
|
+
]);
|
|
104
|
+
|
|
105
|
+
const contactHeaders = [
|
|
106
|
+
'Entreprise',
|
|
107
|
+
'Civilité',
|
|
108
|
+
'Prénom',
|
|
109
|
+
'Nom',
|
|
110
|
+
'Poste',
|
|
111
|
+
'Téléphone',
|
|
112
|
+
'Téléphone secondaire',
|
|
113
|
+
'Email',
|
|
114
|
+
'Adresse',
|
|
115
|
+
'Ville',
|
|
116
|
+
'Code postal',
|
|
117
|
+
'Origine',
|
|
118
|
+
'Statut',
|
|
119
|
+
];
|
|
120
|
+
|
|
121
|
+
const contactRows: string[][] = [];
|
|
122
|
+
for (const company of companies) {
|
|
123
|
+
for (const contact of company.contacts) {
|
|
124
|
+
contactRows.push([
|
|
125
|
+
company.name || '',
|
|
126
|
+
contact.civility || '',
|
|
127
|
+
contact.firstName || '',
|
|
128
|
+
contact.lastName || '',
|
|
129
|
+
contact.jobTitle || '',
|
|
130
|
+
contact.phone || '',
|
|
131
|
+
contact.secondaryPhone || '',
|
|
132
|
+
contact.email || '',
|
|
133
|
+
contact.address || '',
|
|
134
|
+
contact.city || '',
|
|
135
|
+
contact.postalCode || '',
|
|
136
|
+
contact.origin || '',
|
|
137
|
+
contact.status?.name || '',
|
|
138
|
+
]);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const dateStr = new Date().toISOString().split('T')[0];
|
|
143
|
+
|
|
144
|
+
function toCsvBlock(headers: string[], rows: string[][]): string {
|
|
145
|
+
const escape = (cell: string) => `"${String(cell || '').replace(/"/g, '""')}"`;
|
|
146
|
+
return [headers.map(escape).join(','), ...rows.map((row) => row.map(escape).join(','))].join(
|
|
147
|
+
'\n',
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (format === 'csv') {
|
|
152
|
+
const csvContent =
|
|
153
|
+
'--- ENTREPRISES ---\n' +
|
|
154
|
+
toCsvBlock(companyHeaders, companyRows) +
|
|
155
|
+
'\n\n--- CONTACTS RELIÉS ---\n' +
|
|
156
|
+
toCsvBlock(contactHeaders, contactRows);
|
|
157
|
+
|
|
158
|
+
const csvWithBOM = '\uFEFF' + csvContent;
|
|
159
|
+
|
|
160
|
+
return new NextResponse(csvWithBOM, {
|
|
161
|
+
headers: {
|
|
162
|
+
'Content-Type': 'text/csv; charset=utf-8',
|
|
163
|
+
'Content-Disposition': `attachment; filename="entreprises_${dateStr}.csv"`,
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
} else {
|
|
167
|
+
try {
|
|
168
|
+
const XLSX = await import('xlsx');
|
|
169
|
+
const workbook = XLSX.utils.book_new();
|
|
170
|
+
|
|
171
|
+
const wsCompanies = XLSX.utils.aoa_to_sheet([companyHeaders, ...companyRows]);
|
|
172
|
+
XLSX.utils.book_append_sheet(workbook, wsCompanies, 'Entreprises');
|
|
173
|
+
|
|
174
|
+
const wsContacts = XLSX.utils.aoa_to_sheet([contactHeaders, ...contactRows]);
|
|
175
|
+
XLSX.utils.book_append_sheet(workbook, wsContacts, 'Contacts');
|
|
176
|
+
|
|
177
|
+
const excelBuffer = XLSX.write(workbook, { type: 'buffer', bookType: 'xlsx' });
|
|
178
|
+
|
|
179
|
+
return new NextResponse(excelBuffer, {
|
|
180
|
+
headers: {
|
|
181
|
+
'Content-Type': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
182
|
+
'Content-Disposition': `attachment; filename="entreprises_${dateStr}.xlsx"`,
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
} catch {
|
|
186
|
+
const csvContent =
|
|
187
|
+
'--- ENTREPRISES ---\n' +
|
|
188
|
+
toCsvBlock(companyHeaders, companyRows) +
|
|
189
|
+
'\n\n--- CONTACTS RELIÉS ---\n' +
|
|
190
|
+
toCsvBlock(contactHeaders, contactRows);
|
|
191
|
+
|
|
192
|
+
const csvWithBOM = '\uFEFF' + csvContent;
|
|
193
|
+
|
|
194
|
+
return new NextResponse(csvWithBOM, {
|
|
195
|
+
headers: {
|
|
196
|
+
'Content-Type': 'text/csv; charset=utf-8',
|
|
197
|
+
'Content-Disposition': `attachment; filename="entreprises_${dateStr}.csv"`,
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
} catch (error) {
|
|
203
|
+
console.error("Erreur lors de l'export des entreprises:", error);
|
|
204
|
+
return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 });
|
|
205
|
+
}
|
|
206
|
+
}
|