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
This commit is contained in:
145
frontend/src/components/QuickLinksDock.tsx
Normal file
145
frontend/src/components/QuickLinksDock.tsx
Normal file
@@ -0,0 +1,145 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user