UX Pacientes: drawer de ficha clicable global, cumpleanos en dashboard/agenda, badges VIP/alergias/adeudo en ficha y POS

This commit is contained in:
2026-09-01 22:26:09 +00:00
parent 7261da3ab1
commit 2710d9d92d
9 changed files with 492 additions and 20 deletions

View File

@@ -12,6 +12,8 @@ import {
Activity,
TrendingUp,
ArrowRight,
Cake,
MessageCircle,
} from 'lucide-react';
import {
AreaChart,
@@ -39,7 +41,8 @@ import {
toast,
badgeForAppointmentState,
} from '../components/ui';
import { odooApi, type DashboardStats, type Appointment, type Service, type ChartPoint } from '../services/odoo';
import { odooApi, type DashboardStats, type Appointment, type Service, type ChartPoint, type Birthday } from '../services/odoo';
import { usePatientDrawer } from '../components/PatientDrawer';
import { useTheme } from '../lib/theme';
import { useAuth } from '../lib/auth';
@@ -92,6 +95,10 @@ const Dashboard: FC = () => {
const [topServices, setTopServices] = useState<{ service: string; qty: number; revenue: number }[]>([]);
const [aptReport, setAptReport] = useState<{ total: number; by_state: Record<string, number> } | null>(null);
const [loadingExtras, setLoadingExtras] = useState(true);
const { openPatient } = usePatientDrawer();
// Cumpleaños del día (endpoint /reports/birthdays?period=today)
const [birthdays, setBirthdays] = useState<Birthday[]>([]);
const [loadingBirthdays, setLoadingBirthdays] = useState(true);
useEffect(() => {
const loadStats = async () => {
@@ -173,11 +180,24 @@ const Dashboard: FC = () => {
}
};
const loadBirthdays = async () => {
try {
setLoadingBirthdays(true);
const res = await odooApi.getBirthdays('today');
if (res.status === 'success') setBirthdays(res.birthdays || []);
} catch (err) {
console.error(err);
} finally {
setLoadingBirthdays(false);
}
};
loadStats();
loadChart();
loadAppointments();
loadServices();
loadExtras();
loadBirthdays();
}, []);
const statsConfig = useMemo(
@@ -320,6 +340,61 @@ const Dashboard: FC = () => {
))}
</div>
{/* Cumpleaños de hoy */}
<Card className="mb-6 sm:mb-8">
<Card.Header>
<div>
<h3 className="font-heading text-lg text-theme-heading flex items-center gap-2">
<Cake size={18} className="text-pink-500" /> Cumpleaños de hoy
</h3>
<p className="text-xs text-theme-muted">Pacientes que cumplen años hoy — contáctalos</p>
</div>
<Button variant="ghost" size="sm" onClick={() => navigate('/cumpleanos')}>
Ver todos
<ArrowRight size={14} className="ml-1" />
</Button>
</Card.Header>
<Card.Body>
{loadingBirthdays ? (
<Skeleton count={2} className="h-10 w-full" />
) : birthdays.length === 0 ? (
<p className="text-sm text-theme-muted">Ningún paciente cumple años hoy.</p>
) : (
<div className="flex flex-wrap gap-2">
{birthdays.map((b) => (
<div
key={b.id}
className="flex items-center gap-2.5 rounded-xl border border-theme-border px-3 py-2"
>
<button
type="button"
onClick={() => openPatient(b.id)}
className="text-sm font-medium text-theme-heading hover:underline hover:text-theme-accent text-left"
title="Abrir ficha del paciente"
>
{b.name}
</button>
<span className="text-xs text-theme-muted">
cumple {b.turning_age}{b.is_vip ? ' · VIP' : ''}
</span>
{(b.whatsapp || b.phone) && (
<a
href={`https://wa.me/${(b.whatsapp || b.phone).replace(/\D/g, '')}`}
target="_blank"
rel="noreferrer"
className="inline-flex items-center gap-1 text-xs text-theme-heading underline underline-offset-2"
title={`WhatsApp ${b.whatsapp || b.phone}`}
>
<MessageCircle size={12} /> {b.whatsapp || b.phone}
</a>
)}
</div>
))}
</div>
)}
</Card.Body>
</Card>
{/* Chart + appointments */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 sm:gap-6 mb-6 sm:mb-8">
<Card>