create-crm-tmp 1.1.3 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/create-crm-tmp.js +56 -35
- package/package.json +1 -1
- package/template/.prettierignore +2 -0
- package/template/README.md +230 -115
- package/template/components.json +22 -0
- package/template/eslint.config.mjs +13 -0
- package/template/exemple-contacts.csv +54 -0
- package/template/next.config.ts +41 -1
- package/template/package.json +63 -15
- package/template/prisma/migrations/20260318095700_init_db/migration.sql +978 -0
- package/template/prisma/schema.prisma +311 -67
- package/template/src/app/(auth)/invite/[token]/page.tsx +28 -29
- package/template/src/app/(auth)/layout.tsx +1 -1
- package/template/src/app/(auth)/reset-password/complete/page.tsx +21 -27
- package/template/src/app/(auth)/reset-password/page.tsx +14 -10
- package/template/src/app/(auth)/reset-password/verify/page.tsx +14 -10
- package/template/src/app/(auth)/signin/page.tsx +34 -23
- package/template/src/app/(dashboard)/agenda/page.tsx +3655 -2357
- package/template/src/app/(dashboard)/automatisation/[id]/page.tsx +10 -7
- package/template/src/app/(dashboard)/automatisation/_components/workflow-editor.tsx +609 -338
- package/template/src/app/(dashboard)/automatisation/new/page.tsx +11 -8
- package/template/src/app/(dashboard)/automatisation/page.tsx +463 -186
- package/template/src/app/(dashboard)/closing/page.tsx +517 -469
- package/template/src/app/(dashboard)/contacts/[id]/page.tsx +6151 -4210
- package/template/src/app/(dashboard)/contacts/companies/[id]/page.tsx +1702 -0
- package/template/src/app/(dashboard)/contacts/loading.tsx +13 -0
- package/template/src/app/(dashboard)/contacts/page.tsx +4124 -2130
- package/template/src/app/(dashboard)/dashboard/page.tsx +119 -105
- package/template/src/app/(dashboard)/dev/page.tsx +1291 -0
- package/template/src/app/(dashboard)/error.tsx +37 -0
- package/template/src/app/(dashboard)/layout.tsx +6 -2
- 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 +1773 -3362
- package/template/src/app/(dashboard)/templates/page.tsx +504 -303
- package/template/src/app/(dashboard)/users/list/page.tsx +364 -355
- 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 +169 -140
- package/template/src/app/api/agenda/google-events/route.ts +92 -0
- package/template/src/app/api/audit-logs/route.ts +1 -1
- package/template/src/app/api/auth/check-active/route.ts +3 -2
- 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/auth/google/route.ts +2 -1
- package/template/src/app/api/auth/google/status/route.ts +7 -31
- package/template/src/app/api/companies/[id]/activities/route.ts +129 -0
- package/template/src/app/api/companies/[id]/route.ts +194 -0
- package/template/src/app/api/companies/export/route.ts +206 -0
- package/template/src/app/api/companies/route.ts +196 -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 +55 -0
- package/template/src/app/api/contacts/[id]/files/[fileId]/route.ts +20 -48
- package/template/src/app/api/contacts/[id]/files/route.ts +125 -186
- package/template/src/app/api/contacts/[id]/interactions/[interactionId]/route.ts +27 -1
- package/template/src/app/api/contacts/[id]/interactions/route.ts +45 -8
- package/template/src/app/api/contacts/[id]/kyc/route.ts +81 -0
- package/template/src/app/api/contacts/[id]/meet/route.ts +55 -29
- package/template/src/app/api/contacts/[id]/route.ts +184 -21
- package/template/src/app/api/contacts/[id]/send-email/route.ts +33 -11
- package/template/src/app/api/contacts/[id]/workflows/run/route.ts +67 -0
- package/template/src/app/api/contacts/export/route.ts +22 -31
- package/template/src/app/api/contacts/import/route.ts +77 -44
- package/template/src/app/api/contacts/import-preview/route.ts +139 -0
- package/template/src/app/api/contacts/origins/route.ts +63 -0
- package/template/src/app/api/contacts/route.ts +322 -57
- package/template/src/app/api/cron/cleanup-editor-images/route.ts +166 -0
- package/template/src/app/api/dashboard/stats/route.ts +9 -292
- package/template/src/app/api/dashboard/widgets/[id]/route.ts +0 -3
- package/template/src/app/api/dashboard/widgets/route.ts +19 -19
- package/template/src/app/api/dev/reminders/test/route.ts +114 -0
- package/template/src/app/api/editor/upload-image/route.ts +61 -0
- package/template/src/app/api/integrations/google-sheet/jobs/[jobId]/route.ts +47 -0
- package/template/src/app/api/integrations/google-sheet/jobs/usage/route.ts +50 -0
- package/template/src/app/api/integrations/google-sheet/sync/route.ts +28 -542
- package/template/src/app/api/invite/complete/route.ts +20 -23
- package/template/src/app/api/jobs/google-sheet/process/route.ts +84 -0
- package/template/src/app/api/jobs/google-sheet/schedule/route.ts +50 -0
- package/template/src/app/api/reminders/clear/route.ts +120 -0
- package/template/src/app/api/reminders/clear/undo/route.ts +112 -0
- package/template/src/app/api/reminders/route.ts +165 -39
- package/template/src/app/api/reminders/state/route.ts +164 -0
- package/template/src/app/api/reset-password/complete/route.ts +11 -13
- package/template/src/app/api/reset-password/request/route.ts +1 -1
- package/template/src/app/api/reset-password/verify/route.ts +1 -1
- package/template/src/app/api/send/route.ts +25 -47
- 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 +34 -23
- package/template/src/app/api/settings/google-calendar/calendars/route.ts +97 -0
- package/template/src/app/api/settings/google-calendar/route.ts +124 -0
- package/template/src/app/api/settings/google-sheet/[id]/route.ts +48 -23
- package/template/src/app/api/settings/google-sheet/auto-map/route.ts +56 -32
- package/template/src/app/api/settings/google-sheet/preview/route.ts +110 -0
- package/template/src/app/api/settings/google-sheet/route.ts +34 -23
- package/template/src/app/api/settings/integrations/logs/route.ts +93 -0
- package/template/src/app/api/settings/integrations/notifications/route.ts +67 -0
- package/template/src/app/api/settings/meta-leads/[id]/route.ts +20 -24
- package/template/src/app/api/settings/meta-leads/route.ts +34 -25
- package/template/src/app/api/settings/smtp/route.ts +53 -6
- package/template/src/app/api/settings/statuses/[id]/route.ts +29 -32
- 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 +36 -13
- package/template/src/app/api/tasks/[id]/route.ts +357 -145
- package/template/src/app/api/tasks/meet/route.ts +37 -26
- package/template/src/app/api/tasks/route.ts +201 -96
- 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 +22 -16
- 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/list/route.ts +57 -19
- package/template/src/app/api/users/route.ts +89 -34
- package/template/src/app/api/webhooks/google-ads/route.ts +40 -1
- package/template/src/app/api/webhooks/meta-leads/route.ts +38 -1
- package/template/src/app/api/workflows/[id]/route.ts +29 -6
- package/template/src/app/api/workflows/process/route.ts +505 -170
- package/template/src/app/api/workflows/route.ts +42 -4
- package/template/src/app/globals.css +512 -32
- package/template/src/app/layout.tsx +28 -9
- package/template/src/app/page.tsx +37 -7
- package/template/src/components/address-autocomplete.tsx +233 -0
- package/template/src/components/config-error-alert.tsx +46 -0
- package/template/src/components/contacts/filter-bar.tsx +190 -0
- package/template/src/components/contacts/filter-builder.tsx +574 -0
- package/template/src/components/contacts/save-view-dialog.tsx +160 -0
- package/template/src/components/contacts/views-tab-bar.tsx +449 -0
- package/template/src/components/dashboard/activity-chart.tsx +6 -1
- package/template/src/components/dashboard/add-widget-dialog.tsx +13 -17
- package/template/src/components/dashboard/color-picker.tsx +7 -8
- package/template/src/components/dashboard/recent-activity.tsx +2 -5
- package/template/src/components/dashboard/stat-card.tsx +1 -3
- package/template/src/components/dashboard/status-distribution-chart.tsx +0 -1
- package/template/src/components/dashboard/top-contacts-list.tsx +7 -13
- package/template/src/components/dashboard/upcoming-tasks-list.tsx +2 -5
- package/template/src/components/dashboard/widget-wrapper.tsx +3 -6
- package/template/src/components/date-picker.tsx +399 -0
- package/template/src/components/editor/upload-editor-image.ts +42 -0
- package/template/src/components/editor.tsx +188 -35
- package/template/src/components/email-template.tsx +4 -2
- package/template/src/components/global-search.tsx +360 -0
- package/template/src/components/header.tsx +200 -107
- package/template/src/components/inactive-account-guard.tsx +58 -0
- package/template/src/components/integration-notifications-listener.tsx +12 -0
- package/template/src/components/invitation-email-template.tsx +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/settings/integrations/GoogleAdsIntegration.tsx +428 -0
- package/template/src/components/settings/integrations/GoogleSheetConfigMonitoringModal.tsx +680 -0
- package/template/src/components/settings/integrations/GoogleSheetIntegration.tsx +809 -0
- package/template/src/components/settings/integrations/ImportResultDialog.tsx +124 -0
- package/template/src/components/settings/integrations/IntegrationLogPanel.tsx +57 -0
- package/template/src/components/settings/integrations/IntegrationLogsTable.tsx +186 -0
- package/template/src/components/settings/integrations/MetaLeadIntegration.tsx +451 -0
- package/template/src/components/sidebar.tsx +117 -100
- package/template/src/components/skeleton.tsx +128 -45
- 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 +71 -0
- package/template/src/components/ui/components.tsx +1 -1
- package/template/src/components/ui/date-picker.tsx +422 -0
- package/template/src/components/ui/datetime-picker.tsx +338 -0
- package/template/src/components/ui/status-select.tsx +271 -0
- package/template/src/components/ui/tooltip.tsx +37 -0
- package/template/src/components/view-as-banner.tsx +1 -1
- package/template/src/components/view-as-modal.tsx +30 -19
- package/template/src/config/nav-pages.ts +108 -0
- package/template/src/contexts/app-toast-context.tsx +362 -0
- package/template/src/contexts/dashboard-theme-context.tsx +2 -7
- package/template/src/contexts/sidebar-context.tsx +27 -53
- package/template/src/contexts/task-reminder-context.tsx +134 -160
- package/template/src/contexts/view-as-context.tsx +32 -10
- 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/hooks/useIntegrationNotifications.ts +49 -0
- package/template/src/lib/address-api.ts +155 -0
- package/template/src/lib/auth.ts +8 -1
- package/template/src/lib/cache.ts +73 -0
- package/template/src/lib/check-permission.ts +12 -177
- package/template/src/lib/config-links.ts +14 -0
- package/template/src/lib/contact-duplicate.ts +79 -61
- package/template/src/lib/contact-interactions.ts +24 -22
- package/template/src/lib/contact-view-filters.ts +301 -0
- package/template/src/lib/contacts-list-url.ts +190 -0
- package/template/src/lib/dashboard-stats.ts +282 -0
- package/template/src/lib/dashboard-themes.ts +0 -5
- package/template/src/lib/date-utils.ts +176 -0
- package/template/src/lib/default-widgets.ts +0 -2
- package/template/src/lib/editor-html-image-dimensions.ts +172 -0
- package/template/src/lib/editor-image-limits.ts +19 -0
- package/template/src/lib/email-html-sanitize.ts +19 -0
- package/template/src/lib/encryption.ts +9 -6
- package/template/src/lib/fr-geography.ts +192 -0
- package/template/src/lib/get-auth-user.ts +25 -0
- package/template/src/lib/google-calendar-agenda.ts +201 -0
- package/template/src/lib/google-calendar.ts +309 -17
- package/template/src/lib/google-fetch.ts +63 -0
- package/template/src/lib/google-sheet-sync-jobs.ts +96 -0
- package/template/src/lib/google-sheet-sync-runner.ts +514 -0
- package/template/src/lib/integration-import-log.ts +21 -0
- package/template/src/lib/local-storage.ts +34 -0
- package/template/src/lib/permissions.ts +268 -40
- package/template/src/lib/prisma.ts +15 -12
- package/template/src/lib/qstash.ts +65 -0
- package/template/src/lib/reminder-state-server.ts +80 -0
- package/template/src/lib/reminder-state.ts +29 -0
- package/template/src/lib/roles.ts +12 -15
- package/template/src/lib/supabase-storage.ts +113 -0
- package/template/src/lib/template-variables.ts +204 -29
- package/template/src/lib/utils.ts +71 -11
- package/template/src/lib/widget-registry.ts +0 -4
- package/template/src/lib/workflow-executor.ts +391 -228
- package/template/src/proxy.ts +35 -73
- package/template/src/types/contact-views.ts +351 -0
- package/template/vercel.json +5 -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/20260226093949_fix_cascade_on_user_delete/migration.sql +0 -69
- package/template/src/app/(dashboard)/users/layout.tsx +0 -30
- package/template/src/lib/google-drive.ts +0 -380
|
@@ -30,10 +30,6 @@ function getRoleLevel(role: string): number {
|
|
|
30
30
|
return ROLE_HIERARCHY[role] || 999; // Rôle inconnu = niveau très bas
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
/**
|
|
34
|
-
* Vérifie si l'utilisateur a le rôle requis ou un rôle supérieur dans la hiérarchie
|
|
35
|
-
* Un rôle supérieur a automatiquement les permissions des rôles inférieurs
|
|
36
|
-
*/
|
|
37
33
|
function hasRole(userRole: string | undefined, requiredRole: Role): boolean {
|
|
38
34
|
if (!userRole) return false;
|
|
39
35
|
|
|
@@ -44,27 +40,28 @@ function hasRole(userRole: string | undefined, requiredRole: Role): boolean {
|
|
|
44
40
|
return userLevel <= requiredLevel;
|
|
45
41
|
}
|
|
46
42
|
|
|
43
|
+
export function isAdmin(userRole: string | undefined): boolean {
|
|
44
|
+
return userRole === Role.ADMIN;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
47
|
const CUSTOM_ROLE_NAME_TO_ENUM: Record<string, Role> = {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
48
|
+
Administrateur: Role.ADMIN,
|
|
49
|
+
Manager: Role.MANAGER,
|
|
50
|
+
Commercial: Role.COMMERCIAL,
|
|
51
|
+
Télépro: Role.TELEPRO,
|
|
52
|
+
Comptable: Role.COMPTABLE,
|
|
53
53
|
};
|
|
54
54
|
|
|
55
55
|
/**
|
|
56
|
-
* Résout
|
|
57
|
-
* Retourne
|
|
56
|
+
* Résout le Role enum à partir du nom d'un CustomRole.
|
|
57
|
+
* Retourne USER si aucune correspondance n'est trouvée.
|
|
58
58
|
*/
|
|
59
59
|
export function resolveRoleFromCustomRoleName(customRoleName: string | null | undefined): Role {
|
|
60
60
|
if (!customRoleName) return Role.USER;
|
|
61
61
|
return CUSTOM_ROLE_NAME_TO_ENUM[customRoleName] ?? Role.USER;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
|
|
65
|
-
* Middleware pour vérifier le rôle côté serveur
|
|
66
|
-
*/
|
|
67
|
-
export async function requireRole(headers: Headers, requiredRole: Role) {
|
|
64
|
+
async function requireRole(headers: Headers, requiredRole: Role) {
|
|
68
65
|
const session = await auth.api.getSession({ headers });
|
|
69
66
|
|
|
70
67
|
if (!session) {
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { createClient, type SupabaseClient } from '@supabase/supabase-js';
|
|
2
|
+
|
|
3
|
+
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
|
4
|
+
const supabaseServiceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
|
|
5
|
+
|
|
6
|
+
let _adminClient: SupabaseClient | null = null;
|
|
7
|
+
|
|
8
|
+
function getAdminClient(): SupabaseClient {
|
|
9
|
+
if (!_adminClient) {
|
|
10
|
+
if (!supabaseUrl || !supabaseServiceRoleKey) {
|
|
11
|
+
throw new Error(
|
|
12
|
+
'NEXT_PUBLIC_SUPABASE_URL et SUPABASE_SERVICE_ROLE_KEY doivent être configurés.',
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
_adminClient = createClient(supabaseUrl, supabaseServiceRoleKey, {
|
|
16
|
+
auth: { autoRefreshToken: false, persistSession: false },
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
return _adminClient;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const BUCKETS = {
|
|
23
|
+
CONTACTS: 'contacts',
|
|
24
|
+
EDITOR_IMAGES: 'editor-images',
|
|
25
|
+
} as const;
|
|
26
|
+
|
|
27
|
+
export type BucketName = (typeof BUCKETS)[keyof typeof BUCKETS];
|
|
28
|
+
|
|
29
|
+
function buildStoragePath(prefix: string, entityId: string, fileName: string): string {
|
|
30
|
+
const uuid = crypto.randomUUID();
|
|
31
|
+
const sanitized = fileName.replaceAll(/[^a-zA-Z0-9._-]/g, '_');
|
|
32
|
+
return `${prefix}/${entityId}/${uuid}-${sanitized}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function buildContactFilePath(contactId: string, fileName: string): string {
|
|
36
|
+
return buildStoragePath('files', contactId, fileName);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function buildEditorImagePath(fileName: string): string {
|
|
40
|
+
const uuid = crypto.randomUUID();
|
|
41
|
+
const sanitized = fileName.replaceAll(/[^a-zA-Z0-9._-]/g, '_');
|
|
42
|
+
return `images/${uuid}-${sanitized}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function uploadFile(
|
|
46
|
+
bucket: BucketName,
|
|
47
|
+
path: string,
|
|
48
|
+
data: Buffer | Blob,
|
|
49
|
+
contentType: string,
|
|
50
|
+
): Promise<{ storagePath: string }> {
|
|
51
|
+
const client = getAdminClient();
|
|
52
|
+
const { error } = await client.storage.from(bucket).upload(path, data, {
|
|
53
|
+
contentType,
|
|
54
|
+
upsert: true,
|
|
55
|
+
});
|
|
56
|
+
if (error) throw new Error(`Erreur upload Supabase: ${error.message}`);
|
|
57
|
+
return { storagePath: path };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function createSignedUploadUrl(
|
|
61
|
+
bucket: BucketName,
|
|
62
|
+
path: string,
|
|
63
|
+
): Promise<{ signedUrl: string; token: string; storagePath: string }> {
|
|
64
|
+
const client = getAdminClient();
|
|
65
|
+
const { data, error } = await client.storage.from(bucket).createSignedUploadUrl(path);
|
|
66
|
+
if (error || !data) {
|
|
67
|
+
throw new Error(`Erreur création signed upload URL: ${error?.message ?? 'unknown'}`);
|
|
68
|
+
}
|
|
69
|
+
return { signedUrl: data.signedUrl, token: data.token, storagePath: path };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function createSignedDownloadUrl(
|
|
73
|
+
bucket: BucketName,
|
|
74
|
+
path: string,
|
|
75
|
+
expiresIn = 3600,
|
|
76
|
+
): Promise<string> {
|
|
77
|
+
const client = getAdminClient();
|
|
78
|
+
const { data, error } = await client.storage.from(bucket).createSignedUrl(path, expiresIn);
|
|
79
|
+
if (error || !data?.signedUrl) {
|
|
80
|
+
throw new Error(`Erreur création signed download URL: ${error?.message ?? 'unknown'}`);
|
|
81
|
+
}
|
|
82
|
+
return data.signedUrl;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function downloadFile(
|
|
86
|
+
bucket: BucketName,
|
|
87
|
+
path: string,
|
|
88
|
+
): Promise<{ buffer: Buffer; mimeType: string }> {
|
|
89
|
+
const client = getAdminClient();
|
|
90
|
+
const { data, error } = await client.storage.from(bucket).download(path);
|
|
91
|
+
if (error || !data) {
|
|
92
|
+
throw new Error(`Erreur téléchargement Supabase: ${error?.message ?? 'unknown'}`);
|
|
93
|
+
}
|
|
94
|
+
const buffer = Buffer.from(await data.arrayBuffer());
|
|
95
|
+
return { buffer, mimeType: data.type };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function deleteFile(bucket: BucketName, path: string): Promise<void> {
|
|
99
|
+
const client = getAdminClient();
|
|
100
|
+
const { error } = await client.storage.from(bucket).remove([path]);
|
|
101
|
+
if (error) {
|
|
102
|
+
throw new Error(`Erreur suppression Supabase: ${error.message}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export async function deleteFiles(bucket: BucketName, paths: string[]): Promise<void> {
|
|
107
|
+
if (paths.length === 0) return;
|
|
108
|
+
const client = getAdminClient();
|
|
109
|
+
const { error } = await client.storage.from(bucket).remove(paths);
|
|
110
|
+
if (error) {
|
|
111
|
+
throw new Error(`Erreur suppression batch Supabase: ${error.message}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -14,23 +14,96 @@ export interface ContactVariables {
|
|
|
14
14
|
city?: string | null;
|
|
15
15
|
postalCode?: string | null;
|
|
16
16
|
companyName?: string | null;
|
|
17
|
+
company?: {
|
|
18
|
+
name?: string | null;
|
|
19
|
+
address?: string | null;
|
|
20
|
+
city?: string | null;
|
|
21
|
+
postalCode?: string | null;
|
|
22
|
+
} | null;
|
|
23
|
+
jobTitle?: string | null;
|
|
24
|
+
website?: string | null;
|
|
25
|
+
origin?: string | null;
|
|
26
|
+
statusName?: string | null;
|
|
27
|
+
assignedCommercialName?: string | null;
|
|
28
|
+
assignedTeleproName?: string | null;
|
|
29
|
+
closingReason?: string | null;
|
|
30
|
+
createdAt?: Date | string | null;
|
|
17
31
|
}
|
|
18
32
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
{
|
|
27
|
-
{
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
{ key: '{{
|
|
33
|
-
{ key: '{{
|
|
33
|
+
export interface TemplateVariable {
|
|
34
|
+
key: string;
|
|
35
|
+
description: string;
|
|
36
|
+
section: 'contact' | 'crm';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const VARIABLE_SECTIONS = {
|
|
40
|
+
contact: { label: 'Contact', color: 'indigo' },
|
|
41
|
+
crm: { label: 'CRM', color: 'emerald' },
|
|
42
|
+
} as const;
|
|
43
|
+
|
|
44
|
+
export const AVAILABLE_VARIABLES: TemplateVariable[] = [
|
|
45
|
+
// Contact
|
|
46
|
+
{ key: '{{firstName}}', description: 'Prénom du contact (ex. Jean)', section: 'contact' },
|
|
47
|
+
{ key: '{{lastName}}', description: 'Nom de famille du contact.', section: 'contact' },
|
|
48
|
+
{ key: '{{fullName}}', description: 'Prénom et nom en un seul bloc.', section: 'contact' },
|
|
49
|
+
{ key: '{{civility}}', description: 'Civilité du contact : M., Mme, Mlle.', section: 'contact' },
|
|
50
|
+
{ key: '{{email}}', description: 'Adresse email du contact.', section: 'contact' },
|
|
51
|
+
{
|
|
52
|
+
key: '{{phone}}',
|
|
53
|
+
description: 'Numéro de téléphone principal du contact.',
|
|
54
|
+
section: 'contact',
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
key: '{{secondaryPhone}}',
|
|
58
|
+
description: 'Autre numéro de téléphone du contact si renseigné.',
|
|
59
|
+
section: 'contact',
|
|
60
|
+
},
|
|
61
|
+
{ key: '{{address}}', description: 'Adresse postale complète du contact.', section: 'contact' },
|
|
62
|
+
{
|
|
63
|
+
key: '{{companyName}}',
|
|
64
|
+
description: 'Nom de l’entreprise ou structure du contact.',
|
|
65
|
+
section: 'contact',
|
|
66
|
+
},
|
|
67
|
+
// CRM
|
|
68
|
+
{ key: '{{jobTitle}}', description: 'Poste / fonction du contact.', section: 'crm' },
|
|
69
|
+
{ key: '{{website}}', description: 'Site web du contact ou de son entreprise.', section: 'crm' },
|
|
70
|
+
{ key: '{{origin}}', description: 'Origine ou source du lead.', section: 'crm' },
|
|
71
|
+
{ key: '{{statusName}}', description: 'Nom du statut actuel du contact.', section: 'crm' },
|
|
72
|
+
{
|
|
73
|
+
key: '{{assignedCommercialName}}',
|
|
74
|
+
description: 'Nom du commercial assigné au contact.',
|
|
75
|
+
section: 'crm',
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
key: '{{closingReason}}',
|
|
79
|
+
description: 'Motif de fermeture (si le contact a le statut Fermé).',
|
|
80
|
+
section: 'crm',
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
key: '{{assignedTeleproName}}',
|
|
84
|
+
description: 'Nom du télépro assigné au contact.',
|
|
85
|
+
section: 'crm',
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
key: '{{createdAt}}',
|
|
89
|
+
description: 'Date de création du contact (format dd/mm/yyyy).',
|
|
90
|
+
section: 'crm',
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
key: '{{companyAddress}}',
|
|
94
|
+
description: 'Adresse de l\'entreprise liée au contact.',
|
|
95
|
+
section: 'crm',
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
key: '{{companyCity}}',
|
|
99
|
+
description: 'Ville du siège de l\'entreprise.',
|
|
100
|
+
section: 'crm',
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
key: '{{companyPostalCode}}',
|
|
104
|
+
description: 'Code postal de l\'entreprise.',
|
|
105
|
+
section: 'crm',
|
|
106
|
+
},
|
|
34
107
|
];
|
|
35
108
|
|
|
36
109
|
/**
|
|
@@ -49,7 +122,52 @@ export function replaceTemplateVariables(template: string, variables: ContactVar
|
|
|
49
122
|
result = result.replace(/\{\{address\}\}/g, variables.address || '');
|
|
50
123
|
result = result.replace(/\{\{city\}\}/g, variables.city || '');
|
|
51
124
|
result = result.replace(/\{\{postalCode\}\}/g, variables.postalCode || '');
|
|
52
|
-
result = result.replace(
|
|
125
|
+
result = result.replace(
|
|
126
|
+
/\{\{companyName\}\}/g,
|
|
127
|
+
variables.company?.name ?? variables.companyName ?? '',
|
|
128
|
+
);
|
|
129
|
+
result = result.replace(/\{\{jobTitle\}\}/g, variables.jobTitle || '');
|
|
130
|
+
result = result.replace(/\{\{website\}\}/g, variables.website || '');
|
|
131
|
+
result = result.replace(/\{\{origin\}\}/g, variables.origin || '');
|
|
132
|
+
result = result.replace(/\{\{statusName\}\}/g, variables.statusName || '');
|
|
133
|
+
result = result.replace(
|
|
134
|
+
/\{\{assignedCommercialName\}\}/g,
|
|
135
|
+
variables.assignedCommercialName || '',
|
|
136
|
+
);
|
|
137
|
+
result = result.replace(
|
|
138
|
+
/\{\{assignedTeleproName\}\}/g,
|
|
139
|
+
variables.assignedTeleproName || '',
|
|
140
|
+
);
|
|
141
|
+
result = result.replace(/\{\{closingReason\}\}/g, variables.closingReason || '');
|
|
142
|
+
result = result.replace(
|
|
143
|
+
/\{\{companyAddress\}\}/g,
|
|
144
|
+
variables.company?.address ?? '',
|
|
145
|
+
);
|
|
146
|
+
result = result.replace(
|
|
147
|
+
/\{\{companyCity\}\}/g,
|
|
148
|
+
variables.company?.city ?? '',
|
|
149
|
+
);
|
|
150
|
+
result = result.replace(
|
|
151
|
+
/\{\{companyPostalCode\}\}/g,
|
|
152
|
+
variables.company?.postalCode ?? '',
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
// createdAt formaté (dd/mm/yyyy)
|
|
156
|
+
let createdAtStr = '';
|
|
157
|
+
if (variables.createdAt) {
|
|
158
|
+
const d =
|
|
159
|
+
typeof variables.createdAt === 'string'
|
|
160
|
+
? new Date(variables.createdAt)
|
|
161
|
+
: variables.createdAt;
|
|
162
|
+
if (!isNaN(d.getTime())) {
|
|
163
|
+
createdAtStr = d.toLocaleDateString('fr-FR', {
|
|
164
|
+
day: '2-digit',
|
|
165
|
+
month: '2-digit',
|
|
166
|
+
year: 'numeric',
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
result = result.replace(/\{\{createdAt\}\}/g, createdAtStr);
|
|
53
171
|
|
|
54
172
|
// Variable composée : fullName
|
|
55
173
|
const fullName = [variables.firstName, variables.lastName].filter(Boolean).join(' ') || '';
|
|
@@ -59,18 +177,75 @@ export function replaceTemplateVariables(template: string, variables: ContactVar
|
|
|
59
177
|
}
|
|
60
178
|
|
|
61
179
|
/**
|
|
62
|
-
*
|
|
180
|
+
* Construit l'objet ContactVariables à partir d'un contact (shape flexible).
|
|
63
181
|
*/
|
|
64
|
-
export function
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
182
|
+
export function buildContactVariables(contact: {
|
|
183
|
+
firstName?: string | null;
|
|
184
|
+
lastName?: string | null;
|
|
185
|
+
civility?: string | null;
|
|
186
|
+
email?: string | null;
|
|
187
|
+
phone?: string | null;
|
|
188
|
+
secondaryPhone?: string | null;
|
|
189
|
+
address?: string | null;
|
|
190
|
+
city?: string | null;
|
|
191
|
+
postalCode?: string | null;
|
|
192
|
+
company?: {
|
|
193
|
+
name?: string | null;
|
|
194
|
+
address?: string | null;
|
|
195
|
+
city?: string | null;
|
|
196
|
+
postalCode?: string | null;
|
|
197
|
+
} | null;
|
|
198
|
+
companyName?: string | null;
|
|
199
|
+
jobTitle?: string | null;
|
|
200
|
+
website?: string | null;
|
|
201
|
+
origin?: string | null;
|
|
202
|
+
status?: { name?: string | null } | null;
|
|
203
|
+
statusName?: string | null;
|
|
204
|
+
assignedCommercial?: {
|
|
205
|
+
name?: string | null;
|
|
206
|
+
firstName?: string | null;
|
|
207
|
+
lastName?: string | null;
|
|
208
|
+
} | null;
|
|
209
|
+
assignedTelepro?: {
|
|
210
|
+
name?: string | null;
|
|
211
|
+
firstName?: string | null;
|
|
212
|
+
lastName?: string | null;
|
|
213
|
+
} | null;
|
|
214
|
+
closingReason?: string | null;
|
|
215
|
+
createdAt?: Date | string | null;
|
|
216
|
+
}): ContactVariables {
|
|
217
|
+
const commercialName = contact.assignedCommercial
|
|
218
|
+
? (contact.assignedCommercial.name ??
|
|
219
|
+
[contact.assignedCommercial.firstName, contact.assignedCommercial.lastName]
|
|
220
|
+
.filter(Boolean)
|
|
221
|
+
.join(' ')) || ''
|
|
222
|
+
: '';
|
|
223
|
+
const teleproName = contact.assignedTelepro
|
|
224
|
+
? (contact.assignedTelepro.name ??
|
|
225
|
+
[contact.assignedTelepro.firstName, contact.assignedTelepro.lastName]
|
|
226
|
+
.filter(Boolean)
|
|
227
|
+
.join(' ')) || ''
|
|
228
|
+
: '';
|
|
229
|
+
return {
|
|
230
|
+
firstName: contact.firstName || '',
|
|
231
|
+
lastName: contact.lastName || '',
|
|
232
|
+
civility: contact.civility || '',
|
|
233
|
+
email: contact.email || '',
|
|
234
|
+
phone: contact.phone || '',
|
|
235
|
+
secondaryPhone: contact.secondaryPhone || '',
|
|
236
|
+
address: contact.address || '',
|
|
237
|
+
city: contact.city || '',
|
|
238
|
+
postalCode: contact.postalCode || '',
|
|
239
|
+
companyName: contact.company?.name ?? contact.companyName ?? '',
|
|
240
|
+
company: contact.company || null,
|
|
241
|
+
jobTitle: contact.jobTitle || '',
|
|
242
|
+
website: contact.website || '',
|
|
243
|
+
origin: contact.origin || '',
|
|
244
|
+
statusName: contact.status?.name ?? contact.statusName ?? '',
|
|
245
|
+
assignedCommercialName: commercialName,
|
|
246
|
+
assignedTeleproName: teleproName,
|
|
247
|
+
closingReason: contact.closingReason || '',
|
|
248
|
+
createdAt: contact.createdAt || null,
|
|
249
|
+
};
|
|
76
250
|
}
|
|
251
|
+
|
|
@@ -1,19 +1,10 @@
|
|
|
1
|
-
import { clsx } from 'clsx';
|
|
1
|
+
import { clsx, type ClassValue } from 'clsx';
|
|
2
2
|
import { twMerge } from 'tailwind-merge';
|
|
3
3
|
|
|
4
|
-
export function cn(...inputs:
|
|
4
|
+
export function cn(...inputs: ClassValue[]) {
|
|
5
5
|
return twMerge(clsx(inputs));
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
-
export const DELETED_USER_LABEL = 'Ancien utilisateur';
|
|
9
|
-
|
|
10
|
-
export function getUserDisplayName(
|
|
11
|
-
user: { name?: string | null; email?: string | null } | null | undefined,
|
|
12
|
-
): string {
|
|
13
|
-
if (!user) return DELETED_USER_LABEL;
|
|
14
|
-
return user.name || user.email || DELETED_USER_LABEL;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
8
|
/**
|
|
18
9
|
* Normalise un numéro de téléphone au format français : 0X XX XX XX XX
|
|
19
10
|
* - Supprime tous les caractères non numériques
|
|
@@ -53,3 +44,72 @@ export function normalizePhoneNumber(phone: string | null | undefined): string {
|
|
|
53
44
|
// Si le numéro n'a pas 10 chiffres, retourner tel quel (cas d'erreur)
|
|
54
45
|
return digits;
|
|
55
46
|
}
|
|
47
|
+
|
|
48
|
+
const DATE_TIME_FORMATTER = new Intl.DateTimeFormat('fr-FR', {
|
|
49
|
+
day: '2-digit',
|
|
50
|
+
month: '2-digit',
|
|
51
|
+
year: 'numeric',
|
|
52
|
+
hour: '2-digit',
|
|
53
|
+
minute: '2-digit',
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
export function formatDateTime(dateString: string | null): string {
|
|
57
|
+
if (!dateString) return '-';
|
|
58
|
+
return DATE_TIME_FORMATTER.format(new Date(dateString));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Parse une chaîne en Date pour les imports (création de contact).
|
|
63
|
+
* Accepte ISO (yyyy-mm-dd), dd/mm/yyyy, dd-mm-yyyy.
|
|
64
|
+
* Retourne null si invalide ou vide.
|
|
65
|
+
*/
|
|
66
|
+
export function parseImportDate(value: string | null | undefined): Date | null {
|
|
67
|
+
if (value == null || String(value).trim() === '') return null;
|
|
68
|
+
const s = String(value).trim();
|
|
69
|
+
if (/^\d{4}-\d{2}-\d{2}(T|\s|$)/.test(s)) {
|
|
70
|
+
const d = new Date(s);
|
|
71
|
+
return Number.isNaN(d.getTime()) ? null : d;
|
|
72
|
+
}
|
|
73
|
+
const parts = s.match(/^(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{2,4})/);
|
|
74
|
+
if (parts) {
|
|
75
|
+
const [, a, b, year] = parts;
|
|
76
|
+
const y = year.length === 2 ? 2000 + parseInt(year, 10) : parseInt(year, 10);
|
|
77
|
+
const n1 = parseInt(a, 10);
|
|
78
|
+
const n2 = parseInt(b, 10);
|
|
79
|
+
const day = n1 > 31 ? n2 : n1;
|
|
80
|
+
const month = n1 > 31 ? n1 : n2;
|
|
81
|
+
const d = new Date(y, month - 1, day);
|
|
82
|
+
return Number.isNaN(d.getTime()) ? null : d;
|
|
83
|
+
}
|
|
84
|
+
const d = new Date(s);
|
|
85
|
+
return Number.isNaN(d.getTime()) ? null : d;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Retourne un message toast adapte a l'environnement :
|
|
90
|
+
* - En développement : affiche le message utilisateur + le détail technique
|
|
91
|
+
* - En production : affiche uniquement le message utilisateur
|
|
92
|
+
*/
|
|
93
|
+
export function devToast(userMessage: string, devDetail?: unknown): string {
|
|
94
|
+
if (process.env.NODE_ENV === 'development' && devDetail) {
|
|
95
|
+
const detail =
|
|
96
|
+
devDetail instanceof Error
|
|
97
|
+
? devDetail.message
|
|
98
|
+
: typeof devDetail === 'string'
|
|
99
|
+
? devDetail
|
|
100
|
+
: JSON.stringify(devDetail);
|
|
101
|
+
return `${userMessage}\n\n🔧 ${detail}`;
|
|
102
|
+
}
|
|
103
|
+
return userMessage;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function indexToColumn(index: number): string {
|
|
107
|
+
let col = '';
|
|
108
|
+
let n = index + 1;
|
|
109
|
+
while (n > 0) {
|
|
110
|
+
const remainder = (n - 1) % 26;
|
|
111
|
+
col = String.fromCharCode(65 + remainder) + col;
|
|
112
|
+
n = Math.floor((n - 1) / 26);
|
|
113
|
+
}
|
|
114
|
+
return col;
|
|
115
|
+
}
|
|
@@ -28,7 +28,6 @@ export interface WidgetDefinition {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
export const WIDGET_REGISTRY: WidgetDefinition[] = [
|
|
31
|
-
// Statistiques
|
|
32
31
|
{
|
|
33
32
|
type: 'stat_total_contacts',
|
|
34
33
|
label: 'Total Contacts',
|
|
@@ -77,7 +76,6 @@ export const WIDGET_REGISTRY: WidgetDefinition[] = [
|
|
|
77
76
|
minH: 2,
|
|
78
77
|
maxH: 2,
|
|
79
78
|
},
|
|
80
|
-
// Graphiques
|
|
81
79
|
{
|
|
82
80
|
type: 'contacts_chart',
|
|
83
81
|
label: 'Évolution des Contacts',
|
|
@@ -133,7 +131,6 @@ export const WIDGET_REGISTRY: WidgetDefinition[] = [
|
|
|
133
131
|
minW: 4,
|
|
134
132
|
minH: 3,
|
|
135
133
|
},
|
|
136
|
-
// Listes
|
|
137
134
|
{
|
|
138
135
|
type: 'upcoming_tasks',
|
|
139
136
|
label: 'Tâches à Venir',
|
|
@@ -173,5 +170,4 @@ export function getWidgetDefinition(type: string): WidgetDefinition | undefined
|
|
|
173
170
|
return WIDGET_REGISTRY.find((w) => w.type === type);
|
|
174
171
|
}
|
|
175
172
|
|
|
176
|
-
// Ré-export du layout par défaut
|
|
177
173
|
export { DEFAULT_WIDGETS } from './default-widgets';
|