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

@@ -1,7 +1,8 @@
import type { FC } from 'react';
import { useEffect, useState, useCallback, useRef } from 'react';
import { Clock, CheckCircle, XCircle, CheckCheck, UserCheck, UserX, Plus, PackageCheck, Sparkles, RotateCcw, LayoutGrid, List, Rows3, CalendarRange, CalendarClock, ChevronLeft, ChevronRight, Ban, Trash2, Phone } from 'lucide-react';
import { Clock, CheckCircle, XCircle, CheckCheck, UserCheck, UserX, Plus, PackageCheck, Sparkles, RotateCcw, LayoutGrid, List, Rows3, CalendarRange, CalendarClock, ChevronLeft, ChevronRight, Ban, Trash2, Phone, Cake } from 'lucide-react';
import Layout from '../components/Layout';
import { usePatientDrawer } from '../components/PatientDrawer';
import {
Card,
Button,
@@ -44,6 +45,7 @@ const QUARTER_TIME_OPTIONS = (() => {
const Agenda: FC = () => {
const [searchParams] = useSearchParams();
const { user } = useAuth();
const { openPatient } = usePatientDrawer();
// Rol médico: agenda restringida a su propio hr.employee (filtro también en servidor)
const isMedico = user?.role === 'medico';
const medicoEmployeeId = isMedico && user?.employee_id ? user.employee_id : null;
@@ -202,6 +204,12 @@ const Agenda: FC = () => {
);
};
// Icono de cumpleaños: el paciente cumple años el día de la cita (viene del API)
const birthdayIcon = (apt: Appointment, size = 11) =>
apt.patient_birthday ? (
<Cake size={size} className="inline shrink-0 text-pink-500" aria-label="Cumpleaños del paciente" />
) : null;
const confirmTitle = (apt: Appointment): string => {
if (['arrived', 'in_progress', 'done'].includes(apt.state)) return '2da confirmación: llegó a la clínica';
if (apt.phone_confirmed) {
@@ -1260,6 +1268,7 @@ const Agenda: FC = () => {
>
<div className="font-semibold truncate">
<span title={confirmTitle(apt)} className="mr-1 align-middle">{confirmIndicator(apt)}</span>
{apt.patient_birthday && <span title="Cumpleaños del paciente" className="mr-0.5 align-middle">{birthdayIcon(apt)}</span>}
{apt.time} · {apt.patient}
</div>
<div className="truncate opacity-75">{apt.service}</div>
@@ -1366,6 +1375,7 @@ const Agenda: FC = () => {
>
<div className="font-semibold truncate">
<span title={confirmTitle(apt)} className="mr-0.5 align-middle">{confirmIndicator(apt, 10)}</span>
{apt.patient_birthday && <span title="Cumpleaños del paciente" className="mr-0.5 align-middle">{birthdayIcon(apt, 10)}</span>}
{apt.time} · {firstName(apt.patient)}
</div>
</div>
@@ -1461,6 +1471,7 @@ const Agenda: FC = () => {
className={`mb-0.5 rounded border-l-2 px-1 py-0.5 text-[10px] leading-tight shadow-card cursor-pointer transition-shadow hover:shadow-soft ${aptCategoryBar(apt)} ${gridStateClass[apt.state] || 'bg-theme-bg text-theme-heading'}`}
>
<span className="mr-0.5 align-middle">{confirmIndicator(apt, 10)}</span>
{apt.patient_birthday && <span title="Cumpleaños del paciente" className="mr-0.5 align-middle">{birthdayIcon(apt, 10)}</span>}
<span className="font-semibold">{apt.time}</span> {firstName(apt.patient)}
</div>
))
@@ -1496,7 +1507,17 @@ const Agenda: FC = () => {
<Clock size={14} className="inline mr-1 text-theme-muted" />
{apt.time}
</td>
<td className="p-3 text-sm font-medium text-theme-heading">{apt.patient}</td>
<td className="p-3 text-sm font-medium text-theme-heading">
<button
type="button"
onClick={() => openPatient(apt.patient_id)}
className="hover:underline hover:text-theme-accent text-left"
title="Abrir ficha del paciente"
>
{apt.patient_birthday && <span title="Cumpleaños del paciente" className="mr-1 align-middle">{birthdayIcon(apt, 12)}</span>}
{apt.patient}
</button>
</td>
<td className="p-3 text-sm text-theme-muted hidden sm:table-cell">
<button type="button" onClick={() => openAptDetail(apt)} className="hover:underline hover:text-theme-heading text-left">
{apt.service}
@@ -1911,7 +1932,19 @@ const Agenda: FC = () => {
<div className="grid grid-cols-2 gap-3">
<div className="p-3 bg-theme-bg rounded-xl">
<p className="text-xs text-theme-muted mb-1">Paciente</p>
<p className="text-sm font-medium text-theme-heading">{detailApt.patient}</p>
<button
type="button"
onClick={() => openPatient(detailApt.patient_id)}
className="text-sm font-medium text-theme-heading underline underline-offset-2 hover:text-theme-accent text-left"
title="Abrir ficha del paciente"
>
{detailApt.patient}
</button>
{detailApt.patient_birthday && (
<span className="ml-1.5 inline-flex items-center gap-1 rounded-full bg-pink-50 border border-pink-300 px-2 py-0.5 text-[10px] font-medium text-pink-600 align-middle">
{birthdayIcon(detailApt, 10)} Cumpleaños hoy
</span>
)}
</div>
<div className="p-3 bg-theme-bg rounded-xl">
<p className="text-xs text-theme-muted mb-1">Servicio</p>