edtech / apps /admin /src /pages /ClientsManagementView.tsx
CognxSafeTrack
fix(admin): replace vague Meta hints with direct clickable links in Direct Setup modal
295c327
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<Organization[]>([]);
const [loading, setLoading] = useState(true);
const [isModalOpen, setIsModalOpen] = useState(false);
const [creditOrg, setCreditOrg] = useState<Organization | null>(null);
const [creditAmount, setCreditAmount] = useState('');
const [isSavingCredits, setIsSavingCredits] = useState(false);
const [selectedOrgForPersonality, setSelectedOrgForPersonality] = useState<Organization | null>(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<Record<string, string>>({});
const [showGuide, setShowGuide] = useState(false);
const [billingOrg, setBillingOrg] = useState<Organization | null>(null);
const [billingPlan, setBillingPlan] = useState<string>('STARTER');
const [isSavingPlan, setIsSavingPlan] = useState(false);
const [metaStatuses, setMetaStatuses] = useState<Record<string, MetaStatus>>({});
const [setupOrg, setSetupOrg] = useState<Organization | null>(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<string, string> = {};
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 (
<div className="flex flex-col items-center justify-center min-h-[60vh] text-slate-400">
<Loader2 className="w-8 h-8 animate-spin mb-4" />
<p className="font-medium">{t('clients.loading')}</p>
</div>
);
}
return (
<div className="p-8 max-w-6xl mx-auto">
<div className="flex items-center justify-between mb-8">
<div>
<h1 className="text-3xl font-bold tracking-tight text-slate-900">{t('clients.page_title')}</h1>
<p className="text-slate-500 mt-2">{t('clients.page_subtitle')}</p>
</div>
<button
onClick={() => setIsModalOpen(true)}
className="flex items-center gap-2 bg-slate-900 text-white px-4 py-2 rounded-xl font-medium hover:bg-slate-800 transition shadow-sm"
>
<Plus className="w-4 h-4" /> {t('clients.new_org_button')}
</button>
</div>
<div className="grid gap-6">
{clients.length === 0 ? (
<div className="text-center py-20 bg-slate-50 rounded-3xl border-2 border-dashed border-slate-200">
<Building2 className="w-12 h-12 text-slate-300 mx-auto mb-4" />
<h3 className="text-lg font-bold text-slate-900">{t('clients.no_clients_title')}</h3>
<p className="text-slate-500">{t('clients.no_clients_desc')}</p>
</div>
) : clients.map(client => (
<div key={client.id} className="bg-white rounded-2xl border border-slate-200 p-6 shadow-sm hover:shadow-md transition">
<div className="flex items-start justify-between">
<div className="flex gap-4">
<div className="w-12 h-12 bg-slate-100 rounded-2xl flex items-center justify-center">
<Building2 className="w-6 h-6 text-slate-600" />
</div>
<div>
<h3 className="text-xl font-bold text-slate-900">{client.name}</h3>
<div className="flex items-center gap-3 mt-1">
<span className={`text-xs px-2.5 py-0.5 rounded-full font-semibold ${
client.phoneNumbers?.length ? 'bg-emerald-50 text-emerald-700' : 'bg-amber-50 text-amber-700'
}`}>
{client.phoneNumbers?.length ? t('clients.status.operational') : t('clients.status.config_required')}
</span>
<span className="text-xs text-slate-400">ID: {client.id}</span>
</div>
</div>
</div>
<div className="flex gap-2">
{client.phoneNumbers?.length ? (
<div className="flex items-center gap-4 text-sm">
<div className="text-right">
<p className="font-medium text-slate-700">{client.phoneNumbers[0].phoneNumber}</p>
<p className="text-xs text-slate-400">ID: {client.phoneNumbers[0].id}</p>
</div>
<div className="h-10 w-px bg-slate-100"></div>
{metaStatuses[client.id] && !metaStatuses[client.id].loading && !metaStatuses[client.id].configured ? (
<button
onClick={() => { setSetupOrg(client); setDirectSetup({ wabaId: '', metaBusinessId: '', accessToken: '', phoneNumberId: client.phoneNumbers?.[0]?.id || '' }); }}
className="flex items-center gap-2 bg-amber-500 text-white px-4 py-2 rounded-xl font-semibold hover:bg-amber-600 transition text-xs"
>
<AlertTriangle className="w-3.5 h-3.5" /> {t('clients.actions.reconfigure_wa')}
</button>
) : (
<div className="flex items-center gap-2 text-emerald-600">
<Activity className="w-4 h-4" />
<span className="font-medium text-xs">{t('clients.status.online')}</span>
</div>
)}
</div>
) : (
<div className="flex gap-2">
<button
onClick={() => handleEmbeddedSignup(client.id)}
className="flex items-center gap-2 bg-indigo-600 text-white px-5 py-2.5 rounded-xl font-semibold hover:bg-indigo-700 transition shadow-lg shadow-indigo-100"
>
<MessageSquare className="w-4 h-4" /> {t('clients.actions.connect_wa')}
</button>
{isSuperAdmin && (
<button
onClick={() => { setSetupOrg(client); setDirectSetup({ wabaId: client.wabaId || '', metaBusinessId: '', accessToken: '', phoneNumberId: '' }); }}
className="flex items-center gap-2 bg-slate-700 text-white px-4 py-2.5 rounded-xl font-semibold hover:bg-slate-800 transition text-xs"
title={t('clients.actions.direct_setup')}
>
<ShieldCheck className="w-3.5 h-3.5" /> {t('clients.actions.direct_setup')}
</button>
)}
</div>
)}
<button
onClick={() => setSelectedOrgForPersonality(client)}
className="flex items-center gap-2 bg-slate-100 text-slate-700 px-4 py-2.5 rounded-xl font-semibold hover:bg-slate-200 transition"
>
<Activity className="w-4 h-4" /> {t('clients.actions.personality_studio')}
</button>
</div>
</div>
<div className="mt-8 pt-6 border-t border-slate-50 grid grid-cols-4 gap-6">
<MetaStatusCell
ms={metaStatuses[client.id]}
onRefresh={() => fetchMetaStatus(client.id, true)}
onGuide={() => setShowGuide(true)}
/>
<BusinessVerificationCell ms={metaStatuses[client.id]} />
<DailyLimitCell ms={metaStatuses[client.id]} />
<div className="flex items-center justify-end gap-4">
{isSuperAdmin && (
<button
onClick={() => { setCreditOrg(client); setCreditAmount(''); }}
className="text-sm font-bold text-emerald-600 hover:text-emerald-700 underline underline-offset-4"
>
{t('clients.actions.ai_credits')}
</button>
)}
{client.isHardStopped && (
<span className="text-[10px] font-bold bg-red-100 text-red-600 px-2 py-0.5 rounded-full">
{t('clients.status.suspended')}
</span>
)}
<button
onClick={() => { setBillingOrg(client); setBillingPlan(client.subscriptionPlan ?? 'STARTER'); }}
className="text-sm font-bold text-indigo-600 hover:text-indigo-700 underline underline-offset-4"
>
{t('clients.actions.billing_details')}
</button>
</div>
</div>
</div>
))}
</div>
{/* Modal du Guide de VΓ©rification */}
{showGuide && (
<div className="fixed inset-0 bg-slate-900/60 backdrop-blur-xl flex items-center justify-center p-6 z-[100] animate-in fade-in duration-300">
<div className="w-full max-w-5xl max-h-[90vh] overflow-y-auto">
<div className="relative">
<button
onClick={() => setShowGuide(false)}
className="absolute -top-4 -right-4 bg-white p-3 rounded-full shadow-xl hover:scale-110 transition z-[110]"
>
<X className="w-6 h-6 text-slate-900" />
</button>
<MetaVerificationGuide />
</div>
</div>
</div>
)}
{/* Modal de crΓ©ation d'organisation */}
{isModalOpen && (
<div className="fixed inset-0 bg-slate-900/60 backdrop-blur-md flex items-end sm:items-center justify-center sm:p-4 z-50">
<div className="bg-white rounded-t-[2rem] sm:rounded-[2rem] shadow-2xl w-full sm:max-w-xl flex flex-col max-h-[90vh] animate-in zoom-in-95 duration-200">
{/* Fixed header */}
<div className="shrink-0 flex items-center justify-between px-8 pt-8 pb-5 border-b border-slate-100">
<h2 className="text-2xl font-black text-slate-900">{t('clients.create_modal.title')}</h2>
<button onClick={() => { setIsModalOpen(false); setCreateErrors({}); }} className="p-2 hover:bg-slate-100 rounded-full transition">
<X className="w-5 h-5 text-slate-400" />
</button>
</div>
{/* Scrollable body */}
<form onSubmit={handleCreateOrg} className="flex flex-col flex-1 min-h-0">
<div className="overflow-y-auto flex-1 px-8 py-6 space-y-6">
<div className="grid grid-cols-2 gap-4">
<div className="col-span-2">
<label className="block text-xs font-bold uppercase text-slate-400 mb-2">{t('clients.create_modal.company_name_label')} <span className="text-red-400">*</span></label>
<input
type="text"
placeholder={t('clients.create_modal.company_name_placeholder')}
value={newOrg.name}
onChange={e => { 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 && <p className="text-xs text-red-500 mt-1">{createErrors.name}</p>}
</div>
<div>
<label className="block text-xs font-bold uppercase text-slate-400 mb-2">{t('clients.create_modal.slug_label')} <span className="text-red-400">*</span></label>
<input
type="text"
placeholder={t('clients.create_modal.slug_placeholder')}
value={newOrg.slug}
onChange={e => { 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 && <p className="text-xs text-red-500 mt-1">{createErrors.slug}</p>}
</div>
<div>
<label className="block text-xs font-bold uppercase text-slate-400 mb-2">{t('clients.create_modal.use_case_label')}</label>
<select
value={newOrg.mode}
onChange={e => setNewOrg({...newOrg, mode: e.target.value as any})}
className="w-full border border-slate-200 rounded-xl px-4 py-3 outline-none focus:ring-2 focus:ring-slate-900 transition bg-white"
>
<option value="CRM_MARKETING">{t('clients.create_modal.mode_crm')}</option>
<option value="PEDAGOGY">{t('clients.create_modal.mode_pedagogy')}</option>
<option value="CUSTOMER_SERVICE">{t('clients.create_modal.mode_service')}</option>
</select>
</div>
<div className="col-span-2">
<label className="block text-xs font-bold uppercase text-slate-400 mb-2">{t('clients.create_modal.workstream_label')}</label>
<select
value={newOrg.useCase}
onChange={e => {
const val = e.target.value as any;
setNewOrg({
...newOrg,
useCase: val,
mode: val === 'CRM_WHATSAPP' ? 'CRM_MARKETING' : 'PEDAGOGY'
});
}}
className="w-full border border-slate-200 rounded-xl px-4 py-3 outline-none focus:ring-2 focus:ring-slate-900 transition bg-white"
>
<option value="EDUCATION">{t('clients.create_modal.use_case_education')}</option>
<option value="CRM_WHATSAPP">{t('clients.create_modal.use_case_crm')}</option>
</select>
</div>
</div>
<div className="p-6 bg-slate-50 rounded-2xl border border-slate-100 space-y-4">
<h4 className="text-xs font-bold text-slate-500 uppercase tracking-widest">{t('clients.create_modal.admin_section')}</h4>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-xs font-bold text-slate-500 mb-1">{t('clients.create_modal.admin_name_label')} <span className="text-red-400">*</span></label>
<input
type="text"
placeholder={t('clients.create_modal.admin_name_placeholder')}
value={newOrg.adminName}
onChange={e => { 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 && <p className="text-xs text-red-500 mt-1">{createErrors.adminName}</p>}
</div>
<div>
<label className="block text-xs font-bold text-slate-500 mb-1">{t('clients.create_modal.admin_email_label')} <span className="text-red-400">*</span></label>
<input
type="email"
placeholder={t('clients.create_modal.admin_email_placeholder')}
value={newOrg.adminEmail}
onChange={e => { 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 && <p className="text-xs text-red-500 mt-1">{createErrors.adminEmail}</p>}
</div>
<div className="col-span-2">
<label className="block text-xs font-bold text-slate-500 mb-1">{t('clients.create_modal.password_label')} <span className="text-red-400">*</span></label>
<input
type="password"
placeholder={t('clients.create_modal.password_placeholder')}
value={newOrg.password}
onChange={e => { 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 && <p className="text-xs text-red-500 mt-1">{createErrors.password}</p>}
</div>
</div>
</div>
</div>
{/* Fixed footer */}
<div className="shrink-0 px-8 pb-8 pt-4 border-t border-slate-100">
<button
type="submit"
disabled={isCreating}
className="w-full bg-slate-900 text-white py-4 rounded-2xl font-bold hover:bg-slate-800 transition disabled:opacity-50 flex items-center justify-center gap-2 shadow-xl shadow-slate-200"
>
{isCreating ? <Loader2 className="w-4 h-4 animate-spin" /> : t('clients.create_modal.submit')}
</button>
</div>
</form>
</div>
</div>
)}
{/* Billing Modal */}
{billingOrg && (
<div className="fixed inset-0 bg-slate-900/60 backdrop-blur-sm flex items-center justify-center p-6 z-[100]">
<div className="bg-white rounded-3xl shadow-2xl w-full max-w-lg">
<div className="flex items-center justify-between p-6 border-b border-slate-100">
<h2 className="text-xl font-bold text-slate-900">{t('clients.billing_modal.title')}</h2>
<button onClick={() => setBillingOrg(null)} className="p-2 hover:bg-slate-100 rounded-full transition">
<X className="w-5 h-5 text-slate-500" />
</button>
</div>
<div className="p-6 space-y-4">
<div className="flex items-center gap-3 bg-slate-50 rounded-2xl p-4">
<Building2 className="w-8 h-8 text-slate-400 shrink-0" />
<div>
<p className="font-bold text-slate-900">{billingOrg.name}</p>
<p className="text-xs text-slate-400 font-mono">{billingOrg.id}</p>
</div>
</div>
<div className="grid grid-cols-2 gap-3 text-sm">
<div className="bg-slate-50 rounded-2xl p-4">
<p className="text-xs font-bold text-slate-400 uppercase tracking-widest mb-1">{t('clients.billing_modal.mode_label')}</p>
<p className="font-semibold text-slate-800">{billingOrg.mode}</p>
</div>
<div className="bg-slate-50 rounded-2xl p-4">
<p className="text-xs font-bold text-slate-400 uppercase tracking-widest mb-1">{t('clients.billing_modal.waba_status_label')}</p>
<WabaStatusBadge ms={metaStatuses[billingOrg.id]} />
</div>
<div className="bg-slate-50 rounded-2xl p-4">
<p className="text-xs font-bold text-slate-400 uppercase tracking-widest mb-1">{t('clients.billing_modal.daily_limit_label')}</p>
<BillingTierDisplay ms={metaStatuses[billingOrg.id]} />
</div>
<div className="bg-slate-50 rounded-2xl p-4">
<p className="text-xs font-bold text-slate-400 uppercase tracking-widest mb-1">{t('clients.billing_modal.meta_business_label')}</p>
<BusinessBadge ms={metaStatuses[billingOrg.id]} />
</div>
</div>
{billingOrg.wabaId && (
<div className="bg-slate-50 rounded-2xl p-4 text-sm">
<p className="text-xs font-bold text-slate-400 uppercase tracking-widest mb-1">{t('clients.billing_modal.waba_id_label')}</p>
<p className="font-mono text-slate-700">{billingOrg.wabaId}</p>
</div>
)}
</div>
<div className="px-6 pb-4">
<div className="bg-slate-50 rounded-2xl p-4">
<p className="text-xs font-bold text-slate-400 uppercase tracking-widest mb-3">{t('clients.billing_modal.plan_label')}</p>
<select
value={billingPlan}
onChange={e => setBillingPlan(e.target.value)}
className="w-full border border-slate-200 rounded-xl px-3 py-2.5 text-sm bg-white outline-none focus:ring-2 focus:ring-indigo-400 mb-3"
>
<option value="STARTER">{t('clients.billing_modal.plan_starter')}</option>
<option value="GROWTH">{t('clients.billing_modal.plan_growth')}</option>
<option value="SCALE">{t('clients.billing_modal.plan_scale')}</option>
<option value="ENTERPRISE">{t('clients.billing_modal.plan_enterprise')}</option>
</select>
<button
disabled={isSavingPlan || billingPlan === (billingOrg?.subscriptionPlan ?? 'STARTER')}
onClick={async () => {
if (!billingOrg) return;
setIsSavingPlan(true);
try {
await api.put(`/v1/organizations/${billingOrg.id}`, { subscriptionPlan: billingPlan }, token!);
setBillingOrg({ ...billingOrg, subscriptionPlan: billingPlan as any });
await fetchClients();
toast.success(t('clients.billing_modal.plan_updated'));
} catch (err: any) {
toast.error(`${t('clients.toast.error_prefix')}${err.message}`);
} finally {
setIsSavingPlan(false);
}
}}
className="w-full py-2.5 bg-indigo-600 hover:bg-indigo-700 disabled:opacity-40 text-white rounded-xl font-semibold text-sm transition flex items-center justify-center gap-2"
>
{isSavingPlan ? <Loader2 className="w-4 h-4 animate-spin" /> : t('clients.billing_modal.apply_plan')}
</button>
</div>
</div>
<div className="p-6 border-t border-slate-100">
<button
onClick={() => setBillingOrg(null)}
className="w-full py-3 bg-slate-900 text-white rounded-2xl font-bold hover:bg-slate-700 transition"
>
{t('clients.billing_modal.close')}
</button>
</div>
</div>
</div>
)}
{/* Super-admin: Credit Allocation Modal */}
{creditOrg && isSuperAdmin && (
<div className="fixed inset-0 bg-slate-900/60 backdrop-blur-md flex items-end sm:items-center justify-center sm:p-4 z-[110]">
<div className="bg-white rounded-t-[2rem] sm:rounded-[2rem] shadow-2xl w-full sm:max-w-md flex flex-col max-h-[90vh] animate-in zoom-in-95 duration-200">
<div className="shrink-0 flex items-center justify-between px-8 pt-8 pb-5 border-b border-slate-100">
<div>
<h2 className="text-xl font-black text-slate-900">{t('clients.credits_modal.title')}</h2>
<p className="text-sm text-slate-500 mt-0.5">{creditOrg.name}</p>
</div>
<button onClick={() => setCreditOrg(null)} className="p-2 hover:bg-slate-100 rounded-full transition">
<X className="w-5 h-5 text-slate-400" />
</button>
</div>
<div className="overflow-y-auto flex-1 px-8 py-6 space-y-5">
<div className="bg-slate-50 rounded-2xl p-4 flex items-center justify-between">
<span className="text-sm text-slate-500">{t('clients.credits_modal.current_balance')}</span>
<span className="text-base font-bold text-slate-900">
{(creditOrg as any).walletBalance ?? 'β€”'} {t('clients.credits_modal.credits_unit')}
</span>
</div>
<div>
<label className="block text-xs font-bold uppercase text-slate-400 mb-2">
{t('clients.credits_modal.add_credits_label')}
</label>
<input
type="number"
min="1"
value={creditAmount}
onChange={e => 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"
/>
<p className="text-xs text-slate-400 mt-1.5">{t('clients.credits_modal.rate_hint')}</p>
</div>
</div>
<div className="shrink-0 px-8 pb-8 pt-4 border-t border-slate-100">
<button
type="button"
disabled={isSavingCredits || !creditAmount || Number(creditAmount) <= 0}
onClick={async () => {
if (!creditAmount || Number(creditAmount) <= 0) return;
setIsSavingCredits(true);
try {
const data = await api.post('/v1/billing/admin/allocate', { organizationId: creditOrg.id, amount: Number(creditAmount) }, token!);
toast.success(`${creditAmount} ${t('clients.credits_modal.credits_unit')} β†’ ${data.newBalance}`);
setCreditOrg(null);
setCreditAmount('');
} catch {
toast.error(t('clients.credits_modal.error'));
} finally {
setIsSavingCredits(false);
}
}}
className="w-full bg-emerald-600 text-white py-4 rounded-2xl font-bold hover:bg-emerald-700 transition disabled:opacity-50 flex items-center justify-center gap-2"
>
{isSavingCredits ? <Loader2 className="w-4 h-4 animate-spin" /> : t('clients.credits_modal.submit')}
</button>
</div>
</div>
</div>
)}
{/* Personality Studio Modal */}
{selectedOrgForPersonality && (
<PersonalityStudioModal
org={selectedOrgForPersonality}
onClose={() => 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 && (
<div className="fixed inset-0 bg-slate-900/60 backdrop-blur-md flex items-center justify-center p-4 z-50">
<div className="bg-white rounded-3xl shadow-2xl w-full max-w-lg p-8 animate-in zoom-in-95 duration-200">
<div className="flex items-center justify-between mb-6">
<div>
<h2 className="text-xl font-black text-slate-900">{t('clients.wa_setup_modal.title')}</h2>
<p className="text-sm text-slate-500 mt-0.5">{setupOrg.name}</p>
</div>
<button onClick={() => setSetupOrg(null)} className="p-2 hover:bg-slate-100 rounded-full transition">
<X className="w-5 h-5 text-slate-400" />
</button>
</div>
{/* Option A β€” Direct (account dΓ©jΓ  sur Meta) */}
<div className="border border-indigo-100 bg-indigo-50/40 rounded-2xl p-5 mb-4">
<p className="text-xs font-bold uppercase tracking-wider text-indigo-500 mb-3">{t('clients.wa_setup_modal.already_on_meta')}</p>
<div className="space-y-3">
<div>
<label className="block text-xs font-bold text-slate-500 mb-1">{t('clients.wa_setup_modal.waba_id_label')} <span className="text-red-400">*</span></label>
<input
type="text"
placeholder={t('clients.wa_setup_modal.waba_id_placeholder')}
value={directSetup.wabaId}
onChange={e => 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"
/>
<p className="text-[10px] text-slate-400 mt-1">
<a href="https://business.facebook.com/settings/whatsapp-business-accounts" target="_blank" rel="noreferrer" className="text-indigo-500 underline">{t('clients.wa_setup_modal.waba_id_hint_link')}</a>
</p>
</div>
<div>
<label className="block text-xs font-bold text-slate-500 mb-1">{t('clients.wa_setup_modal.business_id_label')} <span className="text-slate-300">{t('clients.wa_setup_modal.business_id_optional')}</span></label>
<input
type="text"
placeholder={t('clients.wa_setup_modal.business_id_placeholder')}
value={directSetup.metaBusinessId}
onChange={e => 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"
/>
<p className="text-[10px] text-slate-400 mt-1">
<a href="https://business.facebook.com/settings/info" target="_blank" rel="noreferrer" className="text-indigo-500 underline">{t('clients.wa_setup_modal.business_id_hint_link')}</a>
</p>
</div>
<div>
<label className="block text-xs font-bold text-slate-500 mb-1">{t('clients.wa_setup_modal.token_label')} <span className="text-slate-300">{t('clients.wa_setup_modal.token_optional')}</span></label>
<input
type="password"
placeholder={t('clients.wa_setup_modal.token_placeholder')}
value={directSetup.accessToken}
onChange={e => 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"
/>
<p className="text-[10px] text-slate-400 mt-1">
<a href="https://business.facebook.com/settings/system-users" target="_blank" rel="noreferrer" className="text-indigo-500 underline">{t('clients.wa_setup_modal.token_hint_link')}</a>
</p>
</div>
{directSetup.phoneNumberId && (
<p className="text-xs text-slate-400">{t('clients.wa_setup_modal.phone_number_detected')} <span className="font-mono text-slate-600">{directSetup.phoneNumberId}</span></p>
)}
</div>
<button
onClick={handleDirectSetupSubmit}
disabled={!directSetup.wabaId || isDirectSetupSaving}
className="mt-4 w-full bg-indigo-600 hover:bg-indigo-700 disabled:opacity-40 text-white py-3 rounded-xl font-bold text-sm transition flex items-center justify-center gap-2"
>
{isDirectSetupSaving ? <Loader2 className="w-4 h-4 animate-spin" /> : <CheckCircle2 className="w-4 h-4" />}
{t('clients.wa_setup_modal.save_config')}
</button>
</div>
{/* Option B β€” Embedded Signup (nouveaux clients) */}
<div className="border border-slate-100 rounded-2xl p-5">
<p className="text-xs font-bold uppercase tracking-wider text-slate-400 mb-3">{t('clients.wa_setup_modal.new_account_heading')}</p>
<p className="text-xs text-slate-500 mb-3">{t('clients.wa_setup_modal.new_account_desc')}</p>
<button
onClick={() => { setSetupOrg(null); handleEmbeddedSignup(setupOrg.id); }}
className="w-full border border-slate-200 hover:bg-slate-50 text-slate-700 py-3 rounded-xl font-semibold text-sm transition flex items-center justify-center gap-2"
>
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="#1877F2"><path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/></svg>
{t('clients.wa_setup_modal.connect_facebook')}
</button>
</div>
</div>
</div>
)}
{/* Meta Compliance Footer */}
<div className="mt-12 p-6 bg-slate-50 rounded-2xl border border-slate-100">
<h4 className="text-xs font-bold text-slate-400 uppercase tracking-widest mb-4">{t('clients.tier.meta_compliance')}</h4>
<div className="flex gap-8 opacity-60 grayscale hover:grayscale-0 transition duration-500">
<img src="https://upload.wikimedia.org/wikipedia/commons/b/be/Facebook_Messenger_logo_2020.svg" className="h-6" alt="Meta" />
<img src="https://upload.wikimedia.org/wikipedia/commons/6/6b/WhatsApp.svg" className="h-6" alt="WhatsApp" />
</div>
</div>
</div>
);
}
// ─── 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<string, string> = {
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 <span className="text-slate-400 text-xs animate-pulse">{t('clients.waba.checking')}</span>;
if (!ms.configured) return <span className="text-slate-400 font-medium text-xs">{t('clients.waba.not_connected')}</span>;
const map: Record<string, { labelKey: string; cls: string }> = {
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 <span className={`font-semibold text-sm ${s.cls}`}>{t(s.labelKey)}</span>;
}
function BusinessBadge({ ms }: { ms?: MetaStatus }) {
const { t } = useTranslation();
if (!ms || ms.loading) return <span className="text-slate-400 text-xs animate-pulse">{t('clients.waba.checking')}</span>;
if (!ms.configured || ms.businessId === undefined) return <span className="text-slate-400 font-medium text-xs">β€”</span>;
return ms.businessVerified
? <span className="font-semibold text-sm text-emerald-600">{t('clients.waba.verified')}</span>
: <span className="font-semibold text-sm text-amber-600">{t('clients.waba.not_verified')}</span>;
}
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 (
<div className="flex items-center gap-3">
<div className={`p-2 rounded-lg ${isLoading ? 'bg-slate-50' : isApproved ? 'bg-emerald-50' : 'bg-amber-50'}`}>
{isLoading
? <ShieldCheck className="w-4 h-4 text-slate-300 animate-pulse" />
: isApproved
? <CheckCircle2 className="w-4 h-4 text-emerald-500" />
: <AlertTriangle className="w-4 h-4 text-amber-500" />}
</div>
<div className="min-w-0">
<div className="flex items-center gap-1.5">
<p className="text-[10px] uppercase font-bold text-slate-400 tracking-wider">{t('clients.cells.waba_header')}</p>
<button onClick={onRefresh} title={t('clients.waba.refresh_tooltip')} className="text-slate-300 hover:text-slate-500 transition">
<RefreshCw className="w-3 h-3" />
</button>
</div>
<div className="flex items-center gap-2 flex-wrap">
<WabaStatusBadge ms={ms} />
{ms && !isLoading && !isApproved && ms.configured && (
<button onClick={onGuide} className="text-[10px] text-indigo-600 hover:underline font-bold whitespace-nowrap">
{t('clients.waba.what_to_do')}
</button>
)}
</div>
{ms?.error && <p className="text-[10px] text-red-400 truncate">{ms.error}</p>}
</div>
</div>
);
}
function BusinessVerificationCell({ ms }: { ms?: MetaStatus }) {
const { t } = useTranslation();
const isLoading = !ms || ms.loading;
return (
<div className="flex items-center gap-3">
<div className={`p-2 rounded-lg ${isLoading ? 'bg-slate-50' : ms?.businessVerified ? 'bg-emerald-50' : ms?.businessId !== undefined ? 'bg-amber-50' : 'bg-slate-50'}`}>
{isLoading
? <ShieldCheck className="w-4 h-4 text-slate-300 animate-pulse" />
: ms?.businessVerified
? <CheckCircle2 className="w-4 h-4 text-emerald-500" />
: ms?.businessId !== undefined
? <XCircle className="w-4 h-4 text-amber-500" />
: <ShieldCheck className="w-4 h-4 text-slate-300" />}
</div>
<div>
<p className="text-[10px] uppercase font-bold text-slate-400 tracking-wider">{t('clients.cells.business_header')}</p>
<div className="flex items-center gap-2">
<BusinessBadge ms={ms} />
{ms && !isLoading && ms.configured && ms.businessId !== undefined && !ms.businessVerified && (
<a
href="https://business.facebook.com/settings/security"
target="_blank" rel="noreferrer"
className="text-[10px] text-indigo-600 hover:underline font-bold whitespace-nowrap"
>
{t('clients.waba.verify_link')}
</a>
)}
</div>
</div>
</div>
);
}
function QualityDot({ rating }: { rating?: string }) {
const { t } = useTranslation();
const map: Record<string, string> = {
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 <span className={`inline-block w-2 h-2 rounded-full ${cls}`} title={`${t('clients.quality.dot_title')}${rating ?? 'β€”'}`} />;
}
const TIER_CONFIG: Record<string, { textCls: string; bgCls: string; iconCls: string; dotCls: string; order: number }> = {
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 (
<>
<div className="flex items-center gap-1.5">
<span className={`font-semibold ${tierCfg?.textCls ?? 'text-slate-800'}`}>{dailyLimitLabel(ms, t)}</span>
{ms?.qualityRating && <QualityDot rating={ms.qualityRating} />}
{ms?.configured && (
<button onClick={() => setShowInfo(true)} title={t('clients.cells.daily_limit_info_tooltip')} className="text-slate-300 hover:text-indigo-500 transition">
<Info className="w-3.5 h-3.5" />
</button>
)}
</div>
<TierInfoModal ms={ms} open={showInfo} onClose={() => 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 (
<AnimatePresence>
{open && (
<motion.div
className="fixed inset-0 z-[200] flex items-end sm:items-center justify-center sm:p-4"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
onClick={onClose}
>
{/* Backdrop */}
<div className="absolute inset-0 bg-slate-900/50 backdrop-blur-sm" />
{/* Panel β€” bottom sheet on mobile, centered card on desktop */}
<motion.div
className="relative bg-white w-full sm:max-w-md sm:rounded-2xl rounded-t-3xl shadow-2xl flex flex-col"
style={{ maxHeight: '85dvh' }}
initial={{ y: '100%' }}
animate={{ y: 0 }}
exit={{ y: '100%' }}
transition={{ type: 'spring', damping: 32, stiffness: 320, mass: 0.8 }}
onClick={e => e.stopPropagation()}
>
{/* Drag handle β€” mobile only */}
<div className="flex justify-center pt-3 pb-0 sm:hidden shrink-0">
<div className="w-9 h-1 bg-slate-200 rounded-full" />
</div>
{/* Sticky header */}
<div className="flex items-center justify-between px-6 pt-4 pb-3 sm:pt-6 shrink-0">
<h3 className="font-bold text-slate-900 text-base">{t('clients.tier_modal.title')}</h3>
<button onClick={onClose} className="p-1.5 hover:bg-slate-100 rounded-lg transition -mr-1">
<X className="w-4 h-4 text-slate-500" />
</button>
</div>
{/* Scrollable body */}
<div className="overflow-y-auto overscroll-contain px-6 pb-8 space-y-5">
{/* Tier ladder */}
<div className="space-y-1.5">
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-wider mb-2">{t('clients.tier_modal.levels_heading')}</p>
{TIERS_IN_ORDER.map(({ key, labelKey, descKey }, i) => {
const isCurrent = key === ms?.messagingLimitTier;
const isPast = i < currentTierOrder;
const cfg = TIER_CONFIG[key];
return (
<div key={key} className={`flex items-start gap-3 p-3 rounded-xl transition-all ${
isCurrent ? 'ring-2 ring-indigo-200 bg-indigo-50' : isPast ? 'opacity-30' : 'opacity-50'
}`}>
<span className={`w-2 h-2 rounded-full mt-1.5 shrink-0 ${isCurrent ? (cfg?.dotCls ?? 'bg-indigo-400') : 'bg-slate-300'}`} />
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 flex-wrap">
<span className={`text-sm font-semibold ${isCurrent ? cfg?.textCls : 'text-slate-500'}`}>{t(labelKey)}</span>
{isCurrent && <span className="text-[10px] font-bold bg-indigo-100 text-indigo-600 px-2 py-0.5 rounded-full">{t('clients.tier_modal.current_badge')}</span>}
</div>
<p className="text-[11px] text-slate-400 mt-0.5 leading-snug">{t(descKey)}</p>
</div>
</div>
);
})}
</div>
{/* Quality rating */}
{ms?.qualityRating && ms.qualityRating !== 'UNKNOWN' && (
<div className="space-y-2">
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-wider">{t('clients.tier_modal.quality_heading')}</p>
<div className="flex gap-2">
{(['GREEN', 'YELLOW', 'RED'] as const).map(r => {
const isCurrent = ms.qualityRating === r;
const q = QUALITY_INFO[r];
return (
<div key={r} className={`flex-1 p-3 rounded-xl text-center transition-all ${
isCurrent ? `ring-2 ring-offset-1 ${q.ringCls}` : 'bg-slate-50 opacity-30'
}`}>
<span className={`inline-block w-3 h-3 rounded-full ${q.dotCls} mb-1.5`} />
<p className="text-xs font-bold text-slate-700">{t(q.labelKey)}</p>
<p className="text-[10px] text-slate-500 mt-0.5 leading-snug">{t(q.descKey)}</p>
</div>
);
})}
</div>
</div>
)}
<p className="text-[11px] text-slate-400 text-center leading-relaxed">
{t('clients.tier_modal.meta_auto_update')}<br />
{t('clients.tier_modal.levels_auto')}
</p>
</div>
</motion.div>
</motion.div>
)}
</AnimatePresence>
);
}
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 (
<>
<div className="flex items-center gap-3">
<div className={`p-2 rounded-lg ${tierCfg?.bgCls ?? 'bg-indigo-50'}`}>
<MessageSquare className={`w-4 h-4 ${tierCfg?.iconCls ?? 'text-indigo-500'}`} />
</div>
<div>
<p className="text-[10px] uppercase font-bold text-slate-400 tracking-wider">{t('clients.cells.daily_limit_header')}</p>
<div className="flex items-center gap-1.5">
{isLoading
? <span className="text-slate-400 text-xs animate-pulse">…</span>
: <p className={`text-sm font-semibold ${tierCfg?.textCls ?? 'text-slate-700'}`}>{dailyLimitLabel(ms, t)}</p>
}
{!isLoading && ms?.qualityRating && <QualityDot rating={ms.qualityRating} />}
{!isLoading && ms?.configured && (
<button onClick={() => setShowInfo(true)} title={t('clients.cells.daily_limit_info_tooltip')} className="text-slate-300 hover:text-indigo-500 transition ml-0.5">
<Info className="w-3.5 h-3.5" />
</button>
)}
</div>
</div>
</div>
<TierInfoModal ms={ms} open={showInfo} onClose={() => 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 (
<div className="fixed inset-0 bg-slate-900/60 backdrop-blur-md flex items-center justify-center p-4 z-[60]">
<div className="bg-white rounded-[2.5rem] shadow-2xl w-full max-w-2xl p-10 animate-in fade-in zoom-in-95 duration-300">
<div className="flex items-center justify-between mb-8">
<div className="flex items-center gap-4">
<div className="w-12 h-12 bg-indigo-50 rounded-2xl flex items-center justify-center">
<Activity className="w-6 h-6 text-indigo-600" />
</div>
<div>
<h2 className="text-2xl font-black text-slate-900">{t('clients.personality_modal.title')}</h2>
<p className="text-slate-500 font-medium text-sm">{t('clients.personality_modal.subtitle', { orgName: org.name })}</p>
</div>
</div>
<button onClick={onClose} className="p-3 hover:bg-slate-100 rounded-full transition">
<X className="w-6 h-6 text-slate-400" />
</button>
</div>
<form onSubmit={handleSubmit} className="space-y-8">
<div className="grid gap-6">
<div>
<label className="block text-sm font-bold text-slate-700 mb-2.5">{t('clients.personality_modal.bot_name_label')}</label>
<input
type="text"
placeholder={t('clients.personality_modal.bot_name_placeholder')}
value={config.botName}
onChange={e => 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"
/>
</div>
<div>
<label className="block text-sm font-bold text-slate-700 mb-2.5">{t('clients.personality_modal.mission_label')}</label>
<textarea
placeholder={t('clients.personality_modal.mission_placeholder')}
value={config.coreMission}
onChange={e => setConfig({...config, coreMission: 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 min-h-[120px] font-medium leading-relaxed"
/>
</div>
<div>
<label className="block text-sm font-bold text-slate-700 mb-2.5">{t('clients.personality_modal.tone_label')}</label>
<textarea
placeholder={t('clients.personality_modal.tone_placeholder')}
value={config.toneDescription}
onChange={e => setConfig({...config, toneDescription: 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 min-h-[120px] font-medium leading-relaxed"
/>
</div>
</div>
<div className="flex gap-4 pt-4">
<button
type="button"
onClick={onClose}
className="flex-1 px-6 py-4 rounded-2xl font-bold text-slate-600 hover:bg-slate-50 transition"
>
{t('clients.personality_modal.cancel')}
</button>
<button
type="submit"
disabled={isSaving}
className="flex-[2] bg-slate-900 text-white py-4 rounded-2xl font-bold hover:bg-slate-800 transition disabled:opacity-50 flex items-center justify-center gap-3 shadow-xl shadow-slate-200"
>
{isSaving ? <Loader2 className="w-5 h-5 animate-spin" /> : <ShieldCheck className="w-5 h-5" />}
{t('clients.personality_modal.submit')}
</button>
</div>
</form>
</div>
</div>
);
}