primer commit

This commit is contained in:
George committed 2026-08-17 11:58:38 -05:00
1 parent ab3c613165
commit b2fb4e2d0a
18 files changed
+1291 -277

No files matched your search

+57
View File
@@ -0,0 +1,57 @@
# Dashboard
Página de inicio del ERP (`/dashboard`). Muestra KPIs, accesos rápidos a las operaciones más comunes y un feed de actividad reciente, organizados en secciones con grillas de cards.
**Importante:** actualmente todo el contenido de `Dashboard.tsx` es **estático/hardcodeado** en el propio componente (arrays `kpiCards`, `actionCards`, `newsCards`, `activityItems`). No hay ningún fetch a `api.ts` ni al backend — los valores de ventas, compras, clientes e inventario son de ejemplo, no datos reales de SAP. La sección "Noticias y guías" está deshabilitada en código (`{false && (...)}`), por lo que no se renderiza aunque el array `newsCards` exista.
## Árbol de Componentes
```mermaid
graph TB
DASH[Dashboard.tsx]
SEC[DashboardSection.tsx]
GRID[DashboardGrid.tsx]
CARD[DashboardCard.tsx]
KPI[DashboardKpiCard.tsx]
ACT[DashboardActionCard.tsx]
NEWS[DashboardNewsCard.tsx]
DASH -->|"KPI"| SEC
DASH -->|"Accesos rápidos"| SEC
DASH -->|"Noticias y guías (deshabilitada)"| SEC
DASH -->|"Actividad reciente (bloque propio, sin card)"| ACTIVITY[div de actividad inline]
SEC --> GRID
GRID --> CARD
CARD -->|type: kpi| KPI
CARD -->|type: action| ACT
CARD -->|type: news| NEWS
```
`DashboardCard` es un dispatcher: según la prop `type` (`"kpi" | "action" | "news"`) delega el render en `DashboardKpiCard`, `DashboardActionCard` o `DashboardNewsCard`. El bloque "Actividad reciente" no usa `DashboardCard`: es una lista renderizada directamente en `Dashboard.tsx`.
## Tipos de card
| Card | Props principales | Comportamiento |
|---|---|---|
| `DashboardKpiCard` | `title`, `icon` (componente de `lucide-react`), `value`, `subtitle`, `action?: {label, to}` | Muestra un valor numérico/texto grande. Si tiene `action`, renderiza un botón que navega con `useNavigate()` a la ruta `to`. |
| `DashboardActionCard` | `title`, `icon`, `to` | Card clickeable completa (ícono + título) que navega a `to` con `useNavigate()`. Tiene hover que cambia el fondo a `--color-primary-active`. |
| `DashboardNewsCard` | `image`, `title`, `description` | Card con imagen de fondo (`backgroundImage`) + texto. Actualmente usa URLs externas de `picsum.photos` como placeholder. No implementada en pantalla porque su sección está deshabilitada. |
Todas las cards usan variables CSS del tema (`--color-surface`, `--color-border`, `--color-primary`, `--color-text-muted`, etc.) vía `style` inline, no clases de Tailwind.
## Archivos Clave
| Archivo | Ruta | Propósito |
|---|---|---|
| `Dashboard.tsx` | `src/views/` | Vista principal; define los datos hardcodeados de KPIs, accesos rápidos, noticias y actividad, y arma las secciones |
| `DashboardSection.tsx` | `src/components/dashboard/` | Título de sección (`h2`) + contenedor de su contenido |
| `DashboardGrid.tsx` | `src/components/dashboard/` | Grid CSS responsivo (`auto-fill`, `minmax(260px, 1fr)`) para las cards |
| `DashboardCard.tsx` | `src/components/dashboard/` | Dispatcher que elige la card concreta según `type` |
| `DashboardKpiCard.tsx` | `src/components/dashboard/` | Card de indicador numérico con botón de acción opcional |
| `DashboardActionCard.tsx` | `src/components/dashboard/` | Card de acceso rápido, navega al hacer clic |
| `DashboardNewsCard.tsx` | `src/components/dashboard/` | Card de noticia con imagen (actualmente sin uso, sección deshabilitada) |
## Pendiente / a tener en cuenta
Si se decide conectar el dashboard a datos reales, haría falta: (1) reemplazar los arrays estáticos por llamadas a `api.ts` hacia endpoints del backend que hoy no existen para este propósito (no hay un endpoint de KPIs agregados en `mini-backend`), y (2) decidir si se reactiva la sección de noticias y de dónde saldría el contenido (hoy usa imágenes externas de `picsum.photos`, no aptas para producción).
+160
View File
@@ -0,0 +1,160 @@
# Flujo de Procesamiento — Datos Maestros de Artículos
## Arquitectura General
```mermaid
graph TB
subgraph "Frontend React (sap-frontend)"
RT[Router App.tsx]
LIST[ItemsList.tsx]
LIST_TMPL[ListViewTemplate.tsx]
VISTA[Items.tsx]
FORM[ItemForm.tsx]
UI_FF[FormattedField.tsx]
UI_SEL[SelectField.tsx]
UI_TABLE[DenseTable.tsx]
API[api.ts]
ENUMS[enums.ts]
TYPES[types/sapItems.ts]
end
subgraph "Backend FastAPI (mini-backend)"
ROUTER[routers/items.py<br/>make_crud_router]
CORE[core/crud_router.py]
SAPCORE[core/sap.py<br/>forward_to_sapb1]
end
subgraph "SAP Business One"
SL[Service Layer<br/>Items / ItemGroups]
end
RT --> LIST
RT --> VISTA
LIST --> LIST_TMPL
LIST_TMPL --> UI_TABLE
LIST_TMPL --> API
VISTA --> FORM
FORM --> UI_FF
FORM --> UI_SEL
FORM --> API
FORM --> ENUMS
FORM --> TYPES
API -->|GET/POST/PATCH| ROUTER
ROUTER --> CORE
CORE --> SAPCORE
SAPCORE -->|B1SESSION| SL
```
**Diferencia clave respecto al flujo de Lotes:** `ItemForm.tsx` no hace mapeo de nombres SAP→amigable (no existe un `mapSapToFriendly`). El estado del formulario (`SapItem`, en `types/sapItems.ts`) usa directamente los nombres de campo de SAP (`ItemCode`, `ItemName`, `ItemsGroupCode`, etc.), tanto para mostrar como para enviar al backend. Tampoco hay tabla de mapeo de nombres en esta sección por ese motivo.
En el backend, `routers/items.py` no tiene lógica propia: es una instancia de la factory genérica `make_crud_router("Items", key_field="ItemCode", key_quoted=True)` definida en `core/crud_router.py` (ver `mini-backend/docs/architecture.md`), que arma las cuatro rutas CRUD y reenvía a SAP vía `core/sap.py`.
## Flujo Vista de Lista → Editar/Crear Artículo
```mermaid
sequenceDiagram
actor User as Usuario
participant Router as App.tsx
participant ListView as ItemsList
participant LVT as ListViewTemplate
participant DTable as DenseTable
participant DetailView as Items
participant Form as ItemForm
participant API as api.ts
participant Backend as backend items.py
participant SAP as SAP Service Layer
User->>Router: /inventario/articulos
Router->>ListView: render
ListView->>API: GET /api/ItemGroups (para mapear ItemsGroupCode → nombre en la columna "Grupo")
ListView->>LVT: <ListViewTemplate endpoint="Items" baseFilter="AssetItem eq 'tNO'" .../>
LVT->>API: GET /api/Items?skip=0&top=50&fields=ItemCode,ItemName,...
API->>Backend: fetch con X-Session-Token
Backend->>SAP: GET Items (con B1SESSION)
SAP-->>Backend: JSON con datos
Backend-->>API: JSON response
API-->>LVT: json.value[]
LVT->>DTable: render rows
DTable-->>User: tabla con artículos (filtros por Grupo y Clase de artículo)
User->>DTable: clic en editar (ItemCode "A0001")
DTable->>Router: navigate("/inventario/articulos/mantenimiento?code=A0001")
Router->>DetailView: render con searchParams.code
DetailView->>Form: <ItemForm itemCode="A0001" backPath="/inventario/articulos" />
Form->>API: GET /api/ItemGroups (para poblar el SelectField de grupo)
Form->>API: GET /api/Items/A0001
API->>Backend: fetch
Backend->>SAP: GET Items('A0001')
SAP-->>Backend: {ItemCode, ItemName, ItemsGroupCode, ItemType, ...}
Backend-->>API: JSON raw SAP
API-->>Form: data
Form->>Form: setItem(data) (sin mapeo de nombres)
Form-->>User: formulario con valores cargados
User->>Form: edita campos y clic "Actualizar Artículo"
Form->>API: PATCH /api/Items/A0001
Note over Form,API: body: {ItemName, BarCode, ItemsGroupCode, ItemType, ...}
API->>Backend: fetch PATCH
Backend->>SAP: PATCH Items('A0001')
SAP-->>Backend: 204 No Content
Backend-->>API: 204
API-->>Form: ok
Form-->>User: "Artículo actualizado exitosamente"
Note over User,SAP: Si itemCode no viene en la URL (alta), el botón dice<br/>"Crear Artículo" y el submit hace POST /api/Items en vez de PATCH.
```
## Árbol de Componentes
```
App.tsx (BrowserRouter)
└── / (ProtectedRoute → MainLayout)
└── /inventario
├── /articulos
│ └── ItemsList.tsx
│ └── ListViewTemplate.tsx
│ ├── SearchInput.tsx
│ ├── PrimaryButton.tsx
│ ├── DenseTable.tsx
│ │ └── formatValue() [formatters.ts]
│ └── PaginationBar.tsx
└── /articulos/mantenimiento
└── Items.tsx
└── ItemForm.tsx
├── FormattedField.tsx (x6)
└── SelectField.tsx (x3: ItemsGroupCode, ItemType, CostAccountingMethod)
```
## Campos del Formulario (nombres SAP, sin mapeo)
| Campo SAP | Tipo | Componente | Notas |
|---|---|---|---|
| `ItemCode` | input text | FormattedField | obligatorio, deshabilitado implícitamente en edición (se usa como key de URL) |
| `ItemName` | input text | FormattedField | obligatorio |
| `BarCode` | input text | FormattedField | |
| `ItemsGroupCode` | select | SelectField | opciones cargadas desde `GET /api/ItemGroups` |
| `ItemType` | select | SelectField | opciones estáticas `ITEM_TYPE` (`utils/enums.ts`) |
| `CostAccountingMethod` | select | SelectField | opciones estáticas `GL_METHOD` (`utils/enums.ts`) |
| `SalesUnit` | input text | FormattedField | |
| `PurchaseUnit` | input text | FormattedField | |
| `InventoryUOM` | input text | FormattedField | |
| `AvgStdPrice` | currency, solo lectura | FormattedField (`mode="display"`) | |
| `PurchaseItem`, `InventoryItem`, `ManageSerialNumbers`, `ManageBatchNumbers`, `Valid`, `Frozen` | checkbox | input nativo | mapean a `"tYES"`/`"tNO"` vía `toCheck`/`fromCheck` |
## Archivos Clave
| Archivo | Ruta | Propósito |
|---|---|---|
| `Items.tsx` | `src/views/inventario/` | Vista detalle, extrae `code` de la URL y renderiza `ItemForm` (sirve tanto para alta como edición) |
| `ItemForm.tsx` | `src/components/` | Formulario alta/edición, fetch + POST/PATCH sin mapeo de nombres |
| `ItemsList.tsx` | `src/views/inventario/` | Lista con filtros por grupo de artículos y clase de artículo, columnas con formato de moneda |
| `SelectField.tsx` | `src/components/ui/` | Select reutilizable |
| `FormattedField.tsx` | `src/components/ui/` | Campo display/input con formato |
| `ListViewTemplate.tsx` | `src/components/ui/` | Template genérico de listado con paginación/búsqueda/filtros |
| `api.ts` | `src/utils/` | Cliente HTTP con sesión, token, timeout |
| `enums.ts` | `src/utils/` | Opciones estáticas `ITEM_TYPE`, `GL_METHOD` |
| `sapItems.ts` | `src/types/` | Interfaz TypeScript `SapItem` |
| `items.py` | `mini-backend/routers/` | Instancia de `make_crud_router` para `Items` (GET lista, GET por código, POST, PATCH) |
| `crud_router.py` | `mini-backend/core/` | Factory genérica de routers CRUD reutilizada por `items.py` y otros routers |
| `sap.py` | `mini-backend/core/` | Proxy universal a SAP Service Layer |
@@ -0,0 +1,169 @@
# Flujo de Procesamiento — Documentos de Marketing (Ventas / Compras)
Cubre los documentos comerciales de SAP Business One que la app gestiona: Órdenes de Venta, Facturas de Venta, Órdenes de Compra y Facturas de Proveedor. Los cuatro comparten un único formulario genérico (`MarketingDocumentForm.tsx`) parametrizado por vista.
## Arquitectura General
```mermaid
graph TB
subgraph "Frontend React (sap-frontend)"
OL[OrdersList.tsx]
O[Orders.tsx]
IL[InvoicesList.tsx]
I[Invoices.tsx]
POL[PurchaseOrdersList.tsx]
PO[PurchaseOrders.tsx]
PIL[PurchaseInvoicesList.tsx]
PI[PurchaseInvoices.tsx]
FORM[MarketingDocumentForm.tsx]
COPY[CopyFromModal.tsx]
SEARCH[ModalSearchTemplate.tsx]
BADGE[StatusBadge.tsx]
LVT[ListViewTemplate.tsx]
TYPES[types/sapMarketing.ts]
API[api.ts]
end
subgraph "Backend FastAPI (mini-backend)"
SALES[routers/sales.py]
PURCH[routers/purchases.py]
CORE[core/sap.py]
end
subgraph "SAP Business One"
SL[Service Layer<br/>Orders / Invoices / PurchaseOrders / PurchaseInvoices]
end
OL --> LVT
IL --> LVT
POL --> LVT
PIL --> LVT
LVT --> BADGE
O --> FORM
I --> FORM
PO --> FORM
PI --> FORM
FORM --> COPY
FORM --> SEARCH
FORM --> TYPES
FORM --> API
API -->|GET/POST/PATCH| SALES
API -->|GET/POST/PATCH| PURCH
SALES --> CORE
PURCH --> CORE
CORE --> SL
```
Cada vista de mantenimiento (`Orders.tsx`, `Invoices.tsx`, `PurchaseOrders.tsx`, `PurchaseInvoices.tsx`) es una capa delgada: mantiene el estado del documento (`doc`, tipado como `MarketingDocument` en `types/sapMarketing.ts`) y le pasa a `MarketingDocumentForm` la configuración específica de ese tipo de documento — el componente de formulario en sí no sabe si está editando una orden o una factura, solo renderiza lo que recibe por props.
## Cómo un solo formulario sirve a los 4 documentos
`MarketingDocumentForm` recibe por props todo lo que varía entre documentos:
| Prop | Qué controla | Ejemplo |
|---|---|---|
| `endpoint` | Endpoint del backend a consultar/enviar (`GET/POST/PATCH /api/{endpoint}`) | `"Orders"`, `"Invoices"`, `"PurchaseOrders"`, `"PurchaseInvoices"` |
| `tipoDocumento` | Etiqueta tipada (`OrdenVenta`, `FacturaVenta`, `OrdenCompra`, `FacturaProveedor`) declarada en `DocumentConfigProps` | — no se usa dentro de la lógica del propio formulario en el código leído; parece ser solo metadata para quien lo invoca |
| `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 |
| `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.
## Flujo: Crear una Factura de Venta copiando una Orden
```mermaid
sequenceDiagram
actor User as Usuario
participant List as InvoicesList
participant View as Invoices.tsx
participant Form as MarketingDocumentForm
participant Copy as CopyFromModal
participant API as api.ts
participant Backend as backend sales.py
participant SAP as SAP Service Layer
User->>List: /ventas/facturas → "Nueva Factura"
List->>View: navigate("/ventas/facturas/mantenimiento")
View->>Form: <MarketingDocumentForm endpoint="Invoices" copyFromSources=[Orden, Entrega] />
Form->>API: GET /api/Currencies
API->>Backend: fetch
Backend->>SAP: GET Currencies
SAP-->>Form: lista de monedas
User->>Form: busca y selecciona Cliente (🔍)
Form->>View: onBuscarSocio() abre ModalSearchTemplate(BusinessPartners)
View-->>Form: setDoc({CardCode, CardName})
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
Backend->>SAP: GET Orders
SAP-->>Copy: documentos abiertos del cliente
User->>Copy: selecciona una orden
Copy-->>Form: onConfirm(lines, sourceLabel)
Note over Form: Si ya hay líneas, confirm() pregunta reemplazar o agregar
Form->>Form: setDoc({DocumentLines: lines}) — cada línea trae BaseType/BaseEntry/BaseLine
User->>Form: clic "Crear Documento"
Form->>API: POST /api/Invoices (doc + DocumentLines, sin BaseRef)
API->>Backend: fetch POST
Backend->>SAP: POST Invoices (con líneas BaseEntry → SAP cierra la orden origen)
SAP-->>Backend: 201 Created
Backend-->>Form: ok
Form-->>User: "Documento creado exitosamente en SAP"
```
El mismo patrón aplica a Facturas de Proveedor copiando desde Órdenes de Compra, solo cambian `endpoint` y `copyFromSources`.
## Árbol de Componentes
```
App.tsx (BrowserRouter)
└── / (ProtectedRoute → MainLayout)
├── /ventas
│ ├── /ordenes → OrdersList.tsx → ListViewTemplate → StatusBadge
│ ├── /ordenes/mantenimiento → Orders.tsx → MarketingDocumentForm
│ ├── /entrega → stub "Entregas / Guías" (sin vista real, ver Hallazgos)
│ └── /facturas
│ ├── (lista) → InvoicesList.tsx → ListViewTemplate → StatusBadge
│ └── /mantenimiento → Invoices.tsx → MarketingDocumentForm
└── /compras
├── /ordenes → PurchaseOrdersList.tsx → ListViewTemplate → StatusBadge
├── /ordenes/mantenimiento → PurchaseOrders.tsx → MarketingDocumentForm
├── /recepcion → stub "Recepciones de Compra" (sin vista real, ver Hallazgos)
└── /facturas
├── (lista) → PurchaseInvoicesList.tsx → ListViewTemplate → StatusBadge
└── /mantenimiento → PurchaseInvoices.tsx → MarketingDocumentForm (+ Retención/Detracción)
MarketingDocumentForm.tsx
├── FormattedField.tsx / SelectField.tsx (cabecera)
├── CopyFromModal.tsx (botón "Copiar de")
└── (por vista) ModalSearchTemplate.tsx ×4 (Socios, Artículos, Almacén, Código de Impuesto)
```
## Archivos Clave
| Archivo | Ruta | Propósito |
|---|---|---|
| `MarketingDocumentForm.tsx` | `src/components/` | Formulario genérico: cabecera, grilla de líneas, totales, copiar-de, submit GET/POST/PATCH |
| `Orders.tsx` / `Invoices.tsx` | `src/views/ventas/` | Configuran el formulario para Órdenes y Facturas de venta |
| `PurchaseOrders.tsx` / `PurchaseInvoices.tsx` | `src/views/compras/` | Configuran el formulario para Compras; Facturas agrega retención/detracción |
| `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` |
| `sunat.ts` | `src/utils/` | `DETRACCION_CODIGOS` (porcentajes SUNAT usados en Facturas de Proveedor) |
| `sales.py` / `purchases.py` | `mini-backend/routers/` | Endpoints REST que proxean a SAP Service Layer |
## Hallazgos (código tal cual está hoy, sin corregir)
- **`ventas/entrega` y `compras/recepcion` son stubs**: en `App.tsx` son componentes en línea que solo muestran un título ("Entregas / Guías" / "Recepciones de Compra"), sin listado ni formulario real todavía.
- **`DeliveryNotesList.tsx` existe pero no está enrutado**: el archivo `src/views/ventas/DeliveryNotesList.tsx` está escrito pero ninguna ruta en `App.tsx` lo importa o lo usa — no es alcanzable desde la navegación actual.
- El endpoint `PurchaseDeliveryNotes` (documentado en `mini-backend/docs/api-reference.md`) tampoco tiene vista de frontend correspondiente todavía.
+3 -3
View File
@@ -18,7 +18,7 @@ graph TB
TYPES[types/sapBatch.ts]
end
subgraph "Backend FastAPI (backend-sn)"
subgraph "Backend FastAPI (mini-backend)"
ROUTER[routers/batch.py]
CORE[core/sap.py<br/>forward_to_sapb1]
end
@@ -156,5 +156,5 @@ App.tsx (BrowserRouter)
| `api.ts` | `src/utils/` | Cliente HTTP con sesión, token, timeout |
| `enums.ts` | `src/utils/` | Mapeos enum SAP (BATCH_STATUS_APROB, etc.) |
| `sapBatch.ts` | `src/types/` | Interfaz TypeScript BatchNumberDetail |
| `batch.py` | `backend-sn/routers/` | Rutas FastAPI GET/PATCH BatchNumberDetails |
| `sap.py` | `backend-sn/core/` | Proxy universal a SAP Service Layer |
| `batch.py` | `mini-backend/routers/` | Rutas FastAPI GET/PATCH BatchNumberDetails |
| `sap.py` | `mini-backend/core/` | Proxy universal a SAP Service Layer |
+172
View File
@@ -0,0 +1,172 @@
# Flujo de Procesamiento — Socios de Negocio (Clientes / Proveedores)
## Arquitectura General
```mermaid
graph TB
subgraph "Frontend React (sap-frontend)"
RT[Router App.tsx]
CLIST[CustomersList.tsx]
SLIST[SuppliersList.tsx]
CVIEW[Customers.tsx]
SVIEW[Suppliers.tsx]
BPFORM[BusinessPartnerForm.tsx]
LIST_TMPL[ListViewTemplate.tsx]
UI_TABLE[DenseTable.tsx]
API[api.ts]
end
subgraph "Backend FastAPI (mini-backend)"
ROUTER[routers/business_partners.py<br/>make_crud_router]
CORE[core/crud_router.py]
SAP_CORE[core/sap.py]
SUNAT[Sunat/BusinessPartners/DocumentType]
end
subgraph "SAP Business One"
SL[Service Layer<br/>BusinessPartners]
end
RT --> CLIST
RT --> SLIST
RT --> CVIEW
RT --> SVIEW
CLIST --> LIST_TMPL
SLIST --> LIST_TMPL
CVIEW --> BPFORM
SVIEW --> BPFORM
LIST_TMPL --> UI_TABLE
LIST_TMPL --> API
BPFORM --> API
API -->|GET/POST/PATCH BusinessPartners| ROUTER
API -->|GET DocumentType| SUNAT
ROUTER --> CORE
CORE --> SAP_CORE
SAP_CORE -->|B1SESSION| SL
```
## Un solo formulario para Clientes y Proveedores
A diferencia de otros módulos, acá **no hay un componente por tipo de socio**: `BusinessPartnerForm.tsx` es un único formulario genérico parametrizado por props, y `Customers.tsx` / `Suppliers.tsx` son wrappers finos que solo le pasan configuración distinta:
| Prop | `Customers.tsx` | `Suppliers.tsx` |
| ----------------- | --------------------------- | --------------------------------------------------- |
| `cardType` | `"cCustomer"` | `"cSupplier"` |
| `entityLabel` | `"Cliente"` | `"Proveedor"` |
| `listPath` | `/socios/clientes` | `/socios/proveedores` |
| `codePlaceholder` | `Ej. C00001` | `Ej. P00001` |
| `extraFields` | `[]` (sin campos extra hoy) | `U_EXX_TIPODOCU` (Tipo de Documento SUNAT, select) |
La diferenciación entre "es cliente" o "es proveedor" para el propio dato de SAP la hace el campo `CardType` (`cCustomer` / `cSupplier`), que viaja fijo en el `body` del POST/PATCH según qué wrapper se use — el formulario en sí no lo deja editar.
El mecanismo `extraFields` (tipo `ExtraFieldConfig[]`, definido en `BusinessPartnerForm.tsx`) permite agregar campos exclusivos de un tipo de socio sin tocar el formulario base: cada entrada define `name`, `label`, `type` (`text` | `email` | `select`), y opcionalmente `loadOptions` (una función async que trae las opciones del backend). Hoy el único caso real es el campo `U_EXX_TIPODOCU` en Proveedores, que carga sus opciones desde `GET /api/Sunat/BusinessPartners/DocumentType` (catálogo SUNAT expuesto vía SAP).
## Flujo Vista de Lista → Editar Socio (caso Clientes)
```mermaid
sequenceDiagram
actor User as Usuario
participant Router as App.tsx
participant ListView as CustomersList
participant LVT as ListViewTemplate
participant DTable as DenseTable
participant DetailView as Customers.tsx
participant Form as BusinessPartnerForm
participant API as api.ts
participant Backend as backend business_partners.py
participant SAP as SAP Service Layer
User->>Router: /socios/clientes
Router->>ListView: render
ListView->>LVT: <ListViewTemplate endpoint="BusinessPartners" baseFilter="CardType eq 'cCustomer'" .../>
LVT->>API: GET /api/BusinessPartners?...&$filter=CardType eq 'cCustomer'
API->>Backend: fetch con X-Session-Token
Backend->>SAP: GET BusinessPartners (con B1SESSION)
SAP-->>Backend: JSON con datos
Backend-->>API: JSON response
API-->>LVT: json.value[]
LVT->>DTable: render rows
DTable-->>User: tabla de clientes
User->>DTable: clic en editar (CardCode "C00001")
DTable->>Router: navigate("/socios/clientes/mantenimiento?code=C00001")
Router->>DetailView: render con searchParams.code
DetailView->>Form: <BusinessPartnerForm cardType="cCustomer" .../>
Form->>API: GET /api/BusinessPartners/C00001
API->>Backend: fetch
Backend->>SAP: GET BusinessPartners('C00001')
SAP-->>Backend: {CardCode, CardName, FederalTaxID, BPAddresses[], ...}
Backend-->>API: JSON raw SAP
API-->>Form: data
Form->>Form: setFormData(data) — sin mapeo de nombres, usa los campos SAP tal cual
Form-->>User: formulario con pestañas "General" / "Direcciones"
User->>Form: edita campos y clic "Actualizar Cliente"
Form->>API: PATCH /api/BusinessPartners/C00001
Note over Form,API: body: {CardName, FederalTaxID, Phone1, EmailAddress, BPAddresses[], ...extraFields}
API->>Backend: fetch PATCH
Backend->>SAP: PATCH BusinessPartners('C00001')
SAP-->>Backend: 204 No Content
Backend-->>API: 204
API-->>Form: ok
Form-->>User: "Cliente actualizado correctamente"
```
El flujo de Proveedores es idéntico, cambiando el `baseFilter` a `CardType eq 'cSupplier'` en `SuppliersList.tsx` y usando `Suppliers.tsx` como vista de detalle.
Para un socio **nuevo** (`isEditMode = false`), el submit hace `POST /api/BusinessPartners` en vez de `PATCH`, y tras la respuesta exitosa navega de vuelta al listado (`listPath`) después de 1.5s.
## Árbol de Componentes
```
App.tsx (BrowserRouter)
└── / (ProtectedRoute → MainLayout)
└── /socios
├── /clientes
│ └── CustomersList.tsx
│ └── ListViewTemplate.tsx
│ ├── SearchInput.tsx
│ ├── PrimaryButton.tsx
│ ├── DenseTable.tsx
│ └── PaginationBar.tsx
├── /clientes/mantenimiento
│ └── Customers.tsx
│ └── BusinessPartnerForm.tsx (cardType="cCustomer")
├── /proveedores
│ └── SuppliersList.tsx
│ └── ListViewTemplate.tsx (mismo árbol que CustomersList)
└── /proveedores/mantenimiento
└── Suppliers.tsx
└── BusinessPartnerForm.tsx (cardType="cSupplier", extraFields=[U_EXX_TIPODOCU])
```
## 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`).
| Campo SAP | Pestaña | Tipo | Notas |
| ------------------------- | ------------ | ------------------------------ | ------------------------------------------------------------- |
| `CardCode` | General | input text (solo lectura en edición) | Llave primaria en SAP; no editable una vez creado el socio |
| `CardName` | General | input text | Razón social / nombre |
| `CardType` | General | fijo, no editable en UI | `cCustomer` o `cSupplier` según el wrapper usado |
| `FederalTaxID` | General | input text | RUC / documento de identidad |
| `Phone1` | General | input text | Teléfono principal |
| `EmailAddress` | General | input email | Correo |
| `U_EXX_TIPODOCU` | General | select (dinámico, solo Proveedores) | Catálogo SUNAT vía `/api/Sunat/BusinessPartners/DocumentType` |
| `BPAddresses[].Street`, `City`, `County`, `GlobalLocationNumber` | Direcciones | input text | Dos direcciones por defecto: `bo_BillTo` (Facturación) y `bo_ShipTo` (Despacho) |
## Archivos Clave
| Archivo | Ruta | Propósito |
| ------------------------- | --------------------------------- | --------------------------------------------------------------------------- |
| `BusinessPartnerForm.tsx` | `src/components/` | Formulario genérico único para Clientes y Proveedores, parametrizable |
| `Customers.tsx` | `src/views/socios/` | Wrapper que configura `BusinessPartnerForm` para `cCustomer` |
| `Suppliers.tsx` | `src/views/socios/` | Wrapper que configura `BusinessPartnerForm` para `cSupplier` + campo SUNAT |
| `CustomersList.tsx` | `src/views/socios/` | Lista de clientes, filtra `CardType eq 'cCustomer'` |
| `SuppliersList.tsx` | `src/views/socios/` | Lista de proveedores, filtra `CardType eq 'cSupplier'` |
| `ListViewTemplate.tsx` | `src/components/ui/` | Template genérico de listado con paginación/búsqueda/filtros |
| `api.ts` | `src/utils/` | Cliente HTTP con sesión, token, timeout |
| `business_partners.py` | `mini-backend/routers/` | Router CRUD genérico (`make_crud_router("BusinessPartners", key_field="CardCode")`) |
| `crud_router.py` | `mini-backend/core/` | Factory que genera GET/POST/PATCH proxificados hacia SAP |
| `sap.py` | `mini-backend/core/` | Proxy universal a SAP Service Layer |
+20
View File
@@ -0,0 +1,20 @@
# Frontend mini-erp — Documentación del Proyecto
SPA en React 19 + TypeScript + Vite + Tailwind CSS + React Router. Consume la API del backend (`mini-backend`) para operar sobre SAP Business One: socios de negocio, artículos, documentos de venta/compra, lotes y reportes.
## Documentos
| Documento | Descripción |
| ----------------------------------------------------------------- | ------------------------------------------------------------------- |
| [Dashboard](dashboard.md) | Pantalla de inicio: KPIs, accesos rápidos y noticias (hoy estático) |
| [Socios de Negocio](flujo-socios-negocio.md) | Clientes y proveedores, formulario compartido |
| [Artículos](flujo-articulos.md) | Alta y edición de artículos de inventario |
| [Lotes](flujo-lotes.md) | Aprobación/liberación de lotes (BatchNumberDetails) |
| [Documentos de Marketing](flujo-documentos-marketing.md) | Órdenes/facturas de venta y compra sobre un formulario común |
| [Reportes](reportes.md) | Los tres sistemas de reportes: HTML, Crystal e Informes |
## Convenciones
- 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).
+187
View File
@@ -0,0 +1,187 @@
# Módulo de Reportes
El frontend expone **tres sistemas de reportes distintos**, con configuración, rutas y componentes propios cada uno. No son variantes del mismo flujo: apuntan a integraciones distintas del lado del backend, y uno de ellos (`informes`) no tiene backend real detrás todavía.
| Sistema | Ruta base | Config | Componente runner | Backend real |
|---|---|---|---|---|
| **Reportes HTML** | `/reportes-html/:module` | `config/htmlReports.ts` → `htmlReportsConfig` | `HtmlReportRunner.tsx` | `mini-backend/routers/reports.py` (Jinja2 + WeasyPrint, datos de HANA) — **sí implementado** |
| **Reportes Crystal** | `/reportes-crystal/:module` | `config/crystalReports.ts` → `crystalReportsConfig` | `CrystalReportRunner.tsx` | `mini-backend/routers/apigateway.py` (SAP B1 API Gateway / Crystal Reports Server) — **sí implementado** |
| **Informes** | `/informes/:module` | `config/reports.ts` → `reportsConfig` | `ReportRunner.tsx` | Ninguno confirmado — ver nota abajo |
### Nota sobre `informes` (`ReportRunner.tsx` + `reportsConfig`)
`reportsConfig` (en `config/reports.ts`) define reportes como "Estado de Cuenta", "Saldos de Clientes", "Ventas por Artículo", "Kardex de Artículos", etc., cada uno con un `endpoint` del tipo `/api/reports/socios/estado-cuenta`, `/api/reports/inventario/kardex`, etc. El único endpoint que `mini-backend/routers/reports.py` expone bajo `/api/reports/` es `POST /api/reports/stickers`. Ninguno de los endpoints que `reportsConfig` referencia existe en el backend actual. Esto sugiere que `ReportRunner.tsx` y `reportsConfig` son un scaffold frontend para reportes planeados a futuro, no un flujo funcional hoy — cualquier reporte del módulo "Informes" fallará al generar hasta que se implemente su endpoint correspondiente en el backend.
## Arquitectura General
```mermaid
graph TB
subgraph "Listados (elegir reporte)"
RHL[HtmlReportsList.tsx]
RCL[CrystalReportsList.tsx]
MRL[ModuleReportsList.tsx]
end
subgraph "Runners (parámetros + ejecución)"
HRR[HtmlReportRunner.tsx]
CRR[CrystalReportRunner.tsx]
RR[ReportRunner.tsx]
RPF[ReportParameterForm.tsx]
RRV[ReportResultsView.tsx]
MST[ModalSearchTemplate.tsx<br/>búsqueda F4]
end
subgraph "Config"
CFG_H[htmlReportsConfig]
CFG_C[crystalReportsConfig]
CFG_R[reportsConfig]
end
subgraph "Backend FastAPI (mini-backend)"
REP[routers/reports.py<br/>POST /api/reports/stickers]
APIGW[routers/apigateway.py<br/>GET/POST /api/apigateway/reports/*]
HANA[core/hana.py<br/>hana_connection]
AGW_CORE[core/api_gateway.py<br/>forward_to_api_gateway]
end
subgraph "Externo"
HANADB[(HANA DB)]
SAPGW[SAP B1 API Gateway<br/>rs/v1 Crystal Reports Server]
end
RHL --> CFG_H --> HRR
RCL --> CFG_C --> CRR
MRL --> CFG_R --> RR
HRR --> RPF
CRR --> RPF
RR --> RPF
RR --> RRV
HRR -.f4-modal.-> MST
CRR -.f4-modal.-> MST
HRR -->|POST /api/reports/:endpoint| REP
REP --> HANA --> HANADB
CRR -->|POST /api/apigateway/reports/:docCode/export| APIGW
RCL -.al listar.-> APIGW
APIGW --> AGW_CORE --> SAPGW
RR -.->|"endpoint del reportsConfig<br/>(no existe en el backend)"| REP
```
## Flujo real: Reporte HTML (ejemplo "stickers")
```mermaid
sequenceDiagram
actor User as Usuario
participant List as HtmlReportsList
participant Runner as HtmlReportRunner
participant Modal as ModalSearchTemplate
participant API as api.ts
participant Backend as reports.py
participant HANA as HANA
User->>List: /reportes-html/inventario
List->>List: lee htmlReportsConfig["inventario"]
List-->>User: tarjeta "Impresión de Etiquetas"
User->>Runner: clic → /reportes-html/inventario/stickers
Runner->>Runner: busca definition por docCode "stickers"
Runner-->>User: formulario (Documento f4-modal, Cantidad, Formato, Papel)
User->>Runner: clic en campo "Documento"
Runner->>Modal: abre ModalSearchTemplate (endpoint DeliveryNotes)
Modal-->>User: tabla de guías de remisión
User->>Modal: selecciona guía
Modal-->>Runner: DocEntry seleccionado
User->>Runner: completa cantidad/formato y clic "Generar Reporte"
Runner->>API: POST /api/reports/stickers {doc_entry, cantidad, formato, papel}
API->>Backend: fetch con X-Session-Token
Backend->>HANA: STICKERS_QUERY (datos de la guía por DocEntry)
HANA-->>Backend: fila con cliente, dirección, etc.
Backend->>Backend: renderiza templates/reports/stickers.html (Jinja2)
alt formato = pdf
Backend->>Backend: WeasyPrint HTML → PDF
Backend-->>API: StreamingResponse application/pdf
else formato = html
Backend-->>API: HTMLResponse
end
API-->>Runner: blob
Runner-->>User: <iframe> con vista previa
```
## Flujo real: Reporte Crystal (ejemplo "DLN20015")
```mermaid
sequenceDiagram
actor User as Usuario
participant List as CrystalReportsList
participant Runner as CrystalReportRunner
participant API as api.ts
participant Backend as apigateway.py
participant Core as core/api_gateway.py
participant SAPGW as SAP API Gateway
User->>List: /reportes-crystal/inventario
List->>List: lee crystalReportsConfig["inventario"]
List-->>User: tarjeta "Impresión de Etiquetas" (docCode DLN20015)
User->>Runner: clic → /reportes-crystal/inventario/DLN20015
Runner-->>User: formulario (Documento f4-modal, Cantidad)
User->>Runner: completa parámetros y clic "Exportar PDF"
Runner->>API: POST /api/apigateway/reports/DLN20015/export<br/>[{name, type, value}]
API->>Backend: fetch con X-Session-Token
Backend->>Core: forward_to_api_gateway(POST rs/v1/ExportPDFData?DocCode=DLN20015)
Core->>SAPGW: request con credenciales APIGATEWAY_* del .env
SAPGW-->>Core: PDF en base64
Core-->>Backend: response
Backend->>Backend: decodifica base64 → bytes
Backend-->>API: StreamingResponse application/pdf
API-->>Runner: blob
Runner-->>User: <iframe> con vista previa PDF
```
Nota: `crystalReportsConfig` solo tiene un reporte cargado, en `inventario` (`DLN20015`, "Impresión de Etiquetas"). Los otros módulos (`socios`, `ventas`, `compras`) están declarados con arrays vacíos — no hay reportes Crystal configurados para ellos todavía. Curiosamente, el comentario en `mini-backend/routers/reports.py` indica que la consulta SQL de `stickers` (el reporte HTML) es "la query original de Crystal Reports para DLN20015, adaptada" — es decir, el mismo reporte de negocio (etiquetas de guía de remisión) existe hoy por **dos caminos en paralelo**: el Crystal Report original vía API Gateway, y su reimplementación propia vía HANA + Jinja + WeasyPrint.
## Árbol de Componentes y Rutas
```
App.tsx (BrowserRouter)
└── / (ProtectedRoute → MainLayout)
├── /informes/:module → ModuleReportsList.tsx
│ └── /informes/:module/:reportId → ReportRunner.tsx
│ ├── ReportParameterForm.tsx
│ └── ReportResultsView.tsx
├── /reportes-crystal/:module → CrystalReportsList.tsx
│ └── /reportes-crystal/:module/:docCode → CrystalReportRunner.tsx
│ └── ModalSearchTemplate.tsx (f4-modal)
└── /reportes-html/:module → HtmlReportsList.tsx
└── /reportes-html/:module/:docCode → HtmlReportRunner.tsx
└── ModalSearchTemplate.tsx (f4-modal)
```
`:module` es uno de `socios | ventas | compras | inventario` (`moduleLabels`/`moduleIcons` en `config/reports.ts`), compartido por los tres sistemas.
## Archivos Clave
| Archivo | Ruta | Propósito |
|---|---|---|
| `ModuleReportsList.tsx` | `src/views/informes/` | Lista reportes de `reportsConfig` por módulo |
| `ReportRunner.tsx` | `src/components/report/` | Ejecuta reportes de `reportsConfig` (endpoints aún no implementados en backend) |
| `ReportParameterForm.tsx` | `src/components/report/` | Formulario de parámetros para `ReportRunner` |
| `ReportResultsView.tsx` | `src/components/report/` | Renderiza tabla/totales/PDF según `outputType` |
| `CrystalReportsList.tsx` | `src/views/reportes/` | Lista reportes de `crystalReportsConfig` por módulo |
| `CrystalReportRunner.tsx` | `src/components/report/` | Arma el body y llama a `/api/apigateway/reports/:docCode/export` |
| `HtmlReportsList.tsx` | `src/views/reportes/` | Lista reportes de `htmlReportsConfig` por módulo |
| `HtmlReportRunner.tsx` | `src/components/report/` | Arma el body y llama a `/api/reports/:endpoint` |
| `ModalSearchTemplate.tsx` | `src/components/ui/` | Modal de búsqueda F4 reutilizado por ambos runners reales |
| `config/reports.ts` | `src/config/` | `reportsConfig`, `moduleLabels`, `moduleIcons` |
| `config/crystalReports.ts` | `src/config/` | `crystalReportsConfig` |
| `config/htmlReports.ts` | `src/config/` | `htmlReportsConfig` |
| `types/reports.ts` | `src/types/` | Tipos `ReportDefinition`, `F4Config`, etc. |
| `reports.py` | `mini-backend/routers/` | `POST /api/reports/stickers` (Jinja2 + WeasyPrint, datos vía `core/hana.py`) |
| `apigateway.py` | `mini-backend/routers/` | `GET /api/apigateway/reports`, `GET .../{docCode}/parameters`, `POST .../{docCode}/export` |
| `api_gateway.py` | `mini-backend/core/` | `forward_to_api_gateway`, proxy hacia el SAP API Gateway |