196 lines
8.1 KiB
TypeScript
196 lines
8.1 KiB
TypeScript
import type { FC } from 'react';
|
|
import { useEffect, useState } from 'react';
|
|
import { Cake, MessageCircle, Star, Download } from 'lucide-react';
|
|
import Layout from '../components/Layout';
|
|
import { Card, PageHeader, Button, toast } from '../components/ui';
|
|
import { odooApi } from '../services/odoo';
|
|
import type { Birthday } from '../services/odoo';
|
|
import { waLink } from '../lib/whatsapp';
|
|
import { exportToExcel } from '../lib/exporter';
|
|
|
|
type Period = 'today' | 'week' | 'month';
|
|
|
|
const PERIODS: { value: Period; label: string }[] = [
|
|
{ value: 'today', label: 'Hoy' },
|
|
{ value: 'week', label: 'Próximos 7 días' },
|
|
{ value: 'month', label: 'Este mes' },
|
|
];
|
|
|
|
const fmtMoney = (n: number) =>
|
|
(n || 0).toLocaleString('es-MX', { style: 'currency', currency: 'MXN', maximumFractionDigits: 0 });
|
|
|
|
const fmtDate = (s: string | null) => {
|
|
if (!s) return '—';
|
|
const d = new Date(`${s}T12:00:00`);
|
|
return d.toLocaleDateString('es-MX', { day: '2-digit', month: 'short' });
|
|
};
|
|
|
|
const waBirthdayText = (name: string) =>
|
|
`¡Feliz cumpleaños ${(name || '').split(' ')[0]}! Le saluda el equipo de SKEEN Derma Experts.`;
|
|
|
|
const Cumpleanos: FC = () => {
|
|
const [period, setPeriod] = useState<Period>('month');
|
|
const [items, setItems] = useState<Birthday[]>([]);
|
|
const [range, setRange] = useState<{ start: string; end: string } | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
let active = true;
|
|
setLoading(true);
|
|
odooApi
|
|
.getBirthdays(period)
|
|
.then((res) => {
|
|
if (!active) return;
|
|
setItems(res.birthdays || []);
|
|
setRange({ start: res.start, end: res.end });
|
|
})
|
|
.catch(() => {
|
|
if (!active) return;
|
|
setItems([]);
|
|
})
|
|
.finally(() => active && setLoading(false));
|
|
return () => {
|
|
active = false;
|
|
};
|
|
}, [period]);
|
|
|
|
const exportBirthdays = async () => {
|
|
try {
|
|
await exportToExcel({
|
|
filename: `cumpleaneros-skeen-${period}-${new Date().toISOString().split('T')[0]}`,
|
|
sheetName: 'Cumpleañeros',
|
|
title: 'Cumpleañeros SKEEN',
|
|
subtitle: `Periodo: ${period} · ${items.length} pacientes`,
|
|
columns: [
|
|
{ header: 'Nombre', key: 'name' },
|
|
{ header: 'Teléfono', key: 'phone' },
|
|
{ header: 'Fecha nacimiento', key: 'birth_date', format: 'date' },
|
|
{ header: 'Cumple el', key: 'occurs_on', format: 'date' },
|
|
{ header: 'Cumple años', key: 'turning_age', format: 'number' },
|
|
{ header: 'Última visita', key: 'last_visit', format: 'date' },
|
|
{ header: 'Total gastado', key: 'total_spent', format: 'currency' },
|
|
{ header: 'Adeudo', key: 'amount_due', format: 'currency' },
|
|
{ header: 'VIP', key: 'vip' },
|
|
],
|
|
rows: items.map((b) => ({
|
|
name: b.name, phone: b.phone, birth_date: b.birth_date, occurs_on: b.occurs_on,
|
|
turning_age: b.turning_age, last_visit: b.last_visit, total_spent: b.total_spent,
|
|
amount_due: b.amount_due, vip: b.is_vip ? 'Sí' : 'No',
|
|
})),
|
|
});
|
|
toast.success(`Excel descargado (${items.length} cumpleañeros)`);
|
|
} catch (err) {
|
|
toast.error('Error al exportar');
|
|
console.error(err);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Layout title="Cumpleañeros" subtitle="Pacientes que cumplen años">
|
|
<PageHeader title="Cumpleañeros" subtitle="Pacientes que cumplen años en el periodo">
|
|
<Button variant="outline" onClick={exportBirthdays} disabled={items.length === 0}>
|
|
<Download size={16} className="mr-2" />
|
|
Exportar CSV
|
|
</Button>
|
|
</PageHeader>
|
|
|
|
<Card className="mb-4">
|
|
<Card.Body>
|
|
<div className="flex flex-wrap items-center gap-2">
|
|
{PERIODS.map((p) => (
|
|
<button
|
|
key={p.value}
|
|
onClick={() => setPeriod(p.value)}
|
|
className={`px-4 py-2 rounded-full text-sm font-medium transition ${
|
|
period === p.value
|
|
? 'bg-[#1A1A1A] text-white'
|
|
: 'bg-theme-bg text-theme-muted hover:bg-theme-border'
|
|
}`}
|
|
>
|
|
{p.label}
|
|
</button>
|
|
))}
|
|
{range && (
|
|
<span className="ml-auto text-xs text-theme-muted">
|
|
{range.start} → {range.end} · {items.length} pacientes
|
|
</span>
|
|
)}
|
|
</div>
|
|
</Card.Body>
|
|
</Card>
|
|
|
|
<Card>
|
|
<Card.Body noPadding>
|
|
{loading ? (
|
|
<p className="p-6 text-sm text-theme-muted">Cargando...</p>
|
|
) : items.length === 0 ? (
|
|
<div className="p-10 text-center text-theme-muted">
|
|
<Cake className="mx-auto mb-2" size={28} />
|
|
<p className="text-sm">No hay cumpleañeros en este periodo.</p>
|
|
</div>
|
|
) : (
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full text-sm">
|
|
<thead className="bg-theme-bg text-theme-muted text-xs uppercase">
|
|
<tr>
|
|
<th className="text-left px-4 py-3">Fecha</th>
|
|
<th className="text-left px-4 py-3">Paciente</th>
|
|
<th className="text-left px-4 py-3">Cumple</th>
|
|
<th className="text-left px-4 py-3">Última visita</th>
|
|
<th className="text-right px-4 py-3">Total gastado</th>
|
|
<th className="text-right px-4 py-3">Adeudo</th>
|
|
<th className="px-4 py-3"></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-theme-border">
|
|
{items.map((b) => (
|
|
<tr key={b.id} className="hover:bg-theme-bg">
|
|
<td className="px-4 py-3 font-medium text-theme-heading">{fmtDate(b.occurs_on)}</td>
|
|
<td className="px-4 py-3">
|
|
<div className="flex items-center gap-2">
|
|
{b.is_vip && <Star size={13} className="text-amber-500 fill-amber-500" />}
|
|
<span className="text-theme-heading">{b.name}</span>
|
|
</div>
|
|
<span className="text-xs text-theme-muted">{b.phone}</span>
|
|
</td>
|
|
<td className="px-4 py-3 text-theme-muted">{b.turning_age} años</td>
|
|
<td className="px-4 py-3 text-theme-muted">{fmtDate(b.last_visit)}</td>
|
|
<td className="px-4 py-3 text-right text-theme-heading">{fmtMoney(b.total_spent)}</td>
|
|
<td className={`px-4 py-3 text-right ${b.amount_due > 0 ? 'text-rose-600' : 'text-theme-muted'}`}>
|
|
{fmtMoney(b.amount_due)}
|
|
</td>
|
|
<td className="px-4 py-3 text-right">
|
|
{waLink(b.whatsapp || b.phone, waBirthdayText(b.name)) ? (
|
|
<a
|
|
href={waLink(b.whatsapp || b.phone, waBirthdayText(b.name)) || '#'}
|
|
target="_blank"
|
|
rel="noreferrer"
|
|
className="inline-flex items-center gap-1 text-theme-heading hover:text-theme-accent text-xs font-medium"
|
|
title={`Abrir WhatsApp: ${b.whatsapp || b.phone}`}
|
|
>
|
|
<MessageCircle size={14} /> WhatsApp
|
|
</a>
|
|
) : (
|
|
<span
|
|
className="inline-flex items-center gap-1 text-xs font-medium text-theme-muted opacity-50 cursor-not-allowed"
|
|
title="Paciente sin teléfono registrado"
|
|
aria-disabled="true"
|
|
>
|
|
<MessageCircle size={14} /> WhatsApp
|
|
</span>
|
|
)}
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)}
|
|
</Card.Body>
|
|
</Card>
|
|
</Layout>
|
|
);
|
|
};
|
|
|
|
export default Cumpleanos;
|