CognxSafeTrack commited on
Commit
ec8f103
·
1 Parent(s): 0da3feb

feat(settings): add WhatsApp reconfiguration form to SettingsPage

Browse files

Business owners can now connect or reconnect their WhatsApp account
directly from Settings without going through the onboarding wizard.
Form accepts WABA ID, System User Token, Phone Number ID, and phone number.

apps/admin/src/pages/SettingsPage.tsx CHANGED
@@ -16,6 +16,9 @@ export default function SettingsPage() {
16
  const [saving, setSaving] = useState(false);
17
  const [message, setMessage] = useState<{ type: 'success' | 'error', text: string } | null>(null);
18
  const [waStatus, setWaStatus] = useState<{ tokenValid: boolean | null } | null>(null);
 
 
 
19
 
20
  const isValidId = !!selectedOrgId;
21
 
@@ -58,6 +61,28 @@ export default function SettingsPage() {
58
  }
59
  };
60
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
  if (!isValidId) {
62
  return (
63
  <div className="p-12 text-center text-slate-400">
@@ -212,7 +237,15 @@ export default function SettingsPage() {
212
  </details>
213
 
214
  <section className="bg-slate-800 p-6 rounded-2xl text-white">
215
- <h2 className="text-lg font-semibold mb-3">{t('settings.whatsapp_config')}</h2>
 
 
 
 
 
 
 
 
216
  <div className="space-y-2 text-sm">
217
  <div className="flex items-center justify-between">
218
  <span className="text-slate-400">{t('settings.wa_account')}</span>
@@ -238,6 +271,56 @@ export default function SettingsPage() {
238
  ⚠️ {t('settings.token_expired_alert')}
239
  </div>
240
  )}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
241
  </section>
242
 
243
  <section className="bg-white p-6 rounded-2xl border border-indigo-100 shadow-sm shadow-indigo-50">
 
16
  const [saving, setSaving] = useState(false);
17
  const [message, setMessage] = useState<{ type: 'success' | 'error', text: string } | null>(null);
18
  const [waStatus, setWaStatus] = useState<{ tokenValid: boolean | null } | null>(null);
19
+ const [waForm, setWaForm] = useState({ wabaId: '', accessToken: '', phoneNumberId: '', phoneNumber: '' });
20
+ const [waFormOpen, setWaFormOpen] = useState(false);
21
+ const [waSaving, setWaSaving] = useState(false);
22
 
23
  const isValidId = !!selectedOrgId;
24
 
 
61
  }
62
  };
63
 
64
+ const handleWaSetup = async (e: React.FormEvent) => {
65
+ e.preventDefault();
66
+ if (!waForm.wabaId || !waForm.accessToken) return;
67
+ setWaSaving(true);
68
+ try {
69
+ await api.post(`/v1/organizations/${selectedOrgId}/whatsapp-setup`, {
70
+ wabaId: waForm.wabaId.trim(),
71
+ accessToken: waForm.accessToken.trim(),
72
+ phoneNumberId: waForm.phoneNumberId.trim() || undefined,
73
+ phoneNumber: waForm.phoneNumber.trim() || undefined,
74
+ }, token);
75
+ setMessage({ type: 'success', text: 'WhatsApp connecté avec succès ✅' });
76
+ setWaFormOpen(false);
77
+ setWaForm({ wabaId: '', accessToken: '', phoneNumberId: '', phoneNumber: '' });
78
+ fetchOrg();
79
+ } catch {
80
+ setMessage({ type: 'error', text: 'Échec de la connexion WhatsApp. Vérifie le token et le WABA ID.' });
81
+ } finally {
82
+ setWaSaving(false);
83
+ }
84
+ };
85
+
86
  if (!isValidId) {
87
  return (
88
  <div className="p-12 text-center text-slate-400">
 
237
  </details>
238
 
239
  <section className="bg-slate-800 p-6 rounded-2xl text-white">
240
+ <div className="flex items-center justify-between mb-3">
241
+ <h2 className="text-lg font-semibold">{t('settings.whatsapp_config')}</h2>
242
+ <button
243
+ onClick={() => setWaFormOpen(v => !v)}
244
+ className="text-xs px-3 py-1.5 bg-slate-700 hover:bg-slate-600 rounded-lg text-slate-300 transition-all"
245
+ >
246
+ {waFormOpen ? 'Annuler' : org.wabaId ? '🔄 Reconfigurer' : '🔗 Connecter'}
247
+ </button>
248
+ </div>
249
  <div className="space-y-2 text-sm">
250
  <div className="flex items-center justify-between">
251
  <span className="text-slate-400">{t('settings.wa_account')}</span>
 
271
  ⚠️ {t('settings.token_expired_alert')}
272
  </div>
273
  )}
274
+ {waFormOpen && (
275
+ <form onSubmit={handleWaSetup} className="mt-5 space-y-3 border-t border-slate-700 pt-5">
276
+ <div>
277
+ <label className="block text-xs text-slate-400 mb-1">WABA ID *</label>
278
+ <input
279
+ required
280
+ value={waForm.wabaId}
281
+ onChange={e => setWaForm(f => ({ ...f, wabaId: e.target.value }))}
282
+ placeholder="ex: 938685848818318"
283
+ className="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-lg text-white text-xs font-mono focus:ring-2 focus:ring-emerald-500 outline-none"
284
+ />
285
+ </div>
286
+ <div>
287
+ <label className="block text-xs text-slate-400 mb-1">System User Token *</label>
288
+ <input
289
+ required
290
+ type="password"
291
+ value={waForm.accessToken}
292
+ onChange={e => setWaForm(f => ({ ...f, accessToken: e.target.value }))}
293
+ placeholder="EAAxxxxxxx..."
294
+ className="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-lg text-white text-xs font-mono focus:ring-2 focus:ring-emerald-500 outline-none"
295
+ />
296
+ </div>
297
+ <div>
298
+ <label className="block text-xs text-slate-400 mb-1">Phone Number ID</label>
299
+ <input
300
+ value={waForm.phoneNumberId}
301
+ onChange={e => setWaForm(f => ({ ...f, phoneNumberId: e.target.value }))}
302
+ placeholder="ex: 969048009628694"
303
+ className="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-lg text-white text-xs font-mono focus:ring-2 focus:ring-emerald-500 outline-none"
304
+ />
305
+ </div>
306
+ <div>
307
+ <label className="block text-xs text-slate-400 mb-1">Numéro de téléphone</label>
308
+ <input
309
+ value={waForm.phoneNumber}
310
+ onChange={e => setWaForm(f => ({ ...f, phoneNumber: e.target.value }))}
311
+ placeholder="+221700000000"
312
+ className="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-lg text-white text-xs font-mono focus:ring-2 focus:ring-emerald-500 outline-none"
313
+ />
314
+ </div>
315
+ <button
316
+ type="submit"
317
+ disabled={waSaving}
318
+ className="w-full py-2.5 bg-emerald-600 hover:bg-emerald-500 disabled:opacity-50 text-white text-sm font-semibold rounded-xl transition-all"
319
+ >
320
+ {waSaving ? 'Connexion...' : 'Connecter WhatsApp'}
321
+ </button>
322
+ </form>
323
+ )}
324
  </section>
325
 
326
  <section className="bg-white p-6 rounded-2xl border border-indigo-100 shadow-sm shadow-indigo-50">
apps/api/src/scripts/copy-wa-config.ts ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { PrismaClient } from '@repo/database';
2
+
3
+ const prisma = new PrismaClient();
4
+
5
+ const TESTCRM_ID = 'ba012b65-2289-4ded-956d-aeaaf908fb30';
6
+ const XAMLE_GLOBAL = 'default-org-id';
7
+
8
+ const NEW_WABA_ID = '1503271284790621';
9
+ const NEW_PHONE_ID = '1135406776315489';
10
+ const NEW_PHONE_NUM = '+221788227676';
11
+
12
+ async function main() {
13
+ // Récupère le systemUserToken chiffré de xamlé global (pour l'API Meta)
14
+ const source = await prisma.organization.findUnique({
15
+ where: { id: XAMLE_GLOBAL },
16
+ select: { systemUserToken: true, openAiApiKey: true, googleAiApiKey: true }
17
+ });
18
+
19
+ if (!source?.systemUserToken) {
20
+ console.error('[ERROR] XAMLÉ Global systemUserToken not found.');
21
+ process.exit(1);
22
+ }
23
+ console.log(`[OK] systemUserToken récupéré de XAMLÉ Global (${source.systemUserToken.length} chars)`);
24
+
25
+ // 1. Mettre à jour testcrm avec le nouveau WABA et le même token
26
+ await prisma.organization.update({
27
+ where: { id: TESTCRM_ID },
28
+ data: {
29
+ wabaId: NEW_WABA_ID,
30
+ systemUserToken: source.systemUserToken,
31
+ openAiApiKey: source.openAiApiKey,
32
+ googleAiApiKey: source.googleAiApiKey,
33
+ }
34
+ });
35
+ console.log(`[1/2] testcrm: wabaId=${NEW_WABA_ID}, token + AI keys copiés`);
36
+
37
+ // 2. Créer le WhatsAppPhoneNumber pour testcrm
38
+ await prisma.whatsAppPhoneNumber.upsert({
39
+ where: { id: NEW_PHONE_ID },
40
+ update: { organizationId: TESTCRM_ID, displayPhone: NEW_PHONE_NUM },
41
+ create: {
42
+ id: NEW_PHONE_ID,
43
+ displayPhone: NEW_PHONE_NUM,
44
+ organizationId: TESTCRM_ID,
45
+ }
46
+ });
47
+ console.log(`[2/2] phoneNumber ${NEW_PHONE_NUM} (${NEW_PHONE_ID}) → testcrm`);
48
+
49
+ // Vérification finale
50
+ const result = await prisma.organization.findUnique({
51
+ where: { id: TESTCRM_ID },
52
+ select: {
53
+ name: true,
54
+ wabaId: true,
55
+ phoneNumbers: { select: { id: true, displayPhone: true } }
56
+ }
57
+ });
58
+ console.log('\n[RESULT testcrm]', JSON.stringify(result, null, 2));
59
+ }
60
+
61
+ main()
62
+ .catch(e => { console.error(e); process.exit(1); })
63
+ .finally(() => prisma.$disconnect());