Personalizacion cliente tagumedica: permisos, refactor de vistas y limpieza de tools
- Agrega permiso de menu para vania.barrios@tagumedica.com - Renombra test/ a tools/ (colecciones de requests SAP/ApiGateway) - Renombra vistas de frontend con sufijo View para consistencia - Refactor de componentes UI compartidos y agrega utilidades (Icon, PageLoader, SecondaryButton, FieldTooltip, errors)
This commit is contained in:
1 parent
266af59bc6
commit
fe22681c66
73 files changed
+917
-367
No files matched your search
@@ -18,5 +18,8 @@
|
||||
"/inventario/lotes",
|
||||
"/ventas/*",
|
||||
"/compras/*"
|
||||
],
|
||||
"vania.barrios@tagumedica.com":[
|
||||
"/inventario/lotes"
|
||||
]
|
||||
}
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -128,6 +128,8 @@ App.tsx (BrowserRouter)
|
||||
|
||||
## Campos del Formulario (nombres SAP, sin mapeo)
|
||||
|
||||
El label de cada campo muestra el nombre técnico SAP en un tooltip (ícono ⓘ, prop `dbField`) en vez de mostrarlo dentro del propio texto del label — ver `FieldTooltip.tsx` y la misma convención en `flujo-lotes.md`.
|
||||
|
||||
| Campo SAP | Tipo | Componente | Notas |
|
||||
|---|---|---|---|
|
||||
| `ItemCode` | input text | FormattedField | obligatorio, deshabilitado implícitamente en edición (se usa como key de URL) |
|
||||
|
||||
@@ -69,12 +69,30 @@ Cada vista de mantenimiento (`Orders.tsx`, `Invoices.tsx`, `PurchaseOrders.tsx`,
|
||||
| `titulo` | Encabezado visible del formulario | `"Factura de Ventas (OINV)"` |
|
||||
| `headerExtraFields` | Campos de cabecera exclusivos de ese documento | Facturas: `FolioPrefixString`, `FolioNumber`, `GroupNum` |
|
||||
| `lineColumns` | Columnas de la grilla de líneas, cada una con un `show(doc)` opcional para ocultar/mostrar según `DocType` | Compras oculta `ItemCode`/`Almacén` si `DocType` es de servicio |
|
||||
| `copyFromSources` | Documentos origen habilitados para el botón "📋 Copiar de" | Facturas de Venta: Orden de Venta y Entrega; Facturas de Proveedor: Orden de Compra |
|
||||
| `copyFromSources` | Documentos origen habilitados para el botón "Copiar de" (ícono de portapapeles) | Facturas de Venta: Orden de Venta y Entrega; Facturas de Proveedor: Orden de Compra |
|
||||
| `retencionConfig` / `detraccionConfig` | Solo presentes en `PurchaseInvoices.tsx` — habilitan los bloques de Retención (3%/6%) y Detracción (SUNAT) en el resumen de totales | No se pasan en Órdenes ni en Facturas de Venta |
|
||||
| `onBuscarSocio`, `onBuscarArticulos`, `onCellSearch` | Callbacks que abren los `ModalSearchTemplate` de Socios, Artículos, Almacén y Código de Impuesto | Cada vista instancia sus propios modales y decide qué endpoint/filtro usar |
|
||||
|
||||
El cálculo de totales (subtotal, impuesto 18% fijo, retención, detracción, total general) vive dentro del propio `MarketingDocumentForm`, no en las vistas.
|
||||
|
||||
Los íconos de acción (búsqueda, copiar de, catálogo, eliminar línea) son SVG inline definidos en `src/components/ui/Icon.tsx` — no hay emojis ni dependencias externas de iconografía en el proyecto.
|
||||
|
||||
### Campo técnico en tooltip, no en el label
|
||||
|
||||
Cada `CampoConfig`/`HeaderExtraFieldConfig` de `genericHeaderFields` acepta un `dbField` (= su propio `name`, ya que ambos son literalmente el nombre del campo en `MarketingDocument`) que `FormattedField`/`SelectField` muestran como un ícono ⓘ junto al label; al pasar el mouse aparece el nombre técnico SAP (ej. `DocEntry`, `CardCode`). Antes el label concatenaba ambos textos ("Documento (DocEntry)"), lo que a veces partía el campo en dos líneas y deformaba la grilla — ver `FieldTooltip.tsx`.
|
||||
|
||||
### Campos de solo lectura (`readOnly` / `readOnlyOnUpdate`)
|
||||
|
||||
`CampoConfig` soporta dos flags que `MarketingDocumentForm` traduce a `mode="display"` (o `disabled` en los `SelectField`):
|
||||
|
||||
| Campo | Flag | Comportamiento |
|
||||
|---|---|---|
|
||||
| `DocEntry` | `readOnly: true` | Nunca editable (ni al crear ni al actualizar) — lo asigna SAP |
|
||||
| `DocNum` | `readOnlyOnUpdate: true` | Editable al crear, bloqueado al actualizar |
|
||||
| `CardCode` | `readOnlyOnUpdate: true` | Editable al crear, bloqueado al actualizar; además oculta el botón de búsqueda de socio cuando está bloqueado |
|
||||
|
||||
`readOnlyOnUpdate` se evalúa contra `isEditMode` (`!!docEntry`), definido dentro del propio `MarketingDocumentForm`.
|
||||
|
||||
## Flujo: Crear una Factura de Venta copiando una Orden
|
||||
|
||||
```mermaid
|
||||
@@ -96,11 +114,11 @@ sequenceDiagram
|
||||
Backend->>SAP: GET Currencies
|
||||
SAP-->>Form: lista de monedas
|
||||
|
||||
User->>Form: busca y selecciona Cliente (🔍)
|
||||
User->>Form: busca y selecciona Cliente (ícono de lupa)
|
||||
Form->>View: onBuscarSocio() abre ModalSearchTemplate(BusinessPartners)
|
||||
View-->>Form: setDoc({CardCode, CardName})
|
||||
|
||||
User->>Form: clic "📋 Copiar de" → "Orden de Venta"
|
||||
User->>Form: clic "Copiar de" → "Orden de Venta"
|
||||
Form->>Copy: <CopyFromModal source={Orden} cardCode docType />
|
||||
Copy->>API: GET /api/Orders?filtro por CardCode/BaseType
|
||||
API->>Backend: fetch
|
||||
@@ -143,11 +161,16 @@ App.tsx (BrowserRouter)
|
||||
└── /mantenimiento → PurchaseInvoices.tsx → MarketingDocumentForm (+ Retención/Detracción)
|
||||
|
||||
MarketingDocumentForm.tsx
|
||||
├── FormattedField.tsx / SelectField.tsx (cabecera)
|
||||
├── FormattedField.tsx / SelectField.tsx (cabecera, con FieldTooltip por campo)
|
||||
├── PrimaryButton.tsx / SecondaryButton.tsx (submit / cancelar)
|
||||
├── Icon.tsx (íconos SVG: buscar, copiar, catálogo, eliminar)
|
||||
├── PageLoader.tsx ("Cargando documento..." mientras hace fetch)
|
||||
├── CopyFromModal.tsx (botón "Copiar de")
|
||||
└── (por vista) ModalSearchTemplate.tsx ×4 (Socios, Artículos, Almacén, Código de Impuesto)
|
||||
```
|
||||
|
||||
Cada vista de mantenimiento (`Orders`, `Invoices`, `PurchaseOrders`, `PurchaseInvoices`) se carga con `React.lazy()` desde `App.tsx`: el JS de un módulo no se descarga hasta que el usuario navega a él (ver `docs/index.md`).
|
||||
|
||||
## Archivos Clave
|
||||
|
||||
| Archivo | Ruta | Propósito |
|
||||
@@ -158,8 +181,11 @@ MarketingDocumentForm.tsx
|
||||
| `OrdersList.tsx`, `InvoicesList.tsx`, `PurchaseOrdersList.tsx`, `PurchaseInvoicesList.tsx` | `src/views/ventas/`, `src/views/compras/` | Listados vía `ListViewTemplate`, con `StatusBadge` por estado del documento |
|
||||
| `CopyFromModal.tsx` | `src/components/ui/` | Modal para copiar líneas desde un documento base (Orden→Entrega→Factura) |
|
||||
| `ModalSearchTemplate.tsx` | `src/components/ui/` | Modal de búsqueda genérico reutilizado para Socios, Artículos, Almacenes, Códigos de Impuesto |
|
||||
| `sapMarketing.ts` | `src/types/` | `MarketingDocument`, `DocumentLine`, `DocumentConfigProps`, `CopyFromSource`, `LineColumnConfig` |
|
||||
| `sapMarketing.ts` | `src/types/` | `MarketingDocument`, `DocumentLine`, `DocumentConfigProps`, `CopyFromSource`, `LineColumnConfig` (`key`/`name` tipados como `keyof DocumentLine`/`keyof MarketingDocument`, no `string` suelto) |
|
||||
| `sunat.ts` | `src/utils/` | `DETRACCION_CODIGOS` (porcentajes SUNAT usados en Facturas de Proveedor) |
|
||||
| `Icon.tsx` | `src/components/ui/` | Registro de íconos SVG inline (reemplaza los emojis que se usaban antes en botones y menú) |
|
||||
| `FieldTooltip.tsx` | `src/components/ui/` | Ícono ⓘ con el nombre técnico SAP del campo, usado por `FormattedField`/`SelectField` |
|
||||
| `errors.ts` | `src/utils/` | `getErrorMessage(err, fallback)` — evita `catch (err: any)` en cada formulario |
|
||||
| `sales.py` / `purchases.py` | `mini-backend/routers/` | Endpoints REST que proxean a SAP Service Layer |
|
||||
|
||||
## Hallazgos (código tal cual está hoy, sin corregir)
|
||||
|
||||
@@ -124,6 +124,8 @@ App.tsx (BrowserRouter)
|
||||
|
||||
## Mapeo de Nombres SAP → Friendly
|
||||
|
||||
El label visible ya no concatena el nombre técnico (antes: "Estado de Aprobación (U_SIS_ESTLOT)", partiéndose en 2 líneas y deformando la grilla). Ahora el label muestra solo el nombre amigable y el campo SAP aparece en un tooltip (ícono ⓘ) vía el prop `dbField` de `FormattedField`/`SelectField` — ver `FieldTooltip.tsx`.
|
||||
|
||||
| Campo SAP | Nombre Amigable | Tipo | Componente |
|
||||
|---|---|---|---|
|
||||
| `Batch` | `Batch` | display | FormattedField |
|
||||
|
||||
@@ -143,7 +143,7 @@ App.tsx (BrowserRouter)
|
||||
|
||||
## Campos del Formulario
|
||||
|
||||
A diferencia del módulo de Lotes, acá **no hay mapeo de nombres SAP → amigable**: el formulario usa los nombres de campo de SAP directamente como `name` de cada input (`CardCode`, `CardName`, `FederalTaxID`, `Phone1`, `EmailAddress`).
|
||||
A diferencia del módulo de Lotes, acá **no hay mapeo de nombres SAP → amigable**: el formulario usa los nombres de campo de SAP directamente como `name` de cada input (`CardCode`, `CardName`, `FederalTaxID`, `Phone1`, `EmailAddress`). El label visible sí es un texto amigable en español; el nombre técnico SAP se muestra en un tooltip (ícono ⓘ, componente `FieldTooltip.tsx`) en vez de ir concatenado al label.
|
||||
|
||||
| Campo SAP | Pestaña | Tipo | Notas |
|
||||
| ------------------------- | ------------ | ------------------------------ | ------------------------------------------------------------- |
|
||||
|
||||
@@ -18,3 +18,14 @@ SPA en React 19 + TypeScript + Vite + Tailwind CSS + React Router. Consume la AP
|
||||
- Todos los documentos siguen el mismo formato: diagrama de arquitectura (mermaid), diagrama de secuencia del flujo principal, árbol de componentes y tabla de "Archivos Clave".
|
||||
- Ver también `mini-erp/CLAUDE.md` (raíz del repo) para la arquitectura general del monorepo (`mini-backend` + `mini-frontend`) y el flujo de trabajo por cliente.
|
||||
- Ver `mini-backend/docs/` para la documentación del backend (API, autenticación, proxy SAP, logging).
|
||||
|
||||
## Rendimiento: carga por ruta (code splitting)
|
||||
|
||||
`App.tsx` no importa las vistas de forma directa: cada una se carga con `React.lazy()`, agrupadas por módulo (Inventario, Socios, Ventas, Compras, Informes), y `<Suspense fallback={<PageLoader />}>` envuelve todo el árbol de rutas. Así el usuario solo descarga el JS de la pantalla que realmente visita, en vez de un único bundle con las 20+ vistas del ERP. `SapLogin` y `MainLayout` (el "shell" de la app) se mantienen fuera de esto porque se necesitan en casi toda visita.
|
||||
|
||||
## Componentes UI compartidos relevantes
|
||||
|
||||
- `Icon.tsx` (`src/components/ui/`) — registro de íconos SVG inline (buscar, copiar, editar, eliminar, etc.). No hay emojis ni librerías de iconografía externas en el proyecto.
|
||||
- `FieldTooltip.tsx` — ícono ⓘ junto al label de un campo, muestra el nombre técnico SAP (prop `dbField` de `FormattedField`/`SelectField`) sin concatenarlo al texto del label.
|
||||
- `PageLoader.tsx` — estado de carga a pantalla completa, reutilizado como fallback de `Suspense` y en los formularios de detalle mientras traen sus datos.
|
||||
- `PrimaryButton.tsx` / `SecondaryButton.tsx` — botones de acción con dos tamaños (`size="sm"` para barras de herramientas, `size="md"` para pies de formulario).
|
||||
@@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>sap-frontend</title>
|
||||
<title>Mini ERP · SAP Business One</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
+174
-78
@@ -1,44 +1,93 @@
|
||||
import { lazy, Suspense } from "react";
|
||||
import type { ReactNode } from "react";
|
||||
import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom";
|
||||
import SapLogin from "./components/SapLogin";
|
||||
import SapLogin from "./components/SapLoginForm";
|
||||
import MainLayout from "./layout/MainLayout";
|
||||
import PageLoader from "./components/ui/PageLoader";
|
||||
import { getToken } from "./utils/api";
|
||||
import CustomersList from "./views/socios/CustomersList";
|
||||
import SuppliersList from "./views/socios/SuppliersList";
|
||||
import Supplier from "./views/socios/Suppliers";
|
||||
import Customer from "./views/socios/Customers";
|
||||
import InvoicesList from "./views/ventas/InvoicesList";
|
||||
import Invoices from "./views/ventas/Invoices";
|
||||
import OrdersList from "./views/ventas/OrdersList";
|
||||
import Orders from "./views/ventas/Orders";
|
||||
import PurchaseOrdersList from "./views/compras/PurchaseOrdersList";
|
||||
import PurchaseOrders from "./views/compras/PurchaseOrders";
|
||||
import PurchaseInvoicesList from "./views/compras/PurchaseInvoicesList";
|
||||
import PurchaseInvoices from "./views/compras/PurchaseInvoices";
|
||||
import ItemsList from "./views/inventario/ItemsList";
|
||||
import Items from "./views/inventario/Items";
|
||||
import BatchNumberDetailsList from "./views/inventario/BatchNumberDetailsList";
|
||||
import BatchNumberDetails from "./views/inventario/BatchNumberDetails";
|
||||
import Dashboard from "./views/Dashboard";
|
||||
import ModuleReportsList from "./views/informes/ModuleReportsList";
|
||||
import ReportRunner from "./components/report/ReportRunner";
|
||||
import CrystalReportsList from "./views/reportes/CrystalReportsList";
|
||||
import CrystalReportRunner from "./components/report/CrystalReportRunner";
|
||||
import HtmlReportsList from "./views/reportes/HtmlReportsList";
|
||||
import HtmlReportRunner from "./components/report/HtmlReportRunner";
|
||||
|
||||
const Delivery = () => (
|
||||
<div className="p-4">
|
||||
<h1 className="text-xl font-bold text-[var(--color-sidebar-title)]">Entregas / Guías</h1>
|
||||
</div>
|
||||
// Cada vista se carga de forma diferida (code splitting por ruta): así el
|
||||
// usuario solo descarga el código de la pantalla que realmente visita, en
|
||||
// vez de recibir en un único bundle el JS de los 20+ módulos del ERP.
|
||||
// SapLogin y MainLayout quedan fuera de esto porque son el "shell" que se
|
||||
// necesita en (casi) toda visita, sin importar el módulo.
|
||||
|
||||
// 0. Dashboard
|
||||
const Dashboard = lazy(() => import("./views/DashboardView"));
|
||||
|
||||
// 1. Inventario
|
||||
const ItemsList = lazy(() => import("./views/inventario/ItemsList"));
|
||||
const Items = lazy(() => import("./views/inventario/ItemsView"));
|
||||
const BatchNumberDetailsList = lazy(
|
||||
() => import("./views/inventario/BatchNumberDetailsList"),
|
||||
);
|
||||
const BatchNumberDetails = lazy(
|
||||
() => import("./views/inventario/BatchNumberDetailsView"),
|
||||
);
|
||||
|
||||
const Receiving = () => (
|
||||
<div className="p-4">
|
||||
<h1 className="text-xl font-bold text-[var(--color-sidebar-title)]">Recepciones de Compra</h1>
|
||||
</div>
|
||||
// 2. Socios de Negocios
|
||||
const CustomersList = lazy(() => import("./views/socios/CustomersList"));
|
||||
const Customer = lazy(() => import("./views/socios/CustomersView"));
|
||||
const SuppliersList = lazy(() => import("./views/socios/SuppliersList"));
|
||||
const Supplier = lazy(() => import("./views/socios/SuppliersView"));
|
||||
|
||||
// 3. Ventas
|
||||
const OrdersList = lazy(() => import("./views/ventas/OrdersList"));
|
||||
const Orders = lazy(() => import("./views/ventas/OrdersView"));
|
||||
const InvoicesList = lazy(() => import("./views/ventas/InvoicesList"));
|
||||
const Invoices = lazy(() => import("./views/ventas/InvoicesView"));
|
||||
|
||||
// 4. Compras
|
||||
const PurchaseOrdersList = lazy(
|
||||
() => import("./views/compras/PurchaseOrdersList"),
|
||||
);
|
||||
const PurchaseOrders = lazy(() => import("./views/compras/PurchaseOrdersView"));
|
||||
const PurchaseInvoicesList = lazy(
|
||||
() => import("./views/compras/PurchaseInvoicesList"),
|
||||
);
|
||||
const PurchaseInvoices = lazy(
|
||||
() => import("./views/compras/PurchaseInvoicesView"),
|
||||
);
|
||||
|
||||
// 5. Informes / Reportes (nativos, Crystal y HTML)
|
||||
const ModuleReportsList = lazy(
|
||||
() => import("./views/informes/ModuleReportsList"),
|
||||
);
|
||||
const ReportRunner = lazy(() => import("./components/report/ReportRunner"));
|
||||
const CrystalReportsList = lazy(
|
||||
() => import("./views/reportes/CrystalReportsList"),
|
||||
);
|
||||
const CrystalReportRunner = lazy(
|
||||
() => import("./components/report/CrystalReportRunner"),
|
||||
);
|
||||
const HtmlReportsList = lazy(() => import("./views/reportes/HtmlReportsList"));
|
||||
const HtmlReportRunner = lazy(
|
||||
() => import("./components/report/HtmlReportRunner"),
|
||||
);
|
||||
|
||||
// Módulos aún sin formulario propio: solo muestran un título de marcador
|
||||
// de posición hasta que se implemente su pantalla real.
|
||||
function Delivery() {
|
||||
return (
|
||||
<div className="p-4">
|
||||
<h1 className="text-xl font-bold text-[var(--color-sidebar-title)]">
|
||||
Entregas / Guías
|
||||
</h1>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Receiving() {
|
||||
return (
|
||||
<div className="p-4">
|
||||
<h1 className="text-xl font-bold text-[var(--color-sidebar-title)]">
|
||||
Recepciones de Compra
|
||||
</h1>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Bloquea el acceso a todo lo que cuelga de "/" si no hay sesión SAP activa.
|
||||
function ProtectedRoute({ children }: { children: ReactNode }) {
|
||||
if (!getToken()) {
|
||||
return <Navigate to="/login" replace />;
|
||||
@@ -49,55 +98,102 @@ function ProtectedRoute({ children }: { children: ReactNode }) {
|
||||
export default function App() {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="/login" element={<SapLogin />} />
|
||||
{/* Suspense envuelve todas las rutas: mientras el chunk de la vista
|
||||
lazy() se descarga, se muestra un único loader consistente en
|
||||
vez de que cada pantalla defina el suyo. */}
|
||||
<Suspense fallback={<PageLoader />}>
|
||||
<Routes>
|
||||
<Route path="/login" element={<SapLogin />} />
|
||||
|
||||
<Route
|
||||
path="/"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<MainLayout />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
>
|
||||
<Route
|
||||
index
|
||||
element={<Navigate to="/dashboard" replace />}
|
||||
/>
|
||||
<Route path="dashboard" element={<Dashboard />} />
|
||||
{/* 1. Inventario */}
|
||||
<Route path="inventario/articulos" element={<ItemsList />} />
|
||||
<Route path="inventario/articulos/mantenimiento" element={<Items />} />
|
||||
<Route path="inventario/lotes" element={<BatchNumberDetailsList />} />
|
||||
<Route path="inventario/lotes/mantenimiento" element={<BatchNumberDetails />} />
|
||||
{/* 2. Socios de Negocios */}
|
||||
<Route path="socios/clientes" element={<CustomersList />} />
|
||||
<Route path="socios/clientes/mantenimiento" element={<Customer />} />
|
||||
<Route path="socios/proveedores" element={<SuppliersList />} />
|
||||
<Route path="socios/proveedores/mantenimiento" element={<Supplier />} />
|
||||
{/* 3. Ventas */}
|
||||
<Route path="ventas/ordenes" element={<OrdersList />} />
|
||||
<Route path="ventas/ordenes/mantenimiento" element={<Orders />} />
|
||||
<Route path="ventas/entrega" element={<Delivery />} />
|
||||
<Route path="ventas/facturas" element={<InvoicesList />} />
|
||||
<Route path="ventas/facturas/mantenimiento" element={<Invoices />} />
|
||||
{/* 4. Compras */}
|
||||
<Route path="compras/ordenes" element={<PurchaseOrdersList />} />
|
||||
<Route path="compras/ordenes/mantenimiento" element={<PurchaseOrders />} />
|
||||
<Route path="compras/recepcion" element={<Receiving />} />
|
||||
<Route path="compras/facturas" element={<PurchaseInvoicesList />} />
|
||||
<Route path="compras/facturas/mantenimiento" element={<PurchaseInvoices />} />
|
||||
{/* 5. Informes */}
|
||||
<Route path="informes/:module" element={<ModuleReportsList />} />
|
||||
<Route path="informes/:module/:reportId" element={<ReportRunner />} />
|
||||
<Route path="reportes-crystal/:module" element={<CrystalReportsList />} />
|
||||
<Route path="reportes-crystal/:module/:docCode" element={<CrystalReportRunner />} />
|
||||
<Route path="reportes-html/:module" element={<HtmlReportsList />} />
|
||||
<Route path="reportes-html/:module/:docCode" element={<HtmlReportRunner />} />
|
||||
</Route>
|
||||
path="/"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<MainLayout />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
>
|
||||
<Route index element={<Navigate to="/dashboard" replace />} />
|
||||
|
||||
<Route path="*" element={<Navigate to="/login" replace />} />
|
||||
</Routes>
|
||||
<Route path="dashboard" element={<Dashboard />} />
|
||||
|
||||
{/* 1. Inventario */}
|
||||
<Route path="inventario/articulos" element={<ItemsList />} />
|
||||
<Route
|
||||
path="inventario/articulos/mantenimiento"
|
||||
element={<Items />}
|
||||
/>
|
||||
<Route
|
||||
path="inventario/lotes"
|
||||
element={<BatchNumberDetailsList />}
|
||||
/>
|
||||
<Route
|
||||
path="inventario/lotes/mantenimiento"
|
||||
element={<BatchNumberDetails />}
|
||||
/>
|
||||
|
||||
{/* 2. Socios de Negocios */}
|
||||
<Route path="socios/clientes" element={<CustomersList />} />
|
||||
<Route
|
||||
path="socios/clientes/mantenimiento"
|
||||
element={<Customer />}
|
||||
/>
|
||||
<Route path="socios/proveedores" element={<SuppliersList />} />
|
||||
<Route
|
||||
path="socios/proveedores/mantenimiento"
|
||||
element={<Supplier />}
|
||||
/>
|
||||
|
||||
{/* 3. Ventas */}
|
||||
<Route path="ventas/ordenes" element={<OrdersList />} />
|
||||
<Route path="ventas/ordenes/mantenimiento" element={<Orders />} />
|
||||
<Route path="ventas/entrega" element={<Delivery />} />
|
||||
<Route path="ventas/facturas" element={<InvoicesList />} />
|
||||
<Route
|
||||
path="ventas/facturas/mantenimiento"
|
||||
element={<Invoices />}
|
||||
/>
|
||||
|
||||
{/* 4. Compras */}
|
||||
<Route path="compras/ordenes" element={<PurchaseOrdersList />} />
|
||||
<Route
|
||||
path="compras/ordenes/mantenimiento"
|
||||
element={<PurchaseOrders />}
|
||||
/>
|
||||
<Route path="compras/recepcion" element={<Receiving />} />
|
||||
<Route path="compras/facturas" element={<PurchaseInvoicesList />} />
|
||||
<Route
|
||||
path="compras/facturas/mantenimiento"
|
||||
element={<PurchaseInvoices />}
|
||||
/>
|
||||
|
||||
{/* 5. Informes */}
|
||||
<Route path="informes/:module" element={<ModuleReportsList />} />
|
||||
<Route
|
||||
path="informes/:module/:reportId"
|
||||
element={<ReportRunner />}
|
||||
/>
|
||||
<Route
|
||||
path="reportes-crystal/:module"
|
||||
element={<CrystalReportsList />}
|
||||
/>
|
||||
<Route
|
||||
path="reportes-crystal/:module/:docCode"
|
||||
element={<CrystalReportRunner />}
|
||||
/>
|
||||
<Route
|
||||
path="reportes-html/:module"
|
||||
element={<HtmlReportsList />}
|
||||
/>
|
||||
<Route
|
||||
path="reportes-html/:module/:docCode"
|
||||
element={<HtmlReportRunner />}
|
||||
/>
|
||||
</Route>
|
||||
|
||||
<Route path="*" element={<Navigate to="/login" replace />} />
|
||||
</Routes>
|
||||
</Suspense>
|
||||
</BrowserRouter>
|
||||
);
|
||||
}
|
||||
@@ -3,12 +3,16 @@ import type { ChangeEvent, FormEvent } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import type { BatchNumberDetail } from "../types/sapBatch";
|
||||
import { API_URL, apiFetch } from "../utils/api";
|
||||
import { getErrorMessage } from "../utils/errors";
|
||||
import {
|
||||
BATCH_STATUS_APROBACION,
|
||||
BATCH_STATUS_LIBERACION,
|
||||
} from "../utils/enums";
|
||||
import FormattedField from "./ui/FormattedField";
|
||||
import SelectField from "./ui/SelectField";
|
||||
import PrimaryButton from "./ui/PrimaryButton";
|
||||
import SecondaryButton from "./ui/SecondaryButton";
|
||||
import PageLoader from "./ui/PageLoader";
|
||||
|
||||
interface BatchFormProps {
|
||||
batchId: string;
|
||||
@@ -32,6 +36,9 @@ export default function BatchForm({ batchId, backPath }: BatchFormProps) {
|
||||
U_SIS_OBSER: "Observaciones",
|
||||
U_SIS_ESTLIB: "EstadoLiberacion",
|
||||
U_SIS_HORAPROB: "HoraAprobacion",
|
||||
U_SIS_MOTEST: "MotivoEstado",
|
||||
U_SIS_COMEST: "ComentarioEstado",
|
||||
U_SIS_ACCTOM: "AccionATomar"
|
||||
};
|
||||
|
||||
function mapSapToFriendly(raw: Record<string, any>): BatchNumberDetail {
|
||||
@@ -53,8 +60,8 @@ export default function BatchForm({ batchId, backPath }: BatchFormProps) {
|
||||
if (!response.ok) throw new Error("Error al cargar el lote");
|
||||
const data = await response.json();
|
||||
setBatch(mapSapToFriendly(data));
|
||||
} catch (err: any) {
|
||||
setError(err.message || "Error al cargar el lote");
|
||||
} catch (err: unknown) {
|
||||
setError(getErrorMessage(err, "Error al cargar el lote"));
|
||||
} finally {
|
||||
setFetchLoading(false);
|
||||
}
|
||||
@@ -97,6 +104,9 @@ export default function BatchForm({ batchId, backPath }: BatchFormProps) {
|
||||
U_SIS_OBSER: batch.Observaciones,
|
||||
U_SIS_ESTLIB: batch.EstadoLiberacion,
|
||||
U_SIS_HORAPROB: batch.HoraAprobacion,
|
||||
U_SIS_MOTEST: batch.MotivoEstado,
|
||||
U_SIS_COMEST: batch.ComentarioEstado,
|
||||
U_SIS_ACCTOM: batch.AccionATomar
|
||||
}),
|
||||
},
|
||||
);
|
||||
@@ -104,21 +114,15 @@ export default function BatchForm({ batchId, backPath }: BatchFormProps) {
|
||||
if (!response.ok) throw new Error("Error al actualizar el lote");
|
||||
|
||||
setSuccess("Lote actualizado exitosamente");
|
||||
} catch (err: any) {
|
||||
setError(err.message || "Error de conexión");
|
||||
} catch (err: unknown) {
|
||||
setError(getErrorMessage(err, "Error de conexión"));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (fetchLoading) {
|
||||
return (
|
||||
<div className="p-4 bg-[var(--color-surface)] h-full flex flex-col font-sans select-none text-xs text-[var(--color-text)]">
|
||||
<div className="text-xs text-[var(--color-primary)] py-4">
|
||||
Cargando lote...
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return <PageLoader message="Cargando lote..." />;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -156,21 +160,25 @@ export default function BatchForm({ batchId, backPath }: BatchFormProps) {
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4 p-3 bg-[var(--color-bg)] border border-[var(--color-border)]">
|
||||
<FormattedField
|
||||
type="text"
|
||||
label="Lote (Batch)"
|
||||
label="Lote"
|
||||
dbField="Batch"
|
||||
value={batch.Batch}
|
||||
mode="display"
|
||||
/>
|
||||
<FormattedField
|
||||
type="text"
|
||||
label="Código Artículo (ItemCode)"
|
||||
label="Código Artículo"
|
||||
dbField="ItemCode"
|
||||
value={batch.ItemCode}
|
||||
mode="display"
|
||||
/>
|
||||
<FormattedField
|
||||
type="text"
|
||||
label="Descripción (ItemDescription)"
|
||||
label="Descripción"
|
||||
dbField="ItemDescription"
|
||||
value={batch.ItemDescription}
|
||||
mode="display"
|
||||
multiline
|
||||
/>
|
||||
<FormattedField
|
||||
type="text"
|
||||
@@ -180,7 +188,8 @@ export default function BatchForm({ batchId, backPath }: BatchFormProps) {
|
||||
/>
|
||||
<FormattedField
|
||||
type="text"
|
||||
label="Atributo 1 (BatchAttribute1)"
|
||||
label="Lote proveedor"
|
||||
dbField="BatchAttribute1"
|
||||
value={batch.BatchAttribute1}
|
||||
mode="input"
|
||||
name="BatchAttribute1"
|
||||
@@ -188,7 +197,8 @@ export default function BatchForm({ batchId, backPath }: BatchFormProps) {
|
||||
/>
|
||||
<FormattedField
|
||||
type="text"
|
||||
label="Atributo 2 (BatchAttribute2)"
|
||||
label="Ubicación"
|
||||
dbField="BatchAttribute2"
|
||||
value={batch.BatchAttribute2}
|
||||
mode="input"
|
||||
name="BatchAttribute2"
|
||||
@@ -196,26 +206,30 @@ export default function BatchForm({ batchId, backPath }: BatchFormProps) {
|
||||
/>
|
||||
<FormattedField
|
||||
type="date"
|
||||
label="Fecha Ingreso (AdmissionDate)"
|
||||
label="Fecha Ingreso"
|
||||
dbField="AdmissionDate"
|
||||
value={batch.AdmissionDate}
|
||||
mode="display"
|
||||
/>
|
||||
<FormattedField
|
||||
type="date"
|
||||
label="Fecha Fabricación (ManufacturingDate)"
|
||||
label="Fecha Fabricación"
|
||||
dbField="ManufacturingDate"
|
||||
value={batch.ManufacturingDate}
|
||||
mode="display"
|
||||
/>
|
||||
<FormattedField
|
||||
type="date"
|
||||
label="Fecha Vencimiento (ExpirationDate)"
|
||||
label="Fecha Vencimiento"
|
||||
dbField="ExpirationDate"
|
||||
value={batch.ExpirationDate}
|
||||
mode="input"
|
||||
name="ExpirationDate"
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<SelectField
|
||||
label="Estado de Aprobación (U_SIS_ESTLOT)"
|
||||
label="Estado de Aprobación"
|
||||
dbField="U_SIS_ESTLOT"
|
||||
options={BATCH_STATUS_APROBACION}
|
||||
value={batch.EstadoAprobacion ?? ""}
|
||||
onChange={handleSelectChange("EstadoAprobacion")}
|
||||
@@ -225,7 +239,8 @@ export default function BatchForm({ batchId, backPath }: BatchFormProps) {
|
||||
|
||||
<FormattedField
|
||||
type="date"
|
||||
label="Fecha de Aprobación (U_SIS_FECAPROB)"
|
||||
label="Fecha de Aprobación"
|
||||
dbField="U_SIS_FECAPROB"
|
||||
value={batch.FechaAprobacion}
|
||||
mode="input"
|
||||
name="FechaAprobacion"
|
||||
@@ -233,7 +248,8 @@ export default function BatchForm({ batchId, backPath }: BatchFormProps) {
|
||||
/>
|
||||
<FormattedField
|
||||
type="text"
|
||||
label="Responsable de Aprobación (U_SIS_RESPAPROB)"
|
||||
label="Responsable de Aprobación"
|
||||
dbField="U_SIS_RESPAPROB"
|
||||
value={batch.ResponsableAprobacion}
|
||||
mode="input"
|
||||
name="ResponsableAprobacion"
|
||||
@@ -241,14 +257,16 @@ export default function BatchForm({ batchId, backPath }: BatchFormProps) {
|
||||
/>
|
||||
<FormattedField
|
||||
type="text"
|
||||
label="Observaciones (U_SIS_OBSER)"
|
||||
label="Observaciones"
|
||||
dbField="U_SIS_OBSER"
|
||||
value={batch.Observaciones}
|
||||
mode="input"
|
||||
name="Observaciones"
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<SelectField
|
||||
label="Estado de Liberación (U_SIS_ESTLIB)"
|
||||
label="Estado de Liberación"
|
||||
dbField="U_SIS_ESTLIB"
|
||||
options={BATCH_STATUS_LIBERACION}
|
||||
value={batch.EstadoLiberacion ?? ""}
|
||||
onChange={handleSelectChange("EstadoLiberacion")}
|
||||
@@ -257,7 +275,8 @@ export default function BatchForm({ batchId, backPath }: BatchFormProps) {
|
||||
/>
|
||||
<FormattedField
|
||||
type="date"
|
||||
label="Fecha de Liberación (U_SIS_FECLIB)"
|
||||
label="Fecha de Liberación"
|
||||
dbField="U_SIS_FECLIB"
|
||||
value={batch.FechaLiberacion}
|
||||
mode="input"
|
||||
name="FechaLiberacion"
|
||||
@@ -265,12 +284,46 @@ export default function BatchForm({ batchId, backPath }: BatchFormProps) {
|
||||
/>
|
||||
<FormattedField
|
||||
type="text"
|
||||
label="Hora de Liberación (U_SIS_HORAPROB)"
|
||||
label="Hora de Liberación"
|
||||
dbField="U_SIS_HORAPROB"
|
||||
value={batch.HoraAprobacion}
|
||||
mode="input"
|
||||
name="HoraAprobacion"
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<FormattedField
|
||||
type="text"
|
||||
label="Motivo de Estado"
|
||||
dbField="U_SIS_MOTEST"
|
||||
value={batch.MotivoEstado}
|
||||
mode="input"
|
||||
name="MotivoEstado"
|
||||
onChange={handleChange}
|
||||
multiline
|
||||
rows={3}
|
||||
/>
|
||||
<FormattedField
|
||||
type="text"
|
||||
label="Comentario de Estado"
|
||||
dbField="U_SIS_COMEST"
|
||||
value={batch.ComentarioEstado}
|
||||
mode="input"
|
||||
name="ComentarioEstado"
|
||||
onChange={handleChange}
|
||||
multiline
|
||||
rows={3}
|
||||
/>
|
||||
<FormattedField
|
||||
type="text"
|
||||
label="Acción a tomar"
|
||||
dbField="U_SIS_ACCTOM"
|
||||
value={batch.AccionATomar}
|
||||
mode="input"
|
||||
name="AccionATomar"
|
||||
onChange={handleChange}
|
||||
multiline
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* <div className="p-3 bg-[var(--color-bg)] border border-[var(--color-border)]">
|
||||
@@ -287,20 +340,12 @@ export default function BatchForm({ batchId, backPath }: BatchFormProps) {
|
||||
</div> */}
|
||||
|
||||
<div className="flex justify-end pt-4 border-t border-[var(--color-border-light)] space-x-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => navigate(backPath)}
|
||||
className="px-4 py-2 bg-[var(--color-surface)] border border-[var(--color-border)] text-[var(--color-sidebar-title)] text-xs font-medium cursor-pointer"
|
||||
>
|
||||
<SecondaryButton type="button" onClick={() => navigate(backPath)}>
|
||||
Cancelar
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="bg-[var(--color-primary)] hover:bg-[var(--color-primary-hover)] text-white text-xs font-medium px-4 py-2 disabled:opacity-50 cursor-pointer border-none"
|
||||
>
|
||||
</SecondaryButton>
|
||||
<PrimaryButton type="submit" size="md" disabled={loading}>
|
||||
{loading ? "Procesando..." : "Actualizar Lote"}
|
||||
</button>
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useState, useEffect } from "react";
|
||||
import type { ChangeEvent, FormEvent } from "react";
|
||||
import { useSearchParams, useNavigate } from "react-router-dom";
|
||||
import { API_URL, apiFetch } from "../utils/api";
|
||||
import FieldTooltip from "./ui/FieldTooltip";
|
||||
|
||||
interface BPAddress {
|
||||
AddressName: string;
|
||||
@@ -287,7 +288,8 @@ export default function BusinessPartnerForm({
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-8 gap-y-4 max-w-4xl text-xs">
|
||||
<div>
|
||||
<label className="block text-[var(--color-text-muted)] font-medium mb-1">
|
||||
Código de {entityLabel} (CardCode)
|
||||
Código de {entityLabel}
|
||||
<FieldTooltip text="CardCode" />
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
@@ -303,7 +305,8 @@ export default function BusinessPartnerForm({
|
||||
|
||||
<div>
|
||||
<label className="block text-[var(--color-text-muted)] font-medium mb-1">
|
||||
RUC / Documento de Identidad (FederalTaxID)
|
||||
RUC / Documento de Identidad
|
||||
<FieldTooltip text="FederalTaxID" />
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
@@ -317,7 +320,8 @@ export default function BusinessPartnerForm({
|
||||
|
||||
<div className="md:col-span-2">
|
||||
<label className="block text-[var(--color-text-muted)] font-medium mb-1">
|
||||
Nombre o Razón Social (CardName)
|
||||
Nombre o Razón Social
|
||||
<FieldTooltip text="CardName" />
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
@@ -333,6 +337,7 @@ export default function BusinessPartnerForm({
|
||||
<div key={f.name} className={f.fullWidth ? "md:col-span-2" : ""}>
|
||||
<label className="block text-[var(--color-text-muted)] font-medium mb-1">
|
||||
{f.label}
|
||||
<FieldTooltip text={f.name} />
|
||||
</label>
|
||||
{f.type === "select" ? (
|
||||
<select
|
||||
@@ -361,7 +366,8 @@ export default function BusinessPartnerForm({
|
||||
|
||||
<div>
|
||||
<label className="block text-[var(--color-text-muted)] font-medium mb-1">
|
||||
Teléfono Principal (Phone1)
|
||||
Teléfono Principal
|
||||
<FieldTooltip text="Phone1" />
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
@@ -374,7 +380,8 @@ export default function BusinessPartnerForm({
|
||||
|
||||
<div>
|
||||
<label className="block text-[var(--color-text-muted)] font-medium mb-1">
|
||||
Correo Electrónico (EmailAddress)
|
||||
Correo Electrónico
|
||||
<FieldTooltip text="EmailAddress" />
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
@@ -406,7 +413,8 @@ export default function BusinessPartnerForm({
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-[var(--color-text-muted)] font-medium mb-1">
|
||||
Calle / Avenida (Street)
|
||||
Calle / Avenida
|
||||
<FieldTooltip text="Street" />
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
@@ -419,7 +427,8 @@ export default function BusinessPartnerForm({
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-[var(--color-text-muted)] font-medium mb-1">
|
||||
Ciudad (City)
|
||||
Ciudad
|
||||
<FieldTooltip text="City" />
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
@@ -432,7 +441,8 @@ export default function BusinessPartnerForm({
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-[var(--color-text-muted)] font-medium mb-1">
|
||||
Condado (County)
|
||||
Condado
|
||||
<FieldTooltip text="County" />
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
@@ -445,7 +455,8 @@ export default function BusinessPartnerForm({
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-[var(--color-text-muted)] font-medium mb-1">
|
||||
Numero Exterior (GlobalLocationNumber)
|
||||
Numero Exterior
|
||||
<FieldTooltip text="GlobalLocationNumber" />
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
|
||||
@@ -3,9 +3,13 @@ import type { ReactNode, ChangeEvent, FormEvent } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import type { SapItem } from "../types/sapItems";
|
||||
import { API_URL, apiFetch } from "../utils/api";
|
||||
import { getErrorMessage } from "../utils/errors";
|
||||
import { ITEM_TYPE, GL_METHOD } from "../utils/enums";
|
||||
import FormattedField from "./ui/FormattedField";
|
||||
import SelectField from "./ui/SelectField";
|
||||
import PrimaryButton from "./ui/PrimaryButton";
|
||||
import SecondaryButton from "./ui/SecondaryButton";
|
||||
import PageLoader from "./ui/PageLoader";
|
||||
|
||||
interface ItemFormProps {
|
||||
itemCode?: string;
|
||||
@@ -55,8 +59,8 @@ export default function ItemForm({
|
||||
if (!response.ok) throw new Error("Error al cargar el artículo");
|
||||
const data = await response.json();
|
||||
setItem(data);
|
||||
} catch (err: any) {
|
||||
setError(err.message || "Error al cargar el artículo");
|
||||
} catch (err: unknown) {
|
||||
setError(getErrorMessage(err, "Error al cargar el artículo"));
|
||||
} finally {
|
||||
setFetchLoading(false);
|
||||
}
|
||||
@@ -115,19 +119,15 @@ export default function ItemForm({
|
||||
if (!isEditMode) {
|
||||
setItem({ ItemCode: "", ItemName: "" });
|
||||
}
|
||||
} catch (err: any) {
|
||||
setError(err.message || "Error de conexión");
|
||||
} catch (err: unknown) {
|
||||
setError(getErrorMessage(err, "Error de conexión"));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (fetchLoading) {
|
||||
return (
|
||||
<div className="p-4 bg-[var(--color-surface)] h-full flex flex-col font-sans select-none text-xs text-[var(--color-text)]">
|
||||
<div className="text-xs text-[var(--color-primary)] py-4">Cargando artículo...</div>
|
||||
</div>
|
||||
);
|
||||
return <PageLoader message="Cargando artículo..." />;
|
||||
}
|
||||
|
||||
const checkbox = (name: keyof SapItem, label: string) => (
|
||||
@@ -182,7 +182,8 @@ export default function ItemForm({
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4 p-3 bg-[var(--color-bg)] border border-[var(--color-border)]">
|
||||
<FormattedField
|
||||
type="text"
|
||||
label="Código (ItemCode)"
|
||||
label="Código"
|
||||
dbField="ItemCode"
|
||||
value={item.ItemCode}
|
||||
mode="input"
|
||||
name="ItemCode"
|
||||
@@ -191,37 +192,44 @@ export default function ItemForm({
|
||||
/>
|
||||
<FormattedField
|
||||
type="text"
|
||||
label="Nombre (ItemName)"
|
||||
label="Nombre"
|
||||
dbField="ItemName"
|
||||
value={item.ItemName}
|
||||
mode="input"
|
||||
name="ItemName"
|
||||
required
|
||||
onChange={handleChange}
|
||||
multiline
|
||||
rows={2}
|
||||
/>
|
||||
<FormattedField
|
||||
type="text"
|
||||
label="Código de Barras (BarCode)"
|
||||
label="Código de Barras"
|
||||
dbField="BarCode"
|
||||
value={item.BarCode}
|
||||
mode="input"
|
||||
name="BarCode"
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<SelectField
|
||||
label="Grupo (ItemsGroupCode)"
|
||||
label="Grupo"
|
||||
dbField="ItemsGroupCode"
|
||||
options={groupOptions}
|
||||
value={item.ItemsGroupCode != null ? String(item.ItemsGroupCode) : ""}
|
||||
onChange={handleSelectChange("ItemsGroupCode")}
|
||||
placeholder="Seleccione grupo"
|
||||
/>
|
||||
<SelectField
|
||||
label="Tipo Artículo (ItemType)"
|
||||
label="Tipo Artículo"
|
||||
dbField="ItemType"
|
||||
options={ITEM_TYPE}
|
||||
value={item.ItemType ?? ""}
|
||||
onChange={handleSelectChange("ItemType")}
|
||||
placeholder="Seleccione tipo"
|
||||
/>
|
||||
<SelectField
|
||||
label="Método Contable (CostAccountingMethod)"
|
||||
label="Método Contable"
|
||||
dbField="CostAccountingMethod"
|
||||
options={GL_METHOD}
|
||||
value={item.CostAccountingMethod ?? ""}
|
||||
onChange={handleSelectChange("CostAccountingMethod")}
|
||||
@@ -229,7 +237,8 @@ export default function ItemForm({
|
||||
/>
|
||||
<FormattedField
|
||||
type="text"
|
||||
label="Unidad Venta (SalesUnit)"
|
||||
label="Unidad Venta"
|
||||
dbField="SalesUnit"
|
||||
value={item.SalesUnit}
|
||||
mode="input"
|
||||
name="SalesUnit"
|
||||
@@ -237,7 +246,8 @@ export default function ItemForm({
|
||||
/>
|
||||
<FormattedField
|
||||
type="text"
|
||||
label="Unidad Compra (PurchaseUnit)"
|
||||
label="Unidad Compra"
|
||||
dbField="PurchaseUnit"
|
||||
value={item.PurchaseUnit}
|
||||
mode="input"
|
||||
name="PurchaseUnit"
|
||||
@@ -245,7 +255,8 @@ export default function ItemForm({
|
||||
/>
|
||||
<FormattedField
|
||||
type="text"
|
||||
label="U. Inventario (InventoryUOM)"
|
||||
label="U. Inventario"
|
||||
dbField="InventoryUOM"
|
||||
value={item.InventoryUOM}
|
||||
mode="input"
|
||||
name="InventoryUOM"
|
||||
@@ -253,7 +264,8 @@ export default function ItemForm({
|
||||
/>
|
||||
<FormattedField
|
||||
type="currency"
|
||||
label="Precio Prom. (AvgStdPrice)"
|
||||
label="Precio Prom."
|
||||
dbField="AvgStdPrice"
|
||||
value={item.AvgStdPrice}
|
||||
mode="display"
|
||||
/>
|
||||
@@ -275,24 +287,16 @@ export default function ItemForm({
|
||||
)}
|
||||
|
||||
<div className="flex justify-end pt-4 border-t border-[var(--color-border-light)] space-x-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => navigate(backPath)}
|
||||
className="px-4 py-2 bg-[var(--color-surface)] border border-[var(--color-border)] text-[var(--color-sidebar-title)] text-xs font-medium cursor-pointer"
|
||||
>
|
||||
<SecondaryButton type="button" onClick={() => navigate(backPath)}>
|
||||
Cancelar
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="bg-[var(--color-primary)] hover:bg-[var(--color-primary-hover)] text-white text-xs font-medium px-4 py-2 disabled:opacity-50 cursor-pointer border-none"
|
||||
>
|
||||
</SecondaryButton>
|
||||
<PrimaryButton type="submit" size="md" disabled={loading}>
|
||||
{loading
|
||||
? "Procesando..."
|
||||
: isEditMode
|
||||
? "Actualizar Artículo"
|
||||
: "Crear Artículo"}
|
||||
</button>
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -15,11 +15,17 @@ import { es } from "date-fns/locale";
|
||||
import "react-datepicker/dist/react-datepicker.css";
|
||||
import CopyFromModal from "./ui/CopyFromModal";
|
||||
import { API_URL, apiFetch } from "../utils/api";
|
||||
import { getErrorMessage } from "../utils/errors";
|
||||
import { DEFAULT_CURRENCY } from "../utils/constants";
|
||||
import { DOC_TYPE } from "../utils/enums";
|
||||
import { DETRACCION_CODIGOS } from "../utils/sunat";
|
||||
import FormattedField from "./ui/FormattedField";
|
||||
import SelectField from "./ui/SelectField";
|
||||
import Icon from "./ui/Icon";
|
||||
import PrimaryButton from "./ui/PrimaryButton";
|
||||
import SecondaryButton from "./ui/SecondaryButton";
|
||||
import FieldTooltip from "./ui/FieldTooltip";
|
||||
import PageLoader from "./ui/PageLoader";
|
||||
|
||||
interface RetencionConfig {
|
||||
porcentajes: number[];
|
||||
@@ -57,12 +63,14 @@ const genericHeaderFields: CampoConfig[] = [
|
||||
label: "Documento",
|
||||
type: "text",
|
||||
required: true,
|
||||
readOnly: true,
|
||||
},
|
||||
{
|
||||
name: "DocNum",
|
||||
label: "Número",
|
||||
type: "text",
|
||||
required: true,
|
||||
readOnlyOnUpdate: true,
|
||||
},
|
||||
{
|
||||
name: "CardCode",
|
||||
@@ -70,12 +78,14 @@ const genericHeaderFields: CampoConfig[] = [
|
||||
type: "text",
|
||||
required: true,
|
||||
placeholder: "Ej. C00001 o P00001",
|
||||
readOnlyOnUpdate: true,
|
||||
},
|
||||
{
|
||||
name: "CardName",
|
||||
label: "Nombre / Razón Social",
|
||||
type: "text",
|
||||
required: true,
|
||||
multiline: true,
|
||||
},
|
||||
{
|
||||
name: "DocType",
|
||||
@@ -192,8 +202,8 @@ export default function MarketingDocumentForm({
|
||||
GroupNum: data.GroupNum,
|
||||
NumAtCard: data.NumAtCard || "",
|
||||
});
|
||||
} catch (err: any) {
|
||||
setError(err.message || "Error al cargar el documento");
|
||||
} catch (err: unknown) {
|
||||
setError(getErrorMessage(err, "Error al cargar el documento"));
|
||||
} finally {
|
||||
setFetchLoading(false);
|
||||
}
|
||||
@@ -226,7 +236,13 @@ export default function MarketingDocumentForm({
|
||||
);
|
||||
detraccionConfig.onChangeImporte(calculado);
|
||||
}
|
||||
}, [detraccionConfig?.codigo]);
|
||||
// No se incluye `detraccionConfig` completo a propósito: cada vista lo
|
||||
// pasa como objeto literal (detraccionConfig={{ ... }}), por lo que es
|
||||
// una referencia nueva en cada render. Si se agregara aquí, el efecto
|
||||
// se ejecutaría en cada render del formulario en vez de solo cuando
|
||||
// cambia el código de detracción o el total del documento.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [detraccionConfig?.codigo, totales.totalGeneral]);
|
||||
|
||||
const handleHeaderChange = (
|
||||
e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>,
|
||||
@@ -265,7 +281,7 @@ export default function MarketingDocumentForm({
|
||||
const handleLineChange = (
|
||||
index: number,
|
||||
field: keyof DocumentLine,
|
||||
value: any,
|
||||
value: string,
|
||||
) => {
|
||||
setDoc((prev) => {
|
||||
const lines = [...prev.DocumentLines];
|
||||
@@ -301,6 +317,10 @@ export default function MarketingDocumentForm({
|
||||
method,
|
||||
body: JSON.stringify({
|
||||
...doc,
|
||||
// BaseRef solo existe en el frontend (se llena al copiar líneas de
|
||||
// otro documento, para mostrar "Doc. Origen" en la tabla); SAP no
|
||||
// lo espera al guardar, por eso se descarta aquí a propósito.
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
DocumentLines: doc.DocumentLines.map(({ BaseRef, ...rest }) => rest),
|
||||
}),
|
||||
});
|
||||
@@ -331,8 +351,8 @@ export default function MarketingDocumentForm({
|
||||
DocumentLines: [],
|
||||
});
|
||||
}
|
||||
} catch (err: any) {
|
||||
setError(err.message || "Error de conexión");
|
||||
} catch (err: unknown) {
|
||||
setError(getErrorMessage(err, "Error de conexión"));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -381,11 +401,7 @@ export default function MarketingDocumentForm({
|
||||
}, [doc, lineColumns, hasCopiedLines]);
|
||||
|
||||
if (fetchLoading) {
|
||||
return (
|
||||
<div className="p-4 bg-[var(--color-surface)] h-full flex flex-col font-sans select-none text-xs text-[var(--color-text)]">
|
||||
<div className="text-xs text-[var(--color-primary)] py-4">Cargando documento...</div>
|
||||
</div>
|
||||
);
|
||||
return <PageLoader message="Cargando documento..." />;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -422,7 +438,9 @@ export default function MarketingDocumentForm({
|
||||
<form onSubmit={handleSubmit} className="flex-1 flex flex-col space-y-4">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4 p-3 bg-[var(--color-bg)] border border-[var(--color-border)]">
|
||||
{genericHeaderFields.map((field) => {
|
||||
const val = (doc as any)[field.name];
|
||||
const val = doc[field.name];
|
||||
const isFieldReadOnly =
|
||||
field.readOnly || (field.readOnlyOnUpdate && isEditMode);
|
||||
return (
|
||||
<div
|
||||
key={field.name}
|
||||
@@ -431,47 +449,57 @@ export default function MarketingDocumentForm({
|
||||
{field.name === "DocType" ? (
|
||||
<SelectField
|
||||
label={field.label}
|
||||
dbField={field.name}
|
||||
options={DOC_TYPE}
|
||||
value={val}
|
||||
// DocType siempre es string en MarketingDocument; el cast
|
||||
// es necesario porque `val` viene tipado como la unión de
|
||||
// TODOS los posibles valores de genericHeaderFields.
|
||||
value={val as string}
|
||||
onChange={(v) =>
|
||||
setDoc((prev) => ({
|
||||
...prev,
|
||||
DocType: v as "dDocument_Items" | "dDocument_Service",
|
||||
}))
|
||||
}
|
||||
disabled={isFieldReadOnly}
|
||||
/>
|
||||
) : field.name === "DocCurrency" ? (
|
||||
<SelectField
|
||||
label={field.label}
|
||||
dbField={field.name}
|
||||
options={Object.fromEntries(
|
||||
currencies.map((c) => [c.Code, `${c.Code} - ${c.Name}`]),
|
||||
)}
|
||||
value={val}
|
||||
// Mismo caso que DocType: DocCurrency es string en
|
||||
// MarketingDocument, pero `val` trae el tipo ancho.
|
||||
value={val as string}
|
||||
onChange={(v) =>
|
||||
setDoc((prev) => ({ ...prev, DocCurrency: v }))
|
||||
}
|
||||
placeholder="Seleccione moneda"
|
||||
disabled={isFieldReadOnly}
|
||||
/>
|
||||
) : field.name === "CardCode" ? (
|
||||
<>
|
||||
<FormattedField
|
||||
type={field.type}
|
||||
label={field.label}
|
||||
dbField={field.name}
|
||||
value={val}
|
||||
mode="input"
|
||||
mode={isFieldReadOnly ? "display" : "input"}
|
||||
name={field.name}
|
||||
required={field.required}
|
||||
placeholder={field.placeholder}
|
||||
onChange={handleHeaderChange}
|
||||
className="flex-1"
|
||||
/>
|
||||
{onBuscarSocio && (
|
||||
{onBuscarSocio && !isFieldReadOnly && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onBuscarSocio}
|
||||
className="px-2 bg-[#f0f0f0] border border-l-0 border-[var(--color-input-border)] hover:bg-[var(--color-sidebar-hover)] text-[var(--color-sidebar-title)] font-bold rounded-none cursor-pointer text-xs mt-[22px]"
|
||||
className="px-2 flex items-center bg-[var(--color-bg)] border border-l-0 border-[var(--color-input-border)] hover:bg-[var(--color-disabled-bg)] text-[var(--color-sidebar-title)] cursor-pointer mt-[22px]"
|
||||
>
|
||||
🔍
|
||||
<Icon name="search" className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
@@ -479,12 +507,14 @@ export default function MarketingDocumentForm({
|
||||
<FormattedField
|
||||
type={field.type}
|
||||
label={field.label}
|
||||
dbField={field.name}
|
||||
value={val}
|
||||
mode="input"
|
||||
mode={isFieldReadOnly ? "display" : "input"}
|
||||
name={field.name}
|
||||
required={field.required}
|
||||
placeholder={field.placeholder}
|
||||
onChange={handleHeaderChange}
|
||||
multiline={field.multiline}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -496,13 +526,14 @@ export default function MarketingDocumentForm({
|
||||
<div key={field.name}>
|
||||
<label className="block text-[var(--color-text-muted)] font-medium mb-1">
|
||||
{field.label}
|
||||
<FieldTooltip text={field.name} />
|
||||
</label>
|
||||
{field.type === "select" ? (
|
||||
<select
|
||||
name={field.name}
|
||||
value={String((doc as any)[field.name] ?? "")}
|
||||
value={String(doc[field.name] ?? "")}
|
||||
onChange={handleHeaderChange}
|
||||
className="w-full px-2 py-1 border border-[var(--color-input-border)] bg-white rounded-none outline-none focus:border-[var(--color-input-focus)]"
|
||||
className="w-full px-2 py-1 border border-[var(--color-input-border)] bg-[var(--color-surface)] rounded-none outline-none focus:border-[var(--color-input-focus)]"
|
||||
>
|
||||
{Object.entries(field.options ?? {}).map(([value, optLabel]) => (
|
||||
<option key={value} value={value}>
|
||||
@@ -517,9 +548,9 @@ export default function MarketingDocumentForm({
|
||||
required={field.required}
|
||||
maxLength={field.maxLength}
|
||||
placeholder={field.placeholder}
|
||||
value={String((doc as any)[field.name] ?? "")}
|
||||
value={String(doc[field.name] ?? "")}
|
||||
onChange={handleHeaderChange}
|
||||
className="w-full px-2 py-1 border border-[var(--color-input-border)] bg-white rounded-none outline-none focus:border-[var(--color-input-focus)]"
|
||||
className="w-full px-2 py-1 border border-[var(--color-input-border)] bg-[var(--color-surface)] rounded-none outline-none focus:border-[var(--color-input-focus)]"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -540,21 +571,21 @@ export default function MarketingDocumentForm({
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleCopyFromClick(copyFromSources[0])}
|
||||
className="bg-[var(--color-bg)] border border-[var(--color-border)] text-[var(--color-sidebar-title)] hover:bg-[var(--color-sidebar-hover)] px-3 py-1 font-medium rounded-none cursor-pointer"
|
||||
className="flex items-center gap-1.5 bg-[var(--color-bg)] border border-[var(--color-border)] text-[var(--color-sidebar-title)] hover:bg-[var(--color-disabled-bg)] px-3 py-1 font-medium rounded-none cursor-pointer"
|
||||
>
|
||||
📋 Copiar de
|
||||
<Icon name="clipboard" className="w-3.5 h-3.5" /> Copiar de
|
||||
</button>
|
||||
) : (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsDropdownOpen(!isDropdownOpen)}
|
||||
className="bg-[var(--color-bg)] border border-[var(--color-border)] text-[var(--color-sidebar-title)] hover:bg-[var(--color-sidebar-hover)] px-3 py-1 font-medium rounded-none cursor-pointer"
|
||||
className="flex items-center gap-1.5 bg-[var(--color-bg)] border border-[var(--color-border)] text-[var(--color-sidebar-title)] hover:bg-[var(--color-disabled-bg)] px-3 py-1 font-medium rounded-none cursor-pointer"
|
||||
>
|
||||
📋 Copiar de ▼
|
||||
<Icon name="clipboard" className="w-3.5 h-3.5" /> Copiar de ▼
|
||||
</button>
|
||||
{isDropdownOpen && (
|
||||
<div className="absolute right-0 top-full mt-1 bg-[var(--color-surface)] border border-[var(--color-border)] shadow-lg z-50 min-w-[180px]">
|
||||
<div className="absolute right-0 top-full mt-1 bg-[var(--color-surface)] border border-[var(--color-border)] z-50 min-w-[180px]">
|
||||
{copyFromSources.map((s) => (
|
||||
<button
|
||||
key={s.endpoint}
|
||||
@@ -575,9 +606,9 @@ export default function MarketingDocumentForm({
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onBuscarArticulos(-1)}
|
||||
className="bg-[var(--color-bg)] border border-[var(--color-border)] text-[var(--color-sidebar-title)] hover:bg-[var(--color-sidebar-hover)] px-3 py-1 font-medium rounded-none cursor-pointer"
|
||||
className="flex items-center gap-1.5 bg-[var(--color-bg)] border border-[var(--color-border)] text-[var(--color-sidebar-title)] hover:bg-[var(--color-disabled-bg)] px-3 py-1 font-medium rounded-none cursor-pointer"
|
||||
>
|
||||
📦 Catálogo Completo
|
||||
<Icon name="box" className="w-3.5 h-3.5" /> Catálogo Completo
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
@@ -606,15 +637,17 @@ export default function MarketingDocumentForm({
|
||||
{col.title}
|
||||
</th>
|
||||
))}
|
||||
<th className="w-12 text-center py-1">Elim.</th>
|
||||
<th className="w-8 text-center py-1">
|
||||
<Icon name="trash" className="w-3.5 h-3.5 inline-block" />
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-[var(--color-sidebar-submenu-border)]">
|
||||
<tbody className="divide-y divide-[var(--color-border-light)]">
|
||||
{doc.DocumentLines.length === 0 ? (
|
||||
<tr>
|
||||
<td
|
||||
colSpan={allLineColumns.length + 2}
|
||||
className="text-center py-6 text-gray-400 italic"
|
||||
className="text-center py-6 text-[var(--color-text-disabled)] italic"
|
||||
>
|
||||
No hay líneas añadidas. Presione "+ Añadir Línea" para
|
||||
comenzar a ingresar información.
|
||||
@@ -622,17 +655,17 @@ export default function MarketingDocumentForm({
|
||||
</tr>
|
||||
) : (
|
||||
doc.DocumentLines.map((line, idx) => (
|
||||
<tr key={idx} className="hover:bg-[var(--color-sidebar-submenu-bg)] h-8">
|
||||
<td className="text-center border-r border-[var(--color-sidebar-submenu-border)] bg-[#fdfdfd]">
|
||||
<tr key={idx} className="hover:bg-[var(--color-bg)] h-8">
|
||||
<td className="text-center border-r border-[var(--color-border-light)] bg-[var(--color-bg)]">
|
||||
{idx + 1}
|
||||
</td>
|
||||
{allLineColumns.map((col) => {
|
||||
const lineVal = (line as any)[col.key];
|
||||
const lineVal = line[col.key];
|
||||
if (col.key === "LineTotal") {
|
||||
return (
|
||||
<td
|
||||
key={col.key}
|
||||
className="border-r border-[var(--color-sidebar-submenu-border)] px-2 py-0.5 text-right font-mono bg-[#fdfdfd]"
|
||||
className="border-r border-[var(--color-border-light)] px-2 py-0.5 text-right font-mono bg-[var(--color-bg)]"
|
||||
>
|
||||
{doc.DocCurrency}{" "}
|
||||
{(line.LineTotal ?? 0).toLocaleString("en-US", {
|
||||
@@ -646,7 +679,7 @@ export default function MarketingDocumentForm({
|
||||
return (
|
||||
<td
|
||||
key={col.key}
|
||||
className="border-r border-[var(--color-sidebar-submenu-border)] p-0.5"
|
||||
className="border-r border-[var(--color-border-light)] p-0.5"
|
||||
>
|
||||
<DatePicker
|
||||
selected={
|
||||
@@ -658,7 +691,7 @@ export default function MarketingDocumentForm({
|
||||
const iso = d ? format(d, "yyyy-MM-dd") : "";
|
||||
handleLineChange(
|
||||
idx,
|
||||
col.key as keyof DocumentLine,
|
||||
col.key,
|
||||
iso,
|
||||
);
|
||||
}}
|
||||
@@ -676,7 +709,7 @@ export default function MarketingDocumentForm({
|
||||
return (
|
||||
<td
|
||||
key={col.key}
|
||||
className="border-r border-[var(--color-sidebar-submenu-border)] p-0.5"
|
||||
className="border-r border-[var(--color-border-light)] p-0.5"
|
||||
>
|
||||
<input
|
||||
type="number"
|
||||
@@ -686,7 +719,7 @@ export default function MarketingDocumentForm({
|
||||
onChange={(e) =>
|
||||
handleLineChange(
|
||||
idx,
|
||||
col.key as keyof DocumentLine,
|
||||
col.key,
|
||||
e.target.value,
|
||||
)
|
||||
}
|
||||
@@ -699,7 +732,7 @@ export default function MarketingDocumentForm({
|
||||
return (
|
||||
<td
|
||||
key={col.key}
|
||||
className="border-r border-[var(--color-sidebar-submenu-border)] p-0"
|
||||
className="border-r border-[var(--color-border-light)] p-0"
|
||||
>
|
||||
<div className="flex items-stretch h-full">
|
||||
<input
|
||||
@@ -708,7 +741,7 @@ export default function MarketingDocumentForm({
|
||||
onChange={(e) =>
|
||||
handleLineChange(
|
||||
idx,
|
||||
col.key as keyof DocumentLine,
|
||||
col.key,
|
||||
e.target.value,
|
||||
)
|
||||
}
|
||||
@@ -717,9 +750,9 @@ export default function MarketingDocumentForm({
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onCellSearch(idx, col.key)}
|
||||
className="shrink-0 px-0.5 bg-[#f0f0f0] hover:bg-[var(--color-sidebar-hover)] cursor-pointer text-[10px] border-none"
|
||||
className="shrink-0 px-0.5 flex items-center bg-[var(--color-bg)] hover:bg-[var(--color-disabled-bg)] cursor-pointer border-none"
|
||||
>
|
||||
🔍
|
||||
<Icon name="search" className="w-3 h-3" />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
@@ -728,7 +761,7 @@ export default function MarketingDocumentForm({
|
||||
return (
|
||||
<td
|
||||
key={col.key}
|
||||
className="border-r border-[var(--color-sidebar-submenu-border)] p-0.5"
|
||||
className="border-r border-[var(--color-border-light)] p-0.5"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
@@ -736,10 +769,11 @@ export default function MarketingDocumentForm({
|
||||
onChange={(e) =>
|
||||
handleLineChange(
|
||||
idx,
|
||||
col.key as keyof DocumentLine,
|
||||
col.key,
|
||||
e.target.value,
|
||||
)
|
||||
}
|
||||
title={lineVal ? String(lineVal) : undefined}
|
||||
className="w-full h-full px-1.5 border-none bg-transparent outline-none focus:bg-[var(--color-primary-active)]"
|
||||
/>
|
||||
</td>
|
||||
@@ -749,9 +783,9 @@ export default function MarketingDocumentForm({
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeLine(idx)}
|
||||
className="text-[var(--color-error)] bg-transparent border-none cursor-pointer hover:text-red-800 font-bold"
|
||||
className="w-full text-[var(--color-error)] bg-transparent border-none cursor-pointer hover:text-[var(--color-danger-hover)] flex items-center justify-center"
|
||||
>
|
||||
✕
|
||||
<Icon name="close" className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -766,6 +800,7 @@ export default function MarketingDocumentForm({
|
||||
<div className="md:col-span-2">
|
||||
<label className="block text-[var(--color-text-muted)] font-medium mb-1">
|
||||
Comentarios del Documento
|
||||
<FieldTooltip text="Comments" />
|
||||
</label>
|
||||
<textarea
|
||||
name="Comments"
|
||||
@@ -919,25 +954,17 @@ export default function MarketingDocumentForm({
|
||||
|
||||
<div className="flex justify-end pt-4 pb-4 border-t border-[var(--color-border-light)] space-x-2">
|
||||
{backPath && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => navigate(backPath)}
|
||||
className="px-4 py-2 bg-[var(--color-surface)] border border-[var(--color-border)] text-[var(--color-sidebar-title)] text-xs font-medium cursor-pointer"
|
||||
>
|
||||
<SecondaryButton type="button" onClick={() => navigate(backPath)}>
|
||||
Cancelar
|
||||
</button>
|
||||
</SecondaryButton>
|
||||
)}
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="bg-[var(--color-primary)] hover:bg-[var(--color-primary-hover)] text-white text-xs font-medium px-4 py-2 disabled:opacity-50 cursor-pointer border-none"
|
||||
>
|
||||
<PrimaryButton type="submit" size="md" disabled={loading}>
|
||||
{loading
|
||||
? "Procesando..."
|
||||
: isEditMode
|
||||
? "Actualizar Documento"
|
||||
: "Crear Documento"}
|
||||
</button>
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
</form>
|
||||
{activeCopySource && (
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ import type { ChangeEvent, FormEvent } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { API_URL, setSession } from "../utils/api";
|
||||
|
||||
export default function SapLogin() {
|
||||
export default function SapLoginForm() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -3,6 +3,7 @@ import { useParams, Link } from "react-router-dom";
|
||||
import { API_URL, apiFetch } from "../../utils/api";
|
||||
import { crystalReportsConfig } from "../../config/crystalReports";
|
||||
import ModalSearchTemplate from "../ui/ModalSearchTemplate";
|
||||
import Icon from "../ui/Icon";
|
||||
import type { F4Config } from "../../types/reports";
|
||||
|
||||
export default function CrystalReportRunner() {
|
||||
@@ -150,10 +151,10 @@ export default function CrystalReportRunner() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setF4State({ param, config })}
|
||||
className="h-[28px] px-1.5 bg-transparent border-none cursor-pointer text-xs text-[var(--color-primary)] shrink-0"
|
||||
className="h-[28px] px-1.5 bg-transparent border-none cursor-pointer text-[var(--color-primary)] shrink-0"
|
||||
title="Buscar"
|
||||
>
|
||||
📋
|
||||
<Icon name="search" className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -180,8 +181,8 @@ export default function CrystalReportRunner() {
|
||||
{definition?.title || docCode}
|
||||
</span>
|
||||
</div>
|
||||
<h1 className="text-xl font-bold text-[var(--color-sidebar-title)]">
|
||||
{definition?.icon} {definition?.title || docCode}
|
||||
<h1 className="text-xl font-bold text-[var(--color-sidebar-title)] flex items-center gap-1.5">
|
||||
<Icon name={definition?.icon || "document"} className="w-5 h-5" /> {definition?.title || docCode}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useParams, Link } from "react-router-dom";
|
||||
import { API_URL, apiFetch } from "../../utils/api";
|
||||
import { htmlReportsConfig } from "../../config/htmlReports";
|
||||
import ModalSearchTemplate from "../ui/ModalSearchTemplate";
|
||||
import Icon from "../ui/Icon";
|
||||
import type { F4Config } from "../../types/reports";
|
||||
|
||||
export default function HtmlReportRunner() {
|
||||
@@ -171,10 +172,10 @@ export default function HtmlReportRunner() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setF4State({ param, config })}
|
||||
className="h-[28px] px-1.5 bg-transparent border-none cursor-pointer text-xs text-[var(--color-primary)] shrink-0"
|
||||
className="h-[28px] px-1.5 bg-transparent border-none cursor-pointer text-[var(--color-primary)] shrink-0"
|
||||
title="Buscar"
|
||||
>
|
||||
📋
|
||||
<Icon name="search" className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -201,8 +202,8 @@ export default function HtmlReportRunner() {
|
||||
{definition?.title || docCode}
|
||||
</span>
|
||||
</div>
|
||||
<h1 className="text-xl font-bold text-[var(--color-sidebar-title)]">
|
||||
{definition?.icon} {definition?.title || docCode}
|
||||
<h1 className="text-xl font-bold text-[var(--color-sidebar-title)] flex items-center gap-1.5">
|
||||
<Icon name={definition?.icon || "document"} className="w-5 h-5" /> {definition?.title || docCode}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { es } from "date-fns/locale";
|
||||
import "react-datepicker/dist/react-datepicker.css";
|
||||
import FormattedField from "../ui/FormattedField";
|
||||
import ModalSearchTemplate from "../ui/ModalSearchTemplate";
|
||||
import Icon from "../ui/Icon";
|
||||
import { API_URL, apiFetch } from "../../utils/api";
|
||||
import { enumOptions } from "../../utils/enums";
|
||||
import type { ReportParameter, F4Config } from "../../types/reports";
|
||||
@@ -311,10 +312,10 @@ export default function ReportParameterForm({
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setF4State({ param, config })}
|
||||
className="h-[28px] px-1.5 bg-transparent border-none cursor-pointer text-xs text-[var(--color-primary)] shrink-0"
|
||||
className="h-[28px] px-1.5 bg-transparent border-none cursor-pointer text-[var(--color-primary)] shrink-0"
|
||||
title="Buscar"
|
||||
>
|
||||
📋
|
||||
<Icon name="search" className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { useParams, useSearchParams, Link } from "react-router-dom";
|
||||
import { API_URL, apiFetch } from "../../utils/api";
|
||||
import { getErrorMessage } from "../../utils/errors";
|
||||
import { reportsConfig, moduleLabels, moduleIcons } from "../../config/reports";
|
||||
import ReportParameterForm from "./ReportParameterForm";
|
||||
import ReportResultsView from "./ReportResultsView";
|
||||
import Icon from "../ui/Icon";
|
||||
import PageLoader from "../ui/PageLoader";
|
||||
import type { ReportDefinition } from "../../types/reports";
|
||||
import type { Column } from "../../types/table";
|
||||
|
||||
@@ -133,8 +136,8 @@ export default function ReportRunner() {
|
||||
totals: json.totals || null,
|
||||
});
|
||||
}
|
||||
} catch (err: any) {
|
||||
setError(err.message || "Error de conexión");
|
||||
} catch (err: unknown) {
|
||||
setError(getErrorMessage(err, "Error de conexión"));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -178,15 +181,11 @@ export default function ReportRunner() {
|
||||
}
|
||||
|
||||
if (!report) {
|
||||
return (
|
||||
<div className="p-4 text-xs text-[var(--color-primary)]">
|
||||
Cargando reporte...
|
||||
</div>
|
||||
);
|
||||
return <PageLoader message="Cargando reporte..." />;
|
||||
}
|
||||
|
||||
const moduleLabel = moduleLabels[moduleKey!] || moduleKey;
|
||||
const moduleIcon = moduleIcons[moduleKey!] || "📁";
|
||||
const moduleIcon = moduleIcons[moduleKey!] || "folder";
|
||||
|
||||
return (
|
||||
<div className="p-4">
|
||||
@@ -194,17 +193,17 @@ export default function ReportRunner() {
|
||||
<div className="flex items-center gap-2 text-xs text-[var(--color-text-muted)] mb-1">
|
||||
<Link
|
||||
to={`/informes/${moduleKey}`}
|
||||
className="decoration-transparent text-[var(--color-primary)] hover:underline"
|
||||
className="flex items-center gap-1 decoration-transparent text-[var(--color-primary)] hover:underline"
|
||||
>
|
||||
{moduleIcon} {moduleLabel}
|
||||
<Icon name={moduleIcon} className="w-3.5 h-3.5" /> {moduleLabel}
|
||||
</Link>
|
||||
<span>/</span>
|
||||
<span className="text-[var(--color-sidebar-title)]">
|
||||
{report.title}
|
||||
</span>
|
||||
</div>
|
||||
<h1 className="text-xl font-bold text-[var(--color-sidebar-title)]">
|
||||
{report.icon} {report.title}
|
||||
<h1 className="text-xl font-bold text-[var(--color-sidebar-title)] flex items-center gap-1.5">
|
||||
<Icon name={report.icon || "document"} className="w-5 h-5" /> {report.title}
|
||||
</h1>
|
||||
{report.description && (
|
||||
<p className="text-xs text-[var(--color-text-muted)] mt-1">
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useState, useEffect } from "react";
|
||||
import type { MouseEvent as ReactMouseEvent } from "react";
|
||||
import type { Column } from "../../types/table";
|
||||
import { formatValue } from "../../utils/formatters";
|
||||
import Icon from "./Icon";
|
||||
|
||||
interface DenseTableProps {
|
||||
columns: Column[];
|
||||
@@ -94,13 +95,15 @@ export default function DenseTable({
|
||||
return (
|
||||
<div className="flex-1 overflow-x-auto overflow-y-auto border border-[var(--color-border)]">
|
||||
<table className="border-collapse table-fixed">
|
||||
<thead className="sticky top-0 bg-[#f7f7f7] text-[var(--color-text)] text-xs z-10">
|
||||
<thead className="sticky top-0 bg-[var(--color-bg)] text-[var(--color-text)] text-xs z-10">
|
||||
<tr>
|
||||
{showEditColumn && (
|
||||
<th className="w-10 px-2 py-1 border-r border-[var(--color-border)]">Editar</th>
|
||||
)}
|
||||
{showCheckColumn && (
|
||||
<th className="w-10 px-2 py-1 border-r border-[var(--color-border)] text-center">✔</th>
|
||||
<th className="w-10 px-2 py-1 border-r border-[var(--color-border)] text-center">
|
||||
<Icon name="check" className="w-3.5 h-3.5 inline-block" />
|
||||
</th>
|
||||
)}
|
||||
{columns.map((column) => (
|
||||
<th
|
||||
@@ -108,7 +111,7 @@ export default function DenseTable({
|
||||
onClick={() => onColumnSelect(column.key)}
|
||||
className={`px-2 py-1 border-r border-[var(--color-border)] cursor-pointer truncate ${
|
||||
selectedColumn === column.key
|
||||
? "bg-blue-100 font-semibold"
|
||||
? "bg-[var(--color-primary-active)] font-semibold"
|
||||
: ""
|
||||
}`}
|
||||
style={{ ...getCellStyle(column), position: "relative" }}
|
||||
@@ -116,14 +119,14 @@ export default function DenseTable({
|
||||
{column.title}
|
||||
<div
|
||||
onMouseDown={(e) => startResize(column.key, column, e)}
|
||||
className="absolute top-0 right-0 bottom-0 w-[5px] cursor-col-resize hover:bg-[#b0b0b0] bg-transparent z-10"
|
||||
className="absolute top-0 right-0 bottom-0 w-[5px] cursor-col-resize hover:bg-[var(--color-text-disabled)] bg-transparent z-10"
|
||||
/>
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody className="text-xs text-[var(--color-text)] divide-y divide-[var(--color-sidebar-submenu-border)]">
|
||||
<tbody className="text-xs text-[var(--color-text)] divide-y divide-[var(--color-border-light)]">
|
||||
{data.length === 0 ? (
|
||||
<tr>
|
||||
<td
|
||||
@@ -132,7 +135,7 @@ export default function DenseTable({
|
||||
(showEditColumn ? 1 : 0) +
|
||||
(showCheckColumn ? 1 : 0)
|
||||
}
|
||||
className="text-center py-4 text-gray-400 italic"
|
||||
className="text-center py-4 text-[var(--color-text-disabled)] italic"
|
||||
>
|
||||
No se encontraron registros.
|
||||
</td>
|
||||
@@ -145,28 +148,28 @@ export default function DenseTable({
|
||||
<tr
|
||||
key={key}
|
||||
onClick={() => handleRowClick(row)}
|
||||
className={`border-b border-[var(--color-sidebar-submenu-border)] h-7 ${
|
||||
className={`border-b border-[var(--color-border-light)] h-7 ${
|
||||
selectionMode !== "none"
|
||||
? "cursor-pointer"
|
||||
: ""
|
||||
} ${
|
||||
selectionMode !== "none" && isSelected
|
||||
? "bg-[var(--color-primary-active)] font-medium text-[var(--color-primary)]"
|
||||
: "hover:bg-[#f5faff]"
|
||||
: "hover:bg-[var(--color-bg)]"
|
||||
}`}
|
||||
>
|
||||
{showEditColumn && (
|
||||
<td className="px-2 py-0.5 border-r border-[var(--color-sidebar-submenu-border)] text-center">
|
||||
<td className="px-2 py-0.5 border-r border-[var(--color-border-light)] text-center">
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); onEdit(row); }}
|
||||
className="text-[var(--color-primary)] hover:text-[var(--color-primary-hover)] bg-transparent border-none p-1 cursor-pointer text-xs"
|
||||
className="text-[var(--color-primary)] hover:text-[var(--color-primary-hover)] bg-transparent border-none p-1 cursor-pointer"
|
||||
>
|
||||
✏️
|
||||
<Icon name="edit" className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</td>
|
||||
)}
|
||||
{showCheckColumn && (
|
||||
<td className="px-2 py-0.5 border-r border-[var(--color-sidebar-submenu-border)] text-center">
|
||||
<td className="px-2 py-0.5 border-r border-[var(--color-border-light)] text-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isSelected}
|
||||
@@ -178,7 +181,7 @@ export default function DenseTable({
|
||||
{columns.map((column) => (
|
||||
<td
|
||||
key={column.key}
|
||||
className={`px-2 py-0.5 border-r border-[var(--color-sidebar-submenu-border)] truncate ${
|
||||
className={`px-2 py-0.5 border-r border-[var(--color-border-light)] truncate select-text ${
|
||||
column.align === "right"
|
||||
? "text-right"
|
||||
: column.align === "center"
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
interface FieldTooltipProps {
|
||||
text?: string;
|
||||
}
|
||||
|
||||
export default function FieldTooltip({ text }: FieldTooltipProps) {
|
||||
if (!text) return null;
|
||||
|
||||
return (
|
||||
<span className="relative inline-flex group align-middle ml-1">
|
||||
<span
|
||||
tabIndex={0}
|
||||
className="inline-flex items-center justify-center w-3.5 h-3.5 rounded-full border border-[var(--color-border)] text-[var(--color-text-muted)] text-[9px] leading-none font-semibold cursor-help select-none"
|
||||
>
|
||||
i
|
||||
</span>
|
||||
<span
|
||||
role="tooltip"
|
||||
className="pointer-events-none absolute left-1/2 bottom-full z-20 mb-1 -translate-x-1/2 whitespace-nowrap border border-[var(--color-border)] bg-[var(--color-surface)] px-2 py-1 text-[11px] font-normal normal-case text-[var(--color-text)] opacity-0 invisible group-hover:visible group-hover:opacity-100 group-focus-within:visible group-focus-within:opacity-100"
|
||||
>
|
||||
{text}
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -7,9 +7,11 @@ import type { Column } from "../../types/table";
|
||||
import type { FilterConfig, FilterValues } from "../../types/filters";
|
||||
import SearchInput from "./SearchInput";
|
||||
import PrimaryButton from "./PrimaryButton";
|
||||
import SecondaryButton from "./SecondaryButton";
|
||||
import ValueHelpModal from "./ValueHelpModal";
|
||||
import Icon from "./Icon";
|
||||
|
||||
const CTRL_CLASS = "w-full h-7 px-2 py-0 border border-[var(--color-input-border)] bg-white rounded-none outline-none focus:border-[var(--color-input-focus)] text-xs";
|
||||
const CTRL_CLASS = "w-full h-7 px-2 py-0 border border-[var(--color-input-border)] bg-[var(--color-surface)] rounded-none outline-none focus:border-[var(--color-input-focus)] text-xs";
|
||||
|
||||
interface FilterBarProps {
|
||||
searchValue: string;
|
||||
@@ -21,6 +23,7 @@ interface FilterBarProps {
|
||||
onFilterChange: (key: string, value: any) => void;
|
||||
onFilterRemove: (key: string) => void;
|
||||
onSearch: () => void;
|
||||
onClear: () => void;
|
||||
}
|
||||
|
||||
export default function FilterBar({
|
||||
@@ -33,6 +36,7 @@ export default function FilterBar({
|
||||
onFilterChange,
|
||||
onFilterRemove,
|
||||
onSearch,
|
||||
onClear,
|
||||
}: FilterBarProps) {
|
||||
const [modalState, setModalState] = useState<{
|
||||
filterKey: string;
|
||||
@@ -106,13 +110,13 @@ export default function FilterBar({
|
||||
<label className="text-[var(--color-text-muted)] text-[10px] leading-none">{f.label}</label>
|
||||
<div className={"flex items-center gap-1 " + CTRL_CLASS}>
|
||||
{arrVal.length === 0 ? (
|
||||
<span className="text-xs text-[#b0b0b0]">Todos</span>
|
||||
<span className="text-xs text-[var(--color-text-disabled)]">Todos</span>
|
||||
) : (
|
||||
<div className="flex flex-wrap gap-0.5 flex-1">
|
||||
{arrVal.map((v) => (
|
||||
<span
|
||||
key={v}
|
||||
className="inline-flex items-center gap-0.5 bg-[#e8f0fe] text-[var(--color-primary)] px-1 py-0.5 text-[10px]"
|
||||
className="inline-flex items-center gap-0.5 bg-[var(--color-primary-active)] text-[var(--color-primary)] px-1 py-0.5 text-[10px]"
|
||||
>
|
||||
{v}
|
||||
<button
|
||||
@@ -127,10 +131,10 @@ export default function FilterBar({
|
||||
)}
|
||||
<button
|
||||
onClick={() => setModalState({ filterKey: f.key, config: f })}
|
||||
className="ml-auto shrink-0 bg-transparent border-none p-0 cursor-pointer text-xs text-[var(--color-primary)]"
|
||||
className="ml-auto shrink-0 bg-transparent border-none p-0 cursor-pointer text-[var(--color-primary)]"
|
||||
title="Seleccionar"
|
||||
>
|
||||
📋
|
||||
<Icon name="search" className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -192,7 +196,10 @@ export default function FilterBar({
|
||||
{filters.map(renderFilter)}
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<label className="text-[var(--color-text-muted)] text-[10px] leading-none"> </label>
|
||||
<PrimaryButton onClick={onSearch}>Ir</PrimaryButton>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<PrimaryButton onClick={onSearch}>Ir</PrimaryButton>
|
||||
<SecondaryButton size="sm" onClick={onClear}>Limpiar</SecondaryButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{modalState && (
|
||||
|
||||
@@ -6,6 +6,7 @@ import "react-datepicker/dist/react-datepicker.css";
|
||||
import { DEFAULT_CURRENCY, DEFAULT_SYMBOL } from "../../utils/constants";
|
||||
import { formatDisplayDate } from "../../utils/dateFormat";
|
||||
import StatusBadge from "./StatusBadge";
|
||||
import FieldTooltip from "./FieldTooltip";
|
||||
import { getStatusColors } from "../../utils/statusColors";
|
||||
|
||||
interface FormattedFieldProps {
|
||||
@@ -14,7 +15,7 @@ interface FormattedFieldProps {
|
||||
value: any;
|
||||
mode: "input" | "display";
|
||||
name?: string;
|
||||
onChange?: (e: ChangeEvent<HTMLInputElement>) => void;
|
||||
onChange?: (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => void;
|
||||
required?: boolean;
|
||||
maxLength?: number;
|
||||
placeholder?: string;
|
||||
@@ -23,6 +24,9 @@ interface FormattedFieldProps {
|
||||
currencySymbol?: string;
|
||||
statusColorsKey?: string;
|
||||
displayLabels?: Record<string, string>;
|
||||
dbField?: string;
|
||||
multiline?: boolean;
|
||||
rows?: number;
|
||||
}
|
||||
|
||||
function formatNumber(value: any): string {
|
||||
@@ -46,6 +50,9 @@ export default function FormattedField({
|
||||
currencySymbol,
|
||||
statusColorsKey,
|
||||
displayLabels,
|
||||
dbField,
|
||||
multiline,
|
||||
rows = 2,
|
||||
}: FormattedFieldProps) {
|
||||
const [focused, setFocused] = useState(false);
|
||||
const symbol = currencySymbol || DEFAULT_SYMBOL;
|
||||
@@ -79,7 +86,10 @@ export default function FormattedField({
|
||||
if (colors) {
|
||||
return (
|
||||
<div className={className}>
|
||||
<label className="block text-[var(--color-text-muted)] font-medium mb-1 text-xs">{label}</label>
|
||||
<label className="block text-[var(--color-text-muted)] font-medium mb-1 text-xs">
|
||||
{label}
|
||||
<FieldTooltip text={dbField} />
|
||||
</label>
|
||||
<StatusBadge label={displayLabel} {...colors} />
|
||||
</div>
|
||||
);
|
||||
@@ -90,8 +100,19 @@ export default function FormattedField({
|
||||
}
|
||||
return (
|
||||
<div className={className}>
|
||||
<label className="block text-[var(--color-text-muted)] font-medium mb-1 text-xs">{label}</label>
|
||||
<div className="px-2 py-1 text-xs text-[var(--color-text)]">{displayValue}</div>
|
||||
<label className="block text-[var(--color-text-muted)] font-medium mb-1 text-xs">
|
||||
{label}
|
||||
<FieldTooltip text={dbField} />
|
||||
</label>
|
||||
<div
|
||||
className={
|
||||
multiline
|
||||
? "w-full px-2 py-1 border border-[var(--color-border)] bg-[var(--color-disabled-bg)] text-xs text-[var(--color-text)] whitespace-pre-wrap break-words"
|
||||
: "w-full px-2 h-[28px] flex items-center border border-[var(--color-border)] bg-[var(--color-disabled-bg)] text-xs text-[var(--color-text)] truncate"
|
||||
}
|
||||
>
|
||||
{displayValue}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -114,6 +135,7 @@ export default function FormattedField({
|
||||
{currencyCode !== DEFAULT_CURRENCY && (
|
||||
<span className="ml-1 text-[var(--color-header-muted)]">({currencyCode})</span>
|
||||
)}
|
||||
<FieldTooltip text={dbField} />
|
||||
</label>
|
||||
<div className="flex items-center border border-[var(--color-input-border)] bg-[var(--color-surface)] focus-within:border-[var(--color-input-focus)]">
|
||||
<span className="px-1.5 text-xs text-[var(--color-text-muted)]">{symbol}</span>
|
||||
@@ -147,6 +169,7 @@ export default function FormattedField({
|
||||
<div className={className}>
|
||||
<label className="block text-[var(--color-text-muted)] font-medium mb-1 text-xs">
|
||||
{label}
|
||||
<FieldTooltip text={dbField} />
|
||||
</label>
|
||||
<input
|
||||
type="time"
|
||||
@@ -166,6 +189,7 @@ export default function FormattedField({
|
||||
<div className={className}>
|
||||
<label className="block text-[var(--color-text-muted)] font-medium mb-1 text-xs">
|
||||
{label}
|
||||
<FieldTooltip text={dbField} />
|
||||
</label>
|
||||
<DateComp
|
||||
selected={dateValue}
|
||||
@@ -193,6 +217,26 @@ export default function FormattedField({
|
||||
inputProps.onChange = onChange;
|
||||
break;
|
||||
default:
|
||||
if (multiline) {
|
||||
return (
|
||||
<div className={className}>
|
||||
<label className="block text-[var(--color-text-muted)] font-medium mb-1 text-xs">
|
||||
{label}
|
||||
<FieldTooltip text={dbField} />
|
||||
</label>
|
||||
<textarea
|
||||
name={name}
|
||||
required={required}
|
||||
maxLength={maxLength}
|
||||
placeholder={placeholder}
|
||||
rows={rows}
|
||||
value={value ?? ""}
|
||||
onChange={onChange}
|
||||
className="w-full px-2 py-1 border border-[var(--color-input-border)] bg-[var(--color-surface)] rounded-none outline-none focus:border-[var(--color-input-focus)] text-xs resize-none"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
inputProps.type = "text";
|
||||
inputProps.value = value ?? "";
|
||||
inputProps.onChange = onChange;
|
||||
@@ -202,6 +246,7 @@ export default function FormattedField({
|
||||
<div className={className}>
|
||||
<label className="block text-[var(--color-text-muted)] font-medium mb-1 text-xs">
|
||||
{label}
|
||||
<FieldTooltip text={dbField} />
|
||||
</label>
|
||||
<input {...inputProps} />
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
import type { SVGProps, ReactNode } from "react";
|
||||
|
||||
const PATHS: Record<string, ReactNode> = {
|
||||
search: (
|
||||
<>
|
||||
<circle cx="11" cy="11" r="8" />
|
||||
<line x1="21" y1="21" x2="16.65" y2="16.65" />
|
||||
</>
|
||||
),
|
||||
clipboard: (
|
||||
<>
|
||||
<rect x="6" y="4" width="12" height="17" rx="1" />
|
||||
<rect x="9" y="2" width="6" height="4" rx="1" />
|
||||
</>
|
||||
),
|
||||
box: (
|
||||
<>
|
||||
<polygon points="12 2 21 7 21 17 12 22 3 17 3 7" />
|
||||
<polyline points="3 7 12 12 21 7" />
|
||||
<line x1="12" y1="12" x2="12" y2="22" />
|
||||
</>
|
||||
),
|
||||
edit: (
|
||||
<>
|
||||
<path d="M12 20h9" />
|
||||
<path d="M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z" />
|
||||
</>
|
||||
),
|
||||
check: <polyline points="20 6 9 17 4 12" />,
|
||||
trash: (
|
||||
<>
|
||||
<polyline points="3 6 5 6 21 6" />
|
||||
<path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6" />
|
||||
<path d="M10 11v6" />
|
||||
<path d="M14 11v6" />
|
||||
<path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2" />
|
||||
</>
|
||||
),
|
||||
close: (
|
||||
<>
|
||||
<line x1="18" y1="6" x2="6" y2="18" />
|
||||
<line x1="6" y1="6" x2="18" y2="18" />
|
||||
</>
|
||||
),
|
||||
chart: (
|
||||
<>
|
||||
<line x1="6" y1="20" x2="6" y2="16" />
|
||||
<line x1="12" y1="20" x2="12" y2="10" />
|
||||
<line x1="18" y1="20" x2="18" y2="4" />
|
||||
</>
|
||||
),
|
||||
users: (
|
||||
<>
|
||||
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" />
|
||||
<circle cx="9" cy="7" r="4" />
|
||||
<path d="M23 21v-2a4 4 0 0 0-3-3.87" />
|
||||
<path d="M16 3.13a4 4 0 0 1 0 7.75" />
|
||||
</>
|
||||
),
|
||||
cart: (
|
||||
<>
|
||||
<circle cx="9" cy="21" r="1" />
|
||||
<circle cx="20" cy="21" r="1" />
|
||||
<path d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6" />
|
||||
</>
|
||||
),
|
||||
briefcase: (
|
||||
<>
|
||||
<rect x="2" y="7" width="20" height="14" rx="2" />
|
||||
<path d="M16 21V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16" />
|
||||
</>
|
||||
),
|
||||
tag: (
|
||||
<>
|
||||
<path d="M11 3H4a1 1 0 0 0-1 1v7a1 1 0 0 0 .29.71l9 9a1 1 0 0 0 1.42 0l7-7a1 1 0 0 0 0-1.42l-9-9A1 1 0 0 0 11 3z" />
|
||||
<circle cx="7" cy="7" r="1.3" />
|
||||
</>
|
||||
),
|
||||
document: (
|
||||
<>
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
||||
<polyline points="14 2 14 8 20 8" />
|
||||
</>
|
||||
),
|
||||
folder: <path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z" />,
|
||||
clock: (
|
||||
<>
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<polyline points="12 6 12 12 16 14" />
|
||||
</>
|
||||
),
|
||||
lock: (
|
||||
<>
|
||||
<rect x="3" y="11" width="18" height="11" rx="2" />
|
||||
<path d="M7 11V7a5 5 0 0 1 10 0v4" />
|
||||
</>
|
||||
),
|
||||
"check-circle": (
|
||||
<>
|
||||
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14" />
|
||||
<polyline points="22 4 12 14.01 9 11.01" />
|
||||
</>
|
||||
),
|
||||
"x-circle": (
|
||||
<>
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<line x1="15" y1="9" x2="9" y2="15" />
|
||||
<line x1="9" y1="9" x2="15" y2="15" />
|
||||
</>
|
||||
),
|
||||
cash: (
|
||||
<>
|
||||
<line x1="12" y1="1" x2="12" y2="23" />
|
||||
<path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6" />
|
||||
</>
|
||||
),
|
||||
};
|
||||
|
||||
export type IconName = keyof typeof PATHS;
|
||||
|
||||
interface IconProps extends Omit<SVGProps<SVGSVGElement>, "name"> {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export default function Icon({ name, className = "w-4 h-4", ...rest }: IconProps) {
|
||||
const content = PATHS[name];
|
||||
if (!content) return null;
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={1.8}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className={className}
|
||||
aria-hidden="true"
|
||||
{...rest}
|
||||
>
|
||||
{content}
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -168,6 +168,14 @@ export default function ListViewTemplate({
|
||||
setSearchToken((t) => t + 1);
|
||||
};
|
||||
|
||||
const handleClear = () => {
|
||||
setFilterValue("");
|
||||
setActiveFilterValue("");
|
||||
setFilterValues(initFilterValues(advFilters));
|
||||
setSkip(0);
|
||||
setSearchToken((t) => t + 1);
|
||||
};
|
||||
|
||||
const handleFilterChange = useCallback((key: string, value: any) => {
|
||||
setFilterValues((prev) => ({ ...prev, [key]: value }));
|
||||
}, []);
|
||||
@@ -199,6 +207,7 @@ export default function ListViewTemplate({
|
||||
onFilterChange={handleFilterChange}
|
||||
onFilterRemove={handleFilterRemove}
|
||||
onSearch={handleBuscar}
|
||||
onClear={handleClear}
|
||||
/>
|
||||
) : (
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
interface PageLoaderProps {
|
||||
message?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Estado de carga a pantalla completa, reutilizado en dos escenarios:
|
||||
* como fallback de <Suspense> mientras React descarga el chunk de una
|
||||
* ruta cargada con lazy(), y en los formularios de detalle mientras
|
||||
* traen sus datos desde el Service Layer. Mantener un solo componente
|
||||
* evita que cada pantalla reinvente su propio "Cargando..." con
|
||||
* estilos ligeramente distintos.
|
||||
*/
|
||||
export default function PageLoader({ message = "Cargando…" }: PageLoaderProps) {
|
||||
return (
|
||||
<div className="p-4 bg-[var(--color-surface)] h-full flex flex-col font-sans select-none text-xs text-[var(--color-text)]">
|
||||
<div className="text-xs text-[var(--color-primary)] py-4">{message}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -31,7 +31,7 @@ export default function PaginationBar({
|
||||
<button
|
||||
onClick={onPrevious}
|
||||
disabled={skip === 0 || loading}
|
||||
className="px-3 py-1 text-xs border border-[var(--color-border)] bg-white disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
className="px-3 py-1 text-xs border border-[var(--color-border)] bg-[var(--color-surface)] disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
◀ Anterior
|
||||
</button>
|
||||
@@ -39,7 +39,7 @@ export default function PaginationBar({
|
||||
<button
|
||||
onClick={onNext}
|
||||
disabled={totalCount != null ? skip + top >= totalCount || loading : currentCount < top || loading}
|
||||
className="px-3 py-1 text-xs border border-[var(--color-border)] bg-white disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
className="px-3 py-1 text-xs border border-[var(--color-border)] bg-[var(--color-surface)] disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
Siguiente ▶
|
||||
</button>
|
||||
|
||||
@@ -4,18 +4,29 @@ interface PrimaryButtonProps {
|
||||
children: ReactNode;
|
||||
onClick?: () => void;
|
||||
type?: "button" | "submit" | "reset";
|
||||
disabled?: boolean;
|
||||
/** "sm": botón compacto de barra de herramientas (h-7, ancho fijo 140px). "md": botón de pie de formulario (px-4 py-2, ancho natural). */
|
||||
size?: "sm" | "md";
|
||||
}
|
||||
|
||||
const SIZE_CLASS: Record<"sm" | "md", string> = {
|
||||
sm: "w-[140px] h-7 px-3 py-0",
|
||||
md: "px-4 py-2",
|
||||
};
|
||||
|
||||
export default function PrimaryButton({
|
||||
children,
|
||||
onClick,
|
||||
type = "button",
|
||||
disabled,
|
||||
size = "sm",
|
||||
}: PrimaryButtonProps) {
|
||||
return (
|
||||
<button
|
||||
type={type}
|
||||
onClick={onClick}
|
||||
className="w-[140px] h-7 bg-[var(--color-primary)] hover:bg-[var(--color-primary-hover)] text-white text-xs font-medium px-3 py-0 rounded-none border-none cursor-pointer"
|
||||
disabled={disabled}
|
||||
className={`${SIZE_CLASS[size]} bg-[var(--color-primary)] hover:bg-[var(--color-primary-hover)] text-white text-xs font-medium rounded-none border-none cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed`}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
|
||||
@@ -20,7 +20,7 @@ export default function SearchInput({
|
||||
placeholder={placeholder}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
onKeyDown={onKeyDown}
|
||||
className="w-full h-7 px-2 py-0 text-xs bg-white border border-[var(--color-input-border)] rounded-none outline-none focus:border-[var(--color-primary)]"
|
||||
className="w-full h-7 px-2 py-0 text-xs bg-[var(--color-surface)] border border-[var(--color-input-border)] rounded-none outline-none focus:border-[var(--color-primary)]"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
interface SecondaryButtonProps {
|
||||
children: ReactNode;
|
||||
onClick?: () => void;
|
||||
type?: "button" | "submit" | "reset";
|
||||
disabled?: boolean;
|
||||
/** "sm": botón compacto de barra de herramientas (h-7). "md": botón de pie de formulario (px-4 py-2). */
|
||||
size?: "sm" | "md";
|
||||
}
|
||||
|
||||
const SIZE_CLASS: Record<"sm" | "md", string> = {
|
||||
sm: "h-7 px-3 py-0",
|
||||
md: "px-4 py-2",
|
||||
};
|
||||
|
||||
export default function SecondaryButton({
|
||||
children,
|
||||
onClick,
|
||||
type = "button",
|
||||
disabled,
|
||||
size = "md",
|
||||
}: SecondaryButtonProps) {
|
||||
return (
|
||||
<button
|
||||
type={type}
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
className={`${SIZE_CLASS[size]} bg-[var(--color-surface)] border border-[var(--color-border)] text-[var(--color-sidebar-title)] text-xs font-medium rounded-none cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed`}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import StatusBadge from "./StatusBadge";
|
||||
import FieldTooltip from "./FieldTooltip";
|
||||
import { getStatusColors } from "../../utils/statusColors";
|
||||
|
||||
interface SelectFieldProps {
|
||||
@@ -10,6 +11,7 @@ interface SelectFieldProps {
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
statusColorsKey?: string;
|
||||
dbField?: string;
|
||||
}
|
||||
|
||||
export default function SelectField({
|
||||
@@ -21,6 +23,7 @@ export default function SelectField({
|
||||
disabled,
|
||||
className = "",
|
||||
statusColorsKey,
|
||||
dbField,
|
||||
}: SelectFieldProps) {
|
||||
const colors = statusColorsKey ? getStatusColors(statusColorsKey, value) : null;
|
||||
const selectedLabel = value ? (options[value] ?? value) : "";
|
||||
@@ -29,13 +32,14 @@ export default function SelectField({
|
||||
{label && (
|
||||
<label className="block text-[var(--color-text-muted)] font-medium mb-1">
|
||||
{label}
|
||||
<FieldTooltip text={dbField} />
|
||||
</label>
|
||||
)}
|
||||
<select
|
||||
value={String(value ?? "")}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
disabled={disabled}
|
||||
className="w-full px-2 py-1 h-[28px] border border-[var(--color-input-border)] bg-white rounded-none outline-none focus:border-[var(--color-input-focus)] disabled:opacity-50 text-xs"
|
||||
className="w-full px-2 py-1 h-[28px] border border-[var(--color-input-border)] bg-[var(--color-surface)] rounded-none outline-none focus:border-[var(--color-input-focus)] disabled:opacity-50 text-xs"
|
||||
>
|
||||
{placeholder && (
|
||||
<option value="" disabled>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import Icon from "./Icon";
|
||||
|
||||
interface StatusBadgeProps {
|
||||
label: string;
|
||||
bg?: string;
|
||||
@@ -9,10 +11,10 @@ export default function StatusBadge({ label, bg, text, icon }: StatusBadgeProps)
|
||||
if (!bg) return <>{label}</>;
|
||||
return (
|
||||
<span
|
||||
className="inline-flex items-center gap-1 px-2 py-0.5 rounded text-xs font-medium"
|
||||
className="inline-flex items-center gap-1 px-2 py-0.5 rounded-none text-xs font-medium"
|
||||
style={{ backgroundColor: bg, color: text ?? "#333" }}
|
||||
>
|
||||
{icon && <span className="text-xs leading-none">{icon}</span>}
|
||||
{icon && <Icon name={icon} className="w-3 h-3 shrink-0" />}
|
||||
{label}
|
||||
</span>
|
||||
);
|
||||
|
||||
@@ -27,7 +27,7 @@ export const crystalReportsConfig: Record<string, CrystalReportDefinition[]> = {
|
||||
{
|
||||
id: "DLN20015",
|
||||
title: "Impresión de Etiquetas",
|
||||
icon: "📊",
|
||||
icon: "chart",
|
||||
parameters: [
|
||||
{
|
||||
name: "Dockey@",
|
||||
|
||||
@@ -37,7 +37,7 @@ export const htmlReportsConfig: Record<string, HtmlReportDefinition[]> = {
|
||||
id: "stickers",
|
||||
title: "Impresión de Etiquetas",
|
||||
description: "Etiquetas de guía de remisión, generadas con plantilla propia (no pasa por Crystal Reports)",
|
||||
icon: "🏷️",
|
||||
icon: "tag",
|
||||
endpoint: "stickers",
|
||||
parameters: [
|
||||
{
|
||||
|
||||
@@ -12,7 +12,7 @@ export const reportsConfig: Record<string, ReportDefinition[]> = {
|
||||
id: "estado-cuenta",
|
||||
title: "Estado de Cuenta",
|
||||
description: "Estado de cuenta detallado por cliente",
|
||||
icon: "📄",
|
||||
icon: "document",
|
||||
endpoint: "/api/reports/socios/estado-cuenta",
|
||||
outputType: "pdf",
|
||||
parameters: [
|
||||
@@ -54,7 +54,7 @@ export const reportsConfig: Record<string, ReportDefinition[]> = {
|
||||
id: "saldos-clientes",
|
||||
title: "Saldos de Clientes",
|
||||
description: "Resumen de saldos pendientes por cliente",
|
||||
icon: "💰",
|
||||
icon: "cash",
|
||||
endpoint: "/api/reports/socios/saldos",
|
||||
outputType: "table+total",
|
||||
parameters: [
|
||||
@@ -77,7 +77,7 @@ export const reportsConfig: Record<string, ReportDefinition[]> = {
|
||||
id: "clientes-por-vendedor",
|
||||
title: "Clientes por Vendedor",
|
||||
description: "Listado de clientes asignados por vendedor",
|
||||
icon: "👥",
|
||||
icon: "users",
|
||||
endpoint: "/api/reports/socios/por-vendedor",
|
||||
outputType: "table",
|
||||
parameters: [
|
||||
@@ -98,7 +98,7 @@ export const reportsConfig: Record<string, ReportDefinition[]> = {
|
||||
id: "ventas-por-articulo",
|
||||
title: "Ventas por Artículo",
|
||||
description: "Reporte de ventas agrupado por artículo",
|
||||
icon: "📊",
|
||||
icon: "chart",
|
||||
endpoint: "/api/reports/ventas/por-articulo",
|
||||
outputType: "table+total",
|
||||
parameters: [
|
||||
@@ -132,7 +132,7 @@ export const reportsConfig: Record<string, ReportDefinition[]> = {
|
||||
id: "facturas-pendientes",
|
||||
title: "Facturas Pendientes",
|
||||
description: "Facturas de venta pendientes de cobro",
|
||||
icon: "📋",
|
||||
icon: "clipboard",
|
||||
endpoint: "/api/reports/ventas/facturas-pendientes",
|
||||
outputType: "table+total",
|
||||
parameters: [
|
||||
@@ -148,7 +148,7 @@ export const reportsConfig: Record<string, ReportDefinition[]> = {
|
||||
id: "ordenes-venta",
|
||||
title: "Órdenes de Venta",
|
||||
description: "Reporte de órdenes de venta por período",
|
||||
icon: "🛒",
|
||||
icon: "cart",
|
||||
endpoint: "/api/reports/ventas/ordenes",
|
||||
outputType: "table",
|
||||
parameters: [
|
||||
@@ -190,7 +190,7 @@ export const reportsConfig: Record<string, ReportDefinition[]> = {
|
||||
id: "compras-por-proveedor",
|
||||
title: "Compras por Proveedor",
|
||||
description: "Reporte de compras agrupado por proveedor",
|
||||
icon: "📊",
|
||||
icon: "chart",
|
||||
endpoint: "/api/reports/compras/por-proveedor",
|
||||
outputType: "table+total",
|
||||
parameters: [
|
||||
@@ -229,7 +229,7 @@ export const reportsConfig: Record<string, ReportDefinition[]> = {
|
||||
id: "facturas-pendientes-compra",
|
||||
title: "Facturas Pendientes",
|
||||
description: "Facturas de compra pendientes de pago",
|
||||
icon: "📋",
|
||||
icon: "clipboard",
|
||||
endpoint: "/api/reports/compras/facturas-pendientes",
|
||||
outputType: "table+total",
|
||||
parameters: [
|
||||
@@ -254,7 +254,7 @@ export const reportsConfig: Record<string, ReportDefinition[]> = {
|
||||
id: "kardex",
|
||||
title: "Kardex de Artículos",
|
||||
description: "Movimiento de artículos por período",
|
||||
icon: "📦",
|
||||
icon: "box",
|
||||
endpoint: "/api/reports/inventario/kardex",
|
||||
outputType: "table",
|
||||
parameters: [
|
||||
@@ -292,7 +292,7 @@ export const reportsConfig: Record<string, ReportDefinition[]> = {
|
||||
id: "inventario-valorizado",
|
||||
title: "Inventario Valorizado",
|
||||
description: "Stock valorizado al costo",
|
||||
icon: "💰",
|
||||
icon: "cash",
|
||||
endpoint: "/api/reports/inventario/valorizado",
|
||||
outputType: "table+total",
|
||||
parameters: [
|
||||
@@ -316,7 +316,7 @@ export const reportsConfig: Record<string, ReportDefinition[]> = {
|
||||
id: "lotes-por-vencer",
|
||||
title: "Lotes por Vencer",
|
||||
description: "Lotes próximos a vencer",
|
||||
icon: "⏰",
|
||||
icon: "clock",
|
||||
endpoint: "/api/reports/inventario/lotes-por-vencer",
|
||||
outputType: "table",
|
||||
parameters: [
|
||||
@@ -346,8 +346,8 @@ export const moduleLabels: Record<string, string> = {
|
||||
};
|
||||
|
||||
export const moduleIcons: Record<string, string> = {
|
||||
socios: "👥",
|
||||
ventas: "🛒",
|
||||
compras: "💼",
|
||||
inventario: "📦",
|
||||
socios: "users",
|
||||
ventas: "cart",
|
||||
compras: "briefcase",
|
||||
inventario: "box",
|
||||
};
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { Link, Outlet, useLocation } from "react-router-dom";
|
||||
import { API_URL, getToken, getUsername, getCompany } from "../utils/api";
|
||||
import Icon from "../components/ui/Icon";
|
||||
|
||||
// Estructura del menú tipada para SAP Business One
|
||||
interface SubMenuItem {
|
||||
@@ -17,12 +18,12 @@ interface MenuItem {
|
||||
const menuConfig: MenuItem[] = [
|
||||
{
|
||||
title: "0. Dashboard",
|
||||
icon: "📊",
|
||||
icon: "chart",
|
||||
subItems: [{ name: "Inicio", path: "/dashboard" }],
|
||||
},
|
||||
{
|
||||
title: "1. Inventario",
|
||||
icon: "📦",
|
||||
icon: "box",
|
||||
subItems: [
|
||||
{ name: "Articulos", path: "/inventario/articulos" },
|
||||
{ name: "Lotes", path: "/inventario/lotes" },
|
||||
@@ -30,7 +31,7 @@ const menuConfig: MenuItem[] = [
|
||||
},
|
||||
{
|
||||
title: "2. Socio de negocios",
|
||||
icon: "👥",
|
||||
icon: "users",
|
||||
subItems: [
|
||||
{ name: "Clientes", path: "/socios/clientes" },
|
||||
{ name: "Proveedores", path: "/socios/proveedores" },
|
||||
@@ -38,7 +39,7 @@ const menuConfig: MenuItem[] = [
|
||||
},
|
||||
{
|
||||
title: "3. Ventas",
|
||||
icon: "🛒",
|
||||
icon: "cart",
|
||||
subItems: [
|
||||
{ name: "Ordenes de venta", path: "/ventas/ordenes" },
|
||||
{ name: "Entregas o Guías", path: "/ventas/entrega" },
|
||||
@@ -47,7 +48,7 @@ const menuConfig: MenuItem[] = [
|
||||
},
|
||||
{
|
||||
title: "4. Compras",
|
||||
icon: "💼",
|
||||
icon: "briefcase",
|
||||
subItems: [
|
||||
{ name: "Ordenes de compra", path: "/compras/ordenes" },
|
||||
{ name: "Recepciones de compras", path: "/compras/recepcion" },
|
||||
@@ -56,7 +57,7 @@ const menuConfig: MenuItem[] = [
|
||||
},
|
||||
{
|
||||
title: "5. Informes",
|
||||
icon: "📊",
|
||||
icon: "chart",
|
||||
subItems: [
|
||||
{ name: "Socios de negocios", path: "/informes/socios" },
|
||||
{ name: "Ventas", path: "/informes/ventas" },
|
||||
@@ -66,7 +67,7 @@ const menuConfig: MenuItem[] = [
|
||||
},
|
||||
{
|
||||
title: "6. Reportes Crystal",
|
||||
icon: "📊",
|
||||
icon: "chart",
|
||||
subItems: [
|
||||
{ name: "Socios de negocios", path: "/reportes-crystal/socios" },
|
||||
{ name: "Ventas", path: "/reportes-crystal/ventas" },
|
||||
@@ -76,7 +77,7 @@ const menuConfig: MenuItem[] = [
|
||||
},
|
||||
{
|
||||
title: "7. Reportes HTML",
|
||||
icon: "🏷️",
|
||||
icon: "tag",
|
||||
subItems: [
|
||||
{ name: "Socios de negocios", path: "/reportes-html/socios" },
|
||||
{ name: "Ventas", path: "/reportes-html/ventas" },
|
||||
@@ -207,7 +208,7 @@ export default function MainLayout() {
|
||||
}`}
|
||||
disabled={!isSidebarOpen}
|
||||
>
|
||||
<span className="text-sm">{module.icon}</span>
|
||||
<Icon name={module.icon} className="w-4 h-4 shrink-0" />
|
||||
{isSidebarOpen && (
|
||||
<>
|
||||
<span className="ml-2 flex-1 truncate">
|
||||
|
||||
@@ -19,17 +19,17 @@
|
||||
--color-sidebar-submenu-bg: #eef1f6;
|
||||
--color-sidebar-submenu-border: #d8dde6;
|
||||
|
||||
--color-bg: #f4f6f9;
|
||||
--color-bg: #eef3fa;
|
||||
--color-surface: #ffffff;
|
||||
--color-surface-alt: #fafbfc;
|
||||
--color-disabled-bg: #eceef1;
|
||||
--color-surface-alt: #f5f8fc;
|
||||
--color-disabled-bg: #e6ebf3;
|
||||
|
||||
--color-text: #181818;
|
||||
--color-text-muted: #5a5a5a;
|
||||
--color-text-disabled: #969696;
|
||||
|
||||
--color-border: #c9d0d9;
|
||||
--color-border-light: #d8dde6;
|
||||
--color-border: #b9c5d6;
|
||||
--color-border-light: #cdd9e8;
|
||||
--color-input-border: #b0b9c6;
|
||||
--color-input-focus: #0070d2;
|
||||
|
||||
|
||||
@@ -19,17 +19,17 @@
|
||||
--color-sidebar-submenu-bg: #f1f5f9;
|
||||
--color-sidebar-submenu-border: #e2e8f0;
|
||||
|
||||
--color-bg: #f1f5f9;
|
||||
--color-bg: #eff6ff;
|
||||
--color-surface: #ffffff;
|
||||
--color-surface-alt: #f8fafc;
|
||||
--color-disabled-bg: #f1f5f9;
|
||||
--color-surface-alt: #f5faff;
|
||||
--color-disabled-bg: #e6f0fe;
|
||||
|
||||
--color-text: #0f172a;
|
||||
--color-text-muted: #64748b;
|
||||
--color-text-disabled: #94a3b8;
|
||||
|
||||
--color-border: #e2e8f0;
|
||||
--color-border-light: #f1f5f9;
|
||||
--color-border: #c7dcf7;
|
||||
--color-border-light: #dbeafe;
|
||||
--color-input-border: #cbd5e1;
|
||||
--color-input-focus: #2563eb;
|
||||
|
||||
|
||||
@@ -19,17 +19,17 @@
|
||||
--color-sidebar-submenu-bg: #1a3355;
|
||||
--color-sidebar-submenu-border: #2a4a6e;
|
||||
|
||||
--color-bg: #f0f4f8;
|
||||
--color-bg: #eaf3fb;
|
||||
--color-surface: #ffffff;
|
||||
--color-surface-alt: #f8fafc;
|
||||
--color-disabled-bg: #e2e8f0;
|
||||
--color-surface-alt: #f2f8fc;
|
||||
--color-disabled-bg: #dceaf5;
|
||||
|
||||
--color-text: #1e293b;
|
||||
--color-text-muted: #64748b;
|
||||
--color-text-disabled: #94a3b8;
|
||||
|
||||
--color-border: #cbd5e1;
|
||||
--color-border-light: #e2e8f0;
|
||||
--color-border: #b7cee3;
|
||||
--color-border-light: #d2e3f0;
|
||||
--color-input-border: #bfdbfe;
|
||||
--color-input-focus: #1a5c9e;
|
||||
|
||||
|
||||
@@ -19,17 +19,17 @@
|
||||
--color-sidebar-submenu-bg: #2d3a4d;
|
||||
--color-sidebar-submenu-border: #475569;
|
||||
|
||||
--color-bg: #f1f5f9;
|
||||
--color-bg: #eefbfa;
|
||||
--color-surface: #ffffff;
|
||||
--color-surface-alt: #f8fafc;
|
||||
--color-disabled-bg: #e2e8f0;
|
||||
--color-surface-alt: #f4fdfc;
|
||||
--color-disabled-bg: #dff5f2;
|
||||
|
||||
--color-text: #0f172a;
|
||||
--color-text-muted: #475569;
|
||||
--color-text-disabled: #94a3b8;
|
||||
|
||||
--color-border: #cbd5e1;
|
||||
--color-border-light: #e2e8f0;
|
||||
--color-border: #b8ded9;
|
||||
--color-border-light: #d3ece8;
|
||||
--color-input-border: #ccfbf1;
|
||||
--color-input-focus: #0d9488;
|
||||
|
||||
|
||||
@@ -17,4 +17,7 @@ export interface BatchNumberDetail {
|
||||
Observaciones?: string;
|
||||
EstadoLiberacion?: string;
|
||||
HoraAprobacion?: string;
|
||||
MotivoEstado?: string;
|
||||
ComentarioEstado? : string;
|
||||
AccionATomar? : string;
|
||||
}
|
||||
@@ -3,16 +3,26 @@ import type { Column } from "./table";
|
||||
export type CampoType = "text" | "number" | "currency" | "date";
|
||||
|
||||
export interface CampoConfig {
|
||||
name: string;
|
||||
/** Debe ser un campo real de MarketingDocument: así MarketingDocumentForm
|
||||
* puede leer doc[name] sin recurrir a "as any". */
|
||||
name: keyof MarketingDocument;
|
||||
label: string;
|
||||
type: CampoType;
|
||||
required?: boolean;
|
||||
maxLength?: number;
|
||||
placeholder?: string;
|
||||
/** No editable en ningún caso (creación ni actualización). */
|
||||
readOnly?: boolean;
|
||||
/** No editable solo cuando se está actualizando un documento existente. */
|
||||
readOnlyOnUpdate?: boolean;
|
||||
/** Muestra el campo en varias líneas (textarea en modo input, texto con wrap en modo display). */
|
||||
multiline?: boolean;
|
||||
}
|
||||
|
||||
export interface LineColumnConfig {
|
||||
key: string;
|
||||
/** Debe ser un campo real de DocumentLine: así la tabla de líneas puede
|
||||
* leer line[key] sin recurrir a "as any". */
|
||||
key: keyof DocumentLine;
|
||||
title: string;
|
||||
width?: string;
|
||||
type?: CampoType;
|
||||
@@ -28,7 +38,8 @@ export interface LineColumnConfig {
|
||||
* nuevo exclusivo de un documento no requiere tocar MarketingDocumentForm.
|
||||
*/
|
||||
export interface HeaderExtraFieldConfig {
|
||||
name: string;
|
||||
/** Igual que en CampoConfig: debe ser un campo real de MarketingDocument. */
|
||||
name: keyof MarketingDocument;
|
||||
label: string;
|
||||
type?: CampoType | "select";
|
||||
required?: boolean;
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* `catch` recibe `unknown` en TypeScript estricto porque JavaScript permite
|
||||
* lanzar cualquier valor (no solo instancias de Error). Este helper evita
|
||||
* repetir la verificación `instanceof Error` en cada bloque catch de la app.
|
||||
*/
|
||||
export function getErrorMessage(err: unknown, fallback: string): string {
|
||||
return err instanceof Error && err.message ? err.message : fallback;
|
||||
}
|
||||
@@ -1,17 +1,17 @@
|
||||
const STATUS_COLORS_MAP: Record<string, Record<string, { bg: string; text: string; icon?: string }>> = {
|
||||
BATCH_STATUS_APROBACION: {
|
||||
"101": { bg: "#fff8e1", text: "#f57f17", icon: "⏳" },
|
||||
"102": { bg: "#e8f5e9", text: "#2e7d32", icon: "✅" },
|
||||
"103": { bg: "#fce4ec", text: "#c62828", icon: "❌" },
|
||||
"104": { bg: "#f5f5f5", text: "#616161", icon: "🔒" },
|
||||
"101": { bg: "#fff8e1", text: "#f57f17", icon: "clock" },
|
||||
"102": { bg: "#e8f5e9", text: "#2e7d32", icon: "check-circle" },
|
||||
"103": { bg: "#fce4ec", text: "#c62828", icon: "x-circle" },
|
||||
"104": { bg: "#f5f5f5", text: "#616161", icon: "lock" },
|
||||
},
|
||||
BATCH_STATUS_LIBERACION: {
|
||||
"101": { bg: "#e8f5e9", text: "#2e7d32", icon: "✅" },
|
||||
"101": { bg: "#e8f5e9", text: "#2e7d32", icon: "check-circle" },
|
||||
"102": { bg: "#fff8e1", text: "#f57f17" },
|
||||
},
|
||||
DOCUMENT_STATUS: {
|
||||
"bost_Open": { bg: "#e8f5e9", text: "#2e7d32", icon: "✅" },
|
||||
"bost_Close": { bg: "#fce4ec", text: "#c62828", icon: "🔒" },
|
||||
"bost_Open": { bg: "#e8f5e9", text: "#2e7d32", icon: "check-circle" },
|
||||
"bost_Close": { bg: "#fce4ec", text: "#c62828", icon: "lock" },
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -106,7 +106,7 @@ const activityItems = [
|
||||
{ id: 4, text: "Artículo 'Laptop Pro' agregado", time: "Hace 2 h" },
|
||||
];
|
||||
|
||||
export default function Dashboard() {
|
||||
export default function DashboardView() {
|
||||
return (
|
||||
<div style={{ padding: "24px" }}>
|
||||
<DashboardSection title="KPI">
|
||||
+1
-1
@@ -18,7 +18,7 @@ const purchaseInvoiceHeaderExtraFields: HeaderExtraFieldConfig[] = [
|
||||
{ name: "NumAtCard", label: "N° Documento Proveedor", type: "text", placeholder: "F001-00000001" },
|
||||
];
|
||||
|
||||
export default function PurchaseInvoices() {
|
||||
export default function PurchaseInvoicesView() {
|
||||
const [searchParams] = useSearchParams();
|
||||
const docEntry = searchParams.get("code") || undefined;
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@ const purchaseOrderHeaderExtraFields: HeaderExtraFieldConfig[] = [
|
||||
{ name: "NumAtCard", label: "O/C del Proveedor", type: "text", placeholder: "OC-12345" },
|
||||
];
|
||||
|
||||
export default function PurchaseOrders() {
|
||||
export default function PurchaseOrdersView() {
|
||||
const [searchParams] = useSearchParams();
|
||||
const docEntry = searchParams.get("code") || undefined;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useParams, Link } from "react-router-dom";
|
||||
import { reportsConfig, moduleLabels, moduleIcons } from "../../config/reports";
|
||||
import Icon from "../../components/ui/Icon";
|
||||
|
||||
const outputTypeLabels: Record<string, string> = {
|
||||
table: "Tabla",
|
||||
@@ -32,13 +33,13 @@ export default function ModuleReportsList() {
|
||||
|
||||
const reports = reportsConfig[moduleKey];
|
||||
const moduleLabel = moduleLabels[moduleKey] || moduleKey;
|
||||
const moduleIcon = moduleIcons[moduleKey] || "📁";
|
||||
const moduleIcon = moduleIcons[moduleKey] || "folder";
|
||||
|
||||
return (
|
||||
<div className="p-4">
|
||||
<div className="mb-4">
|
||||
<h1 className="text-xl font-bold text-[var(--color-sidebar-title)]">
|
||||
{moduleIcon} Informes - {moduleLabel}
|
||||
<h1 className="text-xl font-bold text-[var(--color-sidebar-title)] flex items-center gap-1.5">
|
||||
<Icon name={moduleIcon} className="w-5 h-5" /> Informes - {moduleLabel}
|
||||
</h1>
|
||||
<p className="text-xs text-[var(--color-text-muted)] mt-1">
|
||||
Seleccione un reporte para generar
|
||||
@@ -55,12 +56,10 @@ export default function ModuleReportsList() {
|
||||
<Link
|
||||
key={report.id}
|
||||
to={`/informes/${moduleKey}/${report.id}`}
|
||||
className="block p-4 border border-[var(--color-border)] bg-[var(--color-surface)] hover:border-[var(--color-primary)] hover:shadow-sm transition-all decoration-transparent"
|
||||
className="block p-4 border border-[var(--color-border)] bg-[var(--color-surface)] hover:border-[var(--color-primary)] decoration-transparent"
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<span className="text-xl shrink-0 leading-none">
|
||||
{report.icon || "📄"}
|
||||
</span>
|
||||
<Icon name={report.icon || "document"} className="w-5 h-5 shrink-0 text-[var(--color-primary)]" />
|
||||
<div className="min-w-0">
|
||||
<h3 className="text-sm font-semibold text-[var(--color-sidebar-title)] truncate">
|
||||
{report.title}
|
||||
|
||||
@@ -16,12 +16,12 @@ const filters: FilterConfig[] = [
|
||||
},
|
||||
{
|
||||
key: "BatchAttribute1",
|
||||
label: "Atributo 1",
|
||||
label: "Lote proveedor",
|
||||
type: "text",
|
||||
},
|
||||
{
|
||||
key: "BatchAttribute2",
|
||||
label: "Atributo 2",
|
||||
label: "Ubicación",
|
||||
type: "text",
|
||||
},
|
||||
{
|
||||
@@ -41,8 +41,8 @@ export default function BatchNumberDetailsList() {
|
||||
{ key: "Batch", title: "Lote", width: "140px" },
|
||||
{ key: "ItemCode", title: "Código Artículo", width: "120px" },
|
||||
{ key: "ItemDescription", title: "Descripción", width: "200px" },
|
||||
{ key: "BatchAttribute1", title: "Atributo 1", width: "100px" },
|
||||
{ key: "BatchAttribute2", title: "Atributo 2", width: "100px" },
|
||||
{ key: "BatchAttribute1", title: "Lote proveedor", width: "100px" },
|
||||
{ key: "BatchAttribute2", title: "Ubicación", width: "100px" },
|
||||
{
|
||||
key: "AdmissionDate",
|
||||
title: "Fecha Ingreso",
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import BatchForm from "../../components/BatchForm";
|
||||
|
||||
export default function BatchNumberDetails() {
|
||||
export default function BatchNumberDetailsView() {
|
||||
const [searchParams] = useSearchParams();
|
||||
const batchId = searchParams.get("code");
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import ItemForm from "../../components/ItemForm";
|
||||
|
||||
export default function Items() {
|
||||
export default function ItemsView() {
|
||||
const [searchParams] = useSearchParams();
|
||||
const itemCode = searchParams.get("code") || undefined;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useParams, Link } from "react-router-dom";
|
||||
import { moduleLabels, moduleIcons } from "../../config/reports";
|
||||
import { crystalReportsConfig } from "../../config/crystalReports";
|
||||
import Icon from "../../components/ui/Icon";
|
||||
|
||||
const validModules = ["socios", "ventas", "compras", "inventario"];
|
||||
|
||||
@@ -28,13 +29,13 @@ export default function CrystalReportsList() {
|
||||
|
||||
const reports = crystalReportsConfig[moduleKey] || [];
|
||||
const moduleLabel = moduleLabels[moduleKey] || moduleKey;
|
||||
const moduleIcon = moduleIcons[moduleKey] || "📁";
|
||||
const moduleIcon = moduleIcons[moduleKey] || "folder";
|
||||
|
||||
return (
|
||||
<div className="p-4">
|
||||
<div className="mb-4">
|
||||
<h1 className="text-xl font-bold text-[var(--color-sidebar-title)]">
|
||||
{moduleIcon} Reportes Crystal - {moduleLabel}
|
||||
<h1 className="text-xl font-bold text-[var(--color-sidebar-title)] flex items-center gap-1.5">
|
||||
<Icon name={moduleIcon} className="w-5 h-5" /> Reportes Crystal - {moduleLabel}
|
||||
</h1>
|
||||
<p className="text-xs text-[var(--color-text-muted)] mt-1">
|
||||
Reportes Crystal de SAP Business One vía APIGateway
|
||||
@@ -51,12 +52,10 @@ export default function CrystalReportsList() {
|
||||
<Link
|
||||
key={report.id}
|
||||
to={`/reportes-crystal/${moduleKey}/${report.id}`}
|
||||
className="block p-4 border border-[var(--color-border)] bg-[var(--color-surface)] hover:border-[var(--color-primary)] hover:shadow-sm transition-all decoration-transparent"
|
||||
className="block p-4 border border-[var(--color-border)] bg-[var(--color-surface)] hover:border-[var(--color-primary)] decoration-transparent"
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<span className="text-xl shrink-0 leading-none">
|
||||
{report.icon || "📄"}
|
||||
</span>
|
||||
<Icon name={report.icon || "document"} className="w-5 h-5 shrink-0 text-[var(--color-primary)]" />
|
||||
<div className="min-w-0">
|
||||
<h3 className="text-sm font-semibold text-[var(--color-sidebar-title)] truncate">
|
||||
{report.title}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useParams, Link } from "react-router-dom";
|
||||
import { moduleLabels, moduleIcons } from "../../config/reports";
|
||||
import { htmlReportsConfig } from "../../config/htmlReports";
|
||||
import Icon from "../../components/ui/Icon";
|
||||
|
||||
const validModules = ["socios", "ventas", "compras", "inventario"];
|
||||
|
||||
@@ -28,13 +29,13 @@ export default function HtmlReportsList() {
|
||||
|
||||
const reports = htmlReportsConfig[moduleKey] || [];
|
||||
const moduleLabel = moduleLabels[moduleKey] || moduleKey;
|
||||
const moduleIcon = moduleIcons[moduleKey] || "📁";
|
||||
const moduleIcon = moduleIcons[moduleKey] || "folder";
|
||||
|
||||
return (
|
||||
<div className="p-4">
|
||||
<div className="mb-4">
|
||||
<h1 className="text-xl font-bold text-[var(--color-sidebar-title)]">
|
||||
{moduleIcon} Reportes HTML - {moduleLabel}
|
||||
<h1 className="text-xl font-bold text-[var(--color-sidebar-title)] flex items-center gap-1.5">
|
||||
<Icon name={moduleIcon} className="w-5 h-5" /> Reportes HTML - {moduleLabel}
|
||||
</h1>
|
||||
<p className="text-xs text-[var(--color-text-muted)] mt-1">
|
||||
Reportes propios (Jinja2 + HTML), generados directamente por el backend sin pasar por Crystal Reports
|
||||
@@ -51,12 +52,10 @@ export default function HtmlReportsList() {
|
||||
<Link
|
||||
key={report.id}
|
||||
to={`/reportes-html/${moduleKey}/${report.id}`}
|
||||
className="block p-4 border border-[var(--color-border)] bg-[var(--color-surface)] hover:border-[var(--color-primary)] hover:shadow-sm transition-all decoration-transparent"
|
||||
className="block p-4 border border-[var(--color-border)] bg-[var(--color-surface)] hover:border-[var(--color-primary)] decoration-transparent"
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<span className="text-xl shrink-0 leading-none">
|
||||
{report.icon || "📄"}
|
||||
</span>
|
||||
<Icon name={report.icon || "document"} className="w-5 h-5 shrink-0 text-[var(--color-primary)]" />
|
||||
<div className="min-w-0">
|
||||
<h3 className="text-sm font-semibold text-[var(--color-sidebar-title)] truncate">
|
||||
{report.title}
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ import type { ExtraFieldConfig } from "../../components/BusinessPartnerForm";
|
||||
// sumar una entrada aquí (o quitarla si es exclusiva de Proveedores, en Suppliers.tsx).
|
||||
const customerExtraFields: ExtraFieldConfig[] = [];
|
||||
|
||||
export default function Customer() {
|
||||
export default function CustomersView() {
|
||||
return (
|
||||
<BusinessPartnerForm
|
||||
cardType="cCustomer"
|
||||
+1
-1
@@ -24,7 +24,7 @@ const supplierExtraFields: ExtraFieldConfig[] = [
|
||||
},
|
||||
];
|
||||
|
||||
export default function Supplier() {
|
||||
export default function SuppliersView() {
|
||||
return (
|
||||
<BusinessPartnerForm
|
||||
cardType="cSupplier"
|
||||
+1
-1
@@ -18,7 +18,7 @@ const invoiceHeaderExtraFields: HeaderExtraFieldConfig[] = [
|
||||
{ name: "GroupNum", label: "Condición Pago", type: "number" },
|
||||
];
|
||||
|
||||
export default function Invoices() {
|
||||
export default function InvoicesView() {
|
||||
const [searchParams] = useSearchParams();
|
||||
const docEntry = searchParams.get("code") || undefined;
|
||||
|
||||
+1
-1
@@ -26,7 +26,7 @@ const orderHeaderExtraFields: HeaderExtraFieldConfig[] = [
|
||||
},
|
||||
];
|
||||
|
||||
export default function Orders() {
|
||||
export default function OrdersView() {
|
||||
const [searchParams] = useSearchParams();
|
||||
const docEntry = searchParams.get("code") || undefined;
|
||||
|
||||
Reference in new issue
Block a user