UX Pacientes: drawer de ficha clicable global, cumpleanos en dashboard/agenda, badges VIP/alergias/adeudo en ficha y POS
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user