import type { FC } from 'react'; import { useEffect, useMemo, useState } from 'react'; import { Mail, Phone, Stethoscope, Briefcase, Save, Percent, Eye, CalendarDays, FileText, ShoppingCart, Users, ClipboardList, CreditCard, DollarSign } from 'lucide-react'; import Layout from '../components/Layout'; import { Card, Badge, EmptyState, PageHeader, Skeleton, MobileCard, Input, Button, Modal, toast } from '../components/ui'; import { odooApi, type Doctor, type DoctorHistory } from '../services/odoo'; const medicalTerms = ['doctor', 'dra', 'médico', 'medico', 'especialista', 'dermatólogo', 'dermatologo', 'skin', 'clínico', 'clinico']; const isMedicalTitle = (jobTitle?: string) => { if (!jobTitle) return false; const normalized = jobTitle.toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, ''); return medicalTerms.some((term) => normalized.includes(term)); }; const fmtNum = (n: number | null | undefined, decimals = 2) => (n ?? 0).toLocaleString('es-MX', { maximumFractionDigits: decimals }); const fmtMoney = (n: number) => (n || 0).toLocaleString('es-MX', { style: 'currency', currency: 'MXN', maximumFractionDigits: 0 }); const initials = (name: string) => name.split(' ').filter(Boolean).slice(0, 2).map((w) => w[0]).join('').toUpperCase(); type BadgeVariant = 'default' | 'success' | 'info' | 'pending' | 'cancelled'; const CITA_STATE: Record = { pending: { label: 'Pendiente', variant: 'pending' }, confirmed: { label: 'Confirmada', variant: 'info' }, arrived: { label: 'Llegó', variant: 'info' }, in_progress: { label: 'En progreso', variant: 'info' }, done: { label: 'Completada', variant: 'success' }, cancelled: { label: 'Cancelada', variant: 'cancelled' }, no_show: { label: 'No show', variant: 'cancelled' }, }; const VISITA_STATE: Record = { en_curso: { label: 'En curso', variant: 'info' }, completada: { label: 'Completada', variant: 'success' }, cancelada: { label: 'Cancelada', variant: 'cancelled' }, }; const VENTA_STATE: Record = { draft: { label: 'Borrador', variant: 'default' }, confirmed: { label: 'Confirmada', variant: 'info' }, paid: { label: 'Pagada', variant: 'success' }, partial: { label: 'Parcial', variant: 'pending' }, cancelled: { label: 'Cancelada', variant: 'cancelled' }, }; const StateBadge: FC<{ map: Record; state: string }> = ({ map, state }) => { const meta = map[state] || { label: state, variant: 'default' as BadgeVariant }; return {meta.label}; }; /** Badges de ventas 30d al estilo del sistema legacy: * píldora verde "N ventas" + gris "N recomendaciones" y * píldoras índigo apiladas: usd / m.n. / tarjeta m.n. */ const StatBadges: FC<{ d: Doctor }> = ({ d }) => (
{d.ventas_30d ?? 0} ventas
{d.total_30d_usd == null ? '—' : fmtNum(d.total_30d_usd)} usd {fmtNum(d.total_30d ?? 0, 0)} m.n. {fmtNum(d.card_30d ?? 0, 0)} tarjeta m.n.
); const Medicos: FC = () => { const [employees, setEmployees] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { const load = async () => { try { setLoading(true); setError(null); const res = await odooApi.getDoctors(); if (res.status === 'success') setEmployees(res.doctors); } catch (err) { setError('Error al cargar médicos'); toast.error('Error al cargar médicos'); console.error(err); } finally { setLoading(false); } }; load(); }, []); const doctors = useMemo(() => { const medical = employees.filter((d) => isMedicalTitle(d.job_title)); return medical.length > 0 ? medical : employees; }, [employees]); const [pctDraft, setPctDraft] = useState>({}); const [savingPct, setSavingPct] = useState(null); // Modal de detalle del médico const [selected, setSelected] = useState(null); const [modalTab, setModalTab] = useState<'info' | 'historial'>('info'); const [hist, setHist] = useState(null); const [histLoading, setHistLoading] = useState(false); const [histSection, setHistSection] = useState<'citas' | 'visitas' | 'ventas'>('citas'); const openDoctor = (d: Doctor) => { setSelected(d); setModalTab('info'); setHistSection('citas'); setHist(null); setHistLoading(true); odooApi.getDoctorHistory(d.id) .then((res) => { if (res.status === 'success') setHist(res); }) .catch((err) => { toast.error('No se pudo cargar el historial'); console.error(err); }) .finally(() => setHistLoading(false)); }; const pctValue = (d: Doctor) => (pctDraft[d.id] ?? String(d.commission_pct ?? 0)); const savePct = async (d: Doctor) => { const raw = pctDraft[d.id]; const pct = parseFloat(raw ?? String(d.commission_pct ?? 0)); if (Number.isNaN(pct) || pct < 0 || pct > 100) { toast.error('Porcentaje inválido (0-100)'); return; } setSavingPct(d.id); try { await odooApi.updateDoctor(d.id, { commission_pct: pct }); setEmployees((prev) => prev.map((e) => (e.id === d.id ? { ...e, commission_pct: pct } : e))); setPctDraft((prev) => { const n = { ...prev }; delete n[d.id]; return n; }); toast.success(`Comisión de ${d.name}: ${pct}%`); } catch { toast.error('No se pudo guardar la comisión'); } finally { setSavingPct(null); } }; return ( {error &&

{error}

} {loading ? ( ) : doctors.length === 0 ? ( } /> ) : ( <>
{doctors.map((d) => ( openDoctor(d)} title="Ver detalle del médico"> ))}
Nombre Puesto Teléfono Email % Comisión Últimos 30 días
{d.name} {d.job_title || 'Sin puesto'} {d.work_phone || '-'} {d.work_email ? ( {d.work_email} ) : ( '-' )} e.stopPropagation()}>
setPctDraft((prev) => ({ ...prev, [d.id]: e.target.value }))} className="max-w-[92px]" />
{doctors.map((d) => ( {d.job_title || 'Sin puesto'} } rows={[ { label: 'Teléfono', value: d.work_phone || '-' }, { label: 'Email', value: d.work_email || '-' }, { label: '% Comisión', value: `${d.commission_pct ?? 0}%` }, { label: 'Últimos 30 días', value: }, ]} actions={ } /> ))}
)}
{/* Modal detalle del médico */} setSelected(null)} title="" maxWidth="2xl"> {selected && (
{/* Header */}
{initials(selected.name)}

{selected.name}

{selected.job_title || 'Sin puesto'}

Pacientes

{fmtNum(selected.patient_count ?? 0, 0)}

Citas

{histLoading || !hist ? '…' : fmtNum(hist.totales.citas_total, 0)}

Visitas

{histLoading || !hist ? '…' : fmtNum(hist.totales.visitas_total, 0)}

{/* Tabs */}
{([{ key: 'info', label: 'Info' }, { key: 'historial', label: 'Historial' }] as const).map((t) => ( ))}
{modalTab === 'info' ? (

Resumen de actividad

Pacientes asignados

{fmtNum(selected.patient_count ?? 0, 0)}

Ventas 30d

{selected.ventas_30d ?? 0}

Total 30d

{fmtMoney(selected.total_30d ?? 0)}

Tarjeta 30d

{fmtMoney(selected.card_30d ?? 0)}

{selected.total_30d_usd == null ? '—' : `≈ ${fmtNum(selected.total_30d_usd)}`} usd

Contacto

Teléfono

{selected.work_phone || '—'}

Email

{selected.work_email || '—'}

Comisión

{selected.commission_pct ?? 0}%

Se edita inline en la tabla

) : (
{histLoading ? ( ) : !hist ? ( ) : ( <>

Citas totales

{fmtNum(hist.totales.citas_total, 0)}

Visitas totales

{fmtNum(hist.totales.visitas_total, 0)}

Líneas recetadas

{fmtNum(hist.totales.ventas_total_lineas, 0)}

Monto recetado

{fmtMoney(hist.totales.monto_total)}

{([ { key: 'citas', label: 'Citas', icon: }, { key: 'visitas', label: 'Visitas', icon: }, { key: 'ventas', label: 'Ventas recetadas', icon: }, ] as const).map((s) => ( ))}
{histSection === 'citas' && ( hist.citas.length === 0 ? ( ) : (
{hist.citas.map((c) => ( ))}
Fecha Hora Paciente Servicio Estado
{c.date || '—'} {c.time || '—'} {c.patient} {c.service}
) )} {histSection === 'visitas' && ( hist.visitas.length === 0 ? ( ) : (
{hist.visitas.map((v) => ( ))}
Fecha Paciente Motivo Estado
{v.date_start || '—'} {v.patient} {v.motivo || '—'}
) )} {histSection === 'ventas' && ( hist.ventas.length === 0 ? ( ) : (
{hist.ventas.map((l) => ( ))}
Folio Fecha Paciente Servicio / Artículo Subtotal Estado
{l.venta} {l.date || '—'} {l.patient} {l.item} {fmtMoney(l.subtotal)}
) )} )}
)}
)}
); }; export default Medicos;