CognxSafeTrack commited on
Commit ·
ec8f103
1
Parent(s): 0da3feb
feat(settings): add WhatsApp reconfiguration form to SettingsPage
Browse filesBusiness 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 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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());
|