edtech / apps /admin /src /pages /ContactsPage.tsx
CognxSafeTrack
feat(i18n): complete admin app internationalization across all pages
d80fec4
import { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { Users, Upload, Search, Download, Trash2, Filter, Loader2, FileSpreadsheet, CheckCircle2, Sparkles, BrainCircuit, Send, Copy, RefreshCw } from 'lucide-react';
import { api } from '../lib/api';
import { useAuth } from '../lib/auth';
import { useTenant } from '../lib/tenant';
import { TemplateSelector } from '../components/whatsapp/TemplateSelector';
import { useToast } from '../hooks/useToast';
import { logError, logWarn } from '../lib/logger';
interface Contact {
id: string;
phoneNumber: string;
name?: string;
attributes?: any;
tags?: string[];
createdAt: string;
}
export default function ContactsPage() {
const { t } = useTranslation();
const toast = useToast();
const { token } = useAuth();
const { selectedOrgId } = useTenant();
const [contacts, setContacts] = useState<Contact[]>([]);
const [loading, setLoading] = useState(true);
const [searchQuery, setSearchQuery] = useState('');
const [showImportModal, setShowImportModal] = useState(false);
const [uploading, setUploading] = useState(false);
const [uploadingFileName, setUploadingFileName] = useState<string>('');
const [importStats, setImportStats] = useState<any>(null);
// AI Campaign State
const [selectedContactForAI, setSelectedContactForAI] = useState<Contact | null>(null);
const [generatingAI, setGeneratingAI] = useState(false);
const [aiResult, setAiResult] = useState<{ personalizedMessage: string; reasoning: string; aiSource: string } | null>(null);
const [campaignObjective, setCampaignObjective] = useState("Proposer nos nouveaux services de formation IA");
const [selectedTemplateName, setSelectedTemplateName] = useState('');
// Bulk Selection & Generation State
const [selectedContactIds, setSelectedContactIds] = useState<string[]>([]);
const [showBulkModal, setShowBulkModal] = useState(false);
const [bulkProgress, setBulkProgress] = useState({ current: 0, total: 0, status: 'idle' });
const [bulkResults, setBulkResults] = useState<any[]>([]);
const [activeCount, setActiveCount] = useState<number | null>(null);
const [showFilters, setShowFilters] = useState(false);
// Tags state
const [availableTags, setAvailableTags] = useState<string[]>([]);
const [activeTagFilter, setActiveTagFilter] = useState<string[]>([]);
const [tagInputs, setTagInputs] = useState<Record<string, string>>({});
const [savingTags, setSavingTags] = useState<Record<string, boolean>>({});
const fetchAvailableTags = async () => {
if (!token || !selectedOrgId) return;
try {
const data = await api.get(`/v1/organizations/${selectedOrgId}/contacts/tags`, token);
setAvailableTags(data.tags ?? []);
} catch { /* non-blocking */ }
};
const fetchContacts = async (tagFilter?: string[]) => {
if (!token || !selectedOrgId) return;
setLoading(true);
try {
const tags = tagFilter ?? activeTagFilter;
const qs = tags.length > 0 ? `?tags=${tags.join(',')}` : '';
const data = await api.get(`/v1/organizations/${selectedOrgId}/contacts${qs}`, token);
setContacts(data);
} catch (error) {
logError("Failed to fetch contacts:", error);
} finally {
setLoading(false);
}
};
const toggleTagFilter = (tag: string) => {
const newFilter = activeTagFilter.includes(tag)
? activeTagFilter.filter(t => t !== tag)
: [...activeTagFilter, tag];
setActiveTagFilter(newFilter);
fetchContacts(newFilter);
};
const handleSaveContactTags = async (contactId: string, newTags: string[]) => {
if (!token || !selectedOrgId) return;
setSavingTags(prev => ({ ...prev, [contactId]: true }));
try {
await api.patch(`/v1/organizations/${selectedOrgId}/contacts/${contactId}/tags`, { tags: newTags }, token);
setContacts(prev => prev.map(c => c.id === contactId ? { ...c, tags: newTags } : c));
fetchAvailableTags();
} catch (err: any) {
toast.error(err?.message ?? t('contacts.tags_update_error'));
} finally {
setSavingTags(prev => ({ ...prev, [contactId]: false }));
}
};
const handleAddTag = (contactId: string, currentTags: string[]) => {
const input = (tagInputs[contactId] ?? '').trim().toLowerCase().replace(/\s+/g, '-');
if (!input || currentTags.includes(input)) return;
handleSaveContactTags(contactId, [...currentTags, input]);
setTagInputs(prev => ({ ...prev, [contactId]: '' }));
};
const handleRemoveTag = (contactId: string, tag: string, currentTags: string[]) => {
handleSaveContactTags(contactId, currentTags.filter(t => t !== tag));
};
useEffect(() => {
let cancelled = false;
const loadAll = async () => {
if (!token || !selectedOrgId) return;
setLoading(true);
try {
const tags = activeTagFilter;
const qs = tags.length > 0 ? `?tags=${tags.join(',')}` : '';
const [contactsData, tagsData] = await Promise.all([
api.get(`/v1/organizations/${selectedOrgId}/contacts${qs}`, token),
api.get(`/v1/organizations/${selectedOrgId}/contacts/tags`, token).catch(() => ({ tags: [] })),
]);
if (!cancelled) {
setContacts(contactsData);
setAvailableTags(tagsData.tags ?? []);
}
} catch (error) {
if (!cancelled) logError("Failed to fetch contacts:", error);
} finally {
if (!cancelled) setLoading(false);
}
try {
const data = await api.get('/v1/analytics/usage', token, selectedOrgId);
if (!cancelled) setActiveCount(data?.users?.activeLast24h ?? 0);
} catch (err) {
if (!cancelled) { logWarn('[Contacts] fetchActiveCount failed', err); setActiveCount(0); }
}
};
loadAll();
return () => { cancelled = true; };
}, [token, selectedOrgId]);
const handleFileUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file || !token || !selectedOrgId) return;
setUploading(true);
setUploadingFileName(file.name);
const formData = new FormData();
formData.append('file', file);
try {
const data = await api.upload(`/v1/organizations/${selectedOrgId}/contacts/import`, formData, token, selectedOrgId);
const results = data.results;
setImportStats(results);
fetchContacts();
if (results) {
toast.success(t('contacts.import_success', { created: results.created ?? 0, updated: results.updated ?? 0, errors: results.errors ?? 0 }));
}
} catch (error) {
logError("Import failed:", error);
toast.error(t('contacts.upload_critical_error'));
} finally {
setUploading(false);
setUploadingFileName('');
e.target.value = '';
}
};
const handleGenerateAI = async (contact: Contact) => {
if (!token || !selectedOrgId) return;
setSelectedContactForAI(contact);
setGeneratingAI(true);
setAiResult(null);
try {
const data = await api.post('/v1/ai/crm/generate-campaign', {
contact,
objective: campaignObjective
}, token, selectedOrgId);
setAiResult(data);
} catch (error) {
logError("AI Generation failed:", error);
toast.error(t('contacts.ai_generation_error'));
} finally {
setGeneratingAI(false);
}
};
const handleBulkGenerate = async () => {
if (!token || !selectedOrgId || selectedContactIds.length === 0) return;
setShowBulkModal(true);
setBulkProgress({ current: 0, total: selectedContactIds.length, status: 'processing' });
setBulkResults([]);
const results = [];
const selectedContacts = contacts.filter(c => selectedContactIds.includes(c.id));
for (let i = 0; i < selectedContacts.length; i++) {
const contact = selectedContacts[i];
setBulkProgress(prev => ({ ...prev, current: i + 1 }));
try {
const data = await api.post('/v1/ai/crm/generate-campaign', { contact, objective: campaignObjective }, token, selectedOrgId);
results.push({ contact, ...data });
} catch (err) {
results.push({ contact, error: true, personalizedMessage: t('contacts.generation_error_fallback') });
}
}
setBulkResults(results);
setBulkProgress(prev => ({ ...prev, status: 'completed' }));
};
const toggleSelectAll = () => {
if (selectedContactIds.length === filteredContacts.length) {
setSelectedContactIds([]);
} else {
setSelectedContactIds(filteredContacts.map(c => c.id));
}
};
const toggleSelectContact = (id: string) => {
setSelectedContactIds(prev =>
prev.includes(id) ? prev.filter(i => i !== id) : [...prev, id]
);
};
const handleDeleteContact = async (id: string) => {
if (!token || !selectedOrgId) return;
if (!confirm(t('contacts.confirm_delete_one'))) return;
try {
const res = await api.delete(`/v1/organizations/${selectedOrgId}/contacts/${id}`, token);
if (res.ok || res.success || res) { // api.delete might return true/ok depending on implementation
setContacts(prev => prev.filter(c => c.id !== id));
setSelectedContactIds(prev => prev.filter(i => i !== id));
}
} catch (error) {
logError("Delete failed:", error);
toast.error(t('contacts.delete_error'));
}
};
const handleBulkDelete = async () => {
if (!token || !selectedOrgId || selectedContactIds.length === 0) return;
if (!confirm(t('contacts.confirm_delete_many', { count: selectedContactIds.length }))) return;
try {
await api.post(`/v1/organizations/${selectedOrgId}/contacts/bulk-delete`, { contactIds: selectedContactIds }, token, selectedOrgId);
setContacts(prev => prev.filter(c => !selectedContactIds.includes(c.id)));
setSelectedContactIds([]);
toast.success(t('contacts.bulk_delete_success'));
} catch (error) {
logError("Bulk delete failed:", error);
toast.error(t('contacts.bulk_delete_error'));
}
};
const handleExportCsv = () => {
if (filteredContacts.length === 0) return;
const headers = [t('contacts.csv_name'), t('contacts.csv_phone'), t('contacts.csv_created')];
const rows = filteredContacts.map(c => [
c.name ?? '',
c.phoneNumber,
new Date(c.createdAt).toLocaleDateString('fr-FR')
]);
const csv = [headers, ...rows].map(r => r.map(v => `"${v}"`).join(',')).join('\n');
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `contacts-${new Date().toISOString().slice(0, 10)}.csv`;
a.click();
URL.revokeObjectURL(url);
};
const filteredContacts = contacts.filter(c => {
const searchLower = searchQuery.toLowerCase();
const inName = c.name?.toLowerCase().includes(searchLower);
const inPhone = c.phoneNumber.includes(searchQuery);
// Search in dynamic attributes
const inAttributes = c.attributes && Object.values(c.attributes).some(val =>
String(val).toLowerCase().includes(searchLower)
);
return inName || inPhone || inAttributes;
});
return (
<div className="p-8 max-w-7xl mx-auto">
{/* Header Area */}
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6 mb-10">
<div>
<h1 className="text-4xl font-black text-slate-900 tracking-tight flex items-center gap-3">
<Users className="w-10 h-10 text-blue-600" />
{t('contacts.title')}
</h1>
<p className="text-slate-500 mt-2 font-medium">{t('contacts.subtitle')}</p>
</div>
<div className="flex items-center gap-3">
<button
onClick={() => setShowImportModal(true)}
className="flex items-center gap-2 bg-blue-600 text-white px-6 py-3.5 rounded-2xl font-bold hover:bg-blue-700 transition shadow-xl shadow-blue-100 hover:scale-[1.02] active:scale-[0.98]"
>
<Upload className="w-5 h-5" /> Import Excel/CSV
</button>
<button
onClick={handleExportCsv}
title="Exporter en CSV"
className="p-3.5 bg-white border border-slate-200 text-slate-600 rounded-2xl hover:bg-slate-50 transition shadow-sm"
>
<Download className="w-5 h-5" />
</button>
</div>
</div>
{/* Stats Bar */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
<div className="bg-white p-6 rounded-[2rem] border border-slate-100 shadow-sm flex items-center gap-4">
<div className="w-12 h-12 bg-blue-50 rounded-2xl flex items-center justify-center">
<Users className="w-6 h-6 text-blue-600" />
</div>
<div>
<p className="text-xs font-bold text-slate-400 uppercase tracking-widest">Total Contacts</p>
<p className="text-2xl font-black text-slate-900">{contacts.length}</p>
</div>
</div>
<div className="bg-white p-6 rounded-[2rem] border border-slate-100 shadow-sm flex items-center gap-4">
<div className="w-12 h-12 bg-emerald-50 rounded-2xl flex items-center justify-center">
<CheckCircle2 className="w-6 h-6 text-emerald-600" />
</div>
<div>
<p className="text-xs font-bold text-slate-400 uppercase tracking-widest">Actifs (24h)</p>
<p className="text-2xl font-black text-slate-900">{activeCount ?? '—'}</p>
</div>
</div>
<div className="bg-white p-6 rounded-[2rem] border border-slate-100 shadow-sm flex items-center gap-4">
<div className="w-12 h-12 bg-indigo-50 rounded-2xl flex items-center justify-center">
<Filter className="w-6 h-6 text-indigo-600" />
</div>
<div>
<p className="text-xs font-bold text-slate-400 uppercase tracking-widest">Segments</p>
<p className="text-2xl font-black text-slate-900">{searchQuery ? 1 : contacts.length > 0 ? 1 : 0}</p>
</div>
</div>
</div>
{/* Filters & Table */}
<div className="bg-white rounded-[2.5rem] border border-slate-100 shadow-sm overflow-hidden">
<div className="p-6 border-b border-slate-50 flex flex-col gap-4">
<div className="flex flex-col md:flex-row md:items-center gap-4">
<div className="relative flex-1">
<Search className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-slate-400" />
<input
type="text"
placeholder={t('contacts.search_placeholder')}
value={searchQuery}
onChange={e => setSearchQuery(e.target.value)}
className="w-full bg-slate-50 border-none rounded-2xl pl-12 pr-4 py-4 focus:ring-4 focus:ring-blue-50 transition font-medium"
/>
</div>
<div className="flex items-center gap-2">
<button
onClick={() => setShowFilters(v => !v)}
className={`flex items-center gap-2 px-5 py-4 rounded-2xl font-bold transition ${showFilters || activeTagFilter.length > 0 ? 'bg-blue-50 text-blue-600' : 'text-slate-600 hover:bg-slate-50'}`}
>
<Filter className="w-4 h-4" /> Filtres {activeTagFilter.length > 0 && <span className="bg-blue-600 text-white text-[10px] rounded-full px-1.5 py-0.5">{activeTagFilter.length}</span>}
</button>
</div>
</div>
{/* Tag filter chips */}
{(showFilters || activeTagFilter.length > 0) && availableTags.length > 0 && (
<div className="flex flex-wrap gap-2 pt-1">
<span className="text-xs font-bold text-slate-400 uppercase tracking-wider self-center">Tags :</span>
{availableTags.map(tag => (
<button
key={tag}
onClick={() => toggleTagFilter(tag)}
className={`px-3 py-1 rounded-full text-xs font-bold transition-all ${
activeTagFilter.includes(tag)
? 'bg-blue-600 text-white shadow-sm'
: 'bg-slate-100 text-slate-600 hover:bg-slate-200'
}`}
>
#{tag}
</button>
))}
{activeTagFilter.length > 0 && (
<button
onClick={() => { setActiveTagFilter([]); fetchContacts([]); }}
className="px-3 py-1 rounded-full text-xs font-bold text-red-500 hover:bg-red-50 transition-all"
>
× Effacer
</button>
)}
</div>
)}
{showFilters && availableTags.length === 0 && (
<p className="text-xs text-slate-400 italic">Aucun tag créé. Ajoutez des tags sur vos contacts pour les segmenter.</p>
)}
</div>
<div className="overflow-x-auto">
<table className="w-full text-left">
<thead>
<tr className="bg-slate-50/50">
<th className="px-8 py-5 w-10">
<input
type="checkbox"
className="w-5 h-5 rounded border-slate-300 text-blue-600 focus:ring-blue-500"
checked={selectedContactIds.length > 0 && selectedContactIds.length === filteredContacts.length}
onChange={toggleSelectAll}
/>
</th>
<th className="px-8 py-5 text-xs font-bold text-slate-400 uppercase tracking-widest">Client</th>
<th className="px-8 py-5 text-xs font-bold text-slate-400 uppercase tracking-widest">Téléphone</th>
<th className="px-8 py-5 text-xs font-bold text-slate-400 uppercase tracking-widest">Tags</th>
<th className="px-8 py-5 text-xs font-bold text-slate-400 uppercase tracking-widest">Attributs</th>
<th className="px-8 py-5 text-xs font-bold text-slate-400 uppercase tracking-widest text-right">Actions</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-50">
{loading ? (
<tr>
<td colSpan={6} className="px-8 py-20 text-center">
<Loader2 className="w-10 h-10 animate-spin text-blue-600 mx-auto mb-4" />
<p className="font-bold text-slate-400">Chargement de la base de données...</p>
</td>
</tr>
) : filteredContacts.length === 0 ? (
<tr>
<td colSpan={6} className="px-8 py-20 text-center">
<div className="w-20 h-20 bg-slate-50 rounded-[2rem] flex items-center justify-center mx-auto mb-6">
<Users className="w-10 h-10 text-slate-200" />
</div>
<h3 className="text-xl font-bold text-slate-900 mb-2">{t('contacts.no_contacts')}</h3>
<p className="text-slate-500 max-w-sm mx-auto">{t('contacts.subtitle')}</p>
</td>
</tr>
) : filteredContacts.map(contact => (
<tr key={contact.id} className={`hover:bg-slate-50/50 transition ${selectedContactIds.includes(contact.id) ? 'bg-blue-50/30' : ''}`}>
<td className="px-8 py-6">
<input
type="checkbox"
className="w-5 h-5 rounded border-slate-300 text-blue-600 focus:ring-blue-500"
checked={selectedContactIds.includes(contact.id)}
onChange={() => toggleSelectContact(contact.id)}
/>
</td>
<td className="px-8 py-6">
<div className="flex items-center gap-4">
<div className="w-10 h-10 bg-slate-100 rounded-xl flex items-center justify-center font-bold text-slate-600 uppercase">
{contact.name?.[0] || '?'}
</div>
<div>
<p className="font-bold text-slate-900">{contact.name || 'Inconnu'}</p>
<p className="text-xs text-slate-400">Importé le {new Date(contact.createdAt).toLocaleDateString()}</p>
</div>
</div>
</td>
<td className="px-8 py-6 font-mono text-sm font-bold text-slate-600">
+{contact.phoneNumber}
</td>
{/* Tags column */}
<td className="px-8 py-6">
<div className="flex flex-wrap gap-1.5 max-w-[200px]">
{(contact.tags ?? []).map(tag => (
<span
key={tag}
className="group flex items-center gap-1 px-2 py-0.5 bg-indigo-50 text-indigo-700 rounded-full text-[10px] font-bold"
>
#{tag}
<button
onClick={() => handleRemoveTag(contact.id, tag, contact.tags ?? [])}
className="opacity-0 group-hover:opacity-100 text-indigo-400 hover:text-red-500 transition-opacity leading-none"
</button>
</span>
))}
<div className="flex items-center gap-1">
<input
type="text"
value={tagInputs[contact.id] ?? ''}
onChange={e => setTagInputs(prev => ({ ...prev, [contact.id]: e.target.value }))}
onKeyDown={e => { if (e.key === 'Enter') { e.preventDefault(); handleAddTag(contact.id, contact.tags ?? []); }}}
placeholder="+ tag"
className="w-16 text-[10px] px-1.5 py-0.5 border border-dashed border-slate-300 rounded-full outline-none focus:border-indigo-400 bg-transparent text-slate-500"
/>
{savingTags[contact.id] && <Loader2 className="w-3 h-3 animate-spin text-indigo-400" />}
</div>
</div>
</td>
<td className="px-8 py-6">
<div className="flex flex-wrap gap-2">
{contact.attributes && Object.entries(contact.attributes).slice(0, 3).map(([key, val]) => (
<span key={key} className="text-[10px] px-2 py-1 bg-slate-100 text-slate-500 rounded-lg font-bold uppercase tracking-tight">
{key}: {String(val)}
</span>
))}
{Object.keys(contact.attributes || {}).length > 3 && (
<span className="text-[10px] px-2 py-1 bg-blue-50 text-blue-600 rounded-lg font-bold">
+{Object.keys(contact.attributes).length - 3} plus
</span>
)}
</div>
</td>
<td className="px-8 py-6 text-right">
<div className="flex items-center justify-end gap-2">
<button
onClick={() => handleGenerateAI(contact)}
className="flex items-center gap-2 bg-indigo-50 text-indigo-600 px-4 py-2 rounded-xl font-bold hover:bg-indigo-100 transition"
>
<Sparkles className="w-4 h-4" /> IA
</button>
<button
onClick={() => handleDeleteContact(contact.id)}
className="p-2 text-slate-400 hover:text-red-500 transition"
>
<Trash2 className="w-5 h-5" />
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
{/* Import Modal */}
{showImportModal && (
<div className="fixed inset-0 bg-slate-900/60 backdrop-blur-md flex items-center justify-center p-6 z-50">
<div className="bg-white rounded-[2.5rem] shadow-2xl w-full max-w-xl p-10 animate-in zoom-in-95 duration-300">
<div className="flex items-center justify-between mb-8">
<h2 className="text-3xl font-black text-slate-900">Importation Excel</h2>
<button onClick={() => { setShowImportModal(false); setImportStats(null); }} className="p-2 hover:bg-slate-100 rounded-full transition">
<Trash2 className="w-6 h-6 text-slate-300" />
</button>
</div>
{!importStats ? (
<div className="space-y-8">
<p className="text-slate-500 font-medium leading-relaxed">
Veuillez uploader un fichier <span className="font-bold text-slate-900">.xlsx</span> ou <span className="font-bold text-slate-900">.csv</span>.
L'IA détectera automatiquement les colonnes de noms et de numéros.
</p>
<label className="relative block group cursor-pointer">
<div className="w-full border-4 border-dashed border-slate-100 rounded-[2rem] p-12 flex flex-col items-center justify-center group-hover:border-blue-200 transition bg-slate-50/50">
{uploading ? (
<div className="flex flex-col items-center gap-3">
<Loader2 className="w-12 h-12 animate-spin text-blue-600" />
<p className="text-sm font-semibold text-slate-700">Import en cours…</p>
{uploadingFileName && <p className="text-xs text-slate-400 truncate max-w-[200px]">{uploadingFileName}</p>}
</div>
) : (
<>
<FileSpreadsheet className="w-16 h-16 text-blue-500 mb-4 group-hover:scale-110 transition" />
<p className="text-lg font-bold text-slate-900">Cliquez pour choisir un fichier</p>
<p className="text-slate-400 text-sm">ou glissez-déposez ici</p>
</>
)}
</div>
<input
type="file"
accept=".xlsx, .xls, .csv"
onChange={handleFileUpload}
disabled={uploading}
className="absolute inset-0 opacity-0 cursor-pointer"
/>
</label>
<div className="p-6 bg-amber-50 rounded-2xl border border-amber-100">
<p className="text-xs font-bold text-amber-700 uppercase mb-2 flex items-center gap-2">
💡 Conseil d'importation
</p>
<p className="text-sm text-amber-800 leading-relaxed font-medium">
Assurez-vous que vos numéros incluent l'indicatif pays (ex: 221...) pour une meilleure délivrabilité.
</p>
</div>
</div>
) : (
<div className="text-center py-10 space-y-6">
<div className="w-24 h-24 bg-emerald-100 rounded-[2.5rem] flex items-center justify-center mx-auto shadow-xl shadow-emerald-50">
<CheckCircle2 className="w-12 h-12 text-emerald-600" />
</div>
<div>
<h3 className="text-2xl font-black text-slate-900 mb-2">Importation Terminée !</h3>
<p className="text-slate-500 font-medium">L'IA a traité votre fichier avec succès.</p>
</div>
<div className="grid grid-cols-2 gap-4 max-w-xs mx-auto">
<div className="p-4 bg-slate-50 rounded-2xl">
<p className="text-2xl font-black text-slate-900">{importStats.created}</p>
<p className="text-[10px] font-bold text-slate-400 uppercase">Succès</p>
</div>
<div className="p-4 bg-red-50 rounded-2xl">
<p className="text-2xl font-black text-red-600">{importStats.errors}</p>
<p className="text-[10px] font-bold text-red-400 uppercase">Échecs</p>
</div>
</div>
<button
onClick={() => { setShowImportModal(false); setImportStats(null); }}
className="w-full bg-slate-900 text-white py-4 rounded-2xl font-bold hover:bg-slate-800 transition"
>
Fermer et voir les contacts
</button>
</div>
)}
</div>
</div>
)}
{/* AI Generation Modal */}
{selectedContactForAI && (
<div className="fixed inset-0 bg-slate-900/60 backdrop-blur-md flex items-center justify-center p-6 z-50">
<div className="bg-white rounded-[2.5rem] shadow-2xl w-full max-w-2xl overflow-hidden animate-in fade-in zoom-in duration-300">
{/* Modal Header */}
<div className="bg-indigo-600 p-8 text-white flex items-center justify-between">
<div className="flex items-center gap-4">
<div className="w-12 h-12 bg-white/20 rounded-2xl flex items-center justify-center">
<Sparkles className="w-6 h-6" />
</div>
<div>
<h3 className="text-xl font-black">AI Campaign Studio</h3>
<p className="text-indigo-100 text-sm font-medium">Génération pour {selectedContactForAI.name || 'Inconnu'}</p>
</div>
</div>
<button onClick={() => setSelectedContactForAI(null)} className="p-2 hover:bg-white/10 rounded-full transition">
<Trash2 className="w-6 h-6" />
</button>
</div>
<div className="p-8">
{generatingAI ? (
<div className="py-20 text-center">
<Loader2 className="w-16 h-16 animate-spin text-indigo-600 mx-auto mb-6" />
<h4 className="text-xl font-bold text-slate-900 mb-2">L'IA analyse le contact...</h4>
<p className="text-slate-500 font-medium">Personnalisation du message en cours.</p>
</div>
) : aiResult ? (
<div className="space-y-6">
{/* Objective Field */}
<div>
<label className="text-xs font-bold text-slate-400 uppercase mb-2 block tracking-widest">Objectif de la campagne</label>
<div className="flex gap-2">
<input
className="flex-1 bg-slate-50 border-none rounded-xl p-3 font-medium text-slate-700 focus:ring-2 focus:ring-indigo-100"
value={campaignObjective}
onChange={(e) => setCampaignObjective(e.target.value)}
/>
<button
onClick={() => handleGenerateAI(selectedContactForAI)}
className="p-3 bg-indigo-50 text-indigo-600 rounded-xl hover:bg-indigo-100 transition"
title="Régénérer"
>
<RefreshCw className="w-5 h-5" />
</button>
</div>
</div>
{/* Reasoning */}
<div className="p-5 bg-amber-50 rounded-[1.5rem] border border-amber-100 flex gap-4">
<div className="w-10 h-10 bg-amber-100 rounded-xl flex items-center justify-center flex-shrink-0">
<BrainCircuit className="w-5 h-5 text-amber-600" />
</div>
<div>
<p className="text-xs font-bold text-amber-700 uppercase mb-1">Raisonnement de l'IA ({aiResult.aiSource})</p>
<p className="text-sm text-amber-900 font-medium leading-relaxed">{aiResult.reasoning}</p>
</div>
</div>
{/* Template Selection (Meta Compliance) */}
<div className="bg-slate-50 border border-slate-100 rounded-[1.5rem] p-5">
<TemplateSelector
value={selectedTemplateName}
onChange={setSelectedTemplateName}
/>
</div>
{/* Generated Message */}
<div className="relative group">
<label className="text-xs font-bold text-slate-400 uppercase mb-2 block tracking-widest">Message Personnalisé (WhatsApp)</label>
<textarea
rows={6}
className="w-full bg-slate-50 border-none rounded-[1.5rem] p-6 font-medium text-slate-800 leading-relaxed focus:ring-4 focus:ring-indigo-50 transition"
value={aiResult.personalizedMessage}
onChange={(e) => setAiResult({...aiResult, personalizedMessage: e.target.value})}
/>
<button
onClick={() => { navigator.clipboard.writeText(aiResult.personalizedMessage); toast.success(t('contacts.message_copied')); }}
title="Copier le message"
className="absolute top-10 right-4 p-2 bg-white text-slate-400 rounded-lg hover:text-indigo-600 shadow-sm border border-slate-100 opacity-0 group-hover:opacity-100 transition"
>
<Copy className="w-4 h-4" />
</button>
</div>
<div className="flex gap-3">
<button
onClick={() => {
const url = `https://wa.me/${selectedContactForAI.phoneNumber}?text=${encodeURIComponent(aiResult.personalizedMessage)}`;
window.open(url, '_blank');
}}
className="flex-1 flex items-center justify-center gap-3 bg-indigo-600 text-white py-4 rounded-[1.5rem] font-bold hover:bg-indigo-700 transition shadow-xl shadow-indigo-100"
>
<Send className="w-5 h-5" />
{selectedTemplateName ? `Envoyer via Template: ${selectedTemplateName}` : 'Envoyer via WhatsApp'}
</button>
<button
onClick={() => setSelectedContactForAI(null)}
className="px-8 bg-slate-100 text-slate-600 rounded-[1.5rem] font-bold hover:bg-slate-200 transition"
>
Annuler
</button>
</div>
</div>
) : (
<div className="text-center py-10">
<button
onClick={() => handleGenerateAI(selectedContactForAI)}
className="bg-indigo-600 text-white px-8 py-4 rounded-2xl font-bold hover:bg-indigo-700 transition"
>
Lancer la génération
</button>
</div>
)}
</div>
</div>
</div>
)}
{/* Bulk Action Bar */}
{selectedContactIds.length > 0 && (
<div className="fixed bottom-10 left-1/2 -translate-x-1/2 bg-slate-900 text-white px-8 py-5 rounded-[2rem] shadow-2xl flex items-center gap-8 z-40 animate-in slide-in-from-bottom-10 duration-500">
<div className="flex items-center gap-3 pr-8 border-r border-slate-700">
<div className="w-10 h-10 bg-blue-600 rounded-xl flex items-center justify-center font-black text-sm">
{selectedContactIds.length}
</div>
<p className="font-bold text-sm text-slate-300">Contacts sélectionnés</p>
</div>
<div className="flex items-center gap-4">
<div className="w-64">
<TemplateSelector
value={selectedTemplateName}
onChange={setSelectedTemplateName}
/>
</div>
<button
className="flex items-center gap-2 bg-indigo-600 hover:bg-indigo-700 text-white px-6 py-3 rounded-xl font-bold transition shadow-lg shadow-indigo-900/20"
onClick={handleBulkGenerate}
>
<Sparkles className="w-5 h-5" /> Générer Campagne IA
</button>
<button
onClick={handleBulkDelete}
className="flex items-center gap-2 bg-slate-800 hover:bg-red-900/40 text-slate-300 hover:text-red-400 px-6 py-3 rounded-xl font-bold transition"
>
<Trash2 className="w-5 h-5" /> Supprimer
</button>
<button
onClick={() => setSelectedContactIds([])}
className="text-slate-500 hover:text-white font-bold text-sm transition ml-4"
>
Annuler
</button>
</div>
</div>
)}
{/* Bulk Generation Modal */}
{showBulkModal && (
<div className="fixed inset-0 bg-slate-900/80 backdrop-blur-xl flex items-center justify-center p-6 z-50">
<div className="bg-white rounded-[3rem] shadow-2xl w-full max-w-4xl max-h-[85vh] flex flex-col overflow-hidden animate-in zoom-in-95 duration-300">
{/* Header */}
<div className="p-8 border-b border-slate-100 flex items-center justify-between bg-slate-50/50">
<div className="flex items-center gap-4">
<div className="w-12 h-12 bg-indigo-600 rounded-2xl flex items-center justify-center text-white shadow-lg shadow-indigo-200">
<BrainCircuit className="w-6 h-6" />
</div>
<div>
<h3 className="text-2xl font-black text-slate-900">Génération de Campagne</h3>
<p className="text-slate-500 font-medium">{bulkProgress.current} / {bulkProgress.total} contacts traités</p>
</div>
</div>
{bulkProgress.status === 'completed' && (
<button
onClick={() => setShowBulkModal(false)}
className="p-3 hover:bg-slate-200 rounded-full transition text-slate-400 hover:text-slate-900"
>
<Trash2 className="w-6 h-6" />
</button>
)}
</div>
{/* Progress Bar */}
<div className="h-2 bg-slate-100 w-full overflow-hidden">
<div
className="h-full bg-indigo-600 transition-all duration-500 shadow-[0_0_20px_rgba(79,70,229,0.5)]"
style={{ width: `${(bulkProgress.current / bulkProgress.total) * 100}%` }}
/>
</div>
{/* Results Content */}
<div className="flex-1 overflow-y-auto p-8 space-y-4">
{bulkResults.length === 0 && bulkProgress.status === 'processing' && (
<div className="py-20 text-center space-y-6">
<Loader2 className="w-16 h-16 animate-spin text-indigo-600 mx-auto" />
<p className="text-xl font-bold text-slate-900 italic">"L'intelligence artificielle rédige vos messages personnalisés..."</p>
</div>
)}
{bulkResults.map((result, idx) => (
<div key={idx} className="p-6 bg-slate-50 rounded-3xl border border-slate-100 flex items-start gap-6 hover:bg-white hover:shadow-xl hover:shadow-slate-100 transition duration-300 group">
<div className="w-12 h-12 bg-white rounded-2xl flex items-center justify-center font-bold text-slate-400 border border-slate-100 group-hover:border-indigo-200 group-hover:text-indigo-600 transition">
{idx + 1}
</div>
<div className="flex-1">
<div className="flex items-center justify-between mb-3">
<h4 className="font-bold text-slate-900">{result.contact.name || 'Inconnu'}</h4>
<span className="text-xs font-mono text-slate-400">+{result.contact.phoneNumber}</span>
</div>
<p className="text-sm text-slate-600 leading-relaxed bg-white p-4 rounded-2xl border border-slate-50">
{result.personalizedMessage}
</p>
</div>
<div className="flex flex-col gap-2">
<button
onClick={() => {
const url = `https://wa.me/${result.contact.phoneNumber}?text=${encodeURIComponent(result.personalizedMessage)}`;
window.open(url, '_blank');
}}
className="p-3 bg-emerald-50 text-emerald-600 rounded-xl hover:bg-emerald-600 hover:text-white transition shadow-sm"
title="Envoyer WhatsApp"
>
<Send className="w-5 h-5" />
</button>
<button
onClick={() => {
navigator.clipboard.writeText(result.personalizedMessage);
toast.success(t('contacts.copied'));
}}
className="p-3 bg-slate-100 text-slate-400 rounded-xl hover:bg-slate-200 hover:text-slate-900 transition"
title="Copier"
>
<Copy className="w-5 h-5" />
</button>
</div>
</div>
))}
</div>
{/* Footer */}
{bulkProgress.status === 'completed' && (
<div className="p-8 border-t border-slate-100 bg-slate-50/50 flex items-center justify-between">
<div className="flex items-center gap-2 text-emerald-600 font-bold">
<CheckCircle2 className="w-5 h-5" />
{t('contacts.generation_completed')}
</div>
<div className="flex items-center gap-3">
<button
onClick={() => setShowBulkModal(false)}
className="px-8 py-4 bg-slate-900 text-white rounded-2xl font-bold hover:bg-slate-800 transition shadow-xl"
>
Terminer
</button>
</div>
</div>
)}
</div>
</div>
)}
</div>
);
}