dashboard / src /App.tsx
jimmytousergo's picture
Sync from GitHub via hub-sync
2638a9a verified
Raw
History Blame Contribute Delete
56.9 kB
import { useState, useEffect } from "react";
import {
Phone,
Building2,
Mail,
ExternalLink,
Search,
Copy,
Check,
Loader2,
AlertCircle,
CheckCircle,
MapPin,
ChevronDown,
ChevronUp,
Tag
} from "lucide-react";
// ═══════════════════════════════════════════════════════════════
// TYPES & INTERFACES
// ═══════════════════════════════════════════════════════════════
interface OdooPartner {
id: number;
name: string;
phone: string | false;
mobile: string | false;
email: string | false;
parent_id: [number, string] | false;
city: string | false;
street: string | false;
zip: string | false;
type: string;
company_name?: string | false;
commercial_company_name?: string | false;
}
interface PrestaCustomer {
id: string;
firstname: string;
lastname: string;
email: string;
company?: string;
phone?: string;
phones?: string[];
group_name?: string;
}
interface CustomerProfile {
firstname: string;
lastname: string;
company: string;
phone: string;
email: string;
prestaId?: string | number;
odooId?: string | number;
street?: string;
zip?: string;
city?: string;
groupName?: string;
addresses?: { street: string; zip: string; city: string; isLatest?: boolean }[];
companies?: string[];
}
// ═══════════════════════════════════════════════════════════════
// FORMATAGE DES PRÉNOMS & NOMS
// ═══════════════════════════════════════════════════════════════
function formatFirstname(name: string): string {
if (!name) return "";
const trimmed = name.trim();
const capitalizeWord = (w: string) => {
if (!w) return "";
return w.charAt(0).toUpperCase() + w.slice(1).toLowerCase();
};
return trimmed
.split("-")
.map(part => {
return part
.split(" ")
.map(capitalizeWord)
.join(" ");
})
.join("-");
}
function formatLastname(name: string): string {
if (!name) return "";
return name.trim().toUpperCase();
}
// ═══════════════════════════════════════════════════════════════
// FALLBACK / SÉCURITÉ : JIMMY COCQUEREL & MODE DÉMO
// ═══════════════════════════════════════════════════════════════
const FALLBACK_JIMMY: CustomerProfile = {
firstname: "Jimmy",
lastname: "COCQUEREL",
company: "TousErgo",
phone: "07 80 98 19 98",
email: "jcocquerel@tousergo.com",
prestaId: "2201",
odooId: "42",
street: "1C QUAI MARION",
zip: "59158",
city: "Mortagne-du-Nord",
groupName: "Personnels, 20%",
addresses: [
{ street: "1C QUAI MARION", zip: "59158", city: "Mortagne-du-Nord", isLatest: true },
{ street: "12 Rue de la République", zip: "69003", city: "Lyon" },
{ street: "59 Boulevard de la Liberté", zip: "59000", city: "Lille" },
{ street: "22 Avenue des Champs-Élysées", zip: "75008", city: "Paris" },
{ street: "8 Rue de l'Énergie", zip: "59650", city: "Villeneuve-d'Ascq" }
],
companies: [
"TousErgo",
"ErgoSourcing SAS",
"Mobilité & Autonomie",
"Haut-de-France Santé",
"Cocquerel Holding"
]
};
const FALLBACK_DEFAULT: CustomerProfile = {
firstname: "Sophie",
lastname: "LEFEBVRE",
company: "Ergomed SAS",
phone: "06 12 34 56 78",
email: "sophie.lefebvre@example.fr",
prestaId: "8834",
odooId: "125",
street: "45 Avenue Jean Jaurès",
zip: "69007",
city: "Lyon",
groupName: "Pro à échéance - 0%",
addresses: [
{ street: "45 Avenue Jean Jaurès", zip: "69007", city: "Lyon", isLatest: true },
{ street: "18 Rue Saint-Maur", zip: "75011", city: "Paris" }
],
companies: [
"Ergomed SAS",
"Santé Services",
"Medical Pro"
]
};
function formatPhoneFrench(phone: string, contextPhone?: string) {
if (!phone) return "";
const cleaned = phone.trim();
const digits = cleaned.replace(/[^\d]/g, "");
const hasPlus = cleaned.startsWith("+");
let cc = "";
let isBelgian = false;
let isFrench = false;
let isSwiss = false;
let isLux = false;
if (hasPlus) {
if (cleaned.startsWith("+33")) { cc = "33"; isFrench = true; }
else if (cleaned.startsWith("+32")) { cc = "32"; isBelgian = true; }
else if (cleaned.startsWith("+41")) { cc = "41"; isSwiss = true; }
else if (cleaned.startsWith("+352")) { cc = "352"; isLux = true; }
} else if (cleaned.startsWith("00")) {
if (cleaned.startsWith("0033")) { cc = "33"; isFrench = true; }
else if (cleaned.startsWith("0032")) { cc = "32"; isBelgian = true; }
else if (cleaned.startsWith("0041")) { cc = "41"; isSwiss = true; }
else if (cleaned.startsWith("00352")) { cc = "352"; isLux = true; }
}
if (!cc && contextPhone) {
const ctxClean = contextPhone.trim();
if (ctxClean.startsWith("+33") || ctxClean.startsWith("0033")) { cc = "33"; isFrench = true; }
else if (ctxClean.startsWith("+32") || ctxClean.startsWith("0032")) { cc = "32"; isBelgian = true; }
else if (ctxClean.startsWith("+41") || ctxClean.startsWith("0041")) { cc = "41"; isSwiss = true; }
else if (ctxClean.startsWith("+352") || ctxClean.startsWith("00352")) { cc = "352"; isLux = true; }
}
if (!cc) {
if (digits.length === 10) {
if (digits.startsWith("045") || digits.startsWith("046") || digits.startsWith("047") || digits.startsWith("048") || digits.startsWith("049")) {
cc = "32";
isBelgian = true;
} else {
cc = "33";
isFrench = true;
}
} else if (digits.length === 9 && digits.startsWith("0")) {
cc = "32";
isBelgian = true;
} else {
cc = "33";
isFrench = true;
}
}
let national = digits;
if (hasPlus) {
national = digits.slice(cc.length);
} else if (cleaned.startsWith("00")) {
national = digits.slice(2 + cc.length);
} else if (digits.startsWith(cc) && digits.length > cc.length + 4) {
national = digits.slice(cc.length);
}
if (national.startsWith("0")) {
national = national.slice(1);
}
if (isFrench) {
const padded = national.padEnd(9, " ");
const p1 = padded.slice(0, 1);
const p2 = padded.slice(1, 3);
const p3 = padded.slice(3, 5);
const p4 = padded.slice(5, 7);
const p5 = padded.slice(7, 9);
return `+33 ${p1} ${p2} ${p3} ${p4} ${p5}`.trim();
}
if (isBelgian) {
if (national.length === 9 || national.startsWith("45") || national.startsWith("46") || national.startsWith("47") || national.startsWith("48") || national.startsWith("49")) {
const padded = national.padEnd(9, " ");
const p1 = padded.slice(0, 3);
const p2 = padded.slice(3, 5);
const p3 = padded.slice(5, 7);
const p4 = padded.slice(7, 9);
return `+32 ${p1} ${p2} ${p3} ${p4}`.trim();
} else {
const startsWithTwo = national.startsWith("2") || national.startsWith("3") || national.startsWith("4") || national.startsWith("9");
if (startsWithTwo && national.length === 8) {
const padded = national.padEnd(8, " ");
const p1 = padded.slice(0, 1);
const p2 = padded.slice(1, 4);
const p3 = padded.slice(4, 6);
const p4 = padded.slice(6, 8);
return `+32 ${p1} ${p2} ${p3} ${p4}`.trim();
} else {
const padded = national.padEnd(8, " ");
const p1 = padded.slice(0, 2);
const p2 = padded.slice(2, 4);
const p3 = padded.slice(4, 6);
const p4 = padded.slice(6, 8);
return `+32 ${p1} ${p2} ${p3} ${p4}`.trim();
}
}
}
if (isSwiss) {
const padded = national.padEnd(9, " ");
const p1 = padded.slice(0, 2);
const p2 = padded.slice(2, 5);
const p3 = padded.slice(5, 7);
const p4 = padded.slice(7, 9);
return `+41 ${p1} ${p2} ${p3} ${p4}`.trim();
}
if (isLux) {
if (national.length >= 6) {
const padded = national.padEnd(9, " ");
const p1 = padded.slice(0, 3);
const p2 = padded.slice(3, 6);
const p3 = padded.slice(6, 9).trim();
const space3 = p3 ? " " + p3 : "";
return `+352 ${p1} ${p2}${space3}`.trim();
}
}
return phone;
}
export default function App() {
const [searchQuery, setSearchQuery] = useState("");
const [loading, setLoading] = useState(false);
const [activeProfile, setActiveProfile] = useState<CustomerProfile | null>(null);
const [showOtherAddresses, setShowOtherAddresses] = useState(false);
const [showOtherCompanies, setShowOtherCompanies] = useState(false);
const [odooResults, setOdooResults] = useState<OdooPartner[]>([]);
const [prestaResult, setPrestaResult] = useState<PrestaCustomer | null>(null);
const [prestaResults, setPrestaResults] = useState<PrestaCustomer[]>([]);
const [error, setError] = useState<string | null>(null);
const [isDemoData, setIsDemoData] = useState(false);
const [copiedField, setCopiedField] = useState<string | null>(null);
// Exécuter la recherche globale
const handleSearch = async (phoneToSearch: string) => {
if (!phoneToSearch) return;
const cleanPhone = phoneToSearch.trim().replace(/[\s\-\.()\/]/g, "");
if (!cleanPhone) return;
setLoading(true);
setError(null);
setIsDemoData(false);
setShowOtherAddresses(false);
setShowOtherCompanies(false);
try {
const res = await fetch(`/api/search?phone=${encodeURIComponent(cleanPhone)}`);
if (!res.ok) {
throw new Error(`Erreur serveur (${res.status})`);
}
const data = await res.json();
const odooList: OdooPartner[] = data.odoo || [];
const psMatch: PrestaCustomer | null = data.ps || null;
const psList: PrestaCustomer[] = data.psList || (psMatch ? [psMatch] : []);
setOdooResults(odooList);
setPrestaResult(psMatch);
setPrestaResults(psList);
if (odooList.length > 0 || psMatch || psList.length > 0) {
const activePs = psMatch || psList[0] || null;
if (activePs && !prestaResult) {
setPrestaResult(activePs);
}
const activeEmail = activePs?.email || odooList[0]?.email || "";
const matchingOdoo = odooList.find(partner => {
const pEmail = partner.email ? String(partner.email).trim().toLowerCase() : "";
const actEmail = activeEmail.trim().toLowerCase();
return pEmail && actEmail && pEmail === actEmail;
}) || odooList[0];
const firstname = formatFirstname(activePs?.firstname || matchingOdoo?.name?.split(" ")[0] || "");
const lastname = formatLastname(activePs?.lastname || matchingOdoo?.name?.split(" ").slice(1).join(" ") || "Client Odoo");
const addressesList: { street: string; zip: string; city: string; isLatest?: boolean }[] = [];
const seenAddr = new Set<string>();
if (data.latestOdooOrderAddress) {
const lat = data.latestOdooOrderAddress;
const s = lat.street ? String(lat.street).trim() : "";
const z = lat.zip ? String(lat.zip).trim() : "";
const c = lat.city ? String(lat.city).trim() : "";
if (s || c) {
const key = `${s.toLowerCase()}|${z.toLowerCase()}|${c.toLowerCase()}`;
seenAddr.add(key);
addressesList.push({
street: s,
zip: z,
city: c,
isLatest: true
});
}
}
odooList.forEach((partner) => {
const partnerEmail = partner.email ? String(partner.email).trim().toLowerCase() : "";
const actEmail = activeEmail.trim().toLowerCase();
if (partnerEmail && actEmail && partnerEmail !== actEmail) return;
const s = partner.street ? String(partner.street).trim() : "";
const z = partner.zip ? String(partner.zip).trim() : "";
const c = partner.city ? String(partner.city).trim() : "";
if (s || c) {
const key = `${s.toLowerCase()}|${z.toLowerCase()}|${c.toLowerCase()}`;
if (!seenAddr.has(key)) {
seenAddr.add(key);
addressesList.push({
street: s,
zip: z,
city: c,
isLatest: addressesList.length === 0
});
}
}
});
const companiesList: string[] = [];
const seenComp = new Set<string>();
if (data.latestOdooOrderAddress?.company) {
const cName = data.latestOdooOrderAddress.company.trim();
if (cName && !seenComp.has(cName.toLowerCase())) {
seenComp.add(cName.toLowerCase());
companiesList.push(cName);
}
}
if (activePs?.company && activePs.email?.trim().toLowerCase() === activeEmail.trim().toLowerCase()) {
const cName = activePs.company.trim();
if (cName && !seenComp.has(cName.toLowerCase())) {
seenComp.add(cName.toLowerCase());
companiesList.push(cName);
}
}
odooList.forEach((partner) => {
const partnerEmail = partner.email ? String(partner.email).trim().toLowerCase() : "";
const actEmail = activeEmail.trim().toLowerCase();
if (partnerEmail && actEmail && partnerEmail !== actEmail) return;
if (partner.parent_id && partner.parent_id[1]) {
const cName = partner.parent_id[1].trim();
if (cName && !seenComp.has(cName.toLowerCase())) {
seenComp.add(cName.toLowerCase());
companiesList.push(cName);
}
}
});
const mainStreet = matchingOdoo?.street ? String(matchingOdoo.street) : "";
const mainZip = matchingOdoo?.zip ? String(matchingOdoo.zip) : "";
const mainCity = matchingOdoo?.city ? String(matchingOdoo.city) : "";
if (addressesList.length === 0 && (mainStreet || mainCity)) {
addressesList.push({
street: mainStreet,
zip: mainZip,
city: mainCity,
isLatest: true
});
}
const phone = formatPhoneFrench(activePs?.phone || matchingOdoo?.mobile || matchingOdoo?.phone || phoneToSearch, phoneToSearch);
const email = activeEmail;
const groupName = activePs?.group_name || "Client Standard";
setActiveProfile({
firstname,
lastname,
company: companiesList[0] || "Particulier",
phone,
email,
prestaId: activePs?.id,
odooId: matchingOdoo?.id,
street: addressesList[0]?.street || mainStreet || "",
zip: addressesList[0]?.zip || mainZip || "",
city: addressesList[0]?.city || mainCity || "",
groupName,
addresses: addressesList,
companies: companiesList
});
} else {
if (cleanPhone.includes("0780981998") || cleanPhone.includes("780981998")) {
setActiveProfile(FALLBACK_JIMMY);
setIsDemoData(true);
setPrestaResults([
{ id: "2201", firstname: "Jimmy", lastname: "COCQUEREL", email: "jcocquerel@tousergo.com", company: "TousErgo", phones: ["+33780981998"] },
{ id: "3045", firstname: "Jimmy (Pro)", lastname: "COCQUEREL", email: "jimmy.pro@tousergo.com", company: "ErgoSourcing SAS", phones: ["+32466887672"] }
]);
setPrestaResult({ id: "2201", firstname: "Jimmy", lastname: "COCQUEREL", email: "jcocquerel@tousergo.com", company: "TousErgo", phones: ["+33780981998"] });
} else {
setActiveProfile(null);
setPrestaResults([]);
setPrestaResult(null);
}
}
} catch (err: any) {
console.error("Erreur de recherche:", err);
setError("Impossible de contacter le serveur de recherche en temps réel.");
if (cleanPhone.includes("0780981998") || cleanPhone.includes("780981998")) {
setActiveProfile(FALLBACK_JIMMY);
setIsDemoData(true);
setPrestaResults([
{ id: "2201", firstname: "Jimmy", lastname: "COCQUEREL", email: "jcocquerel@tousergo.com", company: "TousErgo", phones: ["+33780981998"] },
{ id: "3045", firstname: "Jimmy (Pro)", lastname: "COCQUEREL", email: "jimmy.pro@tousergo.com", company: "ErgoSourcing SAS", phones: ["+32466887672"] }
]);
setPrestaResult({ id: "2201", firstname: "Jimmy", lastname: "COCQUEREL", email: "jcocquerel@tousergo.com", company: "TousErgo", phones: ["+33780981998"] });
} else if (cleanPhone.includes("0612345678") || cleanPhone === "0612345678") {
setActiveProfile(FALLBACK_DEFAULT);
setIsDemoData(true);
setPrestaResults([
{ id: "8834", firstname: "Sophie", lastname: "LEFEBVRE", email: "sophie.lefebvre@example.fr", company: "Ergomed SAS", phones: ["0612345678"] }
]);
setPrestaResult({ id: "8834", firstname: "Sophie", lastname: "LEFEBVRE", email: "sophie.lefebvre@example.fr", company: "Ergomed SAS", phones: ["0612345678"] });
} else {
setActiveProfile({
firstname: formatFirstname("Client"),
lastname: formatLastname("Inconnu"),
company: "Non renseigné",
phone: formatPhoneFrench(phoneToSearch, phoneToSearch),
email: "Aucun e-mail trouvé",
});
setIsDemoData(true);
setPrestaResults([]);
setPrestaResult(null);
}
} finally {
setLoading(false);
}
};
useEffect(() => {
const params = new URLSearchParams(window.location.search);
const phoneParam = params.get("phone");
if (phoneParam) {
const clean = phoneParam.trim();
setSearchQuery(clean);
handleSearch(clean);
}
// Pas de paramètre "phone" dans l'URL -> aucune recherche automatique,
// l'agent doit saisir un numéro manuellement.
}, []);
const onSubmitSearch = (e: React.FormEvent) => {
e.preventDefault();
if (searchQuery.trim()) {
const newUrl = `${window.location.pathname}?phone=${encodeURIComponent(searchQuery.trim())}`;
window.history.replaceState({}, "", newUrl);
handleSearch(searchQuery.trim());
}
};
const fetchLatestOrderDetails = async (odooId?: string | number, email?: string) => {
try {
let queryParam = "";
if (odooId) {
queryParam = `odooId=${encodeURIComponent(odooId)}`;
} else if (email) {
queryParam = `email=${encodeURIComponent(email)}`;
} else {
return;
}
const res = await fetch(`/api/search?${queryParam}`);
if (res.ok) {
const data = await res.json();
if (data.latestOdooOrderAddress) {
const lat = data.latestOdooOrderAddress;
const s = lat.street ? String(lat.street).trim() : "";
const z = lat.zip ? String(lat.zip).trim() : "";
const c = lat.city ? String(lat.city).trim() : "";
setActiveProfile(prev => {
if (!prev) return null;
const updatedAddresses = prev.addresses ? [...prev.addresses] : [];
const keyToFind = `${s.toLowerCase()}|${z.toLowerCase()}|${c.toLowerCase()}`;
const filtered = updatedAddresses.filter(addr => {
const k = `${(addr.street || "").toLowerCase()}|${(addr.zip || "").toLowerCase()}|${(addr.city || "").toLowerCase()}`;
return k !== keyToFind;
});
if (s || c) {
filtered.unshift({
street: s,
zip: z,
city: c,
isLatest: true
});
}
const updatedCompanies = prev.companies ? [...prev.companies] : [];
if (lat.company) {
const cName = lat.company.trim();
const filteredComp = updatedCompanies.filter(comp => comp.toLowerCase() !== cName.toLowerCase());
filteredComp.unshift(cName);
return {
...prev,
addresses: filtered,
companies: filteredComp
};
}
return {
...prev,
addresses: filtered
};
});
}
}
} catch (err) {
console.error("Error fetching latest order details:", err);
}
};
const handleSelectPrestaCustomer = (ps: PrestaCustomer) => {
setPrestaResult(ps);
const activeEmail = ps.email || "";
const formattedFirst = formatFirstname(ps.firstname);
const formattedLast = formatLastname(ps.lastname);
const phone = formatPhoneFrench(ps.phone || (ps.phones && ps.phones[0]) || searchQuery, searchQuery);
const matchingOdoo = odooResults.find(partner => {
const pEmail = partner.email ? String(partner.email).trim().toLowerCase() : "";
const actEmail = activeEmail.trim().toLowerCase();
return pEmail && actEmail && pEmail === actEmail;
}) || odooResults[0];
const addressesList: { street: string; zip: string; city: string; isLatest?: boolean }[] = [];
const seenAddr = new Set<string>();
odooResults.forEach((partner) => {
const partnerEmail = partner.email ? String(partner.email).trim().toLowerCase() : "";
if (partnerEmail && activeEmail && partnerEmail !== activeEmail.trim().toLowerCase()) return;
const s = partner.street ? String(partner.street).trim() : "";
const z = partner.zip ? String(partner.zip).trim() : "";
const c = partner.city ? String(partner.city).trim() : "";
if (s || c) {
const key = `${s.toLowerCase()}|${z.toLowerCase()}|${c.toLowerCase()}`;
if (!seenAddr.has(key)) {
seenAddr.add(key);
addressesList.push({
street: s,
zip: z,
city: c,
isLatest: addressesList.length === 0
});
}
}
});
const companiesList: string[] = [];
const seenComp = new Set<string>();
if (ps.company) {
const cName = ps.company.trim();
if (cName && !seenComp.has(cName.toLowerCase())) {
seenComp.add(cName.toLowerCase());
companiesList.push(cName);
}
}
odooResults.forEach((partner) => {
const partnerEmail = partner.email ? String(partner.email).trim().toLowerCase() : "";
if (partnerEmail && activeEmail && partnerEmail !== activeEmail.trim().toLowerCase()) return;
if (partner.parent_id && partner.parent_id[1]) {
const cName = partner.parent_id[1].trim();
if (cName && !seenComp.has(cName.toLowerCase())) {
seenComp.add(cName.toLowerCase());
companiesList.push(cName);
}
}
});
const mainStreet = matchingOdoo?.street ? String(matchingOdoo.street) : "";
const mainZip = matchingOdoo?.zip ? String(matchingOdoo.zip) : "";
const mainCity = matchingOdoo?.city ? String(matchingOdoo.city) : "";
if (addressesList.length === 0 && (mainStreet || mainCity)) {
addressesList.push({
street: mainStreet,
zip: mainZip,
city: mainCity,
isLatest: true
});
}
setActiveProfile({
firstname: formattedFirst,
lastname: formattedLast,
company: companiesList[0] || "Particulier",
phone,
email: activeEmail,
prestaId: ps.id,
odooId: matchingOdoo?.id,
street: addressesList[0]?.street || mainStreet || "",
zip: addressesList[0]?.zip || mainZip || "",
city: addressesList[0]?.city || mainCity || "",
groupName: ps.group_name || "Client Standard",
addresses: addressesList,
companies: companiesList
});
if (ps.email) {
fetchLatestOrderDetails(matchingOdoo?.id || undefined, ps.email);
}
};
const handleCopy = (text: string, fieldName: string) => {
if (!text) return;
navigator.clipboard.writeText(text);
setCopiedField(fieldName);
setTimeout(() => setCopiedField(null), 1500);
};
const getPrestashopUrl = (id?: string | number) => {
const custId = id || "2201";
return `https://www.tousergo.com/admin_ps_t_fr/index.php?controller=AdminCustomers&id_customer=${custId}&viewcustomer`;
};
const getOdooUrl = (id?: string | number) => {
const partnerId = id || "42";
return `https://tousergo.eggs-solutions.fr/web#cids=1&id=${partnerId}&model=res.partner&view_type=form&menu_id=`;
};
const companies = activeProfile?.companies || [];
const companiesToDisplay = companies.slice(0, 2);
const otherCompanies = companies.slice(2);
const addresses = activeProfile?.addresses || [];
const latestAddress = addresses[0] || (activeProfile ? {
street: activeProfile.street || "",
zip: activeProfile.zip || "",
city: activeProfile.city || ""
} : null);
const otherAddresses = addresses.slice(1);
return (
<div className="w-full min-h-screen bg-slate-50 flex flex-col font-sans text-slate-800 pb-12">
{/* ══════════ EN-TÊTE / BANDEAU BLEU PROFESSIONNEL ══════════ */}
<header className="h-14 bg-tousergo-navy border-b border-tousergo-rust/20 text-white flex items-center px-6 justify-between shadow-sm flex-shrink-0">
<div className="flex items-center gap-3">
<div className="bg-white p-1 rounded-lg flex items-center justify-center shadow-sm">
<img
src="https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRU6l9MDfTrCzuUVgdop5vjMkuYMvcwdm1enj1y3qfThsnK6jzy-5vUoOo&s=10"
alt="TOUS ERGO"
className="h-8 w-auto object-contain"
referrerPolicy="no-referrer"
/>
</div>
<div>
<h1 className="text-sm font-black tracking-wide uppercase text-white leading-none">
TOUS ERGO DASHBOARD
</h1>
<p className="text-[10px] text-tousergo-peach font-bold tracking-wider mt-0.5">
V.1 BÊTA TEST - by JCB
</p>
</div>
</div>
{/* Moteur de recherche dédié intégré au bandeau */}
<div className="flex-1 max-w-md mx-8">
<form onSubmit={onSubmitSearch} className="relative flex items-center">
<div className="absolute left-3.5 text-slate-400">
<Search className="w-4 h-4" />
</div>
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Rechercher par numéro de téléphone (ex: 0320819389)..."
className="w-full bg-slate-800 hover:bg-slate-750 focus:bg-white focus:text-slate-950 placeholder-slate-400 text-xs pl-10 pr-24 py-2 rounded-xl border border-slate-700 focus:border-tousergo-rust transition-all outline-none font-medium text-slate-200"
/>
<button
type="submit"
className="absolute right-1.5 bg-tousergo-rust hover:brightness-110 text-white font-bold text-[10px] px-3 py-1.5 rounded-lg transition-colors cursor-pointer shadow-sm uppercase tracking-wider"
>
Rechercher
</button>
</form>
</div>
{/* Lien de test 3CX */}
<div className="hidden md:flex items-center gap-2 text-xs">
<div className="px-2.5 py-1 bg-slate-800 border border-slate-700 rounded-lg text-slate-300 font-mono text-[10px] flex items-center gap-1.5">
<span className="w-1.5 h-1.5 rounded-full bg-tousergo-rust animate-pulse" />
<span>3CX Link: ?phone={searchQuery || "0780981998"}</span>
</div>
</div>
</header>
{/* ══════════ ZONE PRINCIPALE DE CONTEXTE ══════════ */}
<main className="flex-1 p-6 max-w-5xl mx-auto w-full flex flex-col gap-6">
{/* Indicateur de chargement */}
{loading && (
<div className="flex-1 flex flex-col items-center justify-center p-12 bg-white rounded-2xl border border-slate-200 shadow-sm gap-4">
<Loader2 className="w-10 h-10 text-tousergo-rust animate-spin" />
<div className="text-center">
<h3 className="text-sm font-extrabold text-slate-800">Recherche dans les bases de données...</h3>
<p className="text-xs text-slate-400 mt-1">Interrogation en temps réel de PrestaShop et Odoo</p>
</div>
</div>
)}
{/* Contenu de la fiche client */}
{!loading && (
<div className="flex-1 grid grid-cols-1 md:grid-cols-5 gap-6">
{/* COLONNE GAUCHE : LA FICHE CLIENT PRINCIPALE (3/5) */}
<div className="md:col-span-3 flex flex-col gap-5">
{/* SÉLECTEUR DE COMPTE PRESTASHOP MULTIPLE */}
{prestaResults.length > 1 && (
<div className="bg-tousergo-peach/15 border border-tousergo-peach rounded-2xl p-5 shadow-sm flex flex-col gap-3">
<div className="flex items-start gap-3">
<AlertCircle className="w-5 h-5 text-tousergo-rust mt-0.5 flex-shrink-0" />
<div>
<h4 className="text-xs font-black uppercase text-tousergo-navy tracking-wider">
Plusieurs comptes PrestaShop associés à ce numéro
</h4>
<p className="text-xs text-tousergo-navy/80 mt-0.5">
Veuillez sélectionner la fiche client PrestaShop active pour cet appel :
</p>
</div>
</div>
<div className="grid grid-cols-1 gap-2 mt-1">
{prestaResults.map((ps) => {
const isSelected = prestaResult?.id === ps.id;
return (
<button
key={ps.id}
type="button"
onClick={() => handleSelectPrestaCustomer(ps)}
className={`w-full text-left p-3 rounded-xl border transition-all flex items-center justify-between cursor-pointer ${
isSelected
? "bg-tousergo-rust border-tousergo-rust text-white shadow-sm ring-2 ring-tousergo-rust/10"
: "bg-white hover:bg-slate-50 border-slate-200 text-slate-800 hover:border-slate-300"
}`}
>
<div className="flex flex-col gap-0.5 min-w-0">
<div className="flex items-center gap-2 flex-wrap">
<span className={`text-xs font-black ${isSelected ? "text-white" : "text-slate-900"}`}>
{formatFirstname(ps.firstname)} {formatLastname(ps.lastname)}
</span>
<span className={`text-[10px] font-mono px-1.5 py-0.2 rounded ${
isSelected ? "bg-tousergo-navy text-tousergo-peach" : "bg-slate-100 text-slate-600"
}`}>
#{ps.id}
</span>
{ps.company && (
<span className={`text-[9px] font-bold px-1.5 py-0.2 rounded ${
isSelected ? "bg-tousergo-navy text-tousergo-pink" : "bg-emerald-50 text-emerald-800 border border-emerald-100"
}`}>
{ps.company}
</span>
)}
</div>
<span className={`text-[11px] truncate ${isSelected ? "text-tousergo-peach" : "text-slate-500"}`}>
{ps.email}
</span>
</div>
<div className="flex-shrink-0 ml-3">
{isSelected ? (
<CheckCircle className="w-5 h-5 text-white" />
) : (
<span className="text-[10px] font-extrabold uppercase tracking-wider text-tousergo-rust hover:brightness-110">
Choisir
</span>
)}
</div>
</button>
);
})}
</div>
</div>
)}
{activeProfile ? (
<div className="bg-white border border-slate-200 rounded-2xl shadow-sm overflow-hidden flex flex-col">
{/* Header de la carte client */}
<div className="p-6 bg-tousergo-navy text-white flex items-center justify-between">
<div className="flex items-center gap-4">
<div className="w-14 h-14 rounded-2xl bg-tousergo-rust text-white flex items-center justify-center font-black text-2xl shadow-md border border-tousergo-peach/20">
{((activeProfile.firstname?.[0] || "") + (activeProfile.lastname?.[0] || "")).toUpperCase()}
</div>
<div>
<div className="text-[10px] uppercase font-black tracking-widest text-tousergo-peach">
Fiche Client Active
</div>
<h2 className="text-xl font-extrabold tracking-tight mt-0.5">
{activeProfile.firstname} {activeProfile.lastname}
</h2>
</div>
</div>
{isDemoData && (
<span className="px-2.5 py-0.5 bg-amber-500/20 border border-amber-500/30 rounded text-[9px] font-black uppercase tracking-wider text-amber-300">
Mode Secours
</span>
)}
</div>
{/* Corps avec les informations principales */}
<div className="p-6 space-y-5 flex-1">
{/* ID Client Presta */}
<div className="pb-4 border-b border-slate-100">
<div className="p-3.5 bg-tousergo-peach/20 rounded-xl border border-tousergo-peach/60 flex items-center justify-between">
<div className="flex items-center gap-3 text-tousergo-rust">
<Tag className="w-4 h-4 text-tousergo-rust" />
<span className="text-[10px] uppercase font-black text-tousergo-navy tracking-wider">ID Client PrestaShop</span>
</div>
<div className="flex items-center gap-3">
<span className="text-sm font-black text-tousergo-navy font-mono">
{activeProfile.prestaId ? `#${activeProfile.prestaId}` : "Non synchronisé"}
</span>
{activeProfile.prestaId && (
<button
type="button"
onClick={() => handleCopy(String(activeProfile.prestaId), "prestaId")}
className="p-1 hover:bg-tousergo-peach/40 rounded text-tousergo-navy hover:text-tousergo-rust transition-colors cursor-pointer"
title="Copier l'ID PrestaShop"
>
{copiedField === "prestaId" ? <Check className="w-3.5 h-3.5 text-emerald-600" /> : <Copy className="w-3.5 h-3.5" />}
</button>
)}
</div>
</div>
</div>
{/* Société */}
<div className="flex flex-col gap-2 pb-4 border-b border-slate-100">
<div className="flex items-start gap-3">
<div className="mt-0.5 p-1.5 bg-tousergo-peach/40 text-tousergo-rust rounded-lg">
<Building2 className="w-4 h-4" />
</div>
<div className="flex-1">
<span className="text-[10px] uppercase font-black text-slate-400 tracking-wider">Sociétés / Entités</span>
<div className="mt-2 space-y-2">
{companiesToDisplay.length > 0 ? (
companiesToDisplay.map((comp, idx) => (
<div key={idx} className="flex items-center gap-2 flex-wrap">
<span className="text-sm font-bold text-slate-800 bg-slate-100 px-2.5 py-1 rounded-lg border border-slate-200 inline-block">
{comp}
</span>
{idx === 0 && (
<span className="text-[9px] font-extrabold uppercase bg-emerald-100 text-emerald-800 px-1.5 py-0.5 rounded border border-emerald-200">
Dernière active
</span>
)}
{idx === 1 && (
<span className="text-[9px] font-semibold bg-slate-100 text-slate-400 px-1.5 py-0.5 rounded border border-slate-200">
Secondaire
</span>
)}
</div>
))
) : (
<div className="text-sm font-bold text-slate-500 italic">Particulier</div>
)}
</div>
</div>
</div>
{/* Accordéon Autres Sociétés */}
{otherCompanies.length > 0 && (
<div className="ml-10 mt-1">
<button
type="button"
onClick={() => setShowOtherCompanies(!showOtherCompanies)}
className="flex items-center gap-1.5 text-xs text-tousergo-rust hover:text-tousergo-rust/85 font-bold transition-colors focus:outline-none"
>
<span>
{showOtherCompanies ? "Masquer les autres sociétés" : `Autres sociétés (${otherCompanies.length})`}
</span>
{showOtherCompanies ? <ChevronUp className="w-3.5 h-3.5" /> : <ChevronDown className="w-3.5 h-3.5" />}
</button>
{showOtherCompanies && (
<div className="mt-2 pl-2 border-l-2 border-slate-200 space-y-1 bg-slate-50 p-2 rounded-xl border border-slate-100">
{otherCompanies.map((comp, idx) => (
<div key={idx} className="text-xs font-semibold text-slate-600 flex items-center gap-1.5 py-1">
<span className="w-1.5 h-1.5 rounded-full bg-tousergo-rust" />
<span>{comp}</span>
</div>
))}
</div>
)}
</div>
)}
</div>
{/* Téléphone */}
<div className="flex items-start gap-3 pb-4 border-b border-slate-100">
<div className="mt-0.5 p-1.5 bg-emerald-50 text-emerald-600 rounded-lg">
<Phone className="w-4 h-4" />
</div>
<div className="flex-1">
<span className="text-[10px] uppercase font-black text-slate-400 tracking-wider">Téléphone</span>
<div className="flex items-center gap-2 mt-0.5">
<span className="text-sm font-bold font-mono text-slate-800">
{activeProfile.phone}
</span>
<button
type="button"
onClick={() => handleCopy(activeProfile.phone, "phone")}
className="p-1 hover:bg-slate-100 rounded text-slate-400 hover:text-slate-700 transition-colors cursor-pointer"
title="Copier le numéro"
>
{copiedField === "phone" ? <Check className="w-3.5 h-3.5 text-emerald-600" /> : <Copy className="w-3.5 h-3.5" />}
</button>
</div>
</div>
</div>
{/* E-mail */}
<div className="flex items-start gap-3 pb-4 border-b border-slate-100">
<div className="mt-0.5 p-1.5 bg-sky-50 text-sky-600 rounded-lg">
<Mail className="w-4 h-4" />
</div>
<div className="flex-1">
<span className="text-[10px] uppercase font-black text-slate-400 tracking-wider">E-mail</span>
<div className="flex items-center gap-2 mt-0.5">
<span className="text-sm font-bold text-slate-800 select-all truncate">
{activeProfile.email || "Non spécifié"}
</span>
{activeProfile.email && (
<button
type="button"
onClick={() => handleCopy(activeProfile.email, "email")}
className="p-1 hover:bg-slate-100 rounded text-slate-400 hover:text-slate-700 transition-colors cursor-pointer"
title="Copier l'adresse e-mail"
>
{copiedField === "email" ? <Check className="w-3.5 h-3.5 text-emerald-600" /> : <Copy className="w-3.5 h-3.5" />}
</button>
)}
</div>
</div>
</div>
{/* Adresse postale */}
<div className="flex flex-col gap-2">
<div className="flex items-start gap-3">
<div className="mt-0.5 p-1.5 bg-tousergo-peach/40 text-tousergo-rust rounded-lg">
<MapPin className="w-4 h-4" />
</div>
<div className="flex-1">
<span className="text-[10px] uppercase font-black text-slate-400 tracking-wider">Adresse Postale</span>
<div className="text-sm font-bold text-slate-800 mt-1">
{latestAddress && (latestAddress.street || latestAddress.city) ? (
<div className="bg-tousergo-peach/15 p-3 rounded-xl border border-tousergo-peach">
{latestAddress.street && <div className="text-slate-800">{latestAddress.street}</div>}
{(latestAddress.zip || latestAddress.city) && (
<div className="mt-0.5 text-slate-700">
{latestAddress.zip} {latestAddress.city}
</div>
)}
<span className="text-[9px] font-extrabold uppercase bg-tousergo-rust text-white px-1.5 py-0.5 rounded border border-tousergo-rust inline-block mt-2">
Dernière utilisée
</span>
</div>
) : (
<span className="text-slate-400 font-normal italic">Adresse non renseignée</span>
)}
</div>
</div>
</div>
{/* Accordéon Autres Adresses */}
{otherAddresses.length > 0 && (
<div className="ml-10 mt-1">
<button
type="button"
onClick={() => setShowOtherAddresses(!showOtherAddresses)}
className="flex items-center gap-1.5 text-xs text-tousergo-rust hover:text-tousergo-rust/85 font-bold transition-colors focus:outline-none"
>
<span>
{showOtherAddresses ? "Masquer les autres adresses" : `Autres adresses (${otherAddresses.length})`}
</span>
{showOtherAddresses ? <ChevronUp className="w-3.5 h-3.5" /> : <ChevronDown className="w-3.5 h-3.5" />}
</button>
{showOtherAddresses && (
<div className="mt-2 pl-2 border-l-2 border-tousergo-rust space-y-2 bg-slate-50 p-2.5 rounded-xl border border-slate-100">
{otherAddresses.map((addr, idx) => (
<div key={idx} className="text-xs text-slate-600 border-b border-slate-150 last:border-0 pb-1.5 last:pb-0">
{addr.street && <div className="font-semibold text-slate-700">{addr.street}</div>}
{(addr.zip || addr.city) && <div className="text-slate-500 mt-0.5">{addr.zip} {addr.city}</div>}
</div>
))}
</div>
)}
</div>
)}
</div>
</div>
{/* Actions Rapides d'ouverture de fiches directes */}
<div className="p-6 bg-slate-50 border-t border-slate-100 grid grid-cols-2 gap-3">
<a
href={getPrestashopUrl(activeProfile.prestaId)}
target="_blank"
rel="noreferrer"
className="flex items-center justify-center gap-2 py-3 px-4 bg-tousergo-rust hover:opacity-95 text-white text-xs font-bold rounded-xl shadow-sm transition-all cursor-pointer text-center"
id="prestashop-btn"
>
<ExternalLink className="w-4 h-4 text-tousergo-peach" />
Ouvrir PrestaShop
</a>
<a
href={getOdooUrl(activeProfile.odooId)}
target="_blank"
rel="noreferrer"
className="flex items-center justify-center gap-2 py-3 px-4 bg-tousergo-navy hover:opacity-95 text-white text-xs font-bold rounded-xl shadow-sm transition-all cursor-pointer text-center"
id="odoo-btn"
>
<ExternalLink className="w-4 h-4 text-tousergo-peach" />
Ouvrir Odoo
</a>
</div>
</div>
) : (
<div className="bg-white border border-slate-200 rounded-2xl shadow-sm p-8 text-center flex flex-col items-center justify-center gap-3">
<AlertCircle className="w-12 h-12 text-slate-400" />
<div>
<h3 className="font-extrabold text-slate-800 text-sm">Aucune fiche sélectionnée</h3>
<p className="text-xs text-slate-400 mt-1 max-w-sm">
Saisissez un numéro de téléphone dans la barre de recherche ci-dessus pour afficher la fiche client.
</p>
</div>
</div>
)}
</div>
{/* COLONNE DROITE : SYNCHRONISATION ET RECHERCHES ASSOCIEES (2/5) */}
<div className="md:col-span-2 flex flex-col gap-5">
{/* Résultat PrestaShop */}
<div className="bg-white border border-slate-200 rounded-2xl shadow-sm p-5">
<div className="flex items-center justify-between pb-3 border-b border-slate-100 mb-3">
<div className="flex items-center gap-2">
<div className="w-2.5 h-2.5 rounded-full bg-tousergo-rust" />
<h3 className="text-xs font-black uppercase text-slate-500 tracking-wider">
Fiches PrestaShop ({prestaResults.length})
</h3>
</div>
<span className="text-[10px] font-bold text-slate-400">Status API</span>
</div>
{prestaResults.length > 0 ? (
<div className="space-y-3">
{prestaResults.map((ps) => {
const isActive = prestaResult?.id === ps.id;
return (
<div key={ps.id} className={`p-3 rounded-xl border transition-all text-xs ${
isActive
? "bg-tousergo-peach/15 border-tousergo-peach ring-1 ring-tousergo-rust/10"
: "bg-slate-50 border-slate-100"
}`}>
<div className="flex items-center justify-between">
<span className="font-bold text-slate-800">
{formatFirstname(ps.firstname)} {formatLastname(ps.lastname)}
</span>
<span className="text-[9px] px-1.5 py-0.2 bg-tousergo-peach border border-tousergo-peach text-tousergo-rust rounded uppercase font-extrabold">
ID: #{ps.id}
</span>
</div>
<div className="text-slate-500 text-[11px] mt-0.5">{ps.email}</div>
{ps.company && (
<div className="text-slate-500 text-[11px] mt-1 font-semibold flex items-center gap-1">
<Building2 className="w-3 h-3 text-slate-400" />
{ps.company}
</div>
)}
<div className="mt-2.5 pt-2 border-t border-slate-200/50 flex justify-between items-center gap-2">
{isActive ? (
<span className="text-[10px] text-emerald-600 font-extrabold flex items-center gap-1">
<CheckCircle className="w-3 h-3" /> Fiche active
</span>
) : (
<span className="text-[10px] text-slate-400">
Autre compte PrestaShop
</span>
)}
<a
href={getPrestashopUrl(ps.id)}
target="_blank"
rel="noreferrer"
className="inline-flex items-center gap-0.5 text-[10px] text-tousergo-rust hover:brightness-110 font-bold transition-colors"
>
Ouvrir PrestaShop <ExternalLink className="w-2.5 h-2.5" />
</a>
</div>
</div>
);
})}
</div>
) : (
<div className="p-3 bg-tousergo-peach/20 border border-tousergo-peach/40 rounded-xl text-xs text-tousergo-rust italic">
Aucun résultat PrestaShop trouvé pour ce numéro.
</div>
)}
</div>
{/* Résultat Odoo */}
<div className="bg-white border border-slate-200 rounded-2xl shadow-sm p-5">
<div className="flex items-center justify-between pb-3 border-b border-slate-100 mb-3">
<div className="flex items-center gap-2">
<div className="w-2.5 h-2.5 rounded-full bg-tousergo-navy" />
<h3 className="text-xs font-black uppercase text-slate-500 tracking-wider">
Fiches Odoo ({odooResults.length})
</h3>
</div>
<span className="text-[10px] font-bold text-slate-400">Status API</span>
</div>
{odooResults.length > 0 ? (
<div className="space-y-2.5">
{odooResults.map((partner) => {
const isActiveOdoo = partner.id === activeProfile?.odooId;
return (
<div key={partner.id} className={`p-3 rounded-xl border transition-all text-xs ${
isActiveOdoo
? "bg-tousergo-pink/15 border-tousergo-pink/30 ring-1 ring-tousergo-navy/10"
: "bg-slate-50 border-slate-100"
}`}>
<div className="flex items-center justify-between">
<span className="font-bold text-slate-800">{partner.name}</span>
<span className="text-[9px] px-1.5 py-0.2 bg-tousergo-peach border border-tousergo-peach text-tousergo-rust rounded uppercase font-extrabold">
ID: #{partner.id}
</span>
</div>
{partner.email && <div className="text-slate-500 text-[11px] mt-0.5">{partner.email}</div>}
{partner.parent_id && (
<div className="text-slate-500 text-[11px] mt-1 font-semibold flex items-center gap-1">
<Building2 className="w-3 h-3 text-slate-400" />
{partner.parent_id[1]}
</div>
)}
<div className="mt-2 pt-2 border-t border-slate-200/50 flex justify-between items-center">
{isActiveOdoo ? (
<span className="text-[10px] text-emerald-600 font-extrabold flex items-center gap-1">
<CheckCircle className="w-3 h-3" /> Fiche active
</span>
) : (
<span className="text-[10px] text-slate-400">
Autre fiche Odoo
</span>
)}
<a
href={getOdooUrl(partner.id)}
target="_blank"
rel="noreferrer"
className="inline-flex items-center gap-0.5 text-[10px] text-tousergo-navy hover:text-tousergo-rust font-bold transition-colors"
>
Ouvrir Odoo <ExternalLink className="w-2.5 h-2.5" />
</a>
</div>
</div>
);
})}
</div>
) : (
<div className="p-3 bg-tousergo-pink/15 border border-tousergo-pink/20 rounded-xl text-xs text-tousergo-navy italic">
Aucun partenaire Odoo trouvé pour ce numéro.
</div>
)}
</div>
</div>
</div>
)}
</main>
</div>
);
}