diff --git a/mini-backend/menu_permissions.json b/mini-backend/menu_permissions.json index b4d6968..4a0d036 100644 --- a/mini-backend/menu_permissions.json +++ b/mini-backend/menu_permissions.json @@ -18,5 +18,8 @@ "/inventario/lotes", "/ventas/*", "/compras/*" + ], + "vania.barrios@tagumedica.com":[ + "/inventario/lotes" ] } \ No newline at end of file diff --git a/mini-backend/test/ApiGateway/ExportPDFData.yml b/mini-backend/tools/ApiGateway/ExportPDFData.yml similarity index 100% rename from mini-backend/test/ApiGateway/ExportPDFData.yml rename to mini-backend/tools/ApiGateway/ExportPDFData.yml diff --git a/mini-backend/test/ApiGateway/LoadAuthorizedCRList.yml b/mini-backend/tools/ApiGateway/LoadAuthorizedCRList.yml similarity index 100% rename from mini-backend/test/ApiGateway/LoadAuthorizedCRList.yml rename to mini-backend/tools/ApiGateway/LoadAuthorizedCRList.yml diff --git a/mini-backend/test/ApiGateway/LoadCR.yml b/mini-backend/tools/ApiGateway/LoadCR.yml similarity index 100% rename from mini-backend/test/ApiGateway/LoadCR.yml rename to mini-backend/tools/ApiGateway/LoadCR.yml diff --git a/mini-backend/test/ApiGateway/Login.yml b/mini-backend/tools/ApiGateway/Login.yml similarity index 100% rename from mini-backend/test/ApiGateway/Login.yml rename to mini-backend/tools/ApiGateway/Login.yml diff --git a/mini-backend/test/ApiGateway/opencollection.yml b/mini-backend/tools/ApiGateway/opencollection.yml similarity index 100% rename from mini-backend/test/ApiGateway/opencollection.yml rename to mini-backend/tools/ApiGateway/opencollection.yml diff --git a/mini-backend/test/apiSN/Login.yml b/mini-backend/tools/apiSN/Login.yml similarity index 100% rename from mini-backend/test/apiSN/Login.yml rename to mini-backend/tools/apiSN/Login.yml diff --git a/mini-backend/test/apiSN/opencollection.yml b/mini-backend/tools/apiSN/opencollection.yml similarity index 100% rename from mini-backend/test/apiSN/opencollection.yml rename to mini-backend/tools/apiSN/opencollection.yml diff --git a/mini-backend/test/backend/Login.yml b/mini-backend/tools/backend/Login.yml similarity index 100% rename from mini-backend/test/backend/Login.yml rename to mini-backend/tools/backend/Login.yml diff --git a/mini-backend/test/backend/Logout.yml b/mini-backend/tools/backend/Logout.yml similarity index 100% rename from mini-backend/test/backend/Logout.yml rename to mini-backend/tools/backend/Logout.yml diff --git a/mini-backend/test/backend/opencollection.yml b/mini-backend/tools/backend/opencollection.yml similarity index 100% rename from mini-backend/test/backend/opencollection.yml rename to mini-backend/tools/backend/opencollection.yml diff --git a/mini-backend/test/sap-aberio/BusinessPartners.yml b/mini-backend/tools/sap-aberio/BusinessPartners.yml similarity index 100% rename from mini-backend/test/sap-aberio/BusinessPartners.yml rename to mini-backend/tools/sap-aberio/BusinessPartners.yml diff --git a/mini-backend/test/sap-aberio/DeliveryNotes.yml b/mini-backend/tools/sap-aberio/DeliveryNotes.yml similarity index 100% rename from mini-backend/test/sap-aberio/DeliveryNotes.yml rename to mini-backend/tools/sap-aberio/DeliveryNotes.yml diff --git a/mini-backend/test/sap-aberio/Invoices.yml b/mini-backend/tools/sap-aberio/Invoices.yml similarity index 100% rename from mini-backend/test/sap-aberio/Invoices.yml rename to mini-backend/tools/sap-aberio/Invoices.yml diff --git a/mini-backend/test/sap-aberio/Items.yml b/mini-backend/tools/sap-aberio/Items.yml similarity index 100% rename from mini-backend/test/sap-aberio/Items.yml rename to mini-backend/tools/sap-aberio/Items.yml diff --git a/mini-backend/test/sap-aberio/Login.yml b/mini-backend/tools/sap-aberio/Login.yml similarity index 100% rename from mini-backend/test/sap-aberio/Login.yml rename to mini-backend/tools/sap-aberio/Login.yml diff --git a/mini-backend/test/sap-aberio/PurchaseInvoices.yml b/mini-backend/tools/sap-aberio/PurchaseInvoices.yml similarity index 100% rename from mini-backend/test/sap-aberio/PurchaseInvoices.yml rename to mini-backend/tools/sap-aberio/PurchaseInvoices.yml diff --git a/mini-backend/test/sap-aberio/opencollection.yml b/mini-backend/tools/sap-aberio/opencollection.yml similarity index 100% rename from mini-backend/test/sap-aberio/opencollection.yml rename to mini-backend/tools/sap-aberio/opencollection.yml diff --git a/mini-backend/test/sap-tagumedica/sl.http b/mini-backend/tools/sap-tagumedica/sl.http similarity index 100% rename from mini-backend/test/sap-tagumedica/sl.http rename to mini-backend/tools/sap-tagumedica/sl.http diff --git a/mini-frontend/docs/flujo-articulos.md b/mini-frontend/docs/flujo-articulos.md index a2da49c..770d170 100644 --- a/mini-frontend/docs/flujo-articulos.md +++ b/mini-frontend/docs/flujo-articulos.md @@ -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) | diff --git a/mini-frontend/docs/flujo-documentos-marketing.md b/mini-frontend/docs/flujo-documentos-marketing.md index 8400358..840dac9 100644 --- a/mini-frontend/docs/flujo-documentos-marketing.md +++ b/mini-frontend/docs/flujo-documentos-marketing.md @@ -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: 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) diff --git a/mini-frontend/docs/flujo-lotes.md b/mini-frontend/docs/flujo-lotes.md index b172532..17cb126 100644 --- a/mini-frontend/docs/flujo-lotes.md +++ b/mini-frontend/docs/flujo-lotes.md @@ -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 | diff --git a/mini-frontend/docs/flujo-socios-negocio.md b/mini-frontend/docs/flujo-socios-negocio.md index 088fb6b..18f5dd0 100644 --- a/mini-frontend/docs/flujo-socios-negocio.md +++ b/mini-frontend/docs/flujo-socios-negocio.md @@ -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 | | ------------------------- | ------------ | ------------------------------ | ------------------------------------------------------------- | diff --git a/mini-frontend/docs/index.md b/mini-frontend/docs/index.md index 77ab8c8..5b525db 100644 --- a/mini-frontend/docs/index.md +++ b/mini-frontend/docs/index.md @@ -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 `}>` 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). diff --git a/mini-frontend/index.html b/mini-frontend/index.html index 53c1b4a..d02f1ba 100644 --- a/mini-frontend/index.html +++ b/mini-frontend/index.html @@ -4,7 +4,7 @@ - sap-frontend + Mini ERP · SAP Business One
diff --git a/mini-frontend/src/App.tsx b/mini-frontend/src/App.tsx index 1d4ab74..30bc069 100644 --- a/mini-frontend/src/App.tsx +++ b/mini-frontend/src/App.tsx @@ -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 = () => ( -
-

Entregas / Guías

-
+// 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 = () => ( -
-

Recepciones de Compra

-
+// 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 ( +
+

+ Entregas / Guías +

+
+ ); +} + +function Receiving() { + return ( +
+

+ Recepciones de Compra +

+
+ ); +} + +// Bloquea el acceso a todo lo que cuelga de "/" si no hay sesión SAP activa. function ProtectedRoute({ children }: { children: ReactNode }) { if (!getToken()) { return ; @@ -49,55 +98,102 @@ function ProtectedRoute({ children }: { children: ReactNode }) { export default function App() { return ( - - } /> + {/* 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. */} + }> + + } /> - - - - } - > } - /> - } /> - {/* 1. Inventario */} - } /> - } /> - } /> - } /> - {/* 2. Socios de Negocios */} - } /> - } /> - } /> - } /> - {/* 3. Ventas */} - } /> - } /> - } /> - } /> - } /> - {/* 4. Compras */} - } /> - } /> - } /> - } /> - } /> - {/* 5. Informes */} - } /> - } /> - } /> - } /> - } /> - } /> - + path="/" + element={ + + + + } + > + } /> - } /> - + } /> + + {/* 1. Inventario */} + } /> + } + /> + } + /> + } + /> + + {/* 2. Socios de Negocios */} + } /> + } + /> + } /> + } + /> + + {/* 3. Ventas */} + } /> + } /> + } /> + } /> + } + /> + + {/* 4. Compras */} + } /> + } + /> + } /> + } /> + } + /> + + {/* 5. Informes */} + } /> + } + /> + } + /> + } + /> + } + /> + } + /> + + + } /> + +
); } diff --git a/mini-frontend/src/components/BatchForm.tsx b/mini-frontend/src/components/BatchForm.tsx index b457e5d..c516030 100644 --- a/mini-frontend/src/components/BatchForm.tsx +++ b/mini-frontend/src/components/BatchForm.tsx @@ -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): 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 ( -
-
- Cargando lote... -
-
- ); + return ; } return ( @@ -156,21 +160,25 @@ export default function BatchForm({ batchId, backPath }: BatchFormProps) {
+ + +
{/*
@@ -287,20 +340,12 @@ export default function BatchForm({ batchId, backPath }: BatchFormProps) {
*/}
- - +
diff --git a/mini-frontend/src/components/BusinessPartnerForm.tsx b/mini-frontend/src/components/BusinessPartnerForm.tsx index d67d46a..bff41ba 100644 --- a/mini-frontend/src/components/BusinessPartnerForm.tsx +++ b/mini-frontend/src/components/BusinessPartnerForm.tsx @@ -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({
{f.type === "select" ? (
-
Cargando artículo...
-
- ); + return ; } const checkbox = (name: keyof SapItem, label: string) => ( @@ -182,7 +182,8 @@ export default function ItemForm({
@@ -275,24 +287,16 @@ export default function ItemForm({ )}
- - +
diff --git a/mini-frontend/src/components/MarketingDocumentForm.tsx b/mini-frontend/src/components/MarketingDocumentForm.tsx index 17526ed..392a01f 100644 --- a/mini-frontend/src/components/MarketingDocumentForm.tsx +++ b/mini-frontend/src/components/MarketingDocumentForm.tsx @@ -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, @@ -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 ( -
-
Cargando documento...
-
- ); + return ; } return ( @@ -422,7 +438,9 @@ export default function MarketingDocumentForm({
{genericHeaderFields.map((field) => { - const val = (doc as any)[field.name]; + const val = doc[field.name]; + const isFieldReadOnly = + field.readOnly || (field.readOnlyOnUpdate && isEditMode); return (
setDoc((prev) => ({ ...prev, DocType: v as "dDocument_Items" | "dDocument_Service", })) } + disabled={isFieldReadOnly} /> ) : field.name === "DocCurrency" ? ( [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" ? ( <> - {onBuscarSocio && ( + {onBuscarSocio && !isFieldReadOnly && ( )} @@ -479,12 +507,14 @@ export default function MarketingDocumentForm({ )}
@@ -496,13 +526,14 @@ export default function MarketingDocumentForm({
{field.type === "select" ? ( handleLineChange( idx, - col.key as keyof DocumentLine, + col.key, e.target.value, ) } @@ -699,7 +732,7 @@ export default function MarketingDocumentForm({ return (
handleLineChange( idx, - col.key as keyof DocumentLine, + col.key, e.target.value, ) } @@ -717,9 +750,9 @@ export default function MarketingDocumentForm({
@@ -728,7 +761,7 @@ export default function MarketingDocumentForm({ return ( 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)]" /> @@ -749,9 +783,9 @@ export default function MarketingDocumentForm({ @@ -766,6 +800,7 @@ export default function MarketingDocumentForm({