CognxSafeTrack commited on
Commit
a3df350
·
1 Parent(s): 17a3e29

feat(admin): add Direct Setup button for super admins on orgs without phone number

Browse files

- Add "Direct Setup" button (super admin only) on /clients for orgs without a WhatsApp number
- Previously the setup modal was only reachable via the Reconfigure button (orgs with a phone)
- Add i18n keys: clients.actions.direct_setup (en/fr/pt)
- Add scratch scripts for org configuration and DB queries
- Add doc: configurer-token-nouvelle-org.md — how to configure a Meta token on a new org

apps/admin/src/locales/en.json CHANGED
@@ -651,6 +651,7 @@
651
  "actions": {
652
  "reconfigure_wa": "Reconfigure WhatsApp",
653
  "connect_wa": "Connect WhatsApp (Meta)",
 
654
  "personality_studio": "Personality Studio",
655
  "ai_credits": "AI Credits",
656
  "billing_details": "Details & Billing"
 
651
  "actions": {
652
  "reconfigure_wa": "Reconfigure WhatsApp",
653
  "connect_wa": "Connect WhatsApp (Meta)",
654
+ "direct_setup": "Direct Setup",
655
  "personality_studio": "Personality Studio",
656
  "ai_credits": "AI Credits",
657
  "billing_details": "Details & Billing"
apps/admin/src/locales/fr.json CHANGED
@@ -651,6 +651,7 @@
651
  "actions": {
652
  "reconfigure_wa": "Reconfigurer WhatsApp",
653
  "connect_wa": "Connecter WhatsApp (Meta)",
 
654
  "personality_studio": "Personality Studio",
655
  "ai_credits": "Crédits IA",
656
  "billing_details": "Détails & Facturation"
 
651
  "actions": {
652
  "reconfigure_wa": "Reconfigurer WhatsApp",
653
  "connect_wa": "Connecter WhatsApp (Meta)",
654
+ "direct_setup": "Configuration directe",
655
  "personality_studio": "Personality Studio",
656
  "ai_credits": "Crédits IA",
657
  "billing_details": "Détails & Facturation"
apps/admin/src/locales/pt.json CHANGED
@@ -651,6 +651,7 @@
651
  "actions": {
652
  "reconfigure_wa": "Reconfigurar WhatsApp",
653
  "connect_wa": "Ligar WhatsApp (Meta)",
 
654
  "personality_studio": "Personality Studio",
655
  "ai_credits": "Créditos IA",
656
  "billing_details": "Detalhes & Faturação"
 
651
  "actions": {
652
  "reconfigure_wa": "Reconfigurar WhatsApp",
653
  "connect_wa": "Ligar WhatsApp (Meta)",
654
+ "direct_setup": "Configuração direta",
655
  "personality_studio": "Personality Studio",
656
  "ai_credits": "Créditos IA",
657
  "billing_details": "Detalhes & Faturação"
apps/admin/src/pages/ClientsManagementView.tsx CHANGED
@@ -247,12 +247,23 @@ export default function ClientsManagementView() {
247
  )}
248
  </div>
249
  ) : (
250
- <button
251
- onClick={() => handleEmbeddedSignup(client.id)}
252
- className="flex items-center gap-2 bg-indigo-600 text-white px-5 py-2.5 rounded-xl font-semibold hover:bg-indigo-700 transition shadow-lg shadow-indigo-100"
253
- >
254
- <MessageSquare className="w-4 h-4" /> {t('clients.actions.connect_wa')}
255
- </button>
 
 
 
 
 
 
 
 
 
 
 
256
  )}
257
  <button
258
  onClick={() => setSelectedOrgForPersonality(client)}
 
247
  )}
248
  </div>
249
  ) : (
250
+ <div className="flex gap-2">
251
+ <button
252
+ onClick={() => handleEmbeddedSignup(client.id)}
253
+ className="flex items-center gap-2 bg-indigo-600 text-white px-5 py-2.5 rounded-xl font-semibold hover:bg-indigo-700 transition shadow-lg shadow-indigo-100"
254
+ >
255
+ <MessageSquare className="w-4 h-4" /> {t('clients.actions.connect_wa')}
256
+ </button>
257
+ {isSuperAdmin && (
258
+ <button
259
+ onClick={() => { setSetupOrg(client); setDirectSetup({ wabaId: client.wabaId || '', metaBusinessId: '', accessToken: '', phoneNumberId: '' }); }}
260
+ className="flex items-center gap-2 bg-slate-700 text-white px-4 py-2.5 rounded-xl font-semibold hover:bg-slate-800 transition text-xs"
261
+ title={t('clients.actions.direct_setup')}
262
+ >
263
+ <ShieldCheck className="w-3.5 h-3.5" /> {t('clients.actions.direct_setup')}
264
+ </button>
265
+ )}
266
+ </div>
267
  )}
268
  <button
269
  onClick={() => setSelectedOrgForPersonality(client)}
apps/api/scratch/connect_test4.ts ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import 'dotenv/config';
2
+ import { PrismaClient } from '@prisma/client';
3
+
4
+ const prisma = new PrismaClient();
5
+
6
+ const TEST4_ORG_ID = '12247ec1-7629-4650-b695-237654793e0c';
7
+ const TESTCRM_ORG_ID = 'ba012b65-2289-4ded-956d-aeaaf908fb30';
8
+ const PHONE_NUMBER_ID = '1135406776315489';
9
+ const DISPLAY_PHONE = '+221788227676';
10
+ const WABA_ID = '1503271284790621';
11
+
12
+ async function main() {
13
+ // 1. Read testcrm's encrypted token (copy it as-is — same ENCRYPTION_SECRET)
14
+ const testcrm = await prisma.organization.findUnique({
15
+ where: { id: TESTCRM_ORG_ID },
16
+ select: { systemUserToken: true, wabaId: true }
17
+ });
18
+
19
+ if (!testcrm?.systemUserToken) {
20
+ throw new Error('testcrm has no systemUserToken — cannot proceed');
21
+ }
22
+
23
+ console.log(`📋 Source : testcrm | waba: ${testcrm.wabaId} | token: ${testcrm.systemUserToken ? '✅' : '❌'}`);
24
+
25
+ // 2. Free WABA ID from testcrm first (unique constraint)
26
+ await prisma.organization.update({
27
+ where: { id: TESTCRM_ORG_ID },
28
+ data: { wabaId: null }
29
+ });
30
+ console.log(`✅ WABA ID retiré de testcrm`);
31
+
32
+ // 3. Copy WABA ID + encrypted token to test4
33
+ await prisma.organization.update({
34
+ where: { id: TEST4_ORG_ID },
35
+ data: {
36
+ wabaId: WABA_ID,
37
+ systemUserToken: testcrm.systemUserToken,
38
+ systemUserTokenIssuedAt: new Date()
39
+ }
40
+ });
41
+ console.log(`✅ WABA ID + token copiés vers test4 (WABA: ${WABA_ID})`);
42
+
43
+ // 4. Move the phone number from testcrm to test4
44
+ await prisma.whatsAppPhoneNumber.update({
45
+ where: { id: PHONE_NUMBER_ID },
46
+ data: { organizationId: TEST4_ORG_ID }
47
+ });
48
+ console.log(`✅ Numéro transféré vers test4 : ${DISPLAY_PHONE}`);
49
+
50
+ console.log('\n🎉 test4 est maintenant opérationnel avec le numéro', DISPLAY_PHONE);
51
+ await prisma.$disconnect();
52
+ }
53
+
54
+ main().catch(console.error);
apps/api/scratch/connect_testcrm.ts ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import 'dotenv/config';
2
+ import { PrismaClient } from '@prisma/client';
3
+
4
+ const prisma = new PrismaClient();
5
+
6
+ async function main() {
7
+ const TESTCRM_ORG_ID = 'ba012b65-2289-4ded-956d-aeaaf908fb30';
8
+ const PHONE_NUMBER_ID = '1135406776315489';
9
+ const DISPLAY_PHONE = '+221 78 822 76 76';
10
+ const WABA_ID = '1503271284790621';
11
+
12
+ // 1. Retirer le numéro de default-org-id (où on l'avait mis par erreur)
13
+ await prisma.whatsAppPhoneNumber.update({
14
+ where: { id: PHONE_NUMBER_ID },
15
+ data: { organizationId: TESTCRM_ORG_ID }
16
+ });
17
+ console.log(`✅ Numéro transféré vers testcrm : ${DISPLAY_PHONE}`);
18
+
19
+ // 2. Mettre à jour le WABA ID sur testcrm
20
+ await prisma.organization.update({
21
+ where: { id: TESTCRM_ORG_ID },
22
+ data: { wabaId: WABA_ID }
23
+ });
24
+ console.log(`✅ WABA ID mis à jour sur testcrm : ${WABA_ID}`);
25
+
26
+ // 3. Retirer le WABA ID de default-org-id (il était incorrect)
27
+ await prisma.organization.update({
28
+ where: { id: 'default-org-id' },
29
+ data: { wabaId: null }
30
+ });
31
+ console.log(`✅ WABA ID retiré de default-org-id`);
32
+
33
+ console.log('\n🎉 testcrm est maintenant connecté au numéro +221 78 822 76 76');
34
+ await prisma.$disconnect();
35
+ }
36
+
37
+ main().catch(console.error);
apps/api/scratch/list_orgs.ts ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import 'dotenv/config';
2
+ import { PrismaClient } from '@prisma/client';
3
+
4
+ const prisma = new PrismaClient();
5
+
6
+ async function main() {
7
+ const orgs = await prisma.organization.findMany({
8
+ select: { id: true, name: true, slug: true, wabaId: true },
9
+ orderBy: { createdAt: 'desc' }
10
+ });
11
+ console.log('\n📋 Organisations disponibles :');
12
+ orgs.forEach(o => {
13
+ console.log(` - [${o.id}] ${o.name} (slug: ${o.slug || 'aucun'}, waba: ${o.wabaId || 'non configuré'})`);
14
+ });
15
+ await prisma.$disconnect();
16
+ }
17
+
18
+ main().catch(console.error);
apps/api/scratch/register_prod_number.ts ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import 'dotenv/config';
2
+ import { PrismaClient } from '@prisma/client';
3
+
4
+ const prisma = new PrismaClient();
5
+
6
+ async function main() {
7
+ const PHONE_NUMBER_ID = '1135406776315489';
8
+ const DISPLAY_PHONE = '+221 78 822 76 76';
9
+ const WABA_ID = '1503271284790621';
10
+ const ORG_ID = 'default-org-id';
11
+
12
+ // 1. Mise à jour du WABA ID sur l'organisation
13
+ await prisma.organization.update({
14
+ where: { id: ORG_ID },
15
+ data: { wabaId: WABA_ID }
16
+ });
17
+ console.log(`✅ WABA ID mis à jour : ${WABA_ID}`);
18
+
19
+ // 2. Enregistrement du numéro de production
20
+ await prisma.whatsAppPhoneNumber.upsert({
21
+ where: { id: PHONE_NUMBER_ID },
22
+ update: { displayPhone: DISPLAY_PHONE, organizationId: ORG_ID },
23
+ create: { id: PHONE_NUMBER_ID, displayPhone: DISPLAY_PHONE, organizationId: ORG_ID }
24
+ });
25
+ console.log(`✅ Numéro enregistré : ${DISPLAY_PHONE} (ID: ${PHONE_NUMBER_ID})`);
26
+
27
+ await prisma.$disconnect();
28
+ }
29
+
30
+ main().catch(console.error);
docs/organisation-onboarding/configurer-token-nouvelle-org.md ADDED
@@ -0,0 +1,192 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Configurer le token Meta sur une nouvelle organisation
2
+
3
+ **Public :** Super Admin XAMLÉ
4
+ **Durée :** 5 minutes
5
+ **Pré-requis :** Avoir un token Meta valide (`EAA...`) d'une org existante ou obtenu depuis Meta Business Suite
6
+
7
+ ---
8
+
9
+ ## Contexte
10
+
11
+ Quand une organisation est créée, elle n'a pas de connexion WhatsApp. Le dashboard affiche **"Configuration requise"**. Pour qu'elle devienne opérationnelle, il faut lui associer :
12
+
13
+ 1. Un **WABA ID** — l'identifiant du compte WhatsApp Business Account Meta
14
+ 2. Un **token d'accès** (`EAA...`) — long-lived token Meta pour appeler l'API Graph
15
+ 3. Un **phoneNumberId** — l'ID du numéro WhatsApp (peut être fait plus tard si le numéro n'existe pas encore)
16
+
17
+ > **Important — contrainte unique sur wabaId :** Deux organisations ne peuvent pas avoir le même `wabaId` en même temps. Si tu veux que plusieurs orgs opèrent sous le même compte WABA (ex. plusieurs numéros), contacte un développeur pour retirer la contrainte `@unique` du schéma Prisma.
18
+
19
+ ---
20
+
21
+ ## Méthode 1 — Via l'admin UI (recommandé, sans code)
22
+
23
+ Depuis **Mai 2026**, les super admins peuvent configurer directement le token depuis la page `/clients` sans passer par un script.
24
+
25
+ ### Étapes
26
+
27
+ 1. Aller sur `/clients` dans l'admin
28
+ 2. Trouver l'organisation cible (affiche **"Configuration requise"**)
29
+ 3. Cliquer sur le bouton **"Configuration directe"** (visible uniquement pour les super admins)
30
+ 4. Dans le modal qui s'ouvre, remplir :
31
+ - **WABA ID** *(obligatoire)* — ex. `1503271284790621`
32
+ - **Token d'accès** *(obligatoire si le token n'est pas déjà stocké)* — coller le token `EAA...`
33
+ - **Phone Number ID** *(optionnel)* — laisser vide si le numéro n'est pas encore créé sur Meta
34
+ - **Meta Business ID** *(optionnel)*
35
+ 5. Cliquer **"Enregistrer la configuration"**
36
+
37
+ > **Où trouver le WABA ID ?**
38
+ > [business.facebook.com/wa/manage/home](https://business.facebook.com/wa/manage/home) → Accounts → WhatsApp Accounts → l'ID numérique du compte
39
+
40
+ > **Où trouver le token ?**
41
+ > Meta for Developers → ton App → WhatsApp → Configuration → Générer un token
42
+ > OU copier depuis une org existante via le script `scratch/list_orgs.ts` (voir Méthode 2)
43
+
44
+ ### Ce que ça fait en base
45
+
46
+ L'appel `POST /v1/organizations/:id/whatsapp-setup` :
47
+ - Chiffre et stocke le token dans `Organization.systemUserToken` (avec `ENCRYPTION_SECRET`)
48
+ - Enregistre le `wabaId` sur l'organisation
49
+ - Si `phoneNumberId` fourni : crée ou met à jour l'entrée `WhatsAppPhoneNumber` liée à l'org
50
+
51
+ ---
52
+
53
+ ## Méthode 2 — Via script Prisma (quand l'UI ne suffit pas)
54
+
55
+ Utiliser cette méthode pour :
56
+ - Copier le token d'une org existante vers une nouvelle (même `ENCRYPTION_SECRET` → copie directe)
57
+ - Effectuer des opérations en masse
58
+ - Déboguer en production sans déploiement
59
+
60
+ ### Pré-requis
61
+
62
+ ```bash
63
+ # Les variables doivent être dans /Volumes/sms/Edtech/.env ou exportées
64
+ DATABASE_URL=...
65
+ ENCRYPTION_SECRET=...
66
+ ```
67
+
68
+ ### Étape 1 — Trouver les IDs des organisations
69
+
70
+ ```bash
71
+ cd /Volumes/sms/Edtech/apps/api
72
+ npx tsx scratch/list_orgs.ts
73
+ ```
74
+
75
+ Sortie :
76
+ ```
77
+ 📋 Organisations disponibles :
78
+ - [12247ec1-...] test4 (slug: agro-sn, waba: non configuré)
79
+ - [ba012b65-...] testcrm (slug: test, waba: 1503271284790621)
80
+ - [default-org-id] XAMLÉ Global (slug: aucun, waba: 938685848818318)
81
+ ```
82
+
83
+ ### Étape 2 — Créer le script de configuration
84
+
85
+ Créer `apps/api/scratch/connect_<nom_org>.ts` :
86
+
87
+ ```typescript
88
+ import 'dotenv/config';
89
+ import { PrismaClient } from '@prisma/client';
90
+
91
+ const prisma = new PrismaClient();
92
+
93
+ // IDs à remplir
94
+ const NOUVELLE_ORG_ID = 'xxx-xxx-xxx';
95
+ const SOURCE_ORG_ID = 'ba012b65-2289-4ded-956d-aeaaf908fb30'; // org qui a le token
96
+ const WABA_ID = '1503271284790621';
97
+ const PHONE_NUMBER_ID = ''; // laisser vide si pas encore de numéro
98
+ const DISPLAY_PHONE = '';
99
+
100
+ async function main() {
101
+ // 1. Lire le token chiffré depuis l'org source
102
+ const source = await prisma.organization.findUnique({
103
+ where: { id: SOURCE_ORG_ID },
104
+ select: { systemUserToken: true }
105
+ });
106
+ if (!source?.systemUserToken) throw new Error('Org source sans token');
107
+
108
+ // 2. Si WABA_ID est déjà utilisé par une autre org, la libérer d'abord
109
+ // await prisma.organization.update({ where: { id: SOURCE_ORG_ID }, data: { wabaId: null } });
110
+
111
+ // 3. Configurer la nouvelle org
112
+ await prisma.organization.update({
113
+ where: { id: NOUVELLE_ORG_ID },
114
+ data: {
115
+ wabaId: WABA_ID,
116
+ systemUserToken: source.systemUserToken, // copie directe — même clé de chiffrement
117
+ systemUserTokenIssuedAt: new Date()
118
+ }
119
+ });
120
+ console.log('✅ Token et WABA ID configurés');
121
+
122
+ // 4. Associer un numéro (optionnel)
123
+ if (PHONE_NUMBER_ID) {
124
+ await prisma.whatsAppPhoneNumber.upsert({
125
+ where: { id: PHONE_NUMBER_ID },
126
+ update: { displayPhone: DISPLAY_PHONE, organizationId: NOUVELLE_ORG_ID },
127
+ create: { id: PHONE_NUMBER_ID, displayPhone: DISPLAY_PHONE, organizationId: NOUVELLE_ORG_ID }
128
+ });
129
+ console.log('✅ Numéro associé :', DISPLAY_PHONE);
130
+ }
131
+
132
+ await prisma.$disconnect();
133
+ }
134
+
135
+ main().catch(console.error);
136
+ ```
137
+
138
+ ### Étape 3 — Exécuter
139
+
140
+ ```bash
141
+ cd /Volumes/sms/Edtech/apps/api
142
+ npx tsx scratch/connect_<nom_org>.ts
143
+ ```
144
+
145
+ ---
146
+
147
+ ## Contrainte wabaId : pourquoi et quand la retirer
148
+
149
+ Le champ `Organization.wabaId` est `@unique` dans le schéma Prisma. Cela empêche deux orgs d'avoir le même WABA ID, ce qui est la règle aujourd'hui (1 org = 1 WABA = 1 numéro).
150
+
151
+ **Si à l'avenir** tu veux plusieurs orgs sous le même compte WABA (ex. entreprise avec plusieurs départements, chacun avec son propre numéro WhatsApp) :
152
+
153
+ 1. Créer une migration Prisma qui retire `@unique` sur `wabaId` :
154
+ ```prisma
155
+ wabaId String? // supprimer @unique
156
+ ```
157
+ 2. Appliquer : `pnpm --filter @repo/database migrate:dev`
158
+ 3. Depuis lors, plusieurs orgs peuvent partager le même WABA ID
159
+
160
+ ---
161
+
162
+ ## Cas courants
163
+
164
+ ### Nouvelle org — même WABA qu'une org existante (numéros différents)
165
+ → Impossible actuellement sans retirer `@unique` (voir ci-dessus)
166
+
167
+ ### Nouvelle org — WABA différent, token fourni par le client
168
+ → Méthode 1 (admin UI) : remplir WABA ID + token dans le modal "Configuration directe"
169
+
170
+ ### Nouvelle org — pas encore de numéro, token à copier depuis testcrm
171
+ → Méthode 2 (script) avec `PHONE_NUMBER_ID` vide. L'org sera en "Configuration requise" jusqu'à l'ajout du numéro.
172
+
173
+ ### Transférer un numéro d'une org à une autre
174
+ ```typescript
175
+ await prisma.whatsAppPhoneNumber.update({
176
+ where: { id: PHONE_NUMBER_ID },
177
+ data: { organizationId: NOUVELLE_ORG_ID }
178
+ });
179
+ ```
180
+
181
+ ---
182
+
183
+ ## Organisations actuelles en base (Mai 2026)
184
+
185
+ | ID | Nom | WABA ID | Token | Numéro |
186
+ |----|-----|---------|-------|--------|
187
+ | `default-org-id` | XAMLÉ Global | `938685848818318` | ✅ `EAAURe...` | `969048009628694` |
188
+ | `ba012b65-...` | testcrm | `1503271284790621` | ✅ | `+221788227676` |
189
+ | `12247ec1-...` | test4 | null | ✅ (copié) | à configurer |
190
+ | `136f72d9-...` | test | null | — | — |
191
+
192
+ > Mettre ce tableau à jour après chaque onboarding.