- Módulo Visitas completo (auto desde agenda, insumos con descargo de inventario, fotos antes/después y documentos, receta imprimible) - Punto de Venta (catálogo + ticket sticky, cobro con cambio, pago con puntos monedero, ticket imprimible) - WACRM: leads automáticos desde WhatsApp, asignación de conversaciones y leads a agentes, conversión lead→paciente, ficha del paciente en chat - Pacientes: completitud de expediente, alertas clínicas, historial unificado con detalle, foto, WhatsApp, estado de cuenta, filtros rápidos (VIP/recientes/médico), documentos (expediente escaneado + galería) - Agenda: vistas por médico y por hora, filtros rápidos (libres, primera vez, check-in, no-show), modal de acciones, bloqueos por médico, drag&drop para mover citas - Reportes: 18 pestañas (diario, cortes, ingresos, inventario, adeudos, comisiones, pagos, devoluciones, top clientes, horas, paquetes, vendedores, concentrado, recomendaciones, KPIs) con exportación Excel - Temas: nuevo tema Clásico (look legacy AdminLTE) con submenús tipo treeview, selector de tema; accesos rápidos personalizables con 3 presentaciones; búsqueda global; notificaciones reales - Configuración: secciones (clínica, usuarios con permisos por sección, recetas, catálogos de diagnósticos y procedimientos) - Inventario: alertas de caducidad y sugerencia de compra, cron diario que descuenta artículos caducados, compras/bajas - Consultas Médicas, página Expedientes, importadores delta (citas/visitas legacy idempotentes), depuración de duplicados - Infra: tema Tailwind conectado (@config), gzip en nginx, secuencias Odoo corregidas (noupdate, company_id), rollback en validaciones
146 lines
5.3 KiB
TypeScript
146 lines
5.3 KiB
TypeScript
import type { FC } from 'react';
|
|
import { useEffect, useRef, useState } from 'react';
|
|
import { Link, useLocation } from 'react-router-dom';
|
|
import { List, PanelRight, PanelBottom, Settings2 } from 'lucide-react';
|
|
import { useQuickLinks, QUICK_LINKS_MODE_LABELS, type QuickLinksMode } from '../lib/quicklinks';
|
|
|
|
interface DockItem {
|
|
label: string;
|
|
href: string;
|
|
icon: FC<{ size?: number | string; className?: string }>;
|
|
}
|
|
|
|
interface QuickLinksDockProps {
|
|
/** Items visibles del menú (para resolver icono/label de cada href fijado) */
|
|
items: DockItem[];
|
|
}
|
|
|
|
const MODE_ICONS: Record<QuickLinksMode, FC<{ size?: number | string; className?: string }>> = {
|
|
menu: List,
|
|
right: PanelRight,
|
|
bottom: PanelBottom,
|
|
};
|
|
|
|
/** Selector de presentación de los accesos rápidos (menú / derecha / abajo). */
|
|
export const QuickLinksModePicker: FC<{ direction?: 'up' | 'down'; align?: 'left' | 'right' }> = ({
|
|
direction = 'up',
|
|
align = 'right',
|
|
}) => {
|
|
const quick = useQuickLinks();
|
|
const [open, setOpen] = useState(false);
|
|
const ref = useRef<HTMLDivElement>(null);
|
|
|
|
useEffect(() => {
|
|
if (!open) return;
|
|
const close = (e: MouseEvent) => {
|
|
if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false);
|
|
};
|
|
document.addEventListener('mousedown', close);
|
|
return () => document.removeEventListener('mousedown', close);
|
|
}, [open]);
|
|
|
|
return (
|
|
<div ref={ref} className="relative">
|
|
<button
|
|
type="button"
|
|
onClick={() => setOpen(!open)}
|
|
className="flex items-center justify-center w-8 h-8 rounded-xl text-theme-muted hover:bg-theme-accent-bg hover:text-theme-heading transition-colors"
|
|
title="Cambiar presentación de accesos rápidos"
|
|
>
|
|
<Settings2 size={15} />
|
|
</button>
|
|
{open && (
|
|
<div
|
|
className={`absolute z-50 w-56 rounded-2xl border border-theme-border bg-theme-surface shadow-card p-1.5 ${
|
|
direction === 'up' ? 'bottom-full mb-2' : 'top-full mt-2'
|
|
} ${align === 'right' ? 'right-0' : 'left-0'}`}
|
|
>
|
|
<p className="px-2.5 py-1.5 text-[10px] uppercase tracking-wider text-theme-muted font-medium">
|
|
Mostrar accesos rápidos
|
|
</p>
|
|
{(Object.keys(MODE_ICONS) as QuickLinksMode[]).map((m) => {
|
|
const ModeIcon = MODE_ICONS[m];
|
|
const selected = quick.mode === m;
|
|
return (
|
|
<button
|
|
key={m}
|
|
type="button"
|
|
onClick={() => { quick.setMode(m); setOpen(false); }}
|
|
className={`w-full flex items-center gap-2.5 px-2.5 py-2 rounded-xl text-sm transition-colors ${
|
|
selected ? 'bg-theme-accent-bg text-theme-heading font-medium' : 'text-theme-muted hover:bg-theme-bg hover:text-theme-heading'
|
|
}`}
|
|
>
|
|
<ModeIcon size={15} />
|
|
{QUICK_LINKS_MODE_LABELS[m]}
|
|
{selected && <span className="ml-auto text-brand-coral">●</span>}
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Barra flotante de accesos rápidos (iconos), a la derecha o abajo según
|
|
* la preferencia del usuario. En modo 'menu' no se muestra (la sección
|
|
* va dentro del sidebar de cada layout).
|
|
*/
|
|
export const QuickLinksDock: FC<QuickLinksDockProps> = ({ items }) => {
|
|
const location = useLocation();
|
|
const quick = useQuickLinks();
|
|
|
|
if (quick.mode === 'menu') return null;
|
|
|
|
const dockItems = quick.links
|
|
.map((href) => items.find((it) => it.href === href))
|
|
.filter((it): it is DockItem => it !== undefined);
|
|
|
|
const isActive = (href: string) =>
|
|
href === '/' ? location.pathname === '/' : location.pathname === href || location.pathname.startsWith(`${href}/`);
|
|
|
|
const isRight = quick.mode === 'right';
|
|
|
|
return (
|
|
<nav
|
|
aria-label="Accesos rápidos"
|
|
className={
|
|
isRight
|
|
? 'fixed right-0 top-1/2 -translate-y-1/2 z-40 hidden lg:flex flex-col items-center gap-1 py-2 px-1.5 mr-2 rounded-2xl border border-theme-border bg-theme-surface shadow-card'
|
|
: 'fixed bottom-3 left-1/2 -translate-x-1/2 z-40 flex items-center gap-1 px-2 py-1.5 rounded-2xl border border-theme-border bg-theme-surface shadow-card'
|
|
}
|
|
>
|
|
{dockItems.length === 0 && (
|
|
<span className={`text-[11px] text-theme-muted px-2 ${isRight ? 'py-2 [writing-mode:vertical-rl]' : ''}`}>
|
|
Fija accesos con la ★ del menú
|
|
</span>
|
|
)}
|
|
{dockItems.map((item) => {
|
|
const Icon = item.icon;
|
|
const active = isActive(item.href);
|
|
return (
|
|
<Link
|
|
key={item.href}
|
|
to={item.href}
|
|
title={item.label}
|
|
aria-label={item.label}
|
|
className={`flex items-center justify-center w-10 h-10 rounded-xl transition-colors ${
|
|
active
|
|
? 'bg-theme-accent text-theme-inverse shadow-soft'
|
|
: 'text-theme-muted hover:bg-theme-accent-bg hover:text-theme-heading'
|
|
}`}
|
|
>
|
|
<Icon size={18} />
|
|
</Link>
|
|
);
|
|
})}
|
|
<div className={isRight ? 'border-t border-theme-border pt-1 mt-1' : 'border-l border-theme-border pl-1 ml-1'}>
|
|
<QuickLinksModePicker direction={isRight ? 'down' : 'up'} align="right" />
|
|
</div>
|
|
</nav>
|
|
);
|
|
};
|
|
|
|
export default QuickLinksDock;
|