import { useState, useEffect } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { Building2, Plus, MessageSquare, ShieldCheck, Activity, Loader2, X, RefreshCw, CheckCircle2, XCircle, AlertTriangle, Info } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { api } from '../lib/api'; import { useAuth } from '../lib/auth'; import { launchEmbeddedSignup } from '../lib/meta-signup'; import MetaVerificationGuide from '../components/MetaVerificationGuide'; import { useToast } from '../hooks/useToast'; import { logError, logWarn } from '../lib/logger'; interface Organization { id: string; name: string; mode: 'EDTECH' | 'CRM_MARKETING' | 'PEDAGOGY' | 'CUSTOMER_SERVICE'; wabaId?: string; phoneNumbers?: { id: string; phoneNumber: string }[]; subscriptionPlan?: 'STARTER' | 'GROWTH' | 'SCALE' | 'ENTERPRISE'; subscriptionStatus?: string; isHardStopped?: boolean; personalityConfig?: { botName?: string; coreMission?: string; toneDescription?: string; }; } interface MetaStatus { configured: boolean; wabaStatus?: 'APPROVED' | 'PENDING' | 'REJECTED' | 'BANNED' | 'UNKNOWN'; businessId?: string; businessName?: string; businessVerified?: boolean; messagingLimitTier?: string; qualityRating?: 'GREEN' | 'YELLOW' | 'RED' | 'UNKNOWN'; syncedAt?: string; error?: string; loading?: boolean; } interface PersonalityModalProps { org: Organization; onClose: () => void; onSave: (config: any) => void; } export default function ClientsManagementView() { const { t } = useTranslation(); const toast = useToast(); const { token, user } = useAuth(); const isSuperAdmin = user?.role === 'SUPER_ADMIN'; const [clients, setClients] = useState([]); const [loading, setLoading] = useState(true); const [isModalOpen, setIsModalOpen] = useState(false); const [creditOrg, setCreditOrg] = useState(null); const [creditAmount, setCreditAmount] = useState(''); const [isSavingCredits, setIsSavingCredits] = useState(false); const [selectedOrgForPersonality, setSelectedOrgForPersonality] = useState(null); const [newOrg, setNewOrg] = useState({ name: '', slug: '', adminEmail: '', adminName: '', password: '', mode: 'PEDAGOGY' as 'CRM_MARKETING' | 'PEDAGOGY' | 'CUSTOMER_SERVICE', useCase: 'EDUCATION' as 'EDUCATION' | 'CRM_WHATSAPP' }); const [isCreating, setIsCreating] = useState(false); const [createErrors, setCreateErrors] = useState>({}); const [showGuide, setShowGuide] = useState(false); const [billingOrg, setBillingOrg] = useState(null); const [billingPlan, setBillingPlan] = useState('STARTER'); const [isSavingPlan, setIsSavingPlan] = useState(false); const [metaStatuses, setMetaStatuses] = useState>({}); const [setupOrg, setSetupOrg] = useState(null); const [directSetup, setDirectSetup] = useState({ wabaId: '', metaBusinessId: '', accessToken: '', phoneNumberId: '' }); const [isDirectSetupSaving, setIsDirectSetupSaving] = useState(false); const fetchMetaStatus = async (orgId: string, force = false) => { setMetaStatuses(prev => ({ ...prev, [orgId]: { ...prev[orgId], configured: false, loading: true } })); try { const status = force ? await api.post(`/v1/organizations/${orgId}/meta-status/refresh`, {}, token!) : await api.get(`/v1/organizations/${orgId}/meta-status`, token!); setMetaStatuses(prev => ({ ...prev, [orgId]: { ...status, loading: false } })); } catch (err) { logWarn('[Clients] fetchMetaStatus failed', err); setMetaStatuses(prev => ({ ...prev, [orgId]: { configured: false, loading: false, error: t('common.error') } })); } }; const fetchClients = async () => { if (!token) return; try { const data = await api.get('/v1/organizations', token); setClients(data); // Fetch Meta status for all orgs in parallel (non-blocking) data.forEach((org: Organization) => fetchMetaStatus(org.id)); } catch (error) { logError("Failed to fetch organizations:", error); } finally { setLoading(false); } }; useEffect(() => { fetchClients(); }, [token]); const handleDirectSetupSubmit = async () => { if (!setupOrg || !directSetup.wabaId) return; setIsDirectSetupSaving(true); try { await api.post(`/v1/organizations/${setupOrg.id}/whatsapp-setup`, { wabaId: directSetup.wabaId, ...(directSetup.accessToken && { accessToken: directSetup.accessToken }), ...(directSetup.phoneNumberId && { phoneNumberId: directSetup.phoneNumberId }), }, token!); if (directSetup.metaBusinessId) { await api.put(`/v1/organizations/${setupOrg.id}`, { metaBusinessId: directSetup.metaBusinessId }, token!); } toast.success(t('clients.wa_setup_modal.success')); setSetupOrg(null); setDirectSetup({ wabaId: '', metaBusinessId: '', accessToken: '', phoneNumberId: '' }); await fetchClients(); setTimeout(() => fetchMetaStatus(setupOrg.id, true), 1000); } catch (err: any) { toast.error(`${t('clients.toast.error_prefix')}${err.message}`); } finally { setIsDirectSetupSaving(false); } }; const handleCreateOrg = async (e: React.FormEvent) => { e.preventDefault(); const errors: Record = {}; if (!newOrg.name.trim()) errors.name = t('clients.validation.name_required'); if (!newOrg.slug.trim()) errors.slug = t('clients.validation.slug_required'); else if (!/^[a-z0-9-]+$/.test(newOrg.slug)) errors.slug = t('clients.validation.slug_format'); if (!newOrg.adminName.trim()) errors.adminName = t('clients.validation.admin_name_required'); if (!newOrg.adminEmail.trim()) errors.adminEmail = t('clients.validation.email_required'); else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(newOrg.adminEmail)) errors.adminEmail = t('clients.validation.email_invalid'); if (!newOrg.password || newOrg.password.length < 8) errors.password = t('clients.validation.password_min'); if (Object.keys(errors).length > 0) { setCreateErrors(errors); return; } setCreateErrors({}); setIsCreating(true); try { await api.post('/v1/organizations', newOrg, token!); await fetchClients(); setIsModalOpen(false); setNewOrg({ name: '', slug: '', adminEmail: '', adminName: '', password: '', mode: 'PEDAGOGY', useCase: 'EDUCATION' }); } catch (error) { logError("Failed to create organization:", error); toast.error(t('clients.create_modal.error_create')); } finally { setIsCreating(false); } }; const handleEmbeddedSignup = async (orgId: string) => { try { const result = await launchEmbeddedSignup(); await api.post(`/v1/organizations/${orgId}/whatsapp-setup`, { wabaId: result.waba_id, accessToken: result.code, phoneNumberId: result.phone_number_id, phoneNumber: '' }, token!); toast.success(t('clients.toast.wa_connected')); fetchClients(); } catch (error: any) { toast.error(`${t('clients.toast.error_prefix')}${error.message}`); } }; if (loading) { return (

{t('clients.loading')}

); } return (

{t('clients.page_title')}

{t('clients.page_subtitle')}

{clients.length === 0 ? (

{t('clients.no_clients_title')}

{t('clients.no_clients_desc')}

) : clients.map(client => (

{client.name}

{client.phoneNumbers?.length ? t('clients.status.operational') : t('clients.status.config_required')} ID: {client.id}
{client.phoneNumbers?.length ? (

{client.phoneNumbers[0].phoneNumber}

ID: {client.phoneNumbers[0].id}

{metaStatuses[client.id] && !metaStatuses[client.id].loading && !metaStatuses[client.id].configured ? ( ) : (
{t('clients.status.online')}
)}
) : (
{isSuperAdmin && ( )}
)}
fetchMetaStatus(client.id, true)} onGuide={() => setShowGuide(true)} />
{isSuperAdmin && ( )} {client.isHardStopped && ( {t('clients.status.suspended')} )}
))}
{/* Modal du Guide de Vérification */} {showGuide && (
)} {/* Modal de création d'organisation */} {isModalOpen && (
{/* Fixed header */}

{t('clients.create_modal.title')}

{/* Scrollable body */}
{ setNewOrg({...newOrg, name: e.target.value}); setCreateErrors(p => ({...p, name: ''})); }} className={`w-full border rounded-xl px-4 py-3 outline-none focus:ring-2 transition ${createErrors.name ? 'border-red-400 focus:ring-red-200' : 'border-slate-200 focus:ring-slate-900'}`} /> {createErrors.name &&

{createErrors.name}

}
{ setNewOrg({...newOrg, slug: e.target.value.toLowerCase().replace(/\s+/g, '-')}); setCreateErrors(p => ({...p, slug: ''})); }} className={`w-full border rounded-xl px-4 py-3 outline-none focus:ring-2 transition font-mono text-sm ${createErrors.slug ? 'border-red-400 focus:ring-red-200' : 'border-slate-200 focus:ring-slate-900'}`} /> {createErrors.slug &&

{createErrors.slug}

}

{t('clients.create_modal.admin_section')}

{ setNewOrg({...newOrg, adminName: e.target.value}); setCreateErrors(p => ({...p, adminName: ''})); }} className={`w-full border rounded-xl px-4 py-2 text-sm outline-none focus:ring-2 transition ${createErrors.adminName ? 'border-red-400 focus:ring-red-200' : 'border-slate-200 focus:ring-slate-900'}`} /> {createErrors.adminName &&

{createErrors.adminName}

}
{ setNewOrg({...newOrg, adminEmail: e.target.value}); setCreateErrors(p => ({...p, adminEmail: ''})); }} className={`w-full border rounded-xl px-4 py-2 text-sm outline-none focus:ring-2 transition ${createErrors.adminEmail ? 'border-red-400 focus:ring-red-200' : 'border-slate-200 focus:ring-slate-900'}`} /> {createErrors.adminEmail &&

{createErrors.adminEmail}

}
{ setNewOrg({...newOrg, password: e.target.value}); setCreateErrors(p => ({...p, password: ''})); }} className={`w-full border rounded-xl px-4 py-2 text-sm outline-none focus:ring-2 transition ${createErrors.password ? 'border-red-400 focus:ring-red-200' : 'border-slate-200 focus:ring-slate-900'}`} /> {createErrors.password &&

{createErrors.password}

}
{/* Fixed footer */}
)} {/* Billing Modal */} {billingOrg && (

{t('clients.billing_modal.title')}

{billingOrg.name}

{billingOrg.id}

{t('clients.billing_modal.mode_label')}

{billingOrg.mode}

{t('clients.billing_modal.waba_status_label')}

{t('clients.billing_modal.daily_limit_label')}

{t('clients.billing_modal.meta_business_label')}

{billingOrg.wabaId && (

{t('clients.billing_modal.waba_id_label')}

{billingOrg.wabaId}

)}

{t('clients.billing_modal.plan_label')}

)} {/* Super-admin: Credit Allocation Modal */} {creditOrg && isSuperAdmin && (

{t('clients.credits_modal.title')}

{creditOrg.name}

{t('clients.credits_modal.current_balance')} {(creditOrg as any).walletBalance ?? '—'} {t('clients.credits_modal.credits_unit')}
setCreditAmount(e.target.value)} placeholder={t('clients.credits_modal.add_credits_placeholder')} className="w-full border border-slate-200 rounded-xl px-4 py-3 outline-none focus:ring-2 focus:ring-emerald-500/20 focus:border-emerald-500 transition font-mono text-lg" />

{t('clients.credits_modal.rate_hint')}

)} {/* Personality Studio Modal */} {selectedOrgForPersonality && ( setSelectedOrgForPersonality(null)} onSave={async (config) => { try { await api.patch(`/v1/organizations/${selectedOrgForPersonality.id}/personality`, config, token!); toast.success(t('clients.personality_modal.success')); fetchClients(); setSelectedOrgForPersonality(null); } catch (err: any) { toast.error(`${t('clients.toast.error_prefix')}${err.message}`); } }} /> )} {/* WhatsApp Setup Modal */} {setupOrg && (

{t('clients.wa_setup_modal.title')}

{setupOrg.name}

{/* Option A — Direct (account déjà sur Meta) */}

{t('clients.wa_setup_modal.already_on_meta')}

setDirectSetup(s => ({ ...s, wabaId: e.target.value }))} className="w-full border border-slate-200 rounded-xl px-3 py-2.5 text-sm font-mono outline-none focus:ring-2 focus:ring-indigo-500/20 focus:border-indigo-400" />

{t('clients.wa_setup_modal.waba_id_hint_link')}

setDirectSetup(s => ({ ...s, metaBusinessId: e.target.value }))} className="w-full border border-slate-200 rounded-xl px-3 py-2.5 text-sm font-mono outline-none focus:ring-2 focus:ring-indigo-500/20 focus:border-indigo-400" />

{t('clients.wa_setup_modal.business_id_hint_link')}

setDirectSetup(s => ({ ...s, accessToken: e.target.value }))} className="w-full border border-slate-200 rounded-xl px-3 py-2.5 text-sm font-mono outline-none focus:ring-2 focus:ring-indigo-500/20 focus:border-indigo-400" />

{t('clients.wa_setup_modal.token_hint_link')}

{directSetup.phoneNumberId && (

{t('clients.wa_setup_modal.phone_number_detected')} {directSetup.phoneNumberId}

)}
{/* Option B — Embedded Signup (nouveaux clients) */}

{t('clients.wa_setup_modal.new_account_heading')}

{t('clients.wa_setup_modal.new_account_desc')}

)} {/* Meta Compliance Footer */}

{t('clients.tier.meta_compliance')}

Meta WhatsApp
); } // ─── Meta status helpers ────────────────────────────────────────────────────── function dailyLimitLabel(ms: MetaStatus | undefined, t: (key: string) => string): string { if (!ms || ms.loading) return '…'; if (!ms.configured) return '—'; const tierMap: Record = { TIER_50: t('clients.tier.tier_50'), TIER_250: t('clients.tier.tier_250'), TIER_1K: t('clients.tier.tier_1k'), TIER_10K: t('clients.tier.tier_10k'), TIER_100K: t('clients.tier.tier_100k'), UNLIMITED: t('clients.tier.unlimited'), }; if (ms.messagingLimitTier) return tierMap[ms.messagingLimitTier] ?? ms.messagingLimitTier; return '—'; } function WabaStatusBadge({ ms }: { ms?: MetaStatus }) { const { t } = useTranslation(); if (!ms || ms.loading) return {t('clients.waba.checking')}; if (!ms.configured) return {t('clients.waba.not_connected')}; const map: Record = { APPROVED: { labelKey: 'clients.waba.approved', cls: 'text-emerald-600' }, PENDING: { labelKey: 'clients.waba.pending', cls: 'text-amber-600' }, REJECTED: { labelKey: 'clients.waba.rejected', cls: 'text-red-600' }, BANNED: { labelKey: 'clients.waba.banned', cls: 'text-red-600' }, UNKNOWN: { labelKey: 'clients.waba.unknown', cls: 'text-slate-500' }, }; const s = map[ms.wabaStatus ?? 'UNKNOWN'] ?? map.UNKNOWN; return {t(s.labelKey)}; } function BusinessBadge({ ms }: { ms?: MetaStatus }) { const { t } = useTranslation(); if (!ms || ms.loading) return {t('clients.waba.checking')}; if (!ms.configured || ms.businessId === undefined) return ; return ms.businessVerified ? {t('clients.waba.verified')} : {t('clients.waba.not_verified')}; } function MetaStatusCell({ ms, onRefresh, onGuide }: { ms?: MetaStatus; onRefresh: () => void; onGuide: () => void }) { const { t } = useTranslation(); const isApproved = ms?.wabaStatus === 'APPROVED'; const isLoading = !ms || ms.loading; return (
{isLoading ? : isApproved ? : }

{t('clients.cells.waba_header')}

{ms && !isLoading && !isApproved && ms.configured && ( )}
{ms?.error &&

{ms.error}

}
); } function BusinessVerificationCell({ ms }: { ms?: MetaStatus }) { const { t } = useTranslation(); const isLoading = !ms || ms.loading; return (
{isLoading ? : ms?.businessVerified ? : ms?.businessId !== undefined ? : }

{t('clients.cells.business_header')}

{ms && !isLoading && ms.configured && ms.businessId !== undefined && !ms.businessVerified && ( {t('clients.waba.verify_link')} )}
); } function QualityDot({ rating }: { rating?: string }) { const { t } = useTranslation(); const map: Record = { GREEN: 'bg-emerald-400', YELLOW: 'bg-amber-400', RED: 'bg-red-400', UNKNOWN: 'bg-slate-300', }; const cls = map[rating ?? 'UNKNOWN'] ?? 'bg-slate-300'; return ; } const TIER_CONFIG: Record = { TIER_50: { textCls: 'text-red-600', bgCls: 'bg-red-50', iconCls: 'text-red-400', dotCls: 'bg-red-400', order: 0 }, TIER_250: { textCls: 'text-amber-600', bgCls: 'bg-amber-50', iconCls: 'text-amber-400', dotCls: 'bg-amber-400', order: 1 }, TIER_1K: { textCls: 'text-blue-600', bgCls: 'bg-blue-50', iconCls: 'text-blue-400', dotCls: 'bg-blue-400', order: 2 }, TIER_10K: { textCls: 'text-emerald-600', bgCls: 'bg-emerald-50', iconCls: 'text-emerald-400', dotCls: 'bg-emerald-400', order: 3 }, TIER_100K: { textCls: 'text-emerald-600', bgCls: 'bg-emerald-50', iconCls: 'text-emerald-400', dotCls: 'bg-emerald-400', order: 4 }, UNLIMITED: { textCls: 'text-emerald-600', bgCls: 'bg-emerald-50', iconCls: 'text-emerald-400', dotCls: 'bg-emerald-400', order: 5 }, }; function BillingTierDisplay({ ms }: { ms?: MetaStatus }) { const { t } = useTranslation(); const [showInfo, setShowInfo] = useState(false); const tierCfg = TIER_CONFIG[ms?.messagingLimitTier ?? '']; return ( <>
{dailyLimitLabel(ms, t)} {ms?.qualityRating && } {ms?.configured && ( )}
setShowInfo(false)} /> ); } function TierInfoModal({ ms, open, onClose }: { ms?: MetaStatus; open: boolean; onClose: () => void }) { const { t } = useTranslation(); const currentTierOrder = TIER_CONFIG[ms?.messagingLimitTier ?? '']?.order ?? -1; const TIERS_IN_ORDER = [ { key: 'TIER_50', labelKey: 'clients.tier.tier_50', descKey: 'clients.tier_modal.tier_50_desc' }, { key: 'TIER_250', labelKey: 'clients.tier.tier_250', descKey: 'clients.tier_modal.tier_250_desc' }, { key: 'TIER_1K', labelKey: 'clients.tier.tier_1k', descKey: 'clients.tier_modal.tier_1k_desc' }, { key: 'TIER_10K', labelKey: 'clients.tier.tier_10k', descKey: 'clients.tier_modal.tier_10k_desc' }, { key: 'TIER_100K', labelKey: 'clients.tier.tier_100k', descKey: 'clients.tier_modal.tier_100k_desc' }, { key: 'UNLIMITED', labelKey: 'clients.tier.unlimited', descKey: 'clients.tier_modal.unlimited_desc' }, ]; const QUALITY_INFO = { GREEN: { dotCls: 'bg-emerald-400', ringCls: 'ring-emerald-300 bg-emerald-50', labelKey: 'clients.quality.good_label', descKey: 'clients.quality.good_desc' }, YELLOW: { dotCls: 'bg-amber-400', ringCls: 'ring-amber-300 bg-amber-50', labelKey: 'clients.quality.medium_label', descKey: 'clients.quality.medium_desc' }, RED: { dotCls: 'bg-red-400', ringCls: 'ring-red-300 bg-red-50', labelKey: 'clients.quality.risky_label', descKey: 'clients.quality.risky_desc' }, }; useEffect(() => { if (!open) return; const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); }; document.addEventListener('keydown', onKey); document.body.style.overflow = 'hidden'; return () => { document.removeEventListener('keydown', onKey); document.body.style.overflow = ''; }; }, [open, onClose]); return ( {open && ( {/* Backdrop */}
{/* Panel — bottom sheet on mobile, centered card on desktop */} e.stopPropagation()} > {/* Drag handle — mobile only */}
{/* Sticky header */}

{t('clients.tier_modal.title')}

{/* Scrollable body */}
{/* Tier ladder */}

{t('clients.tier_modal.levels_heading')}

{TIERS_IN_ORDER.map(({ key, labelKey, descKey }, i) => { const isCurrent = key === ms?.messagingLimitTier; const isPast = i < currentTierOrder; const cfg = TIER_CONFIG[key]; return (
{t(labelKey)} {isCurrent && {t('clients.tier_modal.current_badge')}}

{t(descKey)}

); })}
{/* Quality rating */} {ms?.qualityRating && ms.qualityRating !== 'UNKNOWN' && (

{t('clients.tier_modal.quality_heading')}

{(['GREEN', 'YELLOW', 'RED'] as const).map(r => { const isCurrent = ms.qualityRating === r; const q = QUALITY_INFO[r]; return (

{t(q.labelKey)}

{t(q.descKey)}

); })}
)}

{t('clients.tier_modal.meta_auto_update')}
{t('clients.tier_modal.levels_auto')}

)} ); } function DailyLimitCell({ ms }: { ms?: MetaStatus }) { const { t } = useTranslation(); const [showInfo, setShowInfo] = useState(false); const isLoading = !ms || ms.loading; const tierCfg = TIER_CONFIG[ms?.messagingLimitTier ?? '']; return ( <>

{t('clients.cells.daily_limit_header')}

{isLoading ? :

{dailyLimitLabel(ms, t)}

} {!isLoading && ms?.qualityRating && } {!isLoading && ms?.configured && ( )}
setShowInfo(false)} /> ); } // ─── Personality Studio Modal ───────────────────────────────────────────────── function PersonalityStudioModal({ org, onClose, onSave }: PersonalityModalProps) { const { t } = useTranslation(); const [config, setConfig] = useState(org.personalityConfig || { botName: '', coreMission: '', toneDescription: '' }); const [isSaving, setIsSaving] = useState(false); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setIsSaving(true); await onSave(config); setIsSaving(false); }; return (

{t('clients.personality_modal.title')}

{t('clients.personality_modal.subtitle', { orgName: org.name })}

setConfig({...config, botName: e.target.value})} className="w-full border border-slate-200 rounded-2xl px-5 py-4 outline-none focus:ring-4 focus:ring-indigo-50 transition font-medium" />