| import { useState, useEffect } from 'react'; |
| import { useTranslation } from 'react-i18next'; |
| import { useAuth } from '@/lib/auth'; |
| import { api } from '@/lib/api'; |
| import { useToast } from '@/hooks/useToast'; |
| import { Phone, RefreshCw, Plus, X, ArrowLeft, Info } from 'lucide-react'; |
|
|
| export default function WhatsAppNumbers() { |
| const { t } = useTranslation(); |
| const { token } = useAuth(); |
| const toast = useToast(); |
| const [numbers, setNumbers] = useState<any[]>([]); |
| const [loading, setLoading] = useState(true); |
|
|
| const [showRegister, setShowRegister] = useState(false); |
| const [regStep, setRegStep] = useState<1 | 2>(1); |
| const [regOrgId, setRegOrgId] = useState(''); |
| const [regPhoneNumberId, setRegPhoneNumberId] = useState(''); |
| const [regPin, setRegPin] = useState(''); |
| const [regCode, setRegCode] = useState(''); |
| const [regLoading, setRegLoading] = useState(false); |
| const [regError, setRegError] = useState(''); |
| const [orgOptions, setOrgOptions] = useState<{ id: string; name: string }[]>([]); |
|
|
| async function load() { |
| if (!token) return; |
| setLoading(true); |
| try { |
| const data = await api.get('/v1/super-admin/whatsapp/numbers', token); |
| setNumbers(data.data ?? []); |
| } catch { toast.error(t('super_admin.err_load_numbers')); } |
| finally { setLoading(false); } |
| } |
|
|
| useEffect(() => { load(); }, [token]); |
|
|
| async function loadOrgs() { |
| if (!token) return; |
| try { |
| const data = await api.get('/v1/super-admin/organizations?limit=100', token); |
| setOrgOptions(data.data ?? []); |
| } catch { } |
| } |
|
|
| function openModal() { |
| setRegStep(1); |
| setRegOrgId(''); |
| setRegPhoneNumberId(''); |
| setRegPin(''); |
| setRegCode(''); |
| setRegError(''); |
| setRegLoading(false); |
| setShowRegister(true); |
| loadOrgs(); |
| } |
|
|
| function closeModal() { |
| setShowRegister(false); |
| setRegStep(1); |
| setRegOrgId(''); |
| setRegPhoneNumberId(''); |
| setRegPin(''); |
| setRegCode(''); |
| setRegError(''); |
| setRegLoading(false); |
| } |
|
|
| async function handleRegister() { |
| setRegError(''); |
| if (!regOrgId) { setRegError(t('super_admin.wa_select_org_error')); return; } |
| if (!regPhoneNumberId || !/^\d{12,18}$/.test(regPhoneNumberId)) { |
| setRegError(t('super_admin.wa_phone_id_error')); |
| return; |
| } |
| const pin = regPin.trim() === '' ? '000000' : regPin.trim(); |
| if (!/^\d{6}$/.test(pin)) { setRegError(t('super_admin.wa_pin_error')); return; } |
| setRegLoading(true); |
| try { |
| const res = await api.post('/v1/super-admin/whatsapp/numbers/register', { |
| orgId: regOrgId, |
| phoneNumberId: regPhoneNumberId, |
| pin, |
| }, token); |
| if (res.ok) { |
| setRegStep(2); |
| } else { |
| setRegError(res.metaResponse?.detail || res.metaResponse?.message || t('super_admin.wa_reg_error')); |
| } |
| } catch (e: any) { |
| setRegError(e?.message || t('super_admin.wa_net_error')); |
| } finally { |
| setRegLoading(false); |
| } |
| } |
|
|
| async function handleVerify() { |
| setRegError(''); |
| if (!regCode || !/^\d{4,8}$/.test(regCode)) { |
| setRegError(t('super_admin.wa_otp_error')); |
| return; |
| } |
| setRegLoading(true); |
| try { |
| const res = await api.post('/v1/super-admin/whatsapp/numbers/verify', { |
| orgId: regOrgId, |
| phoneNumberId: regPhoneNumberId, |
| code: regCode, |
| }, token); |
| if (res.ok) { |
| toast.success(t('super_admin.wa_number_registered')); |
| closeModal(); |
| load(); |
| } else { |
| setRegError(res.metaResponse?.detail || res.metaResponse?.message || t('super_admin.wa_otp_invalid')); |
| } |
| } catch (e: any) { |
| setRegError(e?.message || t('super_admin.wa_net_error')); |
| } finally { |
| setRegLoading(false); |
| } |
| } |
|
|
| const selectedOrg = orgOptions.find(o => o.id === regOrgId); |
|
|
| return ( |
| <div className="space-y-4"> |
| <div className="flex items-center justify-between"> |
| <div> |
| <h1 className="text-xl font-bold text-white">{t('super_admin.wa_numbers_title')}</h1> |
| <p className="text-sm text-slate-400 mt-0.5">{t('super_admin.wa_numbers_total', { count: numbers.length })}</p> |
| </div> |
| <div className="flex items-center gap-2"> |
| <button onClick={load} className="flex items-center gap-2 px-3 py-2 bg-slate-800 hover:bg-slate-700 text-slate-300 text-sm rounded-lg transition-colors"> |
| <RefreshCw className="w-3.5 h-3.5" /> |
| {t('super_admin.wa_refresh')} |
| </button> |
| <button onClick={openModal} className="flex items-center gap-2 px-3 py-2 bg-violet-600 hover:bg-violet-500 text-white text-sm rounded-lg transition-colors font-medium"> |
| <Plus className="w-4 h-4" /> |
| {t('super_admin.wa_register_number')} |
| </button> |
| </div> |
| </div> |
| |
| <div className="bg-slate-900 rounded-xl border border-slate-800 overflow-hidden"> |
| {loading ? ( |
| <div className="p-8 text-center text-slate-500 animate-pulse">{t('super_admin.org_loading')}</div> |
| ) : numbers.length === 0 ? ( |
| <div className="p-12 flex flex-col items-center gap-3 text-slate-500"> |
| <Phone className="w-8 h-8" /> |
| <p>{t('super_admin.wa_no_numbers')}</p> |
| </div> |
| ) : ( |
| <div className="overflow-x-auto"> |
| <table className="w-full text-sm"> |
| <thead> |
| <tr className="border-b border-slate-800"> |
| <th className="text-left px-4 py-3 text-xs font-medium text-slate-400 uppercase tracking-wider">{t('super_admin.col_number')}</th> |
| <th className="text-left px-4 py-3 text-xs font-medium text-slate-400 uppercase tracking-wider">{t('super_admin.col_org')}</th> |
| <th className="text-left px-4 py-3 text-xs font-medium text-slate-400 uppercase tracking-wider">{t('super_admin.col_id')}</th> |
| <th className="text-left px-4 py-3 text-xs font-medium text-slate-400 uppercase tracking-wider">{t('super_admin.col_added_at')}</th> |
| </tr> |
| </thead> |
| <tbody className="divide-y divide-slate-800"> |
| {numbers.map(n => ( |
| <tr key={n.id} className="hover:bg-slate-800/50 transition-colors"> |
| <td className="px-4 py-3 font-medium text-white">{n.displayPhone}</td> |
| <td className="px-4 py-3 text-slate-300">{n.organization?.name || '—'}</td> |
| <td className="px-4 py-3 text-xs font-mono text-slate-500">{n.id}</td> |
| <td className="px-4 py-3 text-xs text-slate-400">{new Date(n.createdAt).toLocaleDateString()}</td> |
| </tr> |
| ))} |
| </tbody> |
| </table> |
| </div> |
| )} |
| </div> |
| |
| {showRegister && ( |
| <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 px-4"> |
| <div className="bg-slate-900 border border-slate-700 rounded-2xl max-w-md w-full p-6 shadow-2xl"> |
| <div className="flex items-start justify-between mb-5"> |
| <div> |
| <h2 className="text-base font-semibold text-white">{t('super_admin.wa_register_title')}</h2> |
| <p className="text-xs text-slate-400 mt-0.5">{t('super_admin.wa_step', { step: regStep })}</p> |
| </div> |
| <button onClick={closeModal} className="text-slate-400 hover:text-white transition-colors ml-4 shrink-0"> |
| <X className="w-5 h-5" /> |
| </button> |
| </div> |
| |
| {regStep === 1 && ( |
| <div className="space-y-4"> |
| <div> |
| <label className="block text-xs font-medium text-slate-300 mb-1.5">{t('super_admin.label_org')}</label> |
| <select |
| value={regOrgId} |
| onChange={e => setRegOrgId(e.target.value)} |
| className="w-full bg-slate-800 border border-slate-700 text-white text-sm rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-violet-500 focus:border-transparent" |
| > |
| <option value="">{t('super_admin.org_select_placeholder')}</option> |
| {orgOptions.map(o => ( |
| <option key={o.id} value={o.id}>{o.name}</option> |
| ))} |
| </select> |
| </div> |
| |
| <div> |
| <label className="block text-xs font-medium text-slate-300 mb-1.5">{t('super_admin.label_phone_number_id')}</label> |
| <input |
| type="text" |
| inputMode="numeric" |
| value={regPhoneNumberId} |
| onChange={e => setRegPhoneNumberId(e.target.value.replace(/\D/g, ''))} |
| placeholder="123456789012345" |
| maxLength={18} |
| className="w-full bg-slate-800 border border-slate-700 text-white text-sm rounded-lg px-3 py-2 placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-violet-500 focus:border-transparent font-mono" |
| /> |
| <p className="text-xs text-slate-500 mt-1">{t('super_admin.phone_id_hint')}</p> |
| </div> |
| |
| <div> |
| <label className="block text-xs font-medium text-slate-300 mb-1.5">{t('super_admin.label_pin')}</label> |
| <input |
| type="password" |
| inputMode="numeric" |
| value={regPin} |
| onChange={e => setRegPin(e.target.value.replace(/\D/g, ''))} |
| placeholder="000000" |
| maxLength={6} |
| className="w-full bg-slate-800 border border-slate-700 text-white text-sm rounded-lg px-3 py-2 placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-violet-500 focus:border-transparent font-mono tracking-widest" |
| /> |
| <p className="text-xs text-slate-500 mt-1">{t('super_admin.pin_hint')}</p> |
| </div> |
| |
| {regError && ( |
| <p className="text-xs text-red-400 bg-red-900/20 border border-red-800/40 rounded-lg px-3 py-2">{regError}</p> |
| )} |
| |
| <button |
| onClick={handleRegister} |
| disabled={regLoading} |
| className="w-full mt-1 flex items-center justify-center gap-2 px-4 py-2.5 bg-violet-600 hover:bg-violet-500 disabled:opacity-50 disabled:cursor-not-allowed text-white text-sm font-medium rounded-lg transition-colors" |
| > |
| {regLoading ? <RefreshCw className="w-4 h-4 animate-spin" /> : null} |
| {regLoading ? t('super_admin.wa_sending') : t('super_admin.wa_send_otp')} |
| </button> |
| </div> |
| )} |
| |
| {regStep === 2 && ( |
| <div className="space-y-4"> |
| <div className="bg-slate-800 rounded-lg px-4 py-3 space-y-1"> |
| <p className="text-xs text-slate-400">{t('super_admin.label_org')} : <span className="text-slate-200 font-medium">{selectedOrg?.name || regOrgId}</span></p> |
| <p className="text-xs text-slate-400">{t('super_admin.label_phone_number_id')} : <span className="text-slate-200 font-mono">{regPhoneNumberId}</span></p> |
| </div> |
| |
| <div className="flex items-start gap-2.5 bg-blue-900/20 border border-blue-800/40 rounded-lg px-3 py-3"> |
| <Info className="w-4 h-4 text-blue-400 shrink-0 mt-0.5" /> |
| <p className="text-xs text-blue-300">{t('super_admin.wa_otp_hint')}</p> |
| </div> |
| |
| <div> |
| <label className="block text-xs font-medium text-slate-300 mb-1.5">{t('super_admin.label_otp')}</label> |
| <input |
| type="text" |
| inputMode="numeric" |
| value={regCode} |
| onChange={e => setRegCode(e.target.value.replace(/\D/g, ''))} |
| placeholder="123456" |
| maxLength={8} |
| className="w-full bg-slate-800 border border-slate-700 text-white text-sm rounded-lg px-3 py-2 placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-violet-500 focus:border-transparent font-mono tracking-widest text-center text-lg" |
| autoFocus |
| /> |
| </div> |
| |
| {regError && ( |
| <p className="text-xs text-red-400 bg-red-900/20 border border-red-800/40 rounded-lg px-3 py-2">{regError}</p> |
| )} |
| |
| <div className="flex gap-2 mt-1"> |
| <button |
| onClick={() => { setRegStep(1); setRegError(''); setRegCode(''); }} |
| disabled={regLoading} |
| className="flex items-center gap-1.5 px-4 py-2.5 bg-slate-800 hover:bg-slate-700 disabled:opacity-50 disabled:cursor-not-allowed text-slate-300 text-sm rounded-lg transition-colors" |
| > |
| <ArrowLeft className="w-3.5 h-3.5" /> |
| {t('super_admin.wa_back')} |
| </button> |
| <button |
| onClick={handleVerify} |
| disabled={regLoading} |
| className="flex-1 flex items-center justify-center gap-2 px-4 py-2.5 bg-violet-600 hover:bg-violet-500 disabled:opacity-50 disabled:cursor-not-allowed text-white text-sm font-medium rounded-lg transition-colors" |
| > |
| {regLoading ? <RefreshCw className="w-4 h-4 animate-spin" /> : null} |
| {regLoading ? t('super_admin.wa_verifying') : t('super_admin.wa_verify')} |
| </button> |
| </div> |
| </div> |
| )} |
| </div> |
| </div> |
| )} |
| </div> |
| ); |
| } |
|
|