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:
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user