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
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Utilitaires pour gérer les fichiers avec Google Drive API
|
|
3
|
-
*
|
|
4
|
-
* Google Drive est configuré de manière centralisée :
|
|
5
|
-
* un seul administrateur connecte son compte Google Drive,
|
|
6
|
-
* et tous les utilisateurs uploadent dans ce même Drive.
|
|
7
3
|
*/
|
|
8
4
|
|
|
9
5
|
import { prisma } from '@/lib/prisma';
|
|
10
6
|
import { getValidAccessToken } from './google-calendar';
|
|
7
|
+
import { encrypt, decrypt } from './encryption';
|
|
8
|
+
import { googleFetch } from './google-fetch';
|
|
11
9
|
|
|
12
10
|
// Nom de l'application (peut être configuré via variable d'environnement)
|
|
13
|
-
const APP_NAME = process.env.APP_NAME || '
|
|
11
|
+
const APP_NAME = process.env.APP_NAME || 'Gold Blessing';
|
|
12
|
+
|
|
13
|
+
const folderIdCache = new Map<string, string>();
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* Récupère le compte Google de l'administrateur
|
|
@@ -29,11 +29,15 @@ export async function getAdminGoogleAccount() {
|
|
|
29
29
|
|
|
30
30
|
if (!adminUser || !adminUser.googleAccount) {
|
|
31
31
|
throw new Error(
|
|
32
|
-
'Aucun compte Google
|
|
32
|
+
'Aucun compte Google configuré. Veuillez demander à un administrateur de connecter son compte Google dans Paramètres > Général > Profil.',
|
|
33
33
|
);
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
return
|
|
36
|
+
return {
|
|
37
|
+
...adminUser.googleAccount,
|
|
38
|
+
accessToken: decrypt(adminUser.googleAccount.accessToken),
|
|
39
|
+
refreshToken: decrypt(adminUser.googleAccount.refreshToken),
|
|
40
|
+
};
|
|
37
41
|
}
|
|
38
42
|
|
|
39
43
|
/**
|
|
@@ -44,9 +48,10 @@ async function getOrCreateFolder(
|
|
|
44
48
|
folderName: string,
|
|
45
49
|
parentId?: string,
|
|
46
50
|
): Promise<string> {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
51
|
+
const cacheKey = `${parentId || 'root'}:${folderName}`;
|
|
52
|
+
const cached = folderIdCache.get(cacheKey);
|
|
53
|
+
if (cached) return cached;
|
|
54
|
+
|
|
50
55
|
const escapedName = folderName.replace(/'/g, "\\'");
|
|
51
56
|
let query = `name='${escapedName}' and mimeType='application/vnd.google-apps.folder' and trashed=false`;
|
|
52
57
|
if (parentId) {
|
|
@@ -58,7 +63,7 @@ async function getOrCreateFolder(
|
|
|
58
63
|
|
|
59
64
|
// Chercher si le dossier existe déjà
|
|
60
65
|
const searchUrl = `https://www.googleapis.com/drive/v3/files?q=${encodeURIComponent(query)}&fields=files(id,name,parents)&pageSize=10`;
|
|
61
|
-
const searchResponse = await
|
|
66
|
+
const searchResponse = await googleFetch(searchUrl, {
|
|
62
67
|
headers: {
|
|
63
68
|
Authorization: `Bearer ${accessToken}`,
|
|
64
69
|
},
|
|
@@ -73,50 +78,33 @@ async function getOrCreateFolder(
|
|
|
73
78
|
|
|
74
79
|
// Si le dossier existe, vérifier qu'il est bien dans le bon parent
|
|
75
80
|
if (searchData.files && searchData.files.length > 0) {
|
|
76
|
-
|
|
81
|
+
let folderId: string;
|
|
82
|
+
|
|
77
83
|
if (!parentId) {
|
|
78
|
-
// Vérifier que le dossier est bien à la racine (parents contient 'root' ou est vide)
|
|
79
84
|
const rootFolder = searchData.files.find((file: any) => {
|
|
80
85
|
if (!file.parents || file.parents.length === 0) return true;
|
|
81
|
-
// Vérifier si le dossier est directement à la racine
|
|
82
86
|
return file.parents.length === 1 && file.parents[0] === 'root';
|
|
83
87
|
});
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
return searchData.files[0].id;
|
|
88
|
+
folderId = rootFolder ? rootFolder.id : searchData.files[0].id;
|
|
89
|
+
} else {
|
|
90
|
+
const matchingFolder = searchData.files.find(
|
|
91
|
+
(file: any) => file.parents && file.parents.includes(parentId),
|
|
92
|
+
);
|
|
93
|
+
folderId = matchingFolder ? matchingFolder.id : searchData.files[0].id;
|
|
91
94
|
}
|
|
92
95
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
(file: any) => file.parents && file.parents.includes(parentId),
|
|
96
|
-
);
|
|
97
|
-
|
|
98
|
-
if (matchingFolder) {
|
|
99
|
-
return matchingFolder.id;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// Si aucun ne correspond exactement, prendre le premier (cas de migration)
|
|
103
|
-
return searchData.files[0].id;
|
|
96
|
+
folderIdCache.set(cacheKey, folderId);
|
|
97
|
+
return folderId;
|
|
104
98
|
}
|
|
105
99
|
|
|
106
100
|
// Sinon, créer le dossier
|
|
107
101
|
const folderData: any = {
|
|
108
102
|
name: folderName,
|
|
109
103
|
mimeType: 'application/vnd.google-apps.folder',
|
|
104
|
+
parents: [parentId || 'root'],
|
|
110
105
|
};
|
|
111
106
|
|
|
112
|
-
|
|
113
|
-
folderData.parents = [parentId];
|
|
114
|
-
} else {
|
|
115
|
-
// Pour la racine, on spécifie explicitement 'root' comme parent
|
|
116
|
-
folderData.parents = ['root'];
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
const createResponse = await fetch('https://www.googleapis.com/drive/v3/files', {
|
|
107
|
+
const createResponse = await googleFetch('https://www.googleapis.com/drive/v3/files', {
|
|
120
108
|
method: 'POST',
|
|
121
109
|
headers: {
|
|
122
110
|
Authorization: `Bearer ${accessToken}`,
|
|
@@ -132,14 +120,13 @@ async function getOrCreateFolder(
|
|
|
132
120
|
|
|
133
121
|
const createdData = await createResponse.json();
|
|
134
122
|
|
|
135
|
-
// Configurer les permissions pour rendre le dossier accessible avec le lien
|
|
136
123
|
try {
|
|
137
124
|
await setFilePublicWithLink(accessToken, createdData.id);
|
|
138
125
|
} catch (permError) {
|
|
139
126
|
console.error('Erreur lors de la configuration des permissions du dossier:', permError);
|
|
140
|
-
// On continue même si la configuration des permissions échoue
|
|
141
127
|
}
|
|
142
128
|
|
|
129
|
+
folderIdCache.set(cacheKey, createdData.id);
|
|
143
130
|
return createdData.id;
|
|
144
131
|
}
|
|
145
132
|
|
|
@@ -148,7 +135,7 @@ async function getOrCreateFolder(
|
|
|
148
135
|
* Type: 'anyone' avec rôle 'reader' = accessible à quiconque possède le lien
|
|
149
136
|
*/
|
|
150
137
|
async function setFilePublicWithLink(accessToken: string, fileId: string): Promise<void> {
|
|
151
|
-
const permissionResponse = await
|
|
138
|
+
const permissionResponse = await googleFetch(
|
|
152
139
|
`https://www.googleapis.com/drive/v3/files/${fileId}/permissions`,
|
|
153
140
|
{
|
|
154
141
|
method: 'POST',
|
|
@@ -171,9 +158,8 @@ async function setFilePublicWithLink(accessToken: string, fileId: string): Promi
|
|
|
171
158
|
|
|
172
159
|
/**
|
|
173
160
|
* Crée un dossier dans Google Drive pour un contact
|
|
174
|
-
* Structure:
|
|
161
|
+
* Structure: Gold Blessing > Contacts > Contact - [Nom]
|
|
175
162
|
* Retourne l'ID du dossier créé ou existant
|
|
176
|
-
* Utilise le compte Google Drive de l'administrateur
|
|
177
163
|
*/
|
|
178
164
|
export async function getOrCreateContactFolder(
|
|
179
165
|
userId: string,
|
|
@@ -195,16 +181,16 @@ export async function getOrCreateContactFolder(
|
|
|
195
181
|
await prisma.userGoogleAccount.update({
|
|
196
182
|
where: { userId: googleAccount.userId },
|
|
197
183
|
data: {
|
|
198
|
-
accessToken,
|
|
184
|
+
accessToken: encrypt(accessToken),
|
|
199
185
|
tokenExpiresAt,
|
|
200
186
|
},
|
|
201
187
|
});
|
|
202
188
|
}
|
|
203
189
|
|
|
204
|
-
// 1. Créer ou récupérer le dossier racine "
|
|
190
|
+
// 1. Créer ou récupérer le dossier racine "Gold Blessing"
|
|
205
191
|
const appFolderId = await getOrCreateFolder(accessToken, APP_NAME);
|
|
206
192
|
|
|
207
|
-
// 2. Créer ou récupérer le dossier "Contacts" dans "
|
|
193
|
+
// 2. Créer ou récupérer le dossier "Contacts" dans "Gold Blessing"
|
|
208
194
|
const contactsFolderId = await getOrCreateFolder(accessToken, 'Contacts', appFolderId);
|
|
209
195
|
|
|
210
196
|
// 3. Créer ou récupérer le dossier du contact dans "Contacts"
|
|
@@ -214,17 +200,106 @@ export async function getOrCreateContactFolder(
|
|
|
214
200
|
return contactFolderId;
|
|
215
201
|
}
|
|
216
202
|
|
|
203
|
+
/**
|
|
204
|
+
* Crée ou récupère un dossier dans Google Drive pour une tournée
|
|
205
|
+
* Structure: [Nom du projet] > Tournées > [Nom] [Numéro]
|
|
206
|
+
*/
|
|
207
|
+
export async function getOrCreateTourFolder(
|
|
208
|
+
userId: string,
|
|
209
|
+
tourId: string,
|
|
210
|
+
tourName: string,
|
|
211
|
+
tourNumber?: string | null,
|
|
212
|
+
): Promise<string> {
|
|
213
|
+
const googleAccount = await getAdminGoogleAccount();
|
|
214
|
+
|
|
215
|
+
const accessToken = await getValidAccessToken(
|
|
216
|
+
googleAccount.accessToken,
|
|
217
|
+
googleAccount.refreshToken,
|
|
218
|
+
googleAccount.tokenExpiresAt,
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
// Mettre à jour le token si nécessaire
|
|
222
|
+
if (accessToken !== googleAccount.accessToken) {
|
|
223
|
+
const tokenExpiresAt = new Date();
|
|
224
|
+
tokenExpiresAt.setSeconds(tokenExpiresAt.getSeconds() + 3600);
|
|
225
|
+
await prisma.userGoogleAccount.update({
|
|
226
|
+
where: { userId: googleAccount.userId },
|
|
227
|
+
data: {
|
|
228
|
+
accessToken: encrypt(accessToken),
|
|
229
|
+
tokenExpiresAt,
|
|
230
|
+
},
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// 1. Dossier racine "[Nom du projet]"
|
|
235
|
+
const appFolderId = await getOrCreateFolder(accessToken, APP_NAME);
|
|
236
|
+
|
|
237
|
+
// 2. Dossier "Tournées"
|
|
238
|
+
const toursFolderId = await getOrCreateFolder(accessToken, 'Tournées', appFolderId);
|
|
239
|
+
|
|
240
|
+
// 3. Dossier de la tournée : "[Nom] [Numéro]"
|
|
241
|
+
const tourFolderName = tourNumber
|
|
242
|
+
? `${tourName || tourId} ${tourNumber}`
|
|
243
|
+
: tourName || `Tournée ${tourId}`;
|
|
244
|
+
const tourFolderId = await getOrCreateFolder(accessToken, tourFolderName, toursFolderId);
|
|
245
|
+
|
|
246
|
+
return tourFolderId;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Crée ou récupère le dossier "Transactions" dans le dossier du contact
|
|
251
|
+
*/
|
|
252
|
+
export async function getOrCreateTransactionsFolder(
|
|
253
|
+
userId: string,
|
|
254
|
+
contactId: string,
|
|
255
|
+
contactName: string,
|
|
256
|
+
): Promise<string> {
|
|
257
|
+
const contactFolderId = await getOrCreateContactFolder(userId, contactId, contactName);
|
|
258
|
+
|
|
259
|
+
const googleAccount = await getAdminGoogleAccount();
|
|
260
|
+
|
|
261
|
+
const accessToken = await getValidAccessToken(
|
|
262
|
+
googleAccount.accessToken,
|
|
263
|
+
googleAccount.refreshToken,
|
|
264
|
+
googleAccount.tokenExpiresAt,
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
// Mettre à jour le token si nécessaire
|
|
268
|
+
if (accessToken !== googleAccount.accessToken) {
|
|
269
|
+
const tokenExpiresAt = new Date();
|
|
270
|
+
tokenExpiresAt.setSeconds(tokenExpiresAt.getSeconds() + 3600);
|
|
271
|
+
await prisma.userGoogleAccount.update({
|
|
272
|
+
where: { userId: googleAccount.userId },
|
|
273
|
+
data: {
|
|
274
|
+
accessToken: encrypt(accessToken),
|
|
275
|
+
tokenExpiresAt,
|
|
276
|
+
},
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Créer ou récupérer le dossier "Transactions" dans le dossier du contact
|
|
281
|
+
const transactionsFolderId = await getOrCreateFolder(
|
|
282
|
+
accessToken,
|
|
283
|
+
'Transactions',
|
|
284
|
+
contactFolderId,
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
return transactionsFolderId;
|
|
288
|
+
}
|
|
289
|
+
|
|
217
290
|
/**
|
|
218
291
|
* Upload un fichier vers Google Drive dans le dossier du contact
|
|
219
|
-
* Utilise le compte Google Drive de l'administrateur
|
|
220
292
|
*/
|
|
221
293
|
export async function uploadFileToDrive(
|
|
222
294
|
userId: string,
|
|
223
295
|
contactId: string,
|
|
224
296
|
contactName: string,
|
|
225
297
|
file: File,
|
|
298
|
+
useTransactionsFolder: boolean = false,
|
|
226
299
|
): Promise<{ fileId: string; webViewLink: string }> {
|
|
227
|
-
const folderId =
|
|
300
|
+
const folderId = useTransactionsFolder
|
|
301
|
+
? await getOrCreateTransactionsFolder(userId, contactId, contactName)
|
|
302
|
+
: await getOrCreateContactFolder(userId, contactId, contactName);
|
|
228
303
|
|
|
229
304
|
const googleAccount = await getAdminGoogleAccount();
|
|
230
305
|
|
|
@@ -241,7 +316,7 @@ export async function uploadFileToDrive(
|
|
|
241
316
|
await prisma.userGoogleAccount.update({
|
|
242
317
|
where: { userId: googleAccount.userId },
|
|
243
318
|
data: {
|
|
244
|
-
accessToken,
|
|
319
|
+
accessToken: encrypt(accessToken),
|
|
245
320
|
tokenExpiresAt,
|
|
246
321
|
},
|
|
247
322
|
});
|
|
@@ -249,7 +324,7 @@ export async function uploadFileToDrive(
|
|
|
249
324
|
|
|
250
325
|
// Vérifier si un fichier avec le même nom existe déjà dans le dossier
|
|
251
326
|
const searchQuery = `name='${encodeURIComponent(file.name)}' and '${folderId}' in parents and trashed=false`;
|
|
252
|
-
const searchResponse = await
|
|
327
|
+
const searchResponse = await googleFetch(
|
|
253
328
|
`https://www.googleapis.com/drive/v3/files?q=${encodeURIComponent(searchQuery)}&fields=files(id,name,webViewLink)`,
|
|
254
329
|
{
|
|
255
330
|
headers: {
|
|
@@ -264,7 +339,7 @@ export async function uploadFileToDrive(
|
|
|
264
339
|
if (searchData.files && searchData.files.length > 0) {
|
|
265
340
|
for (const existingFile of searchData.files) {
|
|
266
341
|
try {
|
|
267
|
-
await
|
|
342
|
+
await googleFetch(`https://www.googleapis.com/drive/v3/files/${existingFile.id}`, {
|
|
268
343
|
method: 'DELETE',
|
|
269
344
|
headers: {
|
|
270
345
|
Authorization: `Bearer ${accessToken}`,
|
|
@@ -289,7 +364,7 @@ export async function uploadFileToDrive(
|
|
|
289
364
|
formData.append('file', file);
|
|
290
365
|
|
|
291
366
|
// Upload le fichier
|
|
292
|
-
const uploadResponse = await
|
|
367
|
+
const uploadResponse = await googleFetch(
|
|
293
368
|
'https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart&fields=id,webViewLink',
|
|
294
369
|
{
|
|
295
370
|
method: 'POST',
|
|
@@ -322,13 +397,17 @@ export async function uploadFileToDrive(
|
|
|
322
397
|
}
|
|
323
398
|
|
|
324
399
|
/**
|
|
325
|
-
*
|
|
326
|
-
* Utilise le compte Google Drive de l'administrateur
|
|
400
|
+
* Crée ou récupère un sous-dossier dans le dossier de la tournée
|
|
327
401
|
*/
|
|
328
|
-
export async function
|
|
402
|
+
export async function getOrCreateTourSubFolder(
|
|
329
403
|
userId: string,
|
|
330
|
-
|
|
331
|
-
|
|
404
|
+
tourId: string,
|
|
405
|
+
tourName: string,
|
|
406
|
+
subFolderName: 'mairie' | 'lieu' | 'campagne',
|
|
407
|
+
tourNumber?: string | null,
|
|
408
|
+
): Promise<string> {
|
|
409
|
+
const tourFolderId = await getOrCreateTourFolder(userId, tourId, tourName, tourNumber);
|
|
410
|
+
|
|
332
411
|
const googleAccount = await getAdminGoogleAccount();
|
|
333
412
|
|
|
334
413
|
const accessToken = await getValidAccessToken(
|
|
@@ -337,27 +416,120 @@ export async function getFileInfo(
|
|
|
337
416
|
googleAccount.tokenExpiresAt,
|
|
338
417
|
);
|
|
339
418
|
|
|
340
|
-
|
|
341
|
-
|
|
419
|
+
if (accessToken !== googleAccount.accessToken) {
|
|
420
|
+
const tokenExpiresAt = new Date();
|
|
421
|
+
tokenExpiresAt.setSeconds(tokenExpiresAt.getSeconds() + 3600);
|
|
422
|
+
await prisma.userGoogleAccount.update({
|
|
423
|
+
where: { userId: googleAccount.userId },
|
|
424
|
+
data: {
|
|
425
|
+
accessToken: encrypt(accessToken),
|
|
426
|
+
tokenExpiresAt,
|
|
427
|
+
},
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Capitaliser la première lettre pour le nom du dossier
|
|
432
|
+
const folderName = subFolderName.charAt(0).toUpperCase() + subFolderName.slice(1);
|
|
433
|
+
const subFolderId = await getOrCreateFolder(accessToken, folderName, tourFolderId);
|
|
434
|
+
|
|
435
|
+
return subFolderId;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Upload un fichier vers Google Drive dans le dossier de la tournée
|
|
440
|
+
*/
|
|
441
|
+
export async function uploadTourFileToDrive(
|
|
442
|
+
userId: string,
|
|
443
|
+
tourId: string,
|
|
444
|
+
tourName: string,
|
|
445
|
+
file: File,
|
|
446
|
+
tourNumber?: string | null,
|
|
447
|
+
subFolder?: 'mairie' | 'lieu' | 'campagne',
|
|
448
|
+
): Promise<{ fileId: string; webViewLink: string }> {
|
|
449
|
+
const folderId = subFolder
|
|
450
|
+
? await getOrCreateTourSubFolder(userId, tourId, tourName, subFolder, tourNumber)
|
|
451
|
+
: await getOrCreateTourFolder(userId, tourId, tourName, tourNumber);
|
|
452
|
+
|
|
453
|
+
const googleAccount = await getAdminGoogleAccount();
|
|
454
|
+
|
|
455
|
+
const accessToken = await getValidAccessToken(
|
|
456
|
+
googleAccount.accessToken,
|
|
457
|
+
googleAccount.refreshToken,
|
|
458
|
+
googleAccount.tokenExpiresAt,
|
|
459
|
+
);
|
|
460
|
+
|
|
461
|
+
if (accessToken !== googleAccount.accessToken) {
|
|
462
|
+
const tokenExpiresAt = new Date();
|
|
463
|
+
tokenExpiresAt.setSeconds(tokenExpiresAt.getSeconds() + 3600);
|
|
464
|
+
await prisma.userGoogleAccount.update({
|
|
465
|
+
where: { userId: googleAccount.userId },
|
|
466
|
+
data: {
|
|
467
|
+
accessToken: encrypt(accessToken),
|
|
468
|
+
tokenExpiresAt,
|
|
469
|
+
},
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const metadata = {
|
|
474
|
+
name: file.name,
|
|
475
|
+
parents: [folderId],
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
const formData = new FormData();
|
|
479
|
+
formData.append('metadata', new Blob([JSON.stringify(metadata)], { type: 'application/json' }));
|
|
480
|
+
formData.append('file', file);
|
|
481
|
+
|
|
482
|
+
const uploadResponse = await googleFetch(
|
|
483
|
+
'https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart&fields=id,webViewLink',
|
|
342
484
|
{
|
|
485
|
+
method: 'POST',
|
|
343
486
|
headers: {
|
|
344
487
|
Authorization: `Bearer ${accessToken}`,
|
|
345
488
|
},
|
|
489
|
+
body: formData,
|
|
346
490
|
},
|
|
347
491
|
);
|
|
348
492
|
|
|
349
|
-
if (!
|
|
350
|
-
|
|
493
|
+
if (!uploadResponse.ok) {
|
|
494
|
+
const error = await uploadResponse.json();
|
|
495
|
+
throw new Error(`Erreur lors de l'upload: ${JSON.stringify(error)}`);
|
|
351
496
|
}
|
|
352
497
|
|
|
353
|
-
|
|
498
|
+
const fileData = await uploadResponse.json();
|
|
499
|
+
|
|
500
|
+
try {
|
|
501
|
+
await setFilePublicWithLink(accessToken, fileData.id);
|
|
502
|
+
} catch (permError) {
|
|
503
|
+
console.error(
|
|
504
|
+
'Erreur lors de la configuration des permissions du fichier (tournée):',
|
|
505
|
+
permError,
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
return {
|
|
510
|
+
fileId: fileData.id,
|
|
511
|
+
webViewLink: fileData.webViewLink || `https://drive.google.com/file/d/${fileData.id}/view`,
|
|
512
|
+
};
|
|
354
513
|
}
|
|
355
514
|
|
|
356
515
|
/**
|
|
357
|
-
*
|
|
358
|
-
*
|
|
516
|
+
* Upload un buffer (PDF) vers Google Drive dans le dossier de la tournée
|
|
517
|
+
* Utilisé pour uploader des fichiers générés comme le CERFA
|
|
359
518
|
*/
|
|
360
|
-
export async function
|
|
519
|
+
export async function uploadTourBufferToDrive(
|
|
520
|
+
userId: string,
|
|
521
|
+
tourId: string,
|
|
522
|
+
tourName: string,
|
|
523
|
+
buffer: Buffer,
|
|
524
|
+
fileName: string,
|
|
525
|
+
mimeType: string = 'application/pdf',
|
|
526
|
+
tourNumber?: string | null,
|
|
527
|
+
subFolder?: 'mairie' | 'lieu' | 'campagne',
|
|
528
|
+
): Promise<{ fileId: string; webViewLink: string }> {
|
|
529
|
+
const folderId = subFolder
|
|
530
|
+
? await getOrCreateTourSubFolder(userId, tourId, tourName, subFolder, tourNumber)
|
|
531
|
+
: await getOrCreateTourFolder(userId, tourId, tourName, tourNumber);
|
|
532
|
+
|
|
361
533
|
const googleAccount = await getAdminGoogleAccount();
|
|
362
534
|
|
|
363
535
|
const accessToken = await getValidAccessToken(
|
|
@@ -366,15 +538,564 @@ export async function deleteFileFromDrive(userId: string, fileId: string): Promi
|
|
|
366
538
|
googleAccount.tokenExpiresAt,
|
|
367
539
|
);
|
|
368
540
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
541
|
+
if (accessToken !== googleAccount.accessToken) {
|
|
542
|
+
const tokenExpiresAt = new Date();
|
|
543
|
+
tokenExpiresAt.setSeconds(tokenExpiresAt.getSeconds() + 3600);
|
|
544
|
+
await prisma.userGoogleAccount.update({
|
|
545
|
+
where: { userId: googleAccount.userId },
|
|
546
|
+
data: {
|
|
547
|
+
accessToken,
|
|
548
|
+
tokenExpiresAt,
|
|
549
|
+
},
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// Vérifier si un fichier avec le même nom existe déjà dans le dossier
|
|
554
|
+
const searchQuery = `name='${fileName.replace(/'/g, "\\'")}' and '${folderId}' in parents and trashed=false`;
|
|
555
|
+
const searchResponse = await googleFetch(
|
|
556
|
+
`https://www.googleapis.com/drive/v3/files?q=${encodeURIComponent(searchQuery)}&fields=files(id,name,webViewLink)`,
|
|
557
|
+
{
|
|
558
|
+
headers: {
|
|
559
|
+
Authorization: `Bearer ${accessToken}`,
|
|
560
|
+
},
|
|
373
561
|
},
|
|
374
|
-
|
|
562
|
+
);
|
|
375
563
|
|
|
376
|
-
if (
|
|
377
|
-
|
|
378
|
-
|
|
564
|
+
if (searchResponse.ok) {
|
|
565
|
+
const searchData = await searchResponse.json();
|
|
566
|
+
// Si un fichier avec le même nom existe déjà, le supprimer avant d'uploader le nouveau
|
|
567
|
+
if (searchData.files && searchData.files.length > 0) {
|
|
568
|
+
for (const existingFile of searchData.files) {
|
|
569
|
+
try {
|
|
570
|
+
await googleFetch(`https://www.googleapis.com/drive/v3/files/${existingFile.id}`, {
|
|
571
|
+
method: 'DELETE',
|
|
572
|
+
headers: {
|
|
573
|
+
Authorization: `Bearer ${accessToken}`,
|
|
574
|
+
},
|
|
575
|
+
});
|
|
576
|
+
} catch (error) {
|
|
577
|
+
console.error('Erreur lors de la suppression du fichier existant:', error);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
const metadata = {
|
|
584
|
+
name: fileName,
|
|
585
|
+
parents: [folderId],
|
|
586
|
+
};
|
|
587
|
+
|
|
588
|
+
// Créer un Blob à partir du Buffer (convertir en Uint8Array pour compatibilité TypeScript)
|
|
589
|
+
const uint8Array = new Uint8Array(buffer);
|
|
590
|
+
const blob = new Blob([uint8Array], { type: mimeType });
|
|
591
|
+
|
|
592
|
+
const formData = new FormData();
|
|
593
|
+
formData.append('metadata', new Blob([JSON.stringify(metadata)], { type: 'application/json' }));
|
|
594
|
+
formData.append('file', blob, fileName);
|
|
595
|
+
|
|
596
|
+
const uploadResponse = await googleFetch(
|
|
597
|
+
'https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart&fields=id,webViewLink',
|
|
598
|
+
{
|
|
599
|
+
method: 'POST',
|
|
600
|
+
headers: {
|
|
601
|
+
Authorization: `Bearer ${accessToken}`,
|
|
602
|
+
},
|
|
603
|
+
body: formData,
|
|
604
|
+
},
|
|
605
|
+
);
|
|
606
|
+
|
|
607
|
+
if (!uploadResponse.ok) {
|
|
608
|
+
const error = await uploadResponse.json();
|
|
609
|
+
throw new Error(`Erreur lors de l'upload: ${JSON.stringify(error)}`);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
const fileData = await uploadResponse.json();
|
|
613
|
+
|
|
614
|
+
try {
|
|
615
|
+
await setFilePublicWithLink(accessToken, fileData.id);
|
|
616
|
+
} catch (permError) {
|
|
617
|
+
console.error(
|
|
618
|
+
'Erreur lors de la configuration des permissions du fichier (tournée):',
|
|
619
|
+
permError,
|
|
620
|
+
);
|
|
379
621
|
}
|
|
622
|
+
|
|
623
|
+
return {
|
|
624
|
+
fileId: fileData.id,
|
|
625
|
+
webViewLink: fileData.webViewLink || `https://drive.google.com/file/d/${fileData.id}/view`,
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* Crée ou récupère un dossier pour les documents requis de la mairie
|
|
631
|
+
* Structure: [Nom du projet] > Paramètres > Documents requis mairie
|
|
632
|
+
*/
|
|
633
|
+
export async function getOrCreateCityHallDocumentsFolder(userId: string): Promise<string> {
|
|
634
|
+
const googleAccount = await getAdminGoogleAccount();
|
|
635
|
+
|
|
636
|
+
const accessToken = await getValidAccessToken(
|
|
637
|
+
googleAccount.accessToken,
|
|
638
|
+
googleAccount.refreshToken,
|
|
639
|
+
googleAccount.tokenExpiresAt,
|
|
640
|
+
);
|
|
641
|
+
|
|
642
|
+
if (accessToken !== googleAccount.accessToken) {
|
|
643
|
+
const tokenExpiresAt = new Date();
|
|
644
|
+
tokenExpiresAt.setSeconds(tokenExpiresAt.getSeconds() + 3600);
|
|
645
|
+
await prisma.userGoogleAccount.update({
|
|
646
|
+
where: { userId: googleAccount.userId },
|
|
647
|
+
data: {
|
|
648
|
+
accessToken,
|
|
649
|
+
tokenExpiresAt,
|
|
650
|
+
},
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// 1. Créer ou récupérer le dossier racine
|
|
655
|
+
const appFolderId = await getOrCreateFolder(accessToken, APP_NAME);
|
|
656
|
+
|
|
657
|
+
// 2. Créer ou récupérer le dossier "Paramètres"
|
|
658
|
+
const settingsFolderId = await getOrCreateFolder(accessToken, 'Paramètres', appFolderId);
|
|
659
|
+
|
|
660
|
+
// 3. Créer ou récupérer le dossier "Documents requis mairie"
|
|
661
|
+
const documentsFolderId = await getOrCreateFolder(
|
|
662
|
+
accessToken,
|
|
663
|
+
'Documents requis mairie',
|
|
664
|
+
settingsFolderId,
|
|
665
|
+
);
|
|
666
|
+
|
|
667
|
+
return documentsFolderId;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* Upload un fichier vers Google Drive dans le dossier des documents requis mairie
|
|
672
|
+
*/
|
|
673
|
+
export async function uploadCityHallRequiredDocument(
|
|
674
|
+
userId: string,
|
|
675
|
+
file: File,
|
|
676
|
+
): Promise<{ fileId: string; webViewLink: string }> {
|
|
677
|
+
const folderId = await getOrCreateCityHallDocumentsFolder(userId);
|
|
678
|
+
|
|
679
|
+
const googleAccount = await getAdminGoogleAccount();
|
|
680
|
+
|
|
681
|
+
const accessToken = await getValidAccessToken(
|
|
682
|
+
googleAccount.accessToken,
|
|
683
|
+
googleAccount.refreshToken,
|
|
684
|
+
googleAccount.tokenExpiresAt,
|
|
685
|
+
);
|
|
686
|
+
|
|
687
|
+
if (accessToken !== googleAccount.accessToken) {
|
|
688
|
+
const tokenExpiresAt = new Date();
|
|
689
|
+
tokenExpiresAt.setSeconds(tokenExpiresAt.getSeconds() + 3600);
|
|
690
|
+
await prisma.userGoogleAccount.update({
|
|
691
|
+
where: { userId: googleAccount.userId },
|
|
692
|
+
data: {
|
|
693
|
+
accessToken,
|
|
694
|
+
tokenExpiresAt,
|
|
695
|
+
},
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// Vérifier si un fichier avec le même nom existe déjà
|
|
700
|
+
const searchQuery = `name='${encodeURIComponent(file.name)}' and '${folderId}' in parents and trashed=false`;
|
|
701
|
+
const searchResponse = await googleFetch(
|
|
702
|
+
`https://www.googleapis.com/drive/v3/files?q=${encodeURIComponent(searchQuery)}&fields=files(id,name,webViewLink)`,
|
|
703
|
+
{
|
|
704
|
+
headers: {
|
|
705
|
+
Authorization: `Bearer ${accessToken}`,
|
|
706
|
+
},
|
|
707
|
+
},
|
|
708
|
+
);
|
|
709
|
+
|
|
710
|
+
if (searchResponse.ok) {
|
|
711
|
+
const searchData = await searchResponse.json();
|
|
712
|
+
if (searchData.files && searchData.files.length > 0) {
|
|
713
|
+
for (const existingFile of searchData.files) {
|
|
714
|
+
try {
|
|
715
|
+
await googleFetch(`https://www.googleapis.com/drive/v3/files/${existingFile.id}`, {
|
|
716
|
+
method: 'DELETE',
|
|
717
|
+
headers: {
|
|
718
|
+
Authorization: `Bearer ${accessToken}`,
|
|
719
|
+
},
|
|
720
|
+
});
|
|
721
|
+
} catch (error) {
|
|
722
|
+
// Ignorer l'erreur
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
const metadata = {
|
|
729
|
+
name: file.name,
|
|
730
|
+
parents: [folderId],
|
|
731
|
+
};
|
|
732
|
+
|
|
733
|
+
const formData = new FormData();
|
|
734
|
+
formData.append('metadata', new Blob([JSON.stringify(metadata)], { type: 'application/json' }));
|
|
735
|
+
formData.append('file', file);
|
|
736
|
+
|
|
737
|
+
const uploadResponse = await googleFetch(
|
|
738
|
+
'https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart&fields=id,webViewLink',
|
|
739
|
+
{
|
|
740
|
+
method: 'POST',
|
|
741
|
+
headers: {
|
|
742
|
+
Authorization: `Bearer ${accessToken}`,
|
|
743
|
+
},
|
|
744
|
+
body: formData,
|
|
745
|
+
},
|
|
746
|
+
);
|
|
747
|
+
|
|
748
|
+
if (!uploadResponse.ok) {
|
|
749
|
+
const error = await uploadResponse.json();
|
|
750
|
+
throw new Error(`Erreur lors de l'upload: ${JSON.stringify(error)}`);
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
const fileData = await uploadResponse.json();
|
|
754
|
+
|
|
755
|
+
try {
|
|
756
|
+
await setFilePublicWithLink(accessToken, fileData.id);
|
|
757
|
+
} catch (permError) {
|
|
758
|
+
console.error('Erreur lors de la configuration des permissions:', permError);
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
return {
|
|
762
|
+
fileId: fileData.id,
|
|
763
|
+
webViewLink: fileData.webViewLink || `https://drive.google.com/file/d/${fileData.id}/view`,
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
/**
|
|
768
|
+
* Récupère les informations d'un fichier depuis Google Drive
|
|
769
|
+
*/
|
|
770
|
+
export async function getFileInfo(
|
|
771
|
+
userId: string,
|
|
772
|
+
fileId: string,
|
|
773
|
+
): Promise<{ name: string; size: string; mimeType: string; webViewLink: string }> {
|
|
774
|
+
const googleAccount = await getAdminGoogleAccount();
|
|
775
|
+
|
|
776
|
+
const accessToken = await getValidAccessToken(
|
|
777
|
+
googleAccount.accessToken,
|
|
778
|
+
googleAccount.refreshToken,
|
|
779
|
+
googleAccount.tokenExpiresAt,
|
|
780
|
+
);
|
|
781
|
+
|
|
782
|
+
const response = await googleFetch(
|
|
783
|
+
`https://www.googleapis.com/drive/v3/files/${fileId}?fields=id,name,size,mimeType,webViewLink`,
|
|
784
|
+
{
|
|
785
|
+
headers: {
|
|
786
|
+
Authorization: `Bearer ${accessToken}`,
|
|
787
|
+
},
|
|
788
|
+
},
|
|
789
|
+
);
|
|
790
|
+
|
|
791
|
+
if (!response.ok) {
|
|
792
|
+
throw new Error('Erreur lors de la récupération du fichier');
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
return await response.json();
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
/**
|
|
799
|
+
* Télécharge un fichier depuis Google Drive
|
|
800
|
+
*/
|
|
801
|
+
export async function downloadFileFromDrive(
|
|
802
|
+
userId: string,
|
|
803
|
+
fileId: string,
|
|
804
|
+
): Promise<{ buffer: Buffer; fileName: string; mimeType: string }> {
|
|
805
|
+
const googleAccount = await getAdminGoogleAccount();
|
|
806
|
+
|
|
807
|
+
const accessToken = await getValidAccessToken(
|
|
808
|
+
googleAccount.accessToken,
|
|
809
|
+
googleAccount.refreshToken,
|
|
810
|
+
googleAccount.tokenExpiresAt,
|
|
811
|
+
);
|
|
812
|
+
|
|
813
|
+
// Récupérer les métadonnées du fichier
|
|
814
|
+
const metadataResponse = await googleFetch(
|
|
815
|
+
`https://www.googleapis.com/drive/v3/files/${fileId}?fields=id,name,mimeType`,
|
|
816
|
+
{
|
|
817
|
+
headers: {
|
|
818
|
+
Authorization: `Bearer ${accessToken}`,
|
|
819
|
+
},
|
|
820
|
+
},
|
|
821
|
+
);
|
|
822
|
+
|
|
823
|
+
if (!metadataResponse.ok) {
|
|
824
|
+
const errorText = await metadataResponse.text();
|
|
825
|
+
|
|
826
|
+
// Erreur d'authentification (token expiré/révoqué)
|
|
827
|
+
if (metadataResponse.status === 401) {
|
|
828
|
+
throw new Error(
|
|
829
|
+
'La connexion Google Drive a expiré. Veuillez demander à un administrateur de reconnecter son compte Google dans Paramètres > Intégrations > Google Drive.',
|
|
830
|
+
);
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
// Fichier non trouvé
|
|
834
|
+
if (metadataResponse.status === 404) {
|
|
835
|
+
throw new Error(
|
|
836
|
+
"Le fichier n'a pas été trouvé sur Google Drive. Veuillez vérifier que le template CERFA est bien uploadé dans Paramètres > App > Modèle CERFA.",
|
|
837
|
+
);
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
throw new Error(`Erreur lors de la récupération des métadonnées du fichier: ${errorText}`);
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
const metadata = await metadataResponse.json();
|
|
844
|
+
|
|
845
|
+
// Télécharger le contenu du fichier
|
|
846
|
+
const downloadResponse = await googleFetch(
|
|
847
|
+
`https://www.googleapis.com/drive/v3/files/${fileId}?alt=media`,
|
|
848
|
+
{
|
|
849
|
+
headers: {
|
|
850
|
+
Authorization: `Bearer ${accessToken}`,
|
|
851
|
+
},
|
|
852
|
+
},
|
|
853
|
+
);
|
|
854
|
+
|
|
855
|
+
if (!downloadResponse.ok) {
|
|
856
|
+
const errorText = await downloadResponse.text();
|
|
857
|
+
|
|
858
|
+
// Erreur d'authentification
|
|
859
|
+
if (downloadResponse.status === 401) {
|
|
860
|
+
throw new Error(
|
|
861
|
+
'La connexion Google Drive a expiré. Veuillez demander à un administrateur de reconnecter son compte Google dans Paramètres > Intégrations > Google Drive.',
|
|
862
|
+
);
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
throw new Error(`Erreur lors du téléchargement du fichier: ${errorText}`);
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
const arrayBuffer = await downloadResponse.arrayBuffer();
|
|
869
|
+
const buffer = Buffer.from(arrayBuffer);
|
|
870
|
+
|
|
871
|
+
return {
|
|
872
|
+
buffer,
|
|
873
|
+
fileName: metadata.name,
|
|
874
|
+
mimeType: metadata.mimeType || 'application/pdf',
|
|
875
|
+
};
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
/**
|
|
879
|
+
* Supprime un fichier de Google Drive
|
|
880
|
+
*/
|
|
881
|
+
export async function deleteFileFromDrive(userId: string, fileId: string): Promise<void> {
|
|
882
|
+
const googleAccount = await getAdminGoogleAccount();
|
|
883
|
+
|
|
884
|
+
const accessToken = await getValidAccessToken(
|
|
885
|
+
googleAccount.accessToken,
|
|
886
|
+
googleAccount.refreshToken,
|
|
887
|
+
googleAccount.tokenExpiresAt,
|
|
888
|
+
);
|
|
889
|
+
|
|
890
|
+
const response = await googleFetch(`https://www.googleapis.com/drive/v3/files/${fileId}`, {
|
|
891
|
+
method: 'DELETE',
|
|
892
|
+
headers: {
|
|
893
|
+
Authorization: `Bearer ${accessToken}`,
|
|
894
|
+
},
|
|
895
|
+
});
|
|
896
|
+
|
|
897
|
+
if (!response.ok && response.status !== 404) {
|
|
898
|
+
// 404 signifie que le fichier n'existe plus, ce qui est OK
|
|
899
|
+
throw new Error('Erreur lors de la suppression du fichier');
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
/**
|
|
904
|
+
* Upload le template PDF Cerfa dans le dossier Paramètres
|
|
905
|
+
* Structure: [Nom du projet] > Paramètres
|
|
906
|
+
*/
|
|
907
|
+
export async function uploadCerfaTemplate(
|
|
908
|
+
userId: string,
|
|
909
|
+
file: File,
|
|
910
|
+
): Promise<{ fileId: string; webViewLink: string }> {
|
|
911
|
+
const googleAccount = await getAdminGoogleAccount();
|
|
912
|
+
|
|
913
|
+
const accessToken = await getValidAccessToken(
|
|
914
|
+
googleAccount.accessToken,
|
|
915
|
+
googleAccount.refreshToken,
|
|
916
|
+
googleAccount.tokenExpiresAt,
|
|
917
|
+
);
|
|
918
|
+
|
|
919
|
+
if (accessToken !== googleAccount.accessToken) {
|
|
920
|
+
const tokenExpiresAt = new Date();
|
|
921
|
+
tokenExpiresAt.setSeconds(tokenExpiresAt.getSeconds() + 3600);
|
|
922
|
+
await prisma.userGoogleAccount.update({
|
|
923
|
+
where: { userId: googleAccount.userId },
|
|
924
|
+
data: {
|
|
925
|
+
accessToken,
|
|
926
|
+
tokenExpiresAt,
|
|
927
|
+
},
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
// 1. Créer ou récupérer le dossier racine
|
|
932
|
+
const appFolderId = await getOrCreateFolder(accessToken, APP_NAME);
|
|
933
|
+
|
|
934
|
+
// 2. Créer ou récupérer le dossier "Paramètres"
|
|
935
|
+
const settingsFolderId = await getOrCreateFolder(accessToken, 'Paramètres', appFolderId);
|
|
936
|
+
|
|
937
|
+
// Vérifier si un fichier avec le même nom existe déjà
|
|
938
|
+
const searchQuery = `name='${encodeURIComponent(file.name)}' and '${settingsFolderId}' in parents and trashed=false`;
|
|
939
|
+
const searchResponse = await googleFetch(
|
|
940
|
+
`https://www.googleapis.com/drive/v3/files?q=${encodeURIComponent(searchQuery)}&fields=files(id,name,webViewLink)`,
|
|
941
|
+
{
|
|
942
|
+
headers: {
|
|
943
|
+
Authorization: `Bearer ${accessToken}`,
|
|
944
|
+
},
|
|
945
|
+
},
|
|
946
|
+
);
|
|
947
|
+
|
|
948
|
+
if (searchResponse.ok) {
|
|
949
|
+
const searchData = await searchResponse.json();
|
|
950
|
+
// Si un fichier avec le même nom existe déjà, le supprimer avant d'uploader le nouveau
|
|
951
|
+
if (searchData.files && searchData.files.length > 0) {
|
|
952
|
+
for (const existingFile of searchData.files) {
|
|
953
|
+
await googleFetch(`https://www.googleapis.com/drive/v3/files/${existingFile.id}`, {
|
|
954
|
+
method: 'DELETE',
|
|
955
|
+
headers: {
|
|
956
|
+
Authorization: `Bearer ${accessToken}`,
|
|
957
|
+
},
|
|
958
|
+
});
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
// Uploader le fichier
|
|
964
|
+
const metadata = {
|
|
965
|
+
name: file.name,
|
|
966
|
+
parents: [settingsFolderId],
|
|
967
|
+
};
|
|
968
|
+
|
|
969
|
+
const formData = new FormData();
|
|
970
|
+
formData.append('metadata', new Blob([JSON.stringify(metadata)], { type: 'application/json' }));
|
|
971
|
+
formData.append('file', file);
|
|
972
|
+
|
|
973
|
+
const uploadResponse = await googleFetch(
|
|
974
|
+
'https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart&fields=id,webViewLink',
|
|
975
|
+
{
|
|
976
|
+
method: 'POST',
|
|
977
|
+
headers: {
|
|
978
|
+
Authorization: `Bearer ${accessToken}`,
|
|
979
|
+
},
|
|
980
|
+
body: formData,
|
|
981
|
+
},
|
|
982
|
+
);
|
|
983
|
+
|
|
984
|
+
if (!uploadResponse.ok) {
|
|
985
|
+
const error = await uploadResponse.json();
|
|
986
|
+
throw new Error(`Erreur lors de l'upload: ${JSON.stringify(error)}`);
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
const fileData = await uploadResponse.json();
|
|
990
|
+
|
|
991
|
+
try {
|
|
992
|
+
await setFilePublicWithLink(accessToken, fileData.id);
|
|
993
|
+
} catch (permError) {
|
|
994
|
+
console.error('Erreur lors de la configuration des permissions:', permError);
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
return {
|
|
998
|
+
fileId: fileData.id,
|
|
999
|
+
webViewLink: fileData.webViewLink || `https://drive.google.com/file/d/${fileData.id}/view`,
|
|
1000
|
+
};
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
/**
|
|
1004
|
+
* Upload un template de contrat de rachat dans Google Drive
|
|
1005
|
+
* Similaire à uploadCerfaTemplate mais pour les contrats de transaction
|
|
1006
|
+
*/
|
|
1007
|
+
export async function uploadContractTemplate(
|
|
1008
|
+
userId: string,
|
|
1009
|
+
file: File,
|
|
1010
|
+
): Promise<{ fileId: string; webViewLink: string }> {
|
|
1011
|
+
const googleAccount = await getAdminGoogleAccount();
|
|
1012
|
+
|
|
1013
|
+
const accessToken = await getValidAccessToken(
|
|
1014
|
+
googleAccount.accessToken,
|
|
1015
|
+
googleAccount.refreshToken,
|
|
1016
|
+
googleAccount.tokenExpiresAt,
|
|
1017
|
+
);
|
|
1018
|
+
|
|
1019
|
+
if (accessToken !== googleAccount.accessToken) {
|
|
1020
|
+
const tokenExpiresAt = new Date();
|
|
1021
|
+
tokenExpiresAt.setSeconds(tokenExpiresAt.getSeconds() + 3600);
|
|
1022
|
+
await prisma.userGoogleAccount.update({
|
|
1023
|
+
where: { userId: googleAccount.userId },
|
|
1024
|
+
data: {
|
|
1025
|
+
accessToken,
|
|
1026
|
+
tokenExpiresAt,
|
|
1027
|
+
},
|
|
1028
|
+
});
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
// 1. Créer ou récupérer le dossier racine
|
|
1032
|
+
const appFolderId = await getOrCreateFolder(accessToken, APP_NAME);
|
|
1033
|
+
|
|
1034
|
+
// 2. Créer ou récupérer le dossier "Paramètres"
|
|
1035
|
+
const settingsFolderId = await getOrCreateFolder(accessToken, 'Paramètres', appFolderId);
|
|
1036
|
+
|
|
1037
|
+
// Vérifier si un fichier avec le même nom existe déjà
|
|
1038
|
+
const searchQuery = `name='${encodeURIComponent(file.name)}' and '${settingsFolderId}' in parents and trashed=false`;
|
|
1039
|
+
const searchResponse = await googleFetch(
|
|
1040
|
+
`https://www.googleapis.com/drive/v3/files?q=${encodeURIComponent(searchQuery)}&fields=files(id,name,webViewLink)`,
|
|
1041
|
+
{
|
|
1042
|
+
headers: {
|
|
1043
|
+
Authorization: `Bearer ${accessToken}`,
|
|
1044
|
+
},
|
|
1045
|
+
},
|
|
1046
|
+
);
|
|
1047
|
+
|
|
1048
|
+
if (searchResponse.ok) {
|
|
1049
|
+
const searchData = await searchResponse.json();
|
|
1050
|
+
// Si un fichier avec le même nom existe déjà, le supprimer avant d'uploader le nouveau
|
|
1051
|
+
if (searchData.files && searchData.files.length > 0) {
|
|
1052
|
+
for (const existingFile of searchData.files) {
|
|
1053
|
+
await googleFetch(`https://www.googleapis.com/drive/v3/files/${existingFile.id}`, {
|
|
1054
|
+
method: 'DELETE',
|
|
1055
|
+
headers: {
|
|
1056
|
+
Authorization: `Bearer ${accessToken}`,
|
|
1057
|
+
},
|
|
1058
|
+
});
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
// Uploader le fichier
|
|
1064
|
+
const metadata = {
|
|
1065
|
+
name: file.name,
|
|
1066
|
+
parents: [settingsFolderId],
|
|
1067
|
+
};
|
|
1068
|
+
|
|
1069
|
+
const formData = new FormData();
|
|
1070
|
+
formData.append('metadata', new Blob([JSON.stringify(metadata)], { type: 'application/json' }));
|
|
1071
|
+
formData.append('file', file);
|
|
1072
|
+
|
|
1073
|
+
const uploadResponse = await googleFetch(
|
|
1074
|
+
'https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart&fields=id,webViewLink',
|
|
1075
|
+
{
|
|
1076
|
+
method: 'POST',
|
|
1077
|
+
headers: {
|
|
1078
|
+
Authorization: `Bearer ${accessToken}`,
|
|
1079
|
+
},
|
|
1080
|
+
body: formData,
|
|
1081
|
+
},
|
|
1082
|
+
);
|
|
1083
|
+
|
|
1084
|
+
if (!uploadResponse.ok) {
|
|
1085
|
+
const error = await uploadResponse.json();
|
|
1086
|
+
throw new Error(`Erreur lors de l'upload: ${JSON.stringify(error)}`);
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
const fileData = await uploadResponse.json();
|
|
1090
|
+
|
|
1091
|
+
try {
|
|
1092
|
+
await setFilePublicWithLink(accessToken, fileData.id);
|
|
1093
|
+
} catch (permError) {
|
|
1094
|
+
console.error('Erreur lors de la configuration des permissions:', permError);
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
return {
|
|
1098
|
+
fileId: fileData.id,
|
|
1099
|
+
webViewLink: fileData.webViewLink || `https://drive.google.com/file/d/${fileData.id}/view`,
|
|
1100
|
+
};
|
|
380
1101
|
}
|