create-crm-tmp 1.1.1 → 1.1.3
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 +1 -0
- package/package.json +1 -1
- package/template/README.md +52 -66
- package/template/package.json +25 -23
- package/template/prisma/migrations/20260226093949_fix_cascade_on_user_delete/migration.sql +69 -0
- package/template/prisma/schema.prisma +16 -16
- package/template/src/app/(dashboard)/agenda/page.tsx +7 -7
- package/template/src/app/(dashboard)/contacts/[id]/page.tsx +66 -96
- package/template/src/app/(dashboard)/contacts/page.tsx +2 -2
- package/template/src/app/api/contacts/export/route.ts +1 -1
- package/template/src/app/api/send/route.ts +1 -43
- package/template/src/app/api/settings/statuses/[id]/route.ts +13 -0
- package/template/src/app/api/tasks/[id]/route.ts +0 -12
- package/template/src/app/api/users/[id]/route.ts +16 -1
- package/template/src/app/api/users/route.ts +14 -30
- package/template/src/app/api/workflows/process/route.ts +0 -1
- package/template/src/app/globals.css +0 -33
- package/template/src/lib/roles.ts +13 -35
- package/template/src/lib/utils.ts +9 -0
- package/template/src/lib/workflow-executor.ts +0 -1
- package/template/src/proxy.ts +2 -2
- package/template/src/components/dashboard/sales-analytics-chart.tsx +0 -77
package/bin/create-crm-tmp.js
CHANGED
|
@@ -72,6 +72,7 @@ APP_NAME="${projectName
|
|
|
72
72
|
|
|
73
73
|
ENCRYPTION_KEY="" # openssl rand -base64 32
|
|
74
74
|
|
|
75
|
+
# API Google à ajouter dans console.cloud.google : Google Drive API, Google Calendar API, Google Sheet API
|
|
75
76
|
GOOGLE_CLIENT_ID=""
|
|
76
77
|
GOOGLE_CLIENT_SECRET=""
|
|
77
78
|
GOOGLE_REDIRECT_URI="http://localhost:3000/api/auth/google/callback"
|
package/package.json
CHANGED
package/template/README.md
CHANGED
|
@@ -53,102 +53,88 @@ pro.
|
|
|
53
53
|
- **Langage**: TypeScript
|
|
54
54
|
- **Gestionnaire de paquets**: pnpm
|
|
55
55
|
|
|
56
|
-
## 🚀
|
|
56
|
+
## 🚀 Installation
|
|
57
57
|
|
|
58
|
-
|
|
59
|
-
Pour créer un nouveau projet basé sur ce CRM :
|
|
58
|
+
1. **Cloner le projet**
|
|
60
59
|
|
|
61
60
|
```bash
|
|
62
|
-
|
|
61
|
+
git clone <votre-repo>
|
|
62
|
+
cd crm-template
|
|
63
63
|
```
|
|
64
64
|
|
|
65
|
-
|
|
65
|
+
2. **Installer les dépendances**
|
|
66
66
|
|
|
67
67
|
```bash
|
|
68
|
-
cd mon-crm
|
|
69
68
|
pnpm install
|
|
70
69
|
```
|
|
71
70
|
|
|
72
|
-
|
|
71
|
+
3. **Configurer les variables d'environnement**
|
|
73
72
|
|
|
74
|
-
|
|
73
|
+
Créez un fichier `.env` à la racine du projet :
|
|
75
74
|
|
|
76
|
-
|
|
75
|
+
```env
|
|
76
|
+
# Database
|
|
77
|
+
DATABASE_URL="postgresql://postgres:password@localhost:5432/crm_db"
|
|
77
78
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
- Attendez que la base de données soit provisionnée.
|
|
82
|
-
2. **Récupérer la connexion Postgres**
|
|
83
|
-
- Dans Supabase, cliquez sur `🔌 Connect`.
|
|
84
|
-
- Sélectionnez `ORMs` et choisissez le Tool `Prisma`
|
|
85
|
-
- Copiez les 2 premières variables d'environnement du contenu dans `.env.local` (`DATABASE_URL & DIRECT_URL`).
|
|
86
|
-
3. **Configurer `DATABASE_URL`**
|
|
87
|
-
- Dans votre projet généré (`mon-crm`), créez ou mettez à jour le fichier `.env` :
|
|
79
|
+
# Better Auth (générer avec: openssl rand -base64 32)
|
|
80
|
+
BETTER_AUTH_SECRET="votre-clé-secrète-minimum-32-caractères"
|
|
81
|
+
BETTER_AUTH_URL="http://localhost:3000"
|
|
88
82
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
83
|
+
# Application
|
|
84
|
+
NEXT_PUBLIC_APP_URL="http://localhost:3000"
|
|
85
|
+
NODE_ENV="development"
|
|
86
|
+
```
|
|
93
87
|
|
|
94
|
-
|
|
95
|
-
BETTER_AUTH_SECRET="votre-clé-secrète-minimum-32-caractères"
|
|
96
|
-
BETTER_AUTH_URL="http://localhost:3000"
|
|
88
|
+
4. **Créer la base de données**
|
|
97
89
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
4. **Appliquer les migrations Prisma sur Supabase**
|
|
90
|
+
```bash
|
|
91
|
+
createdb crm_db
|
|
92
|
+
# Ou: psql -U postgres -c "CREATE DATABASE crm_db;"
|
|
93
|
+
```
|
|
104
94
|
|
|
105
|
-
|
|
106
|
-
pnpm prisma migrate deploy
|
|
107
|
-
```
|
|
95
|
+
5. **Appliquer les migrations**
|
|
108
96
|
|
|
109
|
-
|
|
97
|
+
```bash
|
|
98
|
+
pnpm prisma migrate deploy
|
|
99
|
+
```
|
|
110
100
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
101
|
+
6. **Lancer le serveur de développement**
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
pnpm dev
|
|
105
|
+
```
|
|
114
106
|
|
|
115
107
|
Ouvrez [http://localhost:3000](http://localhost:3000) pour voir l'application.
|
|
116
108
|
|
|
117
|
-
|
|
109
|
+
7. **Créer votre premier admin**
|
|
118
110
|
|
|
119
111
|
```bash
|
|
120
|
-
#
|
|
121
|
-
|
|
112
|
+
# Ouvrir Prisma Studio
|
|
113
|
+
pnpm prisma studio
|
|
122
114
|
|
|
123
|
-
# Modifier
|
|
124
|
-
```
|
|
125
|
-
|
|
126
|
-
7. **Lancez le script permettant d'ajouter les permissions dans la base de données**
|
|
127
|
-
```bash
|
|
128
|
-
# Lancez ceci
|
|
129
|
-
npm tsx scripts/seed-roles.ts
|
|
115
|
+
# Modifier le champ "role" de votre utilisateur en "ADMIN"
|
|
130
116
|
```
|
|
131
117
|
|
|
132
118
|
## 📁 Structure du projet
|
|
133
119
|
|
|
134
120
|
```
|
|
135
|
-
|
|
136
|
-
├──
|
|
137
|
-
│
|
|
138
|
-
├──
|
|
139
|
-
│ ├──
|
|
140
|
-
│ │ ├──
|
|
141
|
-
│ │ ├──
|
|
142
|
-
│ │ ├──
|
|
143
|
-
│ │ ├──
|
|
144
|
-
│ │
|
|
145
|
-
│ ├──
|
|
146
|
-
│
|
|
147
|
-
│
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
└──
|
|
121
|
+
src/
|
|
122
|
+
├── app/
|
|
123
|
+
│ ├── (auth)/ # Groupe de routes d'authentification
|
|
124
|
+
│ ├── (dashboard)/ # Espace connecté (protégé)
|
|
125
|
+
│ │ ├── dashboard/ # Tableau de bord
|
|
126
|
+
│ │ ├── contacts/ # Gestion des contacts
|
|
127
|
+
│ │ ├── agenda/ # Agenda (mois / semaine / jour)
|
|
128
|
+
│ │ ├── automatisation/ # Workflows / automatisations
|
|
129
|
+
│ │ ├── templates/ # Templates d’emails
|
|
130
|
+
│ │ ├── settings/ # Paramètres (profil, entreprise, intégrations…)
|
|
131
|
+
│ │ ├── users/ # Gestion des utilisateurs & rôles (admin)
|
|
132
|
+
│ │ └── layout.tsx # Layout avec sidebar
|
|
133
|
+
│ ├── api/ # API (contacts, workflows, intégrations, users, etc.)
|
|
134
|
+
│ └── page.tsx # Page d'accueil (redirection)
|
|
135
|
+
├── components/ # Composants UI (sidebar, headers, skeletons…)
|
|
136
|
+
├── lib/ # Auth, Prisma, rôles, intégrations Google, workflows…
|
|
137
|
+
└── proxy.ts # Protection des routes (proxy)
|
|
152
138
|
```
|
|
153
139
|
|
|
154
140
|
## 🔒 Protection & rôles
|
package/template/package.json
CHANGED
|
@@ -12,7 +12,6 @@
|
|
|
12
12
|
"format:check": "prettier --check \"**/*.{ts,tsx,js,jsx,json,css,md}\""
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@hookform/resolvers": "^5.2.2",
|
|
16
15
|
"@lexical/html": "^0.38.2",
|
|
17
16
|
"@lexical/list": "^0.38.2",
|
|
18
17
|
"@lexical/markdown": "^0.38.2",
|
|
@@ -21,46 +20,49 @@
|
|
|
21
20
|
"@lexical/selection": "^0.38.2",
|
|
22
21
|
"@lexical/utils": "^0.38.2",
|
|
23
22
|
"@lexkit/editor": "^0.0.38",
|
|
24
|
-
"@prisma/adapter-pg": "^7.
|
|
25
|
-
"@prisma/client": "^7.
|
|
23
|
+
"@prisma/adapter-pg": "^7.4.1",
|
|
24
|
+
"@prisma/client": "^7.4.1",
|
|
26
25
|
"@react-email/render": "^2.0.4",
|
|
27
26
|
"bcryptjs": "^3.0.3",
|
|
28
|
-
"better-auth": "^1.4.
|
|
27
|
+
"better-auth": "^1.4.19",
|
|
29
28
|
"clsx": "^2.1.1",
|
|
30
|
-
"dotenv": "^17.
|
|
31
|
-
"iron-session": "^8.0.4",
|
|
29
|
+
"dotenv": "^17.3.1",
|
|
32
30
|
"lexical": "^0.38.2",
|
|
33
31
|
"lucide-react": "^0.555.0",
|
|
34
|
-
"next": "16.
|
|
32
|
+
"next": "16.1.6",
|
|
35
33
|
"nodemailer": "^7.0.13",
|
|
36
34
|
"papaparse": "^5.5.3",
|
|
37
|
-
"pg": "^8.
|
|
38
|
-
"react": "19.2.
|
|
39
|
-
"react-dom": "19.2.
|
|
35
|
+
"pg": "^8.19.0",
|
|
36
|
+
"react": "19.2.4",
|
|
37
|
+
"react-dom": "19.2.4",
|
|
40
38
|
"react-grid-layout": "^2.2.2",
|
|
41
|
-
"react-hook-form": "^7.71.1",
|
|
42
39
|
"recharts": "^2.15.4",
|
|
43
|
-
"tailwind-merge": "^3.
|
|
44
|
-
"xlsx": "
|
|
45
|
-
"zod": "^4.3.6"
|
|
40
|
+
"tailwind-merge": "^3.5.0",
|
|
41
|
+
"xlsx": "https://cdn.sheetjs.com/xlsx-latest/xlsx-latest.tgz"
|
|
46
42
|
},
|
|
47
43
|
"devDependencies": {
|
|
48
|
-
"@tailwindcss/postcss": "^4.1
|
|
49
|
-
"@types/node": "^20.19.
|
|
50
|
-
"@types/nodemailer": "^7.0.
|
|
44
|
+
"@tailwindcss/postcss": "^4.2.1",
|
|
45
|
+
"@types/node": "^20.19.34",
|
|
46
|
+
"@types/nodemailer": "^7.0.11",
|
|
51
47
|
"@types/papaparse": "^5.5.2",
|
|
52
48
|
"@types/pg": "^8.16.0",
|
|
53
|
-
"@types/react": "
|
|
54
|
-
"@types/react-dom": "
|
|
49
|
+
"@types/react": "19.2.14",
|
|
50
|
+
"@types/react-dom": "19.2.3",
|
|
55
51
|
"@types/react-grid-layout": "^2.1.0",
|
|
56
52
|
"babel-plugin-react-compiler": "1.0.0",
|
|
57
|
-
"eslint": "^9.39.
|
|
58
|
-
"eslint-config-next": "16.
|
|
53
|
+
"eslint": "^9.39.3",
|
|
54
|
+
"eslint-config-next": "16.1.6",
|
|
59
55
|
"prettier": "^3.8.1",
|
|
60
56
|
"prettier-plugin-tailwindcss": "^0.7.2",
|
|
61
|
-
"prisma": "^7.
|
|
62
|
-
"tailwindcss": "^4.1
|
|
57
|
+
"prisma": "^7.4.1",
|
|
58
|
+
"tailwindcss": "^4.2.1",
|
|
63
59
|
"tsx": "^4.21.0",
|
|
64
60
|
"typescript": "^5.9.3"
|
|
61
|
+
},
|
|
62
|
+
"pnpm": {
|
|
63
|
+
"overrides": {
|
|
64
|
+
"@types/react": "19.2.14",
|
|
65
|
+
"@types/react-dom": "19.2.3"
|
|
66
|
+
}
|
|
65
67
|
}
|
|
66
68
|
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
-- DropForeignKey
|
|
2
|
+
ALTER TABLE "contact" DROP CONSTRAINT "contact_createdById_fkey";
|
|
3
|
+
|
|
4
|
+
-- DropForeignKey
|
|
5
|
+
ALTER TABLE "contact_file" DROP CONSTRAINT "contact_file_uploadedById_fkey";
|
|
6
|
+
|
|
7
|
+
-- DropForeignKey
|
|
8
|
+
ALTER TABLE "google_sheet_sync_config" DROP CONSTRAINT "google_sheet_sync_config_ownerUserId_fkey";
|
|
9
|
+
|
|
10
|
+
-- DropForeignKey
|
|
11
|
+
ALTER TABLE "interaction" DROP CONSTRAINT "interaction_userId_fkey";
|
|
12
|
+
|
|
13
|
+
-- DropForeignKey
|
|
14
|
+
ALTER TABLE "task" DROP CONSTRAINT "task_assignedUserId_fkey";
|
|
15
|
+
|
|
16
|
+
-- DropForeignKey
|
|
17
|
+
ALTER TABLE "task" DROP CONSTRAINT "task_createdById_fkey";
|
|
18
|
+
|
|
19
|
+
-- DropForeignKey
|
|
20
|
+
ALTER TABLE "template" DROP CONSTRAINT "template_userId_fkey";
|
|
21
|
+
|
|
22
|
+
-- DropForeignKey
|
|
23
|
+
ALTER TABLE "workflow" DROP CONSTRAINT "workflow_userId_fkey";
|
|
24
|
+
|
|
25
|
+
-- AlterTable
|
|
26
|
+
ALTER TABLE "contact" ALTER COLUMN "createdById" DROP NOT NULL;
|
|
27
|
+
|
|
28
|
+
-- AlterTable
|
|
29
|
+
ALTER TABLE "contact_file" ALTER COLUMN "uploadedById" DROP NOT NULL;
|
|
30
|
+
|
|
31
|
+
-- AlterTable
|
|
32
|
+
ALTER TABLE "google_sheet_sync_config" ALTER COLUMN "ownerUserId" DROP NOT NULL;
|
|
33
|
+
|
|
34
|
+
-- AlterTable
|
|
35
|
+
ALTER TABLE "interaction" ALTER COLUMN "userId" DROP NOT NULL;
|
|
36
|
+
|
|
37
|
+
-- AlterTable
|
|
38
|
+
ALTER TABLE "task" ALTER COLUMN "assignedUserId" DROP NOT NULL,
|
|
39
|
+
ALTER COLUMN "createdById" DROP NOT NULL;
|
|
40
|
+
|
|
41
|
+
-- AlterTable
|
|
42
|
+
ALTER TABLE "template" ALTER COLUMN "userId" DROP NOT NULL;
|
|
43
|
+
|
|
44
|
+
-- AlterTable
|
|
45
|
+
ALTER TABLE "workflow" ALTER COLUMN "userId" DROP NOT NULL;
|
|
46
|
+
|
|
47
|
+
-- AddForeignKey
|
|
48
|
+
ALTER TABLE "contact" ADD CONSTRAINT "contact_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
|
49
|
+
|
|
50
|
+
-- AddForeignKey
|
|
51
|
+
ALTER TABLE "interaction" ADD CONSTRAINT "interaction_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
|
52
|
+
|
|
53
|
+
-- AddForeignKey
|
|
54
|
+
ALTER TABLE "task" ADD CONSTRAINT "task_assignedUserId_fkey" FOREIGN KEY ("assignedUserId") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
|
55
|
+
|
|
56
|
+
-- AddForeignKey
|
|
57
|
+
ALTER TABLE "task" ADD CONSTRAINT "task_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
|
58
|
+
|
|
59
|
+
-- AddForeignKey
|
|
60
|
+
ALTER TABLE "contact_file" ADD CONSTRAINT "contact_file_uploadedById_fkey" FOREIGN KEY ("uploadedById") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
|
61
|
+
|
|
62
|
+
-- AddForeignKey
|
|
63
|
+
ALTER TABLE "google_sheet_sync_config" ADD CONSTRAINT "google_sheet_sync_config_ownerUserId_fkey" FOREIGN KEY ("ownerUserId") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
|
64
|
+
|
|
65
|
+
-- AddForeignKey
|
|
66
|
+
ALTER TABLE "template" ADD CONSTRAINT "template_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
|
67
|
+
|
|
68
|
+
-- AddForeignKey
|
|
69
|
+
ALTER TABLE "workflow" ADD CONSTRAINT "workflow_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
|
@@ -260,8 +260,8 @@ model Contact {
|
|
|
260
260
|
assignedCommercial User? @relation("ContactAssignedCommercial", fields: [assignedCommercialId], references: [id], onDelete: SetNull)
|
|
261
261
|
assignedTeleproId String? // Télépro assigné
|
|
262
262
|
assignedTelepro User? @relation("ContactAssignedTelepro", fields: [assignedTeleproId], references: [id], onDelete: SetNull)
|
|
263
|
-
createdById String // Utilisateur qui a créé le contact
|
|
264
|
-
createdBy User
|
|
263
|
+
createdById String? // Utilisateur qui a créé le contact
|
|
264
|
+
createdBy User? @relation("ContactCreatedBy", fields: [createdById], references: [id], onDelete: SetNull)
|
|
265
265
|
createdAt DateTime @default(now())
|
|
266
266
|
updatedAt DateTime @updatedAt
|
|
267
267
|
interactions Interaction[]
|
|
@@ -286,8 +286,8 @@ model Interaction {
|
|
|
286
286
|
title String?
|
|
287
287
|
content String // Contenu de l'interaction
|
|
288
288
|
metadata Json? // Métadonnées pour stocker les détails des changements (ancienne valeur, nouvelle valeur, etc.)
|
|
289
|
-
userId String // Utilisateur qui a créé l'interaction
|
|
290
|
-
user User
|
|
289
|
+
userId String? // Utilisateur qui a créé l'interaction
|
|
290
|
+
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
|
|
291
291
|
date DateTime? // Date de l'interaction (pour RDV, appels, etc.)
|
|
292
292
|
createdAt DateTime @default(now())
|
|
293
293
|
updatedAt DateTime @updatedAt
|
|
@@ -322,10 +322,10 @@ model Task {
|
|
|
322
322
|
priority TaskPriority @default(MEDIUM)
|
|
323
323
|
scheduledAt DateTime // Date et heure de la tâche
|
|
324
324
|
reminderMinutesBefore Int? // Rappel en minutes avant l'heure prévue (ex: 15, 30, 60)
|
|
325
|
-
assignedUserId String // Utilisateur assigné
|
|
326
|
-
assignedUser User
|
|
327
|
-
createdById String // Utilisateur qui a créé la tâche
|
|
328
|
-
createdBy User
|
|
325
|
+
assignedUserId String? // Utilisateur assigné
|
|
326
|
+
assignedUser User? @relation("TaskAssignedTo", fields: [assignedUserId], references: [id], onDelete: SetNull)
|
|
327
|
+
createdById String? // Utilisateur qui a créé la tâche
|
|
328
|
+
createdBy User? @relation("TaskCreatedBy", fields: [createdById], references: [id], onDelete: SetNull)
|
|
329
329
|
completed Boolean @default(false) // Tâche terminée ou non
|
|
330
330
|
completedAt DateTime? // Date de complétion
|
|
331
331
|
googleEventId String? // ID de l'évènement Google Calendar
|
|
@@ -366,8 +366,8 @@ model ContactFile {
|
|
|
366
366
|
fileSize Int // Taille en octets
|
|
367
367
|
mimeType String // Type MIME du fichier
|
|
368
368
|
googleDriveFileId String // ID du fichier dans Google Drive
|
|
369
|
-
uploadedById String
|
|
370
|
-
uploadedBy User
|
|
369
|
+
uploadedById String? // Utilisateur qui a uploadé le fichier
|
|
370
|
+
uploadedBy User? @relation("ContactFileUploadedBy", fields: [uploadedById], references: [id], onDelete: SetNull)
|
|
371
371
|
createdAt DateTime @default(now())
|
|
372
372
|
updatedAt DateTime @updatedAt
|
|
373
373
|
|
|
@@ -411,8 +411,8 @@ model GoogleAdsLeadConfig {
|
|
|
411
411
|
model GoogleSheetSyncConfig {
|
|
412
412
|
id String @id @default(cuid())
|
|
413
413
|
name String // Nom de la configuration (ex: "Contacts Ventes")
|
|
414
|
-
ownerUserId String
|
|
415
|
-
ownerUser User
|
|
414
|
+
ownerUserId String?
|
|
415
|
+
ownerUser User? @relation("GoogleSheetOwner", fields: [ownerUserId], references: [id], onDelete: SetNull)
|
|
416
416
|
spreadsheetId String
|
|
417
417
|
sheetName String
|
|
418
418
|
headerRow Int
|
|
@@ -457,8 +457,8 @@ model Template {
|
|
|
457
457
|
type TemplateType // Type de template (EMAIL, SMS, NOTE)
|
|
458
458
|
subject String? // Sujet (pour EMAIL uniquement)
|
|
459
459
|
content String // Contenu du template (HTML pour EMAIL, texte pour SMS et NOTE)
|
|
460
|
-
userId String // Utilisateur qui a créé le template
|
|
461
|
-
user User
|
|
460
|
+
userId String? // Utilisateur qui a créé le template
|
|
461
|
+
user User? @relation("TemplateCreatedBy", fields: [userId], references: [id], onDelete: SetNull)
|
|
462
462
|
workflowActions WorkflowAction[] @relation("WorkflowActionEmailTemplate")
|
|
463
463
|
createdAt DateTime @default(now())
|
|
464
464
|
updatedAt DateTime @updatedAt
|
|
@@ -493,8 +493,8 @@ model Workflow {
|
|
|
493
493
|
name String
|
|
494
494
|
description String?
|
|
495
495
|
active Boolean @default(true)
|
|
496
|
-
userId String
|
|
497
|
-
user User
|
|
496
|
+
userId String? // Utilisateur propriétaire du workflow
|
|
497
|
+
user User? @relation("WorkflowOwner", fields: [userId], references: [id], onDelete: SetNull)
|
|
498
498
|
|
|
499
499
|
// Configuration du déclencheur
|
|
500
500
|
triggerType WorkflowTriggerType
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { useState, useEffect, useRef, useMemo } from 'react';
|
|
4
4
|
import { useUserRole } from '@/hooks/use-user-role';
|
|
5
5
|
import { useViewAs } from '@/contexts/view-as-context';
|
|
6
|
-
import { cn } from '@/lib/utils';
|
|
6
|
+
import { cn, getUserDisplayName } from '@/lib/utils';
|
|
7
7
|
import {
|
|
8
8
|
Calendar,
|
|
9
9
|
Clock,
|
|
@@ -43,7 +43,7 @@ interface Task {
|
|
|
43
43
|
id: string;
|
|
44
44
|
name: string;
|
|
45
45
|
eventColor: string | null;
|
|
46
|
-
};
|
|
46
|
+
} | null;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
// Couleurs par type d'événement (fallback si pas de couleur utilisateur)
|
|
@@ -69,7 +69,7 @@ const HOURS = Array.from({ length: 17 }, (_, i) => i + 6); // 6h à 22h
|
|
|
69
69
|
|
|
70
70
|
// Fonction pour obtenir la couleur d'un événement basée sur l'utilisateur
|
|
71
71
|
const getEventColor = (task: Task): string => {
|
|
72
|
-
return task.assignedUser
|
|
72
|
+
return task.assignedUser?.eventColor || TYPE_COLORS[task.type] || '#6366F1';
|
|
73
73
|
};
|
|
74
74
|
|
|
75
75
|
const formatHourLabel = (hour: number) => {
|
|
@@ -498,7 +498,7 @@ export default function AgendaPage() {
|
|
|
498
498
|
// Filtre par utilisateur (si "Voir les autres utilisateurs" est actif)
|
|
499
499
|
let userMatch = true;
|
|
500
500
|
if (showOtherUsersEvents && selectedUserIds.size > 0) {
|
|
501
|
-
userMatch = selectedUserIds.has(task.assignedUser.id);
|
|
501
|
+
userMatch = task.assignedUser ? selectedUserIds.has(task.assignedUser.id) : false;
|
|
502
502
|
}
|
|
503
503
|
|
|
504
504
|
return typeMatch && userMatch;
|
|
@@ -1821,7 +1821,7 @@ export default function AgendaPage() {
|
|
|
1821
1821
|
{isAdmin && (
|
|
1822
1822
|
<div className="flex items-center gap-1">
|
|
1823
1823
|
<User className="h-4 w-4" />
|
|
1824
|
-
{task.assignedUser
|
|
1824
|
+
{getUserDisplayName(task.assignedUser)}
|
|
1825
1825
|
</div>
|
|
1826
1826
|
)}
|
|
1827
1827
|
</div>
|
|
@@ -1943,7 +1943,7 @@ export default function AgendaPage() {
|
|
|
1943
1943
|
{isAdmin && (
|
|
1944
1944
|
<div className="flex items-center gap-1">
|
|
1945
1945
|
<User className="h-4 w-4" />
|
|
1946
|
-
{task.assignedUser
|
|
1946
|
+
{getUserDisplayName(task.assignedUser)}
|
|
1947
1947
|
</div>
|
|
1948
1948
|
)}
|
|
1949
1949
|
</div>
|
|
@@ -2072,7 +2072,7 @@ export default function AgendaPage() {
|
|
|
2072
2072
|
{isAdmin && (
|
|
2073
2073
|
<div className="flex items-center gap-1">
|
|
2074
2074
|
<User className="h-4 w-4" />
|
|
2075
|
-
{task.assignedUser
|
|
2075
|
+
{getUserDisplayName(task.assignedUser)}
|
|
2076
2076
|
</div>
|
|
2077
2077
|
)}
|
|
2078
2078
|
</div>
|
|
@@ -22,13 +22,12 @@ import {
|
|
|
22
22
|
Upload,
|
|
23
23
|
File,
|
|
24
24
|
Target,
|
|
25
|
-
ChevronDown,
|
|
26
25
|
} from 'lucide-react';
|
|
27
26
|
import Link from 'next/link';
|
|
28
27
|
import { Editor, type DefaultTemplateRef } from '@/components/editor';
|
|
29
28
|
import { useUserRole } from '@/hooks/use-user-role';
|
|
30
29
|
import { replaceTemplateVariables } from '@/lib/template-variables';
|
|
31
|
-
import { cn, normalizePhoneNumber } from '@/lib/utils';
|
|
30
|
+
import { cn, normalizePhoneNumber, getUserDisplayName } from '@/lib/utils';
|
|
32
31
|
|
|
33
32
|
interface Status {
|
|
34
33
|
id: string;
|
|
@@ -43,25 +42,6 @@ interface User {
|
|
|
43
42
|
role?: string;
|
|
44
43
|
}
|
|
45
44
|
|
|
46
|
-
function getNewsFeedCardColor(type: string): string {
|
|
47
|
-
switch (type) {
|
|
48
|
-
case 'STATUS_CHANGE':
|
|
49
|
-
return 'bg-purple-50 border-purple-200';
|
|
50
|
-
case 'CONTACT_UPDATE':
|
|
51
|
-
return 'bg-indigo-50 border-indigo-200';
|
|
52
|
-
case 'ASSIGNMENT_CHANGE':
|
|
53
|
-
return 'bg-teal-50 border-teal-200';
|
|
54
|
-
case 'FILE_UPLOADED':
|
|
55
|
-
return 'bg-green-50 border-green-200';
|
|
56
|
-
case 'FILE_REPLACED':
|
|
57
|
-
return 'bg-orange-50 border-orange-200';
|
|
58
|
-
case 'FILE_DELETED':
|
|
59
|
-
return 'bg-red-50 border-red-200';
|
|
60
|
-
default:
|
|
61
|
-
return 'bg-gray-50 border-gray-200';
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
45
|
interface Interaction {
|
|
66
46
|
id: string;
|
|
67
47
|
type:
|
|
@@ -118,8 +98,8 @@ interface Contact {
|
|
|
118
98
|
assignedCommercial: User | null;
|
|
119
99
|
assignedTeleproId: string | null;
|
|
120
100
|
assignedTelepro: User | null;
|
|
121
|
-
createdById: string;
|
|
122
|
-
createdBy: User;
|
|
101
|
+
createdById: string | null;
|
|
102
|
+
createdBy: User | null;
|
|
123
103
|
createdAt: string;
|
|
124
104
|
updatedAt: string;
|
|
125
105
|
interactions: Interaction[];
|
|
@@ -152,7 +132,6 @@ export default function ContactDetailPage() {
|
|
|
152
132
|
const [tasks, setTasks] = useState<any[]>([]);
|
|
153
133
|
const [sendingEmail, setSendingEmail] = useState(false);
|
|
154
134
|
const [creatingTask, setCreatingTask] = useState(false);
|
|
155
|
-
const [showNewsFeed, setShowNewsFeed] = useState(false);
|
|
156
135
|
const [emailTemplates, setEmailTemplates] = useState<any[]>([]);
|
|
157
136
|
const [noteTemplates, setNoteTemplates] = useState<any[]>([]);
|
|
158
137
|
const emailEditorRef = useRef<DefaultTemplateRef | null>(null);
|
|
@@ -2594,12 +2573,9 @@ export default function ContactDetailPage() {
|
|
|
2594
2573
|
},
|
|
2595
2574
|
)}
|
|
2596
2575
|
</p>
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
{interaction.user.name || interaction.user.email}
|
|
2601
|
-
</p>
|
|
2602
|
-
)}
|
|
2576
|
+
<p className="text-xs text-gray-500">
|
|
2577
|
+
Par : {getUserDisplayName(interaction.user)}
|
|
2578
|
+
</p>
|
|
2603
2579
|
</div>
|
|
2604
2580
|
</div>
|
|
2605
2581
|
</div>
|
|
@@ -2615,36 +2591,44 @@ export default function ContactDetailPage() {
|
|
|
2615
2591
|
|
|
2616
2592
|
{/* Section Fil d'actualités */}
|
|
2617
2593
|
<div>
|
|
2618
|
-
<
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2594
|
+
<div className="mb-4 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
|
2595
|
+
<h2 className="text-lg font-semibold text-gray-900">Fil d'actualités</h2>
|
|
2596
|
+
</div>
|
|
2597
|
+
<div className="space-y-4">
|
|
2598
|
+
{Object.keys(groupedNewsFeed).length === 0 ? (
|
|
2599
|
+
<p className="py-6 text-center text-sm text-gray-500">
|
|
2600
|
+
Aucune modification
|
|
2601
|
+
</p>
|
|
2602
|
+
) : (
|
|
2603
|
+
Object.entries(groupedNewsFeed).map(([date, interactions]) => (
|
|
2604
|
+
<div key={date}>
|
|
2605
|
+
<h3 className="mb-3 text-sm font-semibold text-gray-700">{date}</h3>
|
|
2606
|
+
<div className="space-y-2">
|
|
2607
|
+
{interactions.map((interaction) => {
|
|
2608
|
+
const getCardColor = (type: string) => {
|
|
2609
|
+
switch (type) {
|
|
2610
|
+
case 'STATUS_CHANGE':
|
|
2611
|
+
return 'bg-purple-50 border-purple-200';
|
|
2612
|
+
case 'CONTACT_UPDATE':
|
|
2613
|
+
return 'bg-indigo-50 border-indigo-200';
|
|
2614
|
+
case 'ASSIGNMENT_CHANGE':
|
|
2615
|
+
return 'bg-teal-50 border-teal-200';
|
|
2616
|
+
case 'FILE_UPLOADED':
|
|
2617
|
+
return 'bg-green-50 border-green-200';
|
|
2618
|
+
case 'FILE_REPLACED':
|
|
2619
|
+
return 'bg-orange-50 border-orange-200';
|
|
2620
|
+
case 'FILE_DELETED':
|
|
2621
|
+
return 'bg-red-50 border-red-200';
|
|
2622
|
+
default:
|
|
2623
|
+
return 'bg-gray-50 border-gray-200';
|
|
2624
|
+
}
|
|
2625
|
+
};
|
|
2626
|
+
return (
|
|
2643
2627
|
<div
|
|
2644
2628
|
key={interaction.id}
|
|
2645
2629
|
className={cn(
|
|
2646
2630
|
'relative rounded-lg border p-3 sm:p-4',
|
|
2647
|
-
|
|
2631
|
+
getCardColor(interaction.type),
|
|
2648
2632
|
)}
|
|
2649
2633
|
>
|
|
2650
2634
|
<div className="flex items-start gap-2 sm:gap-3">
|
|
@@ -2670,23 +2654,20 @@ export default function ContactDetailPage() {
|
|
|
2670
2654
|
},
|
|
2671
2655
|
)}
|
|
2672
2656
|
</p>
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
{interaction.user.name || interaction.user.email}
|
|
2677
|
-
</p>
|
|
2678
|
-
)}
|
|
2657
|
+
<p className="text-xs text-gray-500">
|
|
2658
|
+
Par : {getUserDisplayName(interaction.user)}
|
|
2659
|
+
</p>
|
|
2679
2660
|
</div>
|
|
2680
2661
|
</div>
|
|
2681
2662
|
</div>
|
|
2682
2663
|
</div>
|
|
2683
|
-
)
|
|
2684
|
-
|
|
2664
|
+
);
|
|
2665
|
+
})}
|
|
2685
2666
|
</div>
|
|
2686
|
-
|
|
2687
|
-
)
|
|
2688
|
-
|
|
2689
|
-
|
|
2667
|
+
</div>
|
|
2668
|
+
))
|
|
2669
|
+
)}
|
|
2670
|
+
</div>
|
|
2690
2671
|
</div>
|
|
2691
2672
|
</div>
|
|
2692
2673
|
)}
|
|
@@ -2860,11 +2841,9 @@ export default function ContactDetailPage() {
|
|
|
2860
2841
|
},
|
|
2861
2842
|
)}
|
|
2862
2843
|
</p>
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
</p>
|
|
2867
|
-
)}
|
|
2844
|
+
<p className="text-xs text-gray-500">
|
|
2845
|
+
Par : {getUserDisplayName(interaction.user)}
|
|
2846
|
+
</p>
|
|
2868
2847
|
</div>
|
|
2869
2848
|
</div>
|
|
2870
2849
|
</div>
|
|
@@ -2935,11 +2914,9 @@ export default function ContactDetailPage() {
|
|
|
2935
2914
|
},
|
|
2936
2915
|
)}
|
|
2937
2916
|
</p>
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
</p>
|
|
2942
|
-
)}
|
|
2917
|
+
<p className="text-xs text-gray-500">
|
|
2918
|
+
Par : {getUserDisplayName(interaction.user)}
|
|
2919
|
+
</p>
|
|
2943
2920
|
</div>
|
|
2944
2921
|
</div>
|
|
2945
2922
|
</div>
|
|
@@ -3011,7 +2988,7 @@ export default function ContactDetailPage() {
|
|
|
3011
2988
|
<span>{formatFileSize(file.fileSize)}</span>
|
|
3012
2989
|
<span>•</span>
|
|
3013
2990
|
<span>
|
|
3014
|
-
Ajouté par {file.uploadedBy
|
|
2991
|
+
Ajouté par {getUserDisplayName(file.uploadedBy)}
|
|
3015
2992
|
</span>
|
|
3016
2993
|
<span>•</span>
|
|
3017
2994
|
<span>
|
|
@@ -3142,11 +3119,9 @@ export default function ContactDetailPage() {
|
|
|
3142
3119
|
},
|
|
3143
3120
|
)}
|
|
3144
3121
|
</p>
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
</p>
|
|
3149
|
-
)}
|
|
3122
|
+
<p className="text-xs text-gray-500">
|
|
3123
|
+
Par : {getUserDisplayName(interaction.user)}
|
|
3124
|
+
</p>
|
|
3150
3125
|
</div>
|
|
3151
3126
|
</div>
|
|
3152
3127
|
</div>
|
|
@@ -5580,9 +5555,7 @@ email2@example.com`}
|
|
|
5580
5555
|
minute: '2-digit',
|
|
5581
5556
|
})}
|
|
5582
5557
|
</span>
|
|
5583
|
-
{viewingActivity.user
|
|
5584
|
-
<span>Par : {viewingActivity.user.name || viewingActivity.user.email}</span>
|
|
5585
|
-
)}
|
|
5558
|
+
<span>Par : {getUserDisplayName(viewingActivity.user)}</span>
|
|
5586
5559
|
</div>
|
|
5587
5560
|
</div>
|
|
5588
5561
|
</div>
|
|
@@ -5989,15 +5962,12 @@ email2@example.com`}
|
|
|
5989
5962
|
</span>
|
|
5990
5963
|
</div>
|
|
5991
5964
|
)}
|
|
5992
|
-
|
|
5993
|
-
<
|
|
5994
|
-
|
|
5995
|
-
|
|
5996
|
-
|
|
5997
|
-
|
|
5998
|
-
</span>
|
|
5999
|
-
</div>
|
|
6000
|
-
)}
|
|
5965
|
+
<div className="flex items-center gap-2">
|
|
5966
|
+
<span className="text-sm font-medium text-gray-700">Assigné à :</span>
|
|
5967
|
+
<span className="text-sm text-gray-600">
|
|
5968
|
+
{getUserDisplayName(viewingAppointment.assignedUser)}
|
|
5969
|
+
</span>
|
|
5970
|
+
</div>
|
|
6001
5971
|
</div>
|
|
6002
5972
|
)}
|
|
6003
5973
|
</div>
|
|
@@ -68,8 +68,8 @@ interface Contact {
|
|
|
68
68
|
assignedCommercial: User | null;
|
|
69
69
|
assignedTeleproId: string | null;
|
|
70
70
|
assignedTelepro: User | null;
|
|
71
|
-
createdById: string;
|
|
72
|
-
createdBy: User;
|
|
71
|
+
createdById: string | null;
|
|
72
|
+
createdBy: User | null;
|
|
73
73
|
createdAt: string;
|
|
74
74
|
updatedAt: string;
|
|
75
75
|
}
|
|
@@ -129,7 +129,7 @@ export async function POST(request: NextRequest) {
|
|
|
129
129
|
minute: '2-digit',
|
|
130
130
|
})
|
|
131
131
|
: '';
|
|
132
|
-
const author = interaction.user?.name || '
|
|
132
|
+
const author = interaction.user?.name || 'Ancien utilisateur';
|
|
133
133
|
const title = interaction.title ? `${interaction.title}: ` : '';
|
|
134
134
|
const content = interaction.content || '';
|
|
135
135
|
|
|
@@ -8,7 +8,7 @@ import nodemailer from 'nodemailer';
|
|
|
8
8
|
import { render } from '@react-email/render';
|
|
9
9
|
import React from 'react';
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
|
|
12
12
|
|
|
13
13
|
function htmlToText(html: string): string {
|
|
14
14
|
if (!html) return '';
|
|
@@ -44,7 +44,6 @@ async function getAdminSmtpConfig(userId: string) {
|
|
|
44
44
|
};
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
console.log('✅ Configuration SMTP trouvée pour:', user.email);
|
|
48
47
|
return { config: user.smtpConfig, error: null };
|
|
49
48
|
} catch (error) {
|
|
50
49
|
console.error('Erreur lors de la récupération de la configuration SMTP:', error);
|
|
@@ -94,7 +93,6 @@ async function getAnyAdminSmtpConfig() {
|
|
|
94
93
|
};
|
|
95
94
|
}
|
|
96
95
|
|
|
97
|
-
console.log("✅ Configuration SMTP trouvée pour l'admin:", smtpConfig.user.email);
|
|
98
96
|
return { config: smtpConfig, error: null };
|
|
99
97
|
} catch (error) {
|
|
100
98
|
console.error('Erreur lors de la récupération de la configuration SMTP admin:', error);
|
|
@@ -123,14 +121,6 @@ export async function POST(request: Request) {
|
|
|
123
121
|
return Response.json({ error: 'Non authentifié' }, { status: 401 });
|
|
124
122
|
}
|
|
125
123
|
|
|
126
|
-
console.log("📧 Tentative d'envoi d'email:", {
|
|
127
|
-
to,
|
|
128
|
-
subject,
|
|
129
|
-
template,
|
|
130
|
-
isDevelopment,
|
|
131
|
-
userId: session?.user?.id || 'none (reset-password)',
|
|
132
|
-
});
|
|
133
|
-
|
|
134
124
|
// Récupérer la configuration SMTP
|
|
135
125
|
let smtpConfig, smtpError;
|
|
136
126
|
if (isResetPassword) {
|
|
@@ -177,30 +167,6 @@ export async function POST(request: Request) {
|
|
|
177
167
|
const emailHtml = await render(emailComponent);
|
|
178
168
|
const emailText = htmlToText(emailHtml);
|
|
179
169
|
|
|
180
|
-
// Logger les informations de l'email (même en production pour le debug)
|
|
181
|
-
if (isDevelopment) {
|
|
182
|
-
console.log("📧 [DEV MODE] Envoi de l'email:");
|
|
183
|
-
console.log({
|
|
184
|
-
from: smtpConfig.fromName
|
|
185
|
-
? `"${smtpConfig.fromName}" <${smtpConfig.fromEmail}>`
|
|
186
|
-
: smtpConfig.fromEmail,
|
|
187
|
-
to,
|
|
188
|
-
subject,
|
|
189
|
-
template,
|
|
190
|
-
data: { ...emailData },
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
// Afficher le lien d'invitation dans la console si c'est une invitation
|
|
194
|
-
if (template === 'invitation' && emailData.invitationUrl) {
|
|
195
|
-
console.log("🔗 Lien d'invitation:", emailData.invitationUrl);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
// Afficher le code de réinitialisation dans la console
|
|
199
|
-
if (template === 'reset-password' && emailData.code) {
|
|
200
|
-
console.log('🔑 Code de réinitialisation:', emailData.code);
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
170
|
// Déchiffrer le mot de passe SMTP
|
|
205
171
|
let password: string;
|
|
206
172
|
try {
|
|
@@ -236,16 +202,8 @@ export async function POST(request: Request) {
|
|
|
236
202
|
html: emailHtml,
|
|
237
203
|
};
|
|
238
204
|
|
|
239
|
-
console.log("📤 Envoi de l'email via SMTP...", {
|
|
240
|
-
from: mailOptions.from,
|
|
241
|
-
to: recipients,
|
|
242
|
-
subject,
|
|
243
|
-
});
|
|
244
|
-
|
|
245
205
|
const info = await transporter.sendMail(mailOptions);
|
|
246
206
|
|
|
247
|
-
console.log('✅ Email envoyé avec succès:', info.messageId);
|
|
248
|
-
|
|
249
207
|
return Response.json({
|
|
250
208
|
id: info.messageId,
|
|
251
209
|
message: 'Email envoyé avec succès',
|
|
@@ -80,6 +80,19 @@ export async function DELETE(
|
|
|
80
80
|
return NextResponse.json({ error: 'Statut non trouvé' }, { status: 404 });
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
const contactsCount = await prisma.contact.count({
|
|
84
|
+
where: { statusId: id },
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
if (contactsCount > 0) {
|
|
88
|
+
return NextResponse.json(
|
|
89
|
+
{
|
|
90
|
+
error: `Ce statut ne peut pas être supprimé car ${contactsCount} contact(s) l'utilisent`,
|
|
91
|
+
},
|
|
92
|
+
{ status: 400 },
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
83
96
|
await prisma.status.delete({
|
|
84
97
|
where: { id },
|
|
85
98
|
});
|
|
@@ -590,12 +590,6 @@ export async function DELETE(
|
|
|
590
590
|
// Créer une interaction pour l'annulation si c'est un Google Meet ou un RDV
|
|
591
591
|
if (task.contactId && (task.type === 'VIDEO_CONFERENCE' || task.type === 'MEETING')) {
|
|
592
592
|
try {
|
|
593
|
-
console.log('Création interaction annulation pour task:', {
|
|
594
|
-
contactId: task.contactId,
|
|
595
|
-
taskId: task.id,
|
|
596
|
-
type: task.type,
|
|
597
|
-
title: task.title,
|
|
598
|
-
});
|
|
599
593
|
await logAppointmentCancelled(
|
|
600
594
|
task.contactId,
|
|
601
595
|
task.id,
|
|
@@ -604,7 +598,6 @@ export async function DELETE(
|
|
|
604
598
|
session.user.id,
|
|
605
599
|
task.type === 'VIDEO_CONFERENCE',
|
|
606
600
|
);
|
|
607
|
-
console.log("Interaction d'annulation créée avec succès");
|
|
608
601
|
} catch (interactionError: any) {
|
|
609
602
|
console.error(
|
|
610
603
|
"Erreur lors de la création de l'interaction d'annulation:",
|
|
@@ -612,11 +605,6 @@ export async function DELETE(
|
|
|
612
605
|
);
|
|
613
606
|
// On continue même si l'interaction échoue
|
|
614
607
|
}
|
|
615
|
-
} else {
|
|
616
|
-
console.log("Pas de création d'interaction - conditions non remplies:", {
|
|
617
|
-
contactId: task.contactId,
|
|
618
|
-
type: task.type,
|
|
619
|
-
});
|
|
620
608
|
}
|
|
621
609
|
|
|
622
610
|
// Supprimer la tâche
|
|
@@ -3,6 +3,7 @@ import { prisma } from '@/lib/prisma';
|
|
|
3
3
|
import { checkPermission } from '@/lib/check-permission';
|
|
4
4
|
import { auth } from '@/lib/auth';
|
|
5
5
|
import { logAudit } from '@/lib/audit-log';
|
|
6
|
+
import { resolveRoleFromCustomRoleName } from '@/lib/roles';
|
|
6
7
|
|
|
7
8
|
// GET /api/users/[id] - Récupérer un utilisateur spécifique
|
|
8
9
|
export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
|
@@ -86,12 +87,26 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
|
|
86
87
|
return NextResponse.json({ error: 'Utilisateur non trouvé' }, { status: 404 });
|
|
87
88
|
}
|
|
88
89
|
|
|
89
|
-
//
|
|
90
|
+
// Résoudre le rôle enum si le profil change
|
|
91
|
+
let resolvedRole: string | undefined;
|
|
92
|
+
if (customRoleId !== undefined) {
|
|
93
|
+
if (customRoleId) {
|
|
94
|
+
const customRole = await prisma.customRole.findUnique({
|
|
95
|
+
where: { id: customRoleId },
|
|
96
|
+
select: { name: true },
|
|
97
|
+
});
|
|
98
|
+
resolvedRole = resolveRoleFromCustomRoleName(customRole?.name ?? null);
|
|
99
|
+
} else {
|
|
100
|
+
resolvedRole = resolveRoleFromCustomRoleName(null);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
90
104
|
const updatedUser = await prisma.user.update({
|
|
91
105
|
where: { id },
|
|
92
106
|
data: {
|
|
93
107
|
...(name && { name }),
|
|
94
108
|
...(customRoleId !== undefined && { customRoleId: customRoleId || null }),
|
|
109
|
+
...(resolvedRole && { role: resolvedRole as any }),
|
|
95
110
|
...(typeof active === 'boolean' && { active }),
|
|
96
111
|
},
|
|
97
112
|
include: {
|
|
@@ -3,6 +3,7 @@ import { prisma } from '@/lib/prisma';
|
|
|
3
3
|
import { checkPermission } from '@/lib/check-permission';
|
|
4
4
|
import { auth } from '@/lib/auth';
|
|
5
5
|
import { logAudit } from '@/lib/audit-log';
|
|
6
|
+
import { resolveRoleFromCustomRoleName } from '@/lib/roles';
|
|
6
7
|
|
|
7
8
|
// GET /api/users - Liste tous les utilisateurs (admin seulement)
|
|
8
9
|
export async function GET(request: NextRequest) {
|
|
@@ -105,60 +106,46 @@ export async function POST(request: NextRequest) {
|
|
|
105
106
|
// Si l'utilisateur existe mais sans compte, on peut régénérer un token
|
|
106
107
|
}
|
|
107
108
|
|
|
109
|
+
// Résoudre le rôle enum à partir du profil attribué
|
|
110
|
+
const customRole = await prisma.customRole.findUnique({
|
|
111
|
+
where: { id: customRoleId },
|
|
112
|
+
select: { name: true },
|
|
113
|
+
});
|
|
114
|
+
const customRoleName = customRole?.name ?? null;
|
|
115
|
+
const resolvedRole = resolveRoleFromCustomRoleName(customRoleName);
|
|
116
|
+
|
|
108
117
|
let user;
|
|
109
118
|
if (existingUser && existingUser.accounts.length === 0) {
|
|
110
|
-
// Utilisateur existe déjà sans compte, on met à jour et régénère le token
|
|
111
119
|
user = await prisma.user.update({
|
|
112
120
|
where: { id: existingUser.id },
|
|
113
121
|
data: {
|
|
114
122
|
name,
|
|
115
123
|
customRoleId,
|
|
124
|
+
role: resolvedRole,
|
|
116
125
|
active: true,
|
|
117
126
|
},
|
|
118
127
|
});
|
|
119
128
|
} else {
|
|
120
|
-
// Générer une couleur aléatoire pour les événements
|
|
121
129
|
const colors = [
|
|
122
|
-
'#EF4444',
|
|
123
|
-
'#
|
|
124
|
-
'#10B981', // Vert
|
|
125
|
-
'#F59E0B', // Orange
|
|
126
|
-
'#8B5CF6', // Violet
|
|
127
|
-
'#EC4899', // Rose
|
|
128
|
-
'#06B6D4', // Cyan
|
|
129
|
-
'#84CC16', // Lime
|
|
130
|
-
'#F97316', // Orange foncé
|
|
131
|
-
'#6366F1', // Indigo
|
|
132
|
-
'#14B8A6', // Teal
|
|
133
|
-
'#A855F7', // Purple
|
|
130
|
+
'#EF4444', '#3B82F6', '#10B981', '#F59E0B', '#8B5CF6', '#EC4899',
|
|
131
|
+
'#06B6D4', '#84CC16', '#F97316', '#6366F1', '#14B8A6', '#A855F7',
|
|
134
132
|
];
|
|
135
133
|
const randomColor = colors[Math.floor(Math.random() * colors.length)];
|
|
136
134
|
|
|
137
|
-
// Créer l'utilisateur SANS mot de passe (sans Account)
|
|
138
135
|
user = await prisma.user.create({
|
|
139
136
|
data: {
|
|
140
137
|
id: crypto.randomUUID(),
|
|
141
138
|
name,
|
|
142
139
|
email,
|
|
143
|
-
role:
|
|
140
|
+
role: resolvedRole,
|
|
144
141
|
customRoleId,
|
|
145
|
-
emailVerified: false,
|
|
142
|
+
emailVerified: false,
|
|
146
143
|
active: true,
|
|
147
144
|
eventColor: randomColor,
|
|
148
145
|
},
|
|
149
146
|
});
|
|
150
147
|
}
|
|
151
148
|
|
|
152
|
-
// Récupérer le nom du profil pour les métadonnées
|
|
153
|
-
let customRoleName: string | null = null;
|
|
154
|
-
if (customRoleId) {
|
|
155
|
-
const customRole = await prisma.customRole.findUnique({
|
|
156
|
-
where: { id: customRoleId },
|
|
157
|
-
select: { name: true },
|
|
158
|
-
});
|
|
159
|
-
customRoleName = customRole?.name || null;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
149
|
// Log d'audit : création ou réactivation d'utilisateur
|
|
163
150
|
await logAudit({
|
|
164
151
|
actorId: session.user.id,
|
|
@@ -226,9 +213,6 @@ export async function POST(request: NextRequest) {
|
|
|
226
213
|
statusText: emailResponse.statusText,
|
|
227
214
|
error: errorData,
|
|
228
215
|
});
|
|
229
|
-
} else {
|
|
230
|
-
const successData = await emailResponse.json().catch(() => ({}));
|
|
231
|
-
console.log("✅ Email d'invitation envoyé avec succès:", successData);
|
|
232
216
|
}
|
|
233
217
|
} catch (emailError: any) {
|
|
234
218
|
console.error("❌ Erreur lors de l'envoi de l'email:", emailError);
|
|
@@ -277,7 +277,6 @@ async function executeScheduledSMS(
|
|
|
277
277
|
}
|
|
278
278
|
|
|
279
279
|
// TODO: Implémenter l'envoi de SMS (nécessite une API SMS)
|
|
280
|
-
console.log(`SMS à envoyer à ${contact.phone}: ${message}`);
|
|
281
280
|
|
|
282
281
|
// Créer une interaction pour tracer le SMS
|
|
283
282
|
await prisma.interaction.create({
|
|
@@ -980,39 +980,6 @@ body {
|
|
|
980
980
|
}
|
|
981
981
|
}
|
|
982
982
|
|
|
983
|
-
@keyframes drag-handle-bounce {
|
|
984
|
-
0% {
|
|
985
|
-
transform: scale(0.8) rotate(-10deg);
|
|
986
|
-
opacity: 0;
|
|
987
|
-
}
|
|
988
|
-
50% {
|
|
989
|
-
transform: scale(1.05) rotate(2deg);
|
|
990
|
-
opacity: 0.8;
|
|
991
|
-
}
|
|
992
|
-
100% {
|
|
993
|
-
transform: scale(1) rotate(0deg);
|
|
994
|
-
opacity: 1;
|
|
995
|
-
}
|
|
996
|
-
}
|
|
997
|
-
|
|
998
|
-
@keyframes button-hover-lift {
|
|
999
|
-
0% {
|
|
1000
|
-
transform: translateY(0) scale(1);
|
|
1001
|
-
}
|
|
1002
|
-
100% {
|
|
1003
|
-
transform: translateY(-2px) scale(1.05);
|
|
1004
|
-
}
|
|
1005
|
-
}
|
|
1006
|
-
|
|
1007
|
-
@keyframes button-press {
|
|
1008
|
-
0% {
|
|
1009
|
-
transform: translateY(-2px) scale(1.05);
|
|
1010
|
-
}
|
|
1011
|
-
100% {
|
|
1012
|
-
transform: translateY(0) scale(0.98);
|
|
1013
|
-
}
|
|
1014
|
-
}
|
|
1015
|
-
|
|
1016
983
|
/* ===== DARK MODE SUPPORT ===== */
|
|
1017
984
|
|
|
1018
985
|
[data-theme='dark'] .lexkit-drag-handle,
|
|
@@ -34,7 +34,7 @@ function getRoleLevel(role: string): number {
|
|
|
34
34
|
* Vérifie si l'utilisateur a le rôle requis ou un rôle supérieur dans la hiérarchie
|
|
35
35
|
* Un rôle supérieur a automatiquement les permissions des rôles inférieurs
|
|
36
36
|
*/
|
|
37
|
-
|
|
37
|
+
function hasRole(userRole: string | undefined, requiredRole: Role): boolean {
|
|
38
38
|
if (!userRole) return false;
|
|
39
39
|
|
|
40
40
|
const userLevel = getRoleLevel(userRole);
|
|
@@ -44,43 +44,21 @@ export function hasRole(userRole: string | undefined, requiredRole: Role): boole
|
|
|
44
44
|
return userLevel <= requiredLevel;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Vérifie si l'utilisateur est manager ou supérieur
|
|
56
|
-
*/
|
|
57
|
-
export function isManagerOrAbove(userRole: string | undefined): boolean {
|
|
58
|
-
if (!userRole) return false;
|
|
59
|
-
return getRoleLevel(userRole) <= 2; // ADMIN ou MANAGER
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Vérifie si l'utilisateur est commercial ou supérieur
|
|
64
|
-
*/
|
|
65
|
-
export function isCommercialOrAbove(userRole: string | undefined): boolean {
|
|
66
|
-
if (!userRole) return false;
|
|
67
|
-
return getRoleLevel(userRole) <= 3; // ADMIN, MANAGER ou COMMERCIAL
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Vérifie si l'utilisateur est télépro ou supérieur
|
|
72
|
-
*/
|
|
73
|
-
export function isTeleproOrAbove(userRole: string | undefined): boolean {
|
|
74
|
-
if (!userRole) return false;
|
|
75
|
-
return getRoleLevel(userRole) <= 4; // ADMIN, MANAGER, COMMERCIAL ou TELEPRO
|
|
76
|
-
}
|
|
47
|
+
const CUSTOM_ROLE_NAME_TO_ENUM: Record<string, Role> = {
|
|
48
|
+
'Administrateur': Role.ADMIN,
|
|
49
|
+
'Manager': Role.MANAGER,
|
|
50
|
+
'Commercial': Role.COMMERCIAL,
|
|
51
|
+
'Télépro': Role.TELEPRO,
|
|
52
|
+
'Comptable': Role.COMPTABLE,
|
|
53
|
+
};
|
|
77
54
|
|
|
78
55
|
/**
|
|
79
|
-
*
|
|
56
|
+
* Résout la valeur de l'enum Role à partir du nom d'un CustomRole.
|
|
57
|
+
* Retourne Role.USER pour les profils personnalisés non reconnus.
|
|
80
58
|
*/
|
|
81
|
-
export function
|
|
82
|
-
if (!
|
|
83
|
-
return
|
|
59
|
+
export function resolveRoleFromCustomRoleName(customRoleName: string | null | undefined): Role {
|
|
60
|
+
if (!customRoleName) return Role.USER;
|
|
61
|
+
return CUSTOM_ROLE_NAME_TO_ENUM[customRoleName] ?? Role.USER;
|
|
84
62
|
}
|
|
85
63
|
|
|
86
64
|
/**
|
|
@@ -5,6 +5,15 @@ export function cn(...inputs: Array<string | false | null | undefined>) {
|
|
|
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
|
+
|
|
8
17
|
/**
|
|
9
18
|
* Normalise un numéro de téléphone au format français : 0X XX XX XX XX
|
|
10
19
|
* - Supprime tous les caractères non numériques
|
|
@@ -413,7 +413,6 @@ export async function sendEmailImmediate(
|
|
|
413
413
|
export async function sendSMSImmediate(workflow: any, contact: any, message: string) {
|
|
414
414
|
try {
|
|
415
415
|
// TODO: Implémenter l'envoi de SMS (nécessite une API SMS)
|
|
416
|
-
console.log(`SMS à envoyer à ${contact.phone}: ${message}`);
|
|
417
416
|
|
|
418
417
|
// Créer une interaction pour tracer le SMS
|
|
419
418
|
await prisma.interaction.create({
|
package/template/src/proxy.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server';
|
|
2
2
|
import type { NextRequest } from 'next/server';
|
|
3
3
|
import { auth } from './lib/auth';
|
|
4
|
-
import {
|
|
4
|
+
import { Role } from './lib/roles';
|
|
5
5
|
import { prisma } from './lib/prisma';
|
|
6
6
|
|
|
7
7
|
// Routes qui nécessitent une authentification
|
|
@@ -65,7 +65,7 @@ export async function proxy(request: NextRequest) {
|
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
// Si l'utilisateur est connecté mais n'est pas admin et tente d'accéder à une route admin
|
|
68
|
-
if (isAuthenticated && isAdminRoute &&
|
|
68
|
+
if (isAuthenticated && isAdminRoute && userRole !== Role.ADMIN) {
|
|
69
69
|
return NextResponse.redirect(new URL('/dashboard', request.url));
|
|
70
70
|
}
|
|
71
71
|
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { Bar, BarChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts';
|
|
4
|
-
|
|
5
|
-
interface SalesAnalyticsChartProps {
|
|
6
|
-
data: Array<{ month: string; count: number }>;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export function SalesAnalyticsChart({ data }: SalesAnalyticsChartProps) {
|
|
10
|
-
// Calculer le total et la croissance
|
|
11
|
-
const total = data.reduce((sum, item) => sum + item.count, 0);
|
|
12
|
-
const previousTotal =
|
|
13
|
-
data.length > 1 ? data.slice(0, -1).reduce((sum, item) => sum + item.count, 0) : 0;
|
|
14
|
-
const growth = previousTotal > 0 ? ((total - previousTotal) / previousTotal) * 100 : 0;
|
|
15
|
-
|
|
16
|
-
return (
|
|
17
|
-
<div className="rounded-xl border border-gray-200/50 bg-white p-6 shadow-lg transition-shadow duration-300 hover:shadow-xl">
|
|
18
|
-
<div className="mb-4 flex items-center justify-between">
|
|
19
|
-
<div>
|
|
20
|
-
<h3 className="text-lg font-bold text-gray-900">Analytiques des Ventes</h3>
|
|
21
|
-
</div>
|
|
22
|
-
<select className="rounded-lg border border-gray-300 bg-white px-3 py-1.5 text-sm font-medium text-gray-700 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none">
|
|
23
|
-
<option>Mensuel</option>
|
|
24
|
-
<option>Hebdomadaire</option>
|
|
25
|
-
<option>Annuel</option>
|
|
26
|
-
</select>
|
|
27
|
-
</div>
|
|
28
|
-
<div className="mb-4 flex items-baseline gap-4">
|
|
29
|
-
<div>
|
|
30
|
-
<p className="text-2xl font-bold text-gray-900">
|
|
31
|
-
{total.toLocaleString('fr-FR')} contacts ce mois
|
|
32
|
-
</p>
|
|
33
|
-
<p className="mt-1 text-sm font-semibold text-emerald-600">
|
|
34
|
-
Augmentation ce mois : +{growth.toFixed(1)}%
|
|
35
|
-
</p>
|
|
36
|
-
</div>
|
|
37
|
-
</div>
|
|
38
|
-
<div className="h-[280px]">
|
|
39
|
-
<ResponsiveContainer width="100%" height="100%">
|
|
40
|
-
<BarChart data={data} barCategoryGap="20%">
|
|
41
|
-
<defs>
|
|
42
|
-
<linearGradient id="barGradient" x1="0" y1="0" x2="0" y2="1">
|
|
43
|
-
<stop offset="0%" stopColor="#8b5cf6" stopOpacity={1} />
|
|
44
|
-
<stop offset="100%" stopColor="#6366f1" stopOpacity={0.8} />
|
|
45
|
-
</linearGradient>
|
|
46
|
-
</defs>
|
|
47
|
-
<XAxis
|
|
48
|
-
dataKey="month"
|
|
49
|
-
stroke="#9ca3af"
|
|
50
|
-
fontSize={12}
|
|
51
|
-
tickLine={false}
|
|
52
|
-
axisLine={false}
|
|
53
|
-
/>
|
|
54
|
-
<YAxis
|
|
55
|
-
stroke="#9ca3af"
|
|
56
|
-
fontSize={12}
|
|
57
|
-
tickLine={false}
|
|
58
|
-
axisLine={false}
|
|
59
|
-
tickFormatter={(value) => `${value}`}
|
|
60
|
-
/>
|
|
61
|
-
<Tooltip
|
|
62
|
-
contentStyle={{
|
|
63
|
-
backgroundColor: 'white',
|
|
64
|
-
border: '1px solid #e5e7eb',
|
|
65
|
-
borderRadius: '12px',
|
|
66
|
-
boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1)',
|
|
67
|
-
}}
|
|
68
|
-
labelStyle={{ color: '#374151', fontWeight: 600 }}
|
|
69
|
-
formatter={(value: number) => [`${value.toLocaleString('fr-FR')}`, 'Contacts']}
|
|
70
|
-
/>
|
|
71
|
-
<Bar dataKey="count" fill="url(#barGradient)" radius={[8, 8, 0, 0]} />
|
|
72
|
-
</BarChart>
|
|
73
|
-
</ResponsiveContainer>
|
|
74
|
-
</div>
|
|
75
|
-
</div>
|
|
76
|
-
);
|
|
77
|
-
}
|