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:
2026-08-13 23:30:08 +00:00
parent a718592291
commit 01f6007e30
109 changed files with 19067 additions and 931 deletions

View File

@@ -1,11 +1,21 @@
import type { FC } from 'react';
import { useCallback, useEffect, useState } from 'react';
import { useSearchParams } from 'react-router-dom';
import { Boxes, Plus, AlertTriangle, PackageCheck, ArrowDownToLine, ArrowUpFromLine, History } from 'lucide-react';
import Layout from '../components/Layout';
import MovimientosPanel from '../components/inventario/MovimientosPanel';
import AlertasPanel from '../components/inventario/AlertasPanel';
import { Card, Button, Input, Select, Modal, PageHeader, toast } from '../components/ui';
import { odooApi } from '../services/odoo';
import type { InventoryItem, InventoryMove, InventoryKind, InventoryLevel, InventoryMoveType, InventorySummary } from '../services/odoo';
const TABS = [
{ key: 'items', label: 'Items' },
{ key: 'compras', label: 'Compras' },
{ key: 'bajas', label: 'Bajas' },
{ key: 'alertas', label: 'Alertas' },
] as const;
const fmtMoney = (n: number) =>
(n || 0).toLocaleString('es-MX', { style: 'currency', currency: 'MXN', maximumFractionDigits: 0 });
@@ -50,6 +60,9 @@ const emptyItem: Partial<InventoryItem> = {
};
const Inventario: FC = () => {
const [searchParams, setSearchParams] = useSearchParams();
const tab = searchParams.get('tab') || 'items';
const setTab = (t: string) => setSearchParams({ tab: t });
const [items, setItems] = useState<InventoryItem[]>([]);
const [summary, setSummary] = useState<InventorySummary>({ levels: { out: 0, critical: 0, low: 0, optimal: 0 }, total_value: 0, count: 0 });
const [loading, setLoading] = useState(true);
@@ -71,6 +84,7 @@ const Inventario: FC = () => {
const [moves, setMoves] = useState<InventoryMove[]>([]);
const load = useCallback(async () => {
if (tab !== 'items') return;
setLoading(true);
try {
const res = await odooApi.getInventory({
@@ -85,7 +99,7 @@ const Inventario: FC = () => {
} finally {
setLoading(false);
}
}, [kind, level, search]);
}, [kind, level, search, tab]);
useEffect(() => { load(); }, [load]);
@@ -147,11 +161,37 @@ const Inventario: FC = () => {
title="Inventario y consumibles"
subtitle="Existencias, niveles y movimientos (compras, ventas, bajas, ajustes)"
>
<Button onClick={() => setNewOpen(true)}>
<Plus size={16} className="mr-2" /> Nuevo item
</Button>
{tab === 'items' && (
<Button onClick={() => setNewOpen(true)}>
<Plus size={16} className="mr-2" /> Nuevo item
</Button>
)}
</PageHeader>
{/* Tabs */}
<div className="flex flex-wrap gap-1.5 mb-4">
{TABS.map((t) => (
<button
key={t.key}
type="button"
onClick={() => setTab(t.key)}
className={`px-4 py-2 text-sm font-medium rounded-full transition ${
tab === t.key ? 'bg-theme-accent text-theme-inverse' : 'bg-theme-accent-bg text-theme-heading hover:bg-theme-bg'
}`}
>
{t.label}
</button>
))}
</div>
{tab === 'compras' ? (
<MovimientosPanel tipo="compra" />
) : tab === 'bajas' ? (
<MovimientosPanel tipo="baja" />
) : tab === 'alertas' ? (
<AlertasPanel />
) : (
<>
{/* KPIs */}
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3 sm:gap-4 mb-4">
<Card><Card.Body>
@@ -308,6 +348,8 @@ const Inventario: FC = () => {
</div>
)}
</Modal>
</>
)}
</Layout>
);
};