Files
SKEEN-Proyecto/frontend/src/components/QuickLinksDock.tsx
Consultoría Alcaraz Salazar 01f6007e30 Mejoras integrales: visitas, POS, WACRM, reportes, temas y módulos legacy
- 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
2026-08-13 23:30:08 +00:00

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;