Initial commit: SKEEN Derma Experts - Sistema Integral de Gestión Clínica

- Frontend React (SKEEN Brand) con Vite, TypeScript, Tailwind
- Frontend Homenest (versión alternativa)
- Módulos Odoo 17 custom (citas, pacientes, monedero, pagos, ventas, inventario, whatsapp)
- WACRM fork (Next.js 16 + Supabase)
- Hermes + Bridge + Skills (Qwen3.6 via Nan Builders)
- Scripts de migración y operación
- Documentación extensiva en docs/
This commit is contained in:
2026-07-20 07:44:23 +00:00
commit a718592291
699 changed files with 324602 additions and 0 deletions

View File

@@ -0,0 +1,758 @@
import type { FC } from 'react';
import { useEffect, useState, useCallback } from 'react';
import {
Search, Plus, Activity, ShoppingBag, User, Edit2, Phone, Mail, Calendar, Droplet, Heart,
MapPin, Briefcase, Users, Baby, Stethoscope, FileText, AlertCircle, CheckCircle2, XCircle,
Download,
} from 'lucide-react';
import Layout from '../components/Layout';
import {
Card,
Button,
Input,
Select,
Modal,
Badge,
EmptyState,
MobileCard,
PageHeader,
Skeleton,
toast,
TextArea,
badgeForAppointmentState,
} from '../components/ui';
import { odooApi, type Patient, type Appointment, type Sale } from '../services/odoo';
import { downloadCsv } from '../lib/utils';
const genderOptions = [
{ value: '', label: 'No especificado' },
{ value: 'male', label: 'Masculino' },
{ value: 'female', label: 'Femenino' },
{ value: 'other', label: 'Otro' },
];
const bloodOptions = [
{ value: '', label: 'No especificado' },
{ value: 'a+', label: 'A+' },
{ value: 'a-', label: 'A-' },
{ value: 'b+', label: 'B+' },
{ value: 'b-', label: 'B-' },
{ value: 'ab+', label: 'AB+' },
{ value: 'ab-', label: 'AB-' },
{ value: 'o+', label: 'O+' },
{ value: 'o-', label: 'O-' },
];
const maritalOptions = [
{ value: '', label: 'No especificado' },
{ value: 'single', label: 'Soltero/a' },
{ value: 'married', label: 'Casado/a' },
{ value: 'divorced', label: 'Divorciado/a' },
{ value: 'widowed', label: 'Viudo/a' },
{ value: 'union', label: 'Unión libre' },
{ value: 'other', label: 'Otro' },
];
const formatGender = (gender?: string | false) => {
if (!gender) return 'No especificado';
const map: Record<string, string> = {
male: 'Masculino',
female: 'Femenino',
other: 'Otro',
};
return map[gender] || gender;
};
const formatMarital = (status?: string) => {
if (!status) return 'No especificado';
const opt = maritalOptions.find((o) => o.value === status);
return opt?.label || status;
};
const formatBlood = (bt?: string | false) => {
if (!bt || typeof bt !== 'string') return 'No especificado';
const opt = bloodOptions.find((o) => o.value === bt.toLowerCase());
return opt?.label || bt.toUpperCase();
};
const Pacientes: FC = () => {
const [patients, setPatients] = useState<Patient[]>([]);
const [loading, setLoading] = useState(true);
const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);
const [search, setSearch] = useState('');
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(50);
const [total, setTotal] = useState(0);
const [totalPages, setTotalPages] = useState(0);
const [selected, setSelected] = useState<Patient | null>(null);
const [history, setHistory] = useState<{ appointments: Appointment[]; sales: Sale[] } | null>(null);
const [historyLoading, setHistoryLoading] = useState(false);
const [tab, setTab] = useState<'info' | 'clinical' | 'appointments' | 'sales'>('info');
const [modalOpen, setModalOpen] = useState(false);
const [editing, setEditing] = useState<Patient | null>(null);
const emptyForm: PatientForm = {
name: '',
phone: '',
email: '',
birth_date: '',
gender: '',
blood_type: '',
birthplace: '',
occupation: '',
marital_status: '',
emergency_contact: '',
emergency_phone: '',
home_phone: '',
mobile: '',
address_notes: '',
referred_by: '',
patient_comments: '',
internal_notes: '',
allergies: '',
medical_history: '',
current_medication: '',
medical_notes: '',
surgeries_notes: '',
children_count: 0,
is_pregnant: false,
is_breastfeeding: false,
uses_contraceptives: false,
kidney_problems: false,
back_pain: false,
heart_disease: false,
respiratory_problems: false,
blood_pressure: false,
diabetes: false,
thyroid: false,
colitis: false,
constipation: false,
liver_problems: false,
surgeries: false,
varicose_veins: false,
migraine: false,
faints_with_needles: false,
};
const [form, setForm] = useState<PatientForm>(emptyForm);
const load = useCallback(async () => {
try {
setLoading(true);
setError(null);
const res = await odooApi.getPatients({ search, page, page_size: pageSize });
if (res.status === 'success') {
setPatients(res.patients);
setTotal(res.total ?? 0);
setTotalPages(res.total_pages ?? 0);
}
} catch (err) {
setError('Error al cargar pacientes');
toast.error('Error al cargar pacientes');
console.error(err);
} finally {
setLoading(false);
}
}, [search, page, pageSize]);
useEffect(() => {
setPage(1);
}, [search]);
useEffect(() => {
load();
}, [load]);
const openHistory = async (patient: Patient) => {
setSelected(patient);
setTab('info');
setHistoryLoading(true);
try {
const res = await odooApi.getPatientHistory(patient.id);
if (res.status === 'success') setHistory({ appointments: res.appointments, sales: res.sales });
} catch (err) {
toast.error('Error al cargar historial');
console.error(err);
} finally {
setHistoryLoading(false);
}
};
const openCreate = () => {
setEditing(null);
setForm(emptyForm);
setModalOpen(true);
};
const openEdit = (patient: Patient) => {
setEditing(patient);
setForm({
...emptyForm,
...patient,
birth_date: patient.birth_date || '',
gender: patient.gender || '',
blood_type: patient.blood_type || '',
marital_status: patient.marital_status || '',
children_count: patient.children_count ?? 0,
is_pregnant: patient.is_pregnant ?? false,
is_breastfeeding: patient.is_breastfeeding ?? false,
uses_contraceptives: patient.uses_contraceptives ?? false,
kidney_problems: patient.kidney_problems ?? false,
back_pain: patient.back_pain ?? false,
heart_disease: patient.heart_disease ?? false,
respiratory_problems: patient.respiratory_problems ?? false,
blood_pressure: patient.blood_pressure ?? false,
diabetes: patient.diabetes ?? false,
thyroid: patient.thyroid ?? false,
colitis: patient.colitis ?? false,
constipation: patient.constipation ?? false,
liver_problems: patient.liver_problems ?? false,
surgeries: patient.surgeries ?? false,
varicose_veins: patient.varicose_veins ?? false,
migraine: patient.migraine ?? false,
faints_with_needles: patient.faints_with_needles ?? false,
});
setModalOpen(true);
};
const updateForm = (patch: Partial<PatientForm>) => {
setForm((prev) => ({ ...prev, ...patch }));
};
const save = async () => {
if (!form.name || !form.phone) {
toast.error('Nombre y teléfono son obligatorios');
return;
}
try {
setSubmitting(true);
const payload: Partial<Patient> = {
...form,
birth_date: form.birth_date || null,
gender: form.gender || undefined,
blood_type: form.blood_type || undefined,
marital_status: form.marital_status || undefined,
};
if (editing) {
await odooApi.updatePatient(editing.id, payload);
toast.success('Paciente actualizado');
} else {
await odooApi.createPatient(payload);
toast.success('Paciente creado');
}
setModalOpen(false);
await load();
} catch (err) {
toast.error(editing ? 'Error al actualizar paciente' : 'Error al crear paciente');
console.error(err);
} finally {
setSubmitting(false);
}
};
const TableHeader = () => (
<thead className="bg-theme-bg">
<tr>
<th className="text-left p-3 text-xs font-medium text-theme-muted uppercase">Nombre</th>
<th className="text-left p-3 text-xs font-medium text-theme-muted uppercase hidden sm:table-cell">Teléfono</th>
<th className="text-left p-3 text-xs font-medium text-theme-muted uppercase hidden md:table-cell">Edad</th>
<th className="text-left p-3 text-xs font-medium text-theme-muted uppercase hidden md:table-cell">Última visita</th>
<th className="text-left p-3 text-xs font-medium text-theme-muted uppercase">Visitas</th>
<th className="text-left p-3 text-xs font-medium text-theme-muted uppercase">Puntos</th>
<th className="text-left p-3 text-xs font-medium text-theme-muted uppercase">Acciones</th>
</tr>
</thead>
);
const InfoRow = ({ label, value, icon: Icon }: { label: string; value: React.ReactNode; icon?: typeof User }) => (
<div className="p-3 bg-theme-bg rounded-xl">
<p className="text-xs text-theme-muted mb-1">{label}</p>
<p className="text-sm font-medium text-theme-heading flex items-start">
{Icon && <Icon size={14} className="mr-1.5 mt-0.5 text-theme-muted shrink-0" />}
<span className="break-words">{value || '-'}</span>
</p>
</div>
);
const ClinicalItem = ({ label, value }: { label: string; value?: boolean }) => (
<div className="flex items-center justify-between p-2.5 bg-theme-surface border border-theme-border rounded-lg">
<span className="text-sm text-theme-heading">{label}</span>
{value ? (
<CheckCircle2 size={16} className="text-theme-heading" />
) : (
<XCircle size={16} className="text-[#D6D3D1]" />
)}
</div>
);
const exportPatients = () => {
const rows: (string | number | boolean | null | undefined)[][] = [
['Expediente', 'Nombre', 'Teléfono', 'Email', 'Fecha nacimiento', 'Género', 'Última visita', 'Visitas', 'Puntos monedero', 'Total gastado', 'Fuente', 'VIP'],
];
patients.forEach((p) =>
rows.push([
p.patient_id, p.name, p.phone, p.email, p.birth_date, p.gender || '', p.last_visit,
p.total_visits, p.wallet_points, p.total_spent, p.source, p.is_vip ? 'Sí' : 'No',
])
);
downloadCsv(`pacientes-skeen-${new Date().toISOString().split('T')[0]}.csv`, rows);
toast.success(`CSV descargado (${patients.length} pacientes de la vista actual)`);
};
return (
<Layout title="Pacientes" subtitle="Directorio de pacientes">
<PageHeader title="Pacientes" subtitle="Busca, crea y gestiona pacientes">
<Button variant="outline" onClick={exportPatients} disabled={patients.length === 0}>
<Download size={16} className="mr-2" />
Exportar CSV
</Button>
<Button onClick={openCreate}>
<Plus size={16} className="mr-2" />
Nuevo paciente
</Button>
</PageHeader>
<Card>
<Card.Body>
<div className="flex flex-col sm:flex-row sm:items-center gap-3 mb-4 sm:mb-6">
<div className="relative w-full sm:max-w-sm">
<Search size={16} className="absolute left-3 top-1/2 -translate-y-1/2 text-theme-muted" />
<Input
placeholder="Buscar por nombre o teléfono..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="pl-10"
/>
</div>
</div>
{error && <p className="text-rose-600 text-sm mb-4">{error}</p>}
{loading ? (
<Skeleton count={6} className="h-12 w-full" />
) : patients.length === 0 ? (
<EmptyState
title="Sin pacientes"
subtitle="No se encontraron pacientes."
actionLabel="Nuevo paciente"
onAction={openCreate}
/>
) : (
<>
<div className="hidden sm:block overflow-x-auto">
<table className="w-full">
<TableHeader />
<tbody className="divide-y">
{patients.map((p) => (
<tr key={p.id} className="hover:bg-theme-bg">
<td className="p-3 text-sm font-medium text-theme-heading">
<button onClick={() => openHistory(p)} className="text-left hover:underline">
{p.name}
</button>
</td>
<td className="p-3 text-sm text-theme-muted hidden sm:table-cell">{p.phone}</td>
<td className="p-3 text-sm text-theme-muted hidden md:table-cell">{p.age ?? '-'}</td>
<td className="p-3 text-sm text-theme-muted hidden md:table-cell">{p.last_visit || '-'}</td>
<td className="p-3 text-sm text-theme-muted">{p.total_visits}</td>
<td className="p-3 text-sm font-medium text-theme-heading">{p.wallet_points} pts</td>
<td className="p-3">
<div className="flex items-center gap-1">
<Button variant="ghost" size="sm" onClick={() => openHistory(p)} title="Ver detalle">
<User size={16} className="text-theme-muted" />
</Button>
<Button variant="ghost" size="sm" onClick={() => openEdit(p)} title="Editar">
<Edit2 size={16} className="text-theme-muted" />
</Button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="sm:hidden space-y-3">
{patients.map((p) => (
<MobileCard
key={p.id}
title={p.name}
subtitle={p.phone}
rows={[
{ label: 'Edad', value: p.age ?? '-' },
{ label: 'Última visita', value: p.last_visit || '-' },
{ label: 'Visitas', value: p.total_visits },
{ label: 'Puntos', value: `${p.wallet_points} pts` },
]}
actions={
<>
<Button variant="ghost" size="sm" onClick={() => openHistory(p)}>
<User size={16} />
</Button>
<Button variant="ghost" size="sm" onClick={() => openEdit(p)}>
<Edit2 size={16} />
</Button>
</>
}
onClick={() => openHistory(p)}
/>
))}
</div>
{/* Paginación */}
<div className="flex flex-col sm:flex-row items-center justify-between gap-3 mt-4 pt-4 border-t border-theme-border">
<p className="text-sm text-theme-muted">
{total} pacientes · página {page} de {totalPages || 1}
</p>
<div className="flex items-center gap-2">
<select
value={pageSize}
onChange={(e) => { setPageSize(parseInt(e.target.value, 10)); setPage(1); }}
className="border border-theme-border-strong rounded-lg px-2 py-1.5 text-sm"
>
<option value={25}>25</option>
<option value={50}>50</option>
<option value={100}>100</option>
<option value={200}>200</option>
</select>
<Button variant="outline" size="sm" disabled={page <= 1} onClick={() => setPage((p) => Math.max(1, p - 1))}>
Anterior
</Button>
<Button variant="outline" size="sm" disabled={page >= totalPages} onClick={() => setPage((p) => p + 1)}>
Siguiente
</Button>
</div>
</div>
</>
)}
</Card.Body>
</Card>
{/* Modal crear/editar */}
<Modal
isOpen={modalOpen}
onClose={() => setModalOpen(false)}
title={editing ? 'Editar paciente' : 'Nuevo paciente'}
maxWidth="2xl"
footer={
<>
<Button variant="outline" onClick={() => setModalOpen(false)}>Cancelar</Button>
<Button onClick={save} loading={submitting}>{editing ? 'Actualizar' : 'Crear'}</Button>
</>
}
>
<div className="space-y-6 max-h-[70vh] overflow-y-auto pr-1">
<section>
<h4 className="text-sm font-semibold text-theme-heading mb-3 flex items-center">
<User size={16} className="mr-2 text-theme-muted" /> Datos generales
</h4>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<Input label="Nombre *" value={form.name} onChange={(e) => updateForm({ name: e.target.value })} />
<Input label="Teléfono *" value={form.phone} onChange={(e) => updateForm({ phone: e.target.value })} />
<Input label="Email" type="email" value={form.email} onChange={(e) => updateForm({ email: e.target.value })} />
<Input label="Celular" value={form.mobile} onChange={(e) => updateForm({ mobile: e.target.value })} />
<Input label="Teléfono casa" value={form.home_phone} onChange={(e) => updateForm({ home_phone: e.target.value })} />
<Input label="Fecha de nacimiento" type="date" value={form.birth_date} onChange={(e) => updateForm({ birth_date: e.target.value })} />
<Select label="Género" options={genderOptions} value={form.gender} onChange={(e) => updateForm({ gender: e.target.value })} />
<Select label="Tipo sanguíneo" options={bloodOptions} value={form.blood_type} onChange={(e) => updateForm({ blood_type: e.target.value })} />
</div>
</section>
<section>
<h4 className="text-sm font-semibold text-theme-heading mb-3 flex items-center">
<MapPin size={16} className="mr-2 text-theme-muted" /> Datos personales
</h4>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<Input label="Lugar de nacimiento" value={form.birthplace} onChange={(e) => updateForm({ birthplace: e.target.value })} />
<Input label="Empleo / Ocupación" value={form.occupation} onChange={(e) => updateForm({ occupation: e.target.value })} />
<Select label="Estado civil" options={maritalOptions} value={form.marital_status} onChange={(e) => updateForm({ marital_status: e.target.value })} />
<Input label="Recomendado por" value={form.referred_by} onChange={(e) => updateForm({ referred_by: e.target.value })} />
<Input label="Contacto de emergencia" value={form.emergency_contact} onChange={(e) => updateForm({ emergency_contact: e.target.value })} />
<Input label="Teléfono de emergencia" value={form.emergency_phone} onChange={(e) => updateForm({ emergency_phone: e.target.value })} />
</div>
<div className="mt-4">
<TextArea label="Dirección completa" value={form.address_notes} onChange={(e) => updateForm({ address_notes: e.target.value })} />
</div>
</section>
<section>
<h4 className="text-sm font-semibold text-theme-heading mb-3 flex items-center">
<Baby size={16} className="mr-2 text-theme-muted" /> Antecedentes gineco-obstétricos
</h4>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<label className="flex items-center gap-3 p-3 border border-theme-border-strong rounded-xl cursor-pointer hover:bg-theme-bg">
<input
type="checkbox"
checked={form.is_pregnant}
onChange={(e) => updateForm({ is_pregnant: e.target.checked })}
className="w-4 h-4 rounded border-theme-border-strong text-theme-heading focus:ring-[#EDA588]"
/>
<span className="text-sm text-theme-heading">¿Está embarazada?</span>
</label>
<label className="flex items-center gap-3 p-3 border border-theme-border-strong rounded-xl cursor-pointer hover:bg-theme-bg">
<input
type="checkbox"
checked={form.is_breastfeeding}
onChange={(e) => updateForm({ is_breastfeeding: e.target.checked })}
className="w-4 h-4 rounded border-theme-border-strong text-theme-heading focus:ring-[#EDA588]"
/>
<span className="text-sm text-theme-heading">¿Está lactando?</span>
</label>
<label className="flex items-center gap-3 p-3 border border-theme-border-strong rounded-xl cursor-pointer hover:bg-theme-bg">
<input
type="checkbox"
checked={form.uses_contraceptives}
onChange={(e) => updateForm({ uses_contraceptives: e.target.checked })}
className="w-4 h-4 rounded border-theme-border-strong text-theme-heading focus:ring-[#EDA588]"
/>
<span className="text-sm text-theme-heading">¿Usa anticonceptivos?</span>
</label>
<Input
label="Número de hijos"
type="number"
min={0}
value={String(form.children_count ?? 0)}
onChange={(e) => updateForm({ children_count: parseInt(e.target.value || '0', 10) })}
/>
</div>
</section>
<section>
<h4 className="text-sm font-semibold text-theme-heading mb-3 flex items-center">
<Stethoscope size={16} className="mr-2 text-theme-muted" /> Historia clínica
</h4>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
{[
['kidney_problems', 'Problemas de riñón'],
['back_pain', 'Dolor de espalda'],
['heart_disease', 'Enfermedades cardíacas'],
['respiratory_problems', 'Problemas respiratorios'],
['blood_pressure', 'Presión arterial'],
['diabetes', 'Diabetes'],
['thyroid', 'Problemas de tiroides'],
['colitis', 'Colitis'],
['constipation', 'Estreñimiento'],
['liver_problems', 'Problemas de hígado'],
['surgeries', 'Cirugías'],
['varicose_veins', 'Varices'],
['migraine', 'Migraña'],
['faints_with_needles', 'Se desmaya con agujas'],
].map(([key, label]) => {
const checked = !!form[key as keyof PatientForm];
return (
<label key={key} className="flex items-center gap-3 p-3 border border-theme-border-strong rounded-xl cursor-pointer hover:bg-theme-bg">
<input
type="checkbox"
checked={typeof checked === 'boolean' ? checked : false}
onChange={(e) => updateForm({ [key]: e.target.checked } as Partial<PatientForm>)}
className="w-4 h-4 rounded border-theme-border-strong text-theme-heading focus:ring-[#EDA588]"
/>
<span className="text-sm text-theme-heading">{label}</span>
</label>
);
})}
</div>
<div className="grid grid-cols-1 gap-4 mt-4">
<TextArea label="Alergias" value={form.allergies} onChange={(e) => updateForm({ allergies: e.target.value })} />
<TextArea label="Historial médico" value={form.medical_history} onChange={(e) => updateForm({ medical_history: e.target.value })} />
<TextArea label="Medicación actual" value={form.current_medication} onChange={(e) => updateForm({ current_medication: e.target.value })} />
<TextArea label="Detalle de cirugías" value={form.surgeries_notes} onChange={(e) => updateForm({ surgeries_notes: e.target.value })} />
<TextArea label="Notas médicas adicionales" value={form.medical_notes} onChange={(e) => updateForm({ medical_notes: e.target.value })} />
<TextArea label="Comentarios del paciente" value={form.patient_comments} onChange={(e) => updateForm({ patient_comments: e.target.value })} />
<TextArea label="Notas internas (solo equipo, no visible para el paciente)" value={form.internal_notes} onChange={(e) => updateForm({ internal_notes: e.target.value })} />
</div>
</section>
</div>
</Modal>
{/* Modal detalle */}
<Modal
isOpen={!!selected}
onClose={() => setSelected(null)}
title={selected?.name || 'Detalle del paciente'}
maxWidth="2xl"
>
<div className="space-y-4">
<div className="flex gap-2 border-b border-theme-border pb-2 overflow-x-auto">
{([
{ key: 'info', label: 'Información general' },
{ key: 'clinical', label: 'Historia clínica' },
{ key: 'appointments', label: 'Citas' },
{ key: 'sales', label: 'Ventas' },
] as const).map(({ key, label }) => (
<button
key={key}
onClick={() => setTab(key)}
className={`px-3 py-1.5 text-sm font-medium rounded-lg whitespace-nowrap ${
tab === key ? 'bg-[#1A1A1A] text-white' : 'text-theme-muted hover:bg-theme-bg'
}`}
>
{label}
</button>
))}
</div>
{historyLoading ? (
<Skeleton count={4} className="h-16 w-full" />
) : tab === 'info' ? (
<div className="space-y-4">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
<InfoRow label="Folio" value={selected?.patient_id} icon={FileText} />
<InfoRow label="Teléfono" value={selected?.phone} icon={Phone} />
<InfoRow label="Celular" value={selected?.mobile} icon={Phone} />
<InfoRow label="Email" value={selected?.email} icon={Mail} />
<InfoRow label="Teléfono casa" value={selected?.home_phone} icon={Phone} />
<InfoRow label="Fecha de nacimiento" value={selected?.birth_date} icon={Calendar} />
<InfoRow label="Edad" value={selected?.age} icon={Calendar} />
<InfoRow label="Género" value={formatGender(selected?.gender)} icon={User} />
<InfoRow label="Tipo sanguíneo" value={formatBlood(selected?.blood_type)} icon={Droplet} />
<InfoRow label="Lugar de nacimiento" value={selected?.birthplace} icon={MapPin} />
<InfoRow label="Empleo / Ocupación" value={selected?.occupation} icon={Briefcase} />
<InfoRow label="Estado civil" value={formatMarital(selected?.marital_status)} icon={Users} />
<InfoRow label="Contacto de emergencia" value={selected?.emergency_contact} icon={AlertCircle} />
<InfoRow label="Tel. emergencia" value={selected?.emergency_phone} icon={Phone} />
<InfoRow label="Recomendado por" value={selected?.referred_by} icon={Users} />
<InfoRow label="Puntos" value={`${selected?.wallet_points ?? 0} pts`} icon={Heart} />
</div>
{selected?.address_notes && (
<div className="p-3 bg-theme-bg rounded-xl">
<p className="text-xs text-theme-muted mb-1">Dirección</p>
<p className="text-sm text-theme-heading whitespace-pre-wrap">{selected.address_notes}</p>
</div>
)}
{selected?.patient_comments && (
<div className="p-3 bg-theme-bg rounded-xl">
<p className="text-xs text-theme-muted mb-1">Comentarios</p>
<p className="text-sm text-theme-heading whitespace-pre-wrap">{selected.patient_comments}</p>
</div>
)}
{selected?.internal_notes && (
<div className="p-3 bg-amber-50 border border-theme-bg rounded-xl">
<p className="text-xs text-theme-warning-text mb-1">🔒 Notas internas (solo equipo)</p>
<p className="text-sm text-theme-heading whitespace-pre-wrap">{selected.internal_notes}</p>
</div>
)}
</div>
) : tab === 'clinical' ? (
<div className="space-y-4">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
<ClinicalItem label="Embarazo" value={selected?.is_pregnant} />
<ClinicalItem label="Lactancia" value={selected?.is_breastfeeding} />
<ClinicalItem label="Anticonceptivos" value={selected?.uses_contraceptives} />
<ClinicalItem label="Problemas de riñón" value={selected?.kidney_problems} />
<ClinicalItem label="Dolor de espalda" value={selected?.back_pain} />
<ClinicalItem label="Enfermedades cardíacas" value={selected?.heart_disease} />
<ClinicalItem label="Problemas respiratorios" value={selected?.respiratory_problems} />
<ClinicalItem label="Presión arterial" value={selected?.blood_pressure} />
<ClinicalItem label="Diabetes" value={selected?.diabetes} />
<ClinicalItem label="Problemas de tiroides" value={selected?.thyroid} />
<ClinicalItem label="Colitis" value={selected?.colitis} />
<ClinicalItem label="Estreñimiento" value={selected?.constipation} />
<ClinicalItem label="Problemas de hígado" value={selected?.liver_problems} />
<ClinicalItem label="Cirugías" value={selected?.surgeries} />
<ClinicalItem label="Varices" value={selected?.varicose_veins} />
<ClinicalItem label="Migraña" value={selected?.migraine} />
<ClinicalItem label="Se desmaya con agujas" value={selected?.faints_with_needles} />
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<InfoRow label="Número de hijos" value={selected?.children_count} />
</div>
{[
{ label: 'Alergias', value: selected?.allergies },
{ label: 'Historial médico', value: selected?.medical_history },
{ label: 'Medicación actual', value: selected?.current_medication },
{ label: 'Detalle de cirugías', value: selected?.surgeries_notes },
{ label: 'Notas médicas adicionales', value: selected?.medical_notes },
].map(({ label, value }) =>
value ? (
<div key={label} className="p-3 bg-theme-bg rounded-xl">
<p className="text-xs text-theme-muted mb-1">{label}</p>
<p className="text-sm text-theme-heading whitespace-pre-wrap">{value}</p>
</div>
) : null
)}
</div>
) : tab === 'appointments' ? (
history && history.appointments.length > 0 ? (
<div className="space-y-2">
{history.appointments.map((a) => (
<div key={a.id} className="p-3 border border-theme-border rounded-xl text-sm">
<div className="flex items-center justify-between">
<p className="font-medium text-theme-heading">
{a.date} {a.time}
</p>
<Badge variant={badgeForAppointmentState(a.state)}>{a.state}</Badge>
</div>
<p className="text-theme-muted mt-1">{a.service}</p>
</div>
))}
</div>
) : (
<EmptyState title="Sin citas" subtitle="Este paciente no tiene citas registradas." icon={<Activity size={28} />} />
)
) : (
history && history.sales.length > 0 ? (
<div className="space-y-2">
{history.sales.map((s) => (
<div key={s.id} className="p-3 border border-theme-border rounded-xl text-sm">
<div className="flex items-center justify-between">
<p className="font-medium text-theme-heading">{s.name}</p>
<p className="font-semibold text-theme-heading">${s.total}</p>
</div>
<p className="text-theme-muted mt-1">{s.date} — {s.state}</p>
</div>
))}
</div>
) : (
<EmptyState title="Sin ventas" subtitle="Este paciente no tiene ventas registradas." icon={<ShoppingBag size={28} />} />
)
)}
</div>
</Modal>
</Layout>
);
};
interface PatientForm {
name: string;
phone: string;
email: string;
birth_date: string;
gender: string;
blood_type: string;
birthplace: string;
occupation: string;
marital_status: string;
emergency_contact: string;
emergency_phone: string;
home_phone: string;
mobile: string;
address_notes: string;
referred_by: string;
patient_comments: string;
internal_notes: string;
allergies: string;
medical_history: string;
current_medication: string;
medical_notes: string;
surgeries_notes: string;
children_count: number;
is_pregnant: boolean;
is_breastfeeding: boolean;
uses_contraceptives: boolean;
kidney_problems: boolean;
back_pain: boolean;
heart_disease: boolean;
respiratory_problems: boolean;
blood_pressure: boolean;
diabetes: boolean;
thyroid: boolean;
colitis: boolean;
constipation: boolean;
liver_problems: boolean;
surgeries: boolean;
varicose_veins: boolean;
migraine: boolean;
faints_with_needles: boolean;
}
export default Pacientes;