edtech / apps /admin /src /pages /super-admin /WhatsAppNumbers.tsx
CognxSafeTrack
feat(i18n): complete super-admin i18n — all 11 pages fully translated
b92ea37
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 { /* ignore */ }
}
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>
);
}