@asteby/metacore-runtime-react 47.1.0 → 49.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +51 -0
- package/dist/action-modal-dispatcher.d.ts +17 -3
- package/dist/action-modal-dispatcher.d.ts.map +1 -1
- package/dist/action-modal-dispatcher.js +80 -13
- package/dist/action-priority.d.ts +34 -0
- package/dist/action-priority.d.ts.map +1 -0
- package/dist/action-priority.js +60 -0
- package/dist/business/catalog-product-search.d.ts +32 -0
- package/dist/business/catalog-product-search.d.ts.map +1 -0
- package/dist/business/catalog-product-search.js +71 -0
- package/dist/business/catalog.d.ts.map +1 -1
- package/dist/business/catalog.js +2 -0
- package/dist/business/document-editor-model.d.ts +119 -0
- package/dist/business/document-editor-model.d.ts.map +1 -0
- package/dist/business/document-editor-model.js +317 -0
- package/dist/business/document-editor-panels.d.ts +64 -0
- package/dist/business/document-editor-panels.d.ts.map +1 -0
- package/dist/business/document-editor-panels.js +58 -0
- package/dist/business/document-editor.d.ts +27 -0
- package/dist/business/document-editor.d.ts.map +1 -0
- package/dist/business/document-editor.js +493 -0
- package/dist/business/document-lines.d.ts.map +1 -1
- package/dist/business/document-lines.js +5 -1
- package/dist/business/index.d.ts +5 -0
- package/dist/business/index.d.ts.map +1 -1
- package/dist/business/index.js +5 -0
- package/dist/business/line-items.d.ts +16 -1
- package/dist/business/line-items.d.ts.map +1 -1
- package/dist/business/line-items.js +14 -2
- package/dist/business/payment-allocator.d.ts +19 -0
- package/dist/business/payment-allocator.d.ts.map +1 -0
- package/dist/business/payment-allocator.js +31 -0
- package/dist/business/product-search.d.ts +4 -0
- package/dist/business/product-search.d.ts.map +1 -1
- package/dist/dialogs/dynamic-record.d.ts.map +1 -1
- package/dist/dialogs/dynamic-record.js +14 -7
- package/dist/document/document-page.d.ts.map +1 -1
- package/dist/document/document-page.js +8 -1
- package/dist/document/evaluate.d.ts.map +1 -1
- package/dist/document/evaluate.js +17 -1
- package/dist/document/index.d.ts +1 -0
- package/dist/document/index.d.ts.map +1 -1
- package/dist/document/index.js +1 -0
- package/dist/document/secondary-bar.d.ts +20 -0
- package/dist/document/secondary-bar.d.ts.map +1 -0
- package/dist/document/secondary-bar.js +44 -0
- package/dist/document/types.d.ts +8 -0
- package/dist/document/types.d.ts.map +1 -1
- package/dist/document-form-dialog.d.ts +31 -2
- package/dist/document-form-dialog.d.ts.map +1 -1
- package/dist/document-form-dialog.js +46 -11
- package/dist/dynamic-columns-helpers.d.ts +9 -0
- package/dist/dynamic-columns-helpers.d.ts.map +1 -1
- package/dist/dynamic-columns-helpers.js +19 -0
- package/dist/dynamic-columns.d.ts +9 -13
- package/dist/dynamic-columns.d.ts.map +1 -1
- package/dist/dynamic-columns.js +27 -21
- package/dist/dynamic-crud-page.d.ts +18 -0
- package/dist/dynamic-crud-page.d.ts.map +1 -1
- package/dist/dynamic-crud-page.js +15 -6
- package/dist/dynamic-form-schema.d.ts.map +1 -1
- package/dist/dynamic-form-schema.js +15 -3
- package/dist/dynamic-form.d.ts +16 -1
- package/dist/dynamic-form.d.ts.map +1 -1
- package/dist/dynamic-form.js +78 -20
- package/dist/dynamic-kanban.d.ts +9 -3
- package/dist/dynamic-kanban.d.ts.map +1 -1
- package/dist/dynamic-kanban.js +31 -12
- package/dist/dynamic-relation-helpers.d.ts.map +1 -1
- package/dist/dynamic-relation-helpers.js +8 -0
- package/dist/dynamic-relation.js +1 -1
- package/dist/dynamic-row-actions.d.ts +8 -0
- package/dist/dynamic-row-actions.d.ts.map +1 -1
- package/dist/dynamic-row-actions.js +12 -2
- package/dist/dynamic-table.d.ts.map +1 -1
- package/dist/dynamic-table.js +64 -63
- package/dist/index.d.ts +7 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +19 -2
- package/dist/installed-addons-context.d.ts +15 -0
- package/dist/installed-addons-context.d.ts.map +1 -0
- package/dist/installed-addons-context.js +39 -0
- package/dist/org-runtime-context.d.ts +8 -0
- package/dist/org-runtime-context.d.ts.map +1 -1
- package/dist/org-runtime-context.js +8 -0
- package/dist/org-runtime-provider.d.ts +3 -1
- package/dist/org-runtime-provider.d.ts.map +1 -1
- package/dist/org-runtime-provider.js +3 -3
- package/dist/primitives/aging.d.ts +34 -0
- package/dist/primitives/aging.d.ts.map +1 -0
- package/dist/primitives/aging.js +48 -0
- package/dist/primitives/allocation.d.ts +42 -0
- package/dist/primitives/allocation.d.ts.map +1 -0
- package/dist/primitives/allocation.js +59 -0
- package/dist/primitives/approvals.d.ts +71 -0
- package/dist/primitives/approvals.d.ts.map +1 -0
- package/dist/primitives/approvals.js +48 -0
- package/dist/primitives/contributions.d.ts +93 -0
- package/dist/primitives/contributions.d.ts.map +1 -0
- package/dist/primitives/contributions.js +163 -0
- package/dist/primitives/document-kinds.d.ts +25 -0
- package/dist/primitives/document-kinds.d.ts.map +1 -0
- package/dist/primitives/document-kinds.js +26 -0
- package/dist/primitives/index.d.ts +8 -0
- package/dist/primitives/index.d.ts.map +1 -0
- package/dist/primitives/index.js +23 -0
- package/dist/primitives/panels.d.ts +68 -0
- package/dist/primitives/panels.d.ts.map +1 -0
- package/dist/primitives/panels.js +3 -0
- package/dist/primitives/use-contributions.d.ts +15 -0
- package/dist/primitives/use-contributions.d.ts.map +1 -0
- package/dist/primitives/use-contributions.js +30 -0
- package/dist/record-actions.d.ts +21 -0
- package/dist/record-actions.d.ts.map +1 -0
- package/dist/record-actions.js +76 -0
- package/dist/record-prefill-bar.d.ts +11 -0
- package/dist/record-prefill-bar.d.ts.map +1 -0
- package/dist/record-prefill-bar.js +33 -0
- package/dist/row-actions-menu.d.ts +32 -0
- package/dist/row-actions-menu.d.ts.map +1 -0
- package/dist/row-actions-menu.js +59 -0
- package/dist/slot.d.ts +4 -21
- package/dist/slot.d.ts.map +1 -1
- package/dist/slot.js +14 -36
- package/dist/types.d.ts +134 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/catalog-product-search.test.ts +61 -0
- package/src/__tests__/document-editor-model.test.ts +221 -0
- package/src/__tests__/document-editor.test.tsx +340 -0
- package/src/__tests__/document-form-dialog.test.tsx +158 -3
- package/src/__tests__/dynamic-form-select-edit.test.tsx +155 -0
- package/src/__tests__/dynamic-kanban.test.tsx +24 -3
- package/src/__tests__/dynamic-record-extension-label.test.tsx +88 -0
- package/src/__tests__/installed-addons-context.test.tsx +50 -0
- package/src/__tests__/line-item-totals.test.ts +28 -0
- package/src/__tests__/prefill-scalar-from-record.test.ts +52 -1
- package/src/__tests__/primitives-federated.test.tsx +156 -0
- package/src/__tests__/primitives.test.ts +224 -0
- package/src/__tests__/record-prefill-bar.test.tsx +50 -0
- package/src/__tests__/row-action-link-navigation.test.ts +17 -0
- package/src/__tests__/secondary-actions.test.tsx +261 -0
- package/src/action-modal-dispatcher.tsx +97 -12
- package/src/action-priority.ts +82 -0
- package/src/business/business.test.ts +1 -1
- package/src/business/catalog-product-search.ts +105 -0
- package/src/business/catalog.ts +2 -0
- package/src/business/document-editor-model.ts +367 -0
- package/src/business/document-editor-panels.tsx +164 -0
- package/src/business/document-editor.tsx +781 -0
- package/src/business/document-lines.ts +5 -1
- package/src/business/index.ts +5 -0
- package/src/business/line-items.ts +31 -2
- package/src/business/payment-allocator.tsx +110 -0
- package/src/business/product-search.ts +4 -0
- package/src/dialogs/dynamic-record.tsx +29 -6
- package/src/document/document-page.tsx +15 -0
- package/src/document/evaluate.ts +17 -1
- package/src/document/index.ts +1 -0
- package/src/document/secondary-bar.tsx +92 -0
- package/src/document/types.ts +8 -0
- package/src/document-form-dialog.tsx +96 -12
- package/src/dynamic-columns-helpers.ts +22 -0
- package/src/dynamic-columns.tsx +35 -62
- package/src/dynamic-crud-page.tsx +33 -5
- package/src/dynamic-form-schema.ts +15 -2
- package/src/dynamic-form.tsx +117 -39
- package/src/dynamic-kanban.tsx +34 -7
- package/src/dynamic-relation-helpers.ts +9 -0
- package/src/dynamic-relation.tsx +7 -2
- package/src/dynamic-row-actions.tsx +12 -2
- package/src/dynamic-table.tsx +3 -0
- package/src/index.ts +46 -1
- package/src/installed-addons-context.tsx +58 -0
- package/src/org-runtime-context.ts +10 -0
- package/src/org-runtime-provider.tsx +7 -2
- package/src/primitives/aging.ts +76 -0
- package/src/primitives/allocation.ts +104 -0
- package/src/primitives/approvals.ts +103 -0
- package/src/primitives/contributions.ts +255 -0
- package/src/primitives/document-kinds.ts +62 -0
- package/src/primitives/index.ts +23 -0
- package/src/primitives/panels.ts +59 -0
- package/src/primitives/use-contributions.ts +42 -0
- package/src/record-actions.ts +101 -0
- package/src/record-prefill-bar.tsx +62 -0
- package/src/row-action-stage-gate.test.ts +33 -0
- package/src/row-actions-menu.tsx +159 -0
- package/src/slot.tsx +18 -53
- package/src/types.ts +129 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,56 @@
|
|
|
1
1
|
# @asteby/metacore-runtime-react
|
|
2
2
|
|
|
3
|
+
## 49.0.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 4d97e6b: «Crear desde» con renglones y cantidad pendiente en el DocumentEditor. Una fuente (`document_forms.types[].sources[]`) que declara `line_link_field`, `remaining_qty_field` o `remaining_endpoint` precarga sus renglones desde lo que sirve el host (`<endpoint>/source-lines`, kernel ≥ v0.191): la cantidad sugerida y el tope son lo pendiente (cantidad − lo ya facturado/devuelto, calculado en el servidor), los renglones ya cubiertos se omiten con un aviso breve y cada renglón guarda `source_line_id`, que se envía en la columna `line_link_field` para que el servidor valide al guardar (su rechazo en español se muestra tal cual). Sin `source-lines` (404) cae a la relación como antes. Un tipo con `sources` usa el editor salvo `layout: "wizard"`; `DocumentFormDialog` abre el editor cuando recibe `initialSource`, y `DynamicCRUDPage` acepta `initialCreate` para abrir el alta con tipo y origen (p. ej. desde la URL). Las acciones de fila `type: "link"` con query (`?create=…&from_id={id}`) navegan con `search` (antes la query quedaba en el path y la ruta no casaba); los valores interpolados van codificados. `serializeLineItems(lines, { sourceLineField })`, `sourceLoadSummary` y `sourceTracksRemaining` exportados del modelo del editor; cada renglón serializado lleva además `subtotal` y `tax_amount` calculados (modelos de renglón sin tasa no pierden el IVA) y `linesFromSource` respeta `discount_mode: "amount"`. i18n es/en de los avisos de carga.
|
|
8
|
+
- f9616d1: Un solo registro para modales, slots y acciones, y acciones secundarias fuera de la vista principal:
|
|
9
|
+
|
|
10
|
+
- `@asteby/metacore-sdk` es la única fuente de verdad: `registerModalComponent` (modales por slug, con addon dueño y `load` perezoso), `registerRecordAction` (acciones secundarias sobre registros con su proveedor `requires: { addon | capability }`) y `slotStore`. `Registry.scope(addon).registerModal/registerAction/registerSlot` escriben ahí (antes `registerModal` no tenía lector) y `unbind` lo retira todo. `adaptActionProps` entrega a un remote ambos contratos de props (ActionModalProps y recordId/payload/close).
|
|
11
|
+
- `registerFederatedModal` queda como alias deprecado y compatible de `registerModalComponent`; `slotRegistry` y `<Slot>` de runtime-react leen el `slotStore` del SDK y no pintan contribuciones de addons no instalados.
|
|
12
|
+
- Acciones primarias/secundarias: `priority` del manifest v3 (o convención por clave: print, share, email, mail, whatsapp, chat, download, pdf, xml, acuse). `RowActionsMenu` (menú «…» por defecto de DynamicTable) muestra una primaria destacada y agrupa las secundarias y las aportadas por addons en «Más…»; las acciones por capacidad sin proveedor activo no se ofrecen. `DocumentPage` manda las secundarias de su layout a `DocumentSecondaryBar` (pie) y `resolveActions` devuelve el nuevo grupo `footer`.
|
|
13
|
+
- i18n: `datatable.more_actions`, `datatable.open_menu`, `datatable.secondary_actions` (es/en).
|
|
14
|
+
|
|
15
|
+
### Patch Changes
|
|
16
|
+
|
|
17
|
+
- b8a2ba3: Acciones abiertas desde una fila y buscador del host en documentos (retest Pitsline 2026-10-05)
|
|
18
|
+
|
|
19
|
+
- Un campo de acción que apunta al MISMO modelo de la fila y que la fila no tiene como columna se precarga con el id de esa fila: «Registrar pago» desde una factura abre con la factura elegida (`invoice_id` → Invoice). Una columna propia vacía (`parent_id` sin padre) no se siembra.
|
|
20
|
+
- `DocumentFormDialog` aplica el IVA de la org (`OrgRuntimeProvider` `taxRate`) a los productos del buscador del host (`searchProducts`) que no traen su tasa; antes sólo el buscador por defecto lo hacía y el renglón entraba sin IVA.
|
|
21
|
+
|
|
22
|
+
- Updated dependencies [f9616d1]
|
|
23
|
+
- @asteby/metacore-sdk@3.14.0
|
|
24
|
+
|
|
25
|
+
## 48.0.0
|
|
26
|
+
|
|
27
|
+
### Minor Changes
|
|
28
|
+
|
|
29
|
+
- 7532829: Primitivos de negocio para hosts y addons federados:
|
|
30
|
+
|
|
31
|
+
- `InstalledAddonsProvider` + `useAddonInstalled` / `useCapabilityProvided` / `useInstalledAddons`: una sola respuesta a «¿qué hay instalado?» (sin provider devuelven `undefined`, nunca «ausente»).
|
|
32
|
+
- `allocatePayment` / `validateAllocation` (PaymentAllocator) y `computeAging` / `bucketFor` (AgingTable) como funciones puras.
|
|
33
|
+
- Bandeja de aprobaciones: `approvalsClient`, `registerApprovalCategory`, `groupApprovals` y el tipo `ApprovalRequestDTO`.
|
|
34
|
+
- Contribuciones federadas por documento: `registerDocumentContribution` (con disposer y carga perezosa), `resolveContributions` (pura) y el hook `useDocumentContributions`.
|
|
35
|
+
- Modales federados: `registerFederatedModal` / `resolveFederatedModal`. `ActionModalDispatcher` pinta el modal registrado sin esperar al host y, si el addon del `modal` no está instalado, abre el formulario genérico de la acción de inmediato en lugar de esperar 20 s.
|
|
36
|
+
- Tipos de documento (`registerDocumentKind`, `CORE_DOCUMENT_KINDS`) y contratos de PartyCard / TotalsPanel / PreviewPanel / ValidationChecklist.
|
|
37
|
+
- Manifest `document_forms`: `lines.kind`, `lines.open_documents`, `sources`, `preview` y `submit_action` en los tipos.
|
|
38
|
+
- Ayudantes de captura por modelo: `registerRecordPrefill` (también en `registry.scope(addon)`, se retira en el `unbind`) y `RecordPrefillBar`, que `DynamicRecordDialog` monta arriba del formulario; solo aplica claves que el formulario declara.
|
|
39
|
+
|
|
40
|
+
- 830349b: `DocumentEditor`: alta de documentos en una sola pantalla por secciones cuando el tipo de `document_forms` declara `layout: "editor"` (factura, nota de crédito, cobro/REP, cotización, OC). A la vista solo lo esencial (contraparte con su tarjeta, «Cargar desde…», renglones o reparto del pago, totales); los campos fiscales (`fiscal_data.*`, catálogos con default, contribuciones `header.fields`) van en «Opciones fiscales», plegada por defecto, y la vista previa también es opcional y plegada. Una sola acción primaria. Prefill desde el documento origen (`sources`, `initialSource`, `default_from_record` vía la nueva prop `record`) y validación inline. `lines.kind: "allocation"` reparte el cobro con `PaymentAllocator` (del más vencido o manual, con parcialidad) y guarda por `submit_action`. Nuevos exports: `DocumentEditor`, `PaymentAllocator`, `PartyCard`, `TotalsPanel`, `ValidationChecklist`, `PreviewPanel`, `CollapsibleSection`, `EditorSection` y las reglas puras de `document-editor-model`. Sin `layout: "editor"` el wizard sigue igual.
|
|
41
|
+
- 7d1bdf9: Alta guiada acotada a la vista filtrada: `scopeDocumentFormsToFilter(forms, filter)` y la prop `filter` de `DynamicCRUDPage`. Si el filtro fijo de la vista (el `filter` del nav) va sobre el `type_field` de `document_forms`, «Crear» abre ese tipo directo; si no hay formulario para ese tipo, no se ofrece. Antes «Documentos fiscales → Facturas → Crear» abría el formulario del REP.
|
|
42
|
+
- 9c34b09: Crear factura (DocumentFormDialog): el paso de renglones busca en el catálogo de productos aunque el host no pase `searchProducts`. Antes solo ofrecía «Renglón libre» y la factura se guardaba en $0. Al elegir un producto, el renglón entra con precio de venta, unidad, SKU, claves de extensión (`fiscal_data`), cantidad 1 y la tasa de impuesto del producto o, si no trae, la de la organización (`OrgRuntimeProvider taxRate`, nuevo). Nuevos: `createCatalogProductSearch`, props `productModel`/`defaultTaxRate` y `lines.discount_mode` en `document_forms`. Una cantidad vacía al leer renglones ya no queda en 0.
|
|
43
|
+
|
|
44
|
+
### Patch Changes
|
|
45
|
+
|
|
46
|
+
- 3c6e8eb: Corrige que al editar un registro (p. ej. un renglón dentro de una relación) un campo select con valor se borrara al guardar sin tocarlo. `DynamicForm` siembra los valores del registro de forma síncrona, ignora el `onValueChange('')` espurio del Select cuando ya hay valor y `''` no es opción válida, y en edición no manda selects vacíos que el usuario no tocó. `deriveRelationFormFields` ahora copia `readonly` de la columna, y el modal de alta/edición de renglón limita su alto al viewport con cuerpo con scroll y botones fijos abajo.
|
|
47
|
+
- 153dff8: El formulario de alta/edición (`DynamicRecordDialog`) ya no muestra claves de catálogo crudas como etiqueta de campo (p. ej. `FISCAL_MEXICO.EXT.CUSTOMER.RFC_RECEPTOR` en los campos que un addon agrega por extensión a Clientes o Productos). Si la etiqueta llega del servidor sin resolver, se traduce con el catálogo del cliente y, si tampoco está ahí, se muestra legible (`Codigo Postal`). Las etiquetas que ya llegan en texto humano no se tocan. Nuevo helper exportado `localizeFieldLabel`.
|
|
48
|
+
- db934ed: Kanban: los carriles de etapas ya no se pintan sobre otra columna. El host sirve un solo `group_by` por modelo, el del ÚLTIMO tablero de la navegación (en Taller, `service_bay_id`). Un tablero abierto sin `?group_by=` (por ejemplo, con el cambio de vista desde la lista) agrupaba las etapas por la bahía: todas las OT caían en «Sin etapa», los conteos de cada carril pedían `f_service_bay_id=diagnosis` (422) y al soltar una tarjeta se enviaba `service_bay_id = 'diagnosis'` (422 «no es un UUID válido»). `withGroupBy` ahora agrupa por el `stage_field` cuando no hay `?group_by=`. El tablero de la columna servida conserva la máquina de etapas solo si esa columna es la de etapas.
|
|
49
|
+
- 4ed3225: Renglones de formularios de acción: cuando la columna `total` es el precio unitario (`price`, `unit_price`…) y el renglón tiene cantidad pero ninguna columna de importe, la fila de total suma cantidad × precio − descuento de cada renglón y ya no los precios sueltos. En la OT de taller, cantidad 2 × 750 mostraba 750 en el total, aunque al guardar quedaba bien (1500). Si hay columna de importe (`subtotal`, `line_total`…), el comportamiento no cambia.
|
|
50
|
+
- f2e31cf: Las acciones por fila con `requiresState` se filtran contra el `stage_field` del modelo cuando hay máquina de etapas, igual que el kernel. Antes se comparaban con `status`, y por eso la OT de taller no mostraba «Procesar», «Esperar refacciones» ni «Cancelar» en la tabla ni en el kanban. Nuevo export `lifecycleStageField(metadata)`.
|
|
51
|
+
- Updated dependencies [7532829]
|
|
52
|
+
- @asteby/metacore-sdk@3.13.0
|
|
53
|
+
|
|
3
54
|
## 47.1.0
|
|
4
55
|
|
|
5
56
|
### Minor Changes
|
|
@@ -65,8 +65,22 @@ export declare function prefillFromFieldRelationRequest(req: PrefillFromFieldReq
|
|
|
65
65
|
} | null;
|
|
66
66
|
export declare function unwrapRecordScalar(value: unknown): unknown;
|
|
67
67
|
export declare function readRecordPath(record: any, path: string): unknown;
|
|
68
|
-
/**
|
|
69
|
-
|
|
68
|
+
/**
|
|
69
|
+
* ¿`ref` (FK de un campo) apunta al modelo `model` de la fila? Tolera prefijo de
|
|
70
|
+
* addon, PascalCase vs tabla y el plural de la tabla (`Invoice` ↔ `invoices`,
|
|
71
|
+
* `CustomerAddress` ↔ `customer_addresses`).
|
|
72
|
+
*/
|
|
73
|
+
export declare function refTargetsModel(ref: string | undefined | null, model: string | undefined | null): boolean;
|
|
74
|
+
/**
|
|
75
|
+
* Scalar seed for one action field from the row being acted on.
|
|
76
|
+
*
|
|
77
|
+
* Sin `default_from_record` declarado, un campo se siembra con la columna
|
|
78
|
+
* homónima de la fila; y un selector que apunta al MISMO modelo de la fila y
|
|
79
|
+
* que la fila no tiene como columna (`invoice_id` → Invoice en «Registrar pago»
|
|
80
|
+
* de una factura) se siembra con el id de esa fila: la acción se abrió desde
|
|
81
|
+
* ella (retest Pitsline 2026-10-05).
|
|
82
|
+
*/
|
|
83
|
+
export declare function scalarDefaultFromRecord(field: ActionFieldDef, record: any, model?: string): unknown;
|
|
70
84
|
export declare function ActionModalDispatcher({ open, onOpenChange, action, model, record, endpoint, onSuccess: onSuccessProp, }: ActionModalProps): import("react").JSX.Element | null;
|
|
71
85
|
/** How long an action with `modal` waits for its federated component. */
|
|
72
86
|
export declare const FEDERATED_ACTION_MODAL_TIMEOUT_MS = 20000;
|
|
@@ -75,7 +89,7 @@ export declare const FEDERATED_ACTION_MODAL_TIMEOUT_MS = 20000;
|
|
|
75
89
|
* other failure (500, network) -> the server's own message and cause. Unlike
|
|
76
90
|
* the toast it stays on screen until the next attempt. */
|
|
77
91
|
export declare function actionErrorBannerMessage(err: unknown, fields: readonly ActionFieldDef[] | undefined, t: Translate, language?: string): string;
|
|
78
|
-
export declare function buildFieldDefaults(fields: ActionFieldDef[], record: any): Record<string, any>;
|
|
92
|
+
export declare function buildFieldDefaults(fields: ActionFieldDef[], record: any, model?: string): Record<string, any>;
|
|
79
93
|
/** Renders one action field (same widgets as the action modals). Shared with DocumentFormDialog. */
|
|
80
94
|
export declare function renderField(field: ActionFieldDef, value: any, onChange: (value: any) => void, formValues?: Record<string, any>, record?: Record<string, any>, fieldErrors?: Record<string, string>): import("react").JSX.Element;
|
|
81
95
|
//# sourceMappingURL=action-modal-dispatcher.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"action-modal-dispatcher.d.ts","sourceRoot":"","sources":["../src/action-modal-dispatcher.tsx"],"names":[],"mappings":"AAmDA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AA8B/C,OAAO,KAAK,EAAE,cAAc,EAAmC,MAAM,SAAS,CAAA;AAE9E,OAAO,EACH,KAAK,cAAc,EACnB,KAAK,gBAAgB,
|
|
1
|
+
{"version":3,"file":"action-modal-dispatcher.d.ts","sourceRoot":"","sources":["../src/action-modal-dispatcher.tsx"],"names":[],"mappings":"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"}
|
|
@@ -16,7 +16,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
16
16
|
// The host injects its axios-like client via <ApiProvider>; we no longer
|
|
17
17
|
// depend on a bundler alias to `@/lib/api`.
|
|
18
18
|
import { emitRecordMutation } from './record-mutation-events';
|
|
19
|
-
import { useState, useCallback, useEffect, useMemo, useRef, useSyncExternalStore } from 'react';
|
|
19
|
+
import { Suspense, useState, useCallback, useEffect, useMemo, useRef, useSyncExternalStore } from 'react';
|
|
20
20
|
import { useTranslation } from 'react-i18next';
|
|
21
21
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, Button, Input, Textarea, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Switch, } from '@asteby/metacore-ui/primitives';
|
|
22
22
|
import { Loader2 } from 'lucide-react';
|
|
@@ -44,8 +44,11 @@ import { FieldGrid, FieldCell, FieldLabel } from './field-grid';
|
|
|
44
44
|
import { useMetadataCache } from './metadata-cache';
|
|
45
45
|
import { ViewValue, objectLabel, relationSiblingValue, isLineItemsField as isRecordLineItemsField, } from './dialogs/dynamic-record';
|
|
46
46
|
// Canonical registry lives in @asteby/metacore-sdk
|
|
47
|
-
import { getActionComponent, subscribeActionComponents, } from '@asteby/metacore-sdk';
|
|
47
|
+
import { adaptActionProps, getActionComponent, subscribeActionComponents, } from '@asteby/metacore-sdk';
|
|
48
48
|
import { requestFederatedAction } from './federated-action-loader';
|
|
49
|
+
import { useInstalledAddons } from './installed-addons-context';
|
|
50
|
+
import { federatedModalComponent, resolveFederatedModal } from './primitives/contributions';
|
|
51
|
+
import { installedPredicate, useContributionsVersion } from './primitives/use-contributions';
|
|
49
52
|
export function isPrefillSpec(v) {
|
|
50
53
|
return (typeof v === 'object' &&
|
|
51
54
|
v !== null &&
|
|
@@ -212,8 +215,46 @@ function defaultFromRecordSpec(field) {
|
|
|
212
215
|
const f = field;
|
|
213
216
|
return f.defaultFromRecord ?? f.default_from_record;
|
|
214
217
|
}
|
|
215
|
-
/**
|
|
216
|
-
|
|
218
|
+
/** `customers.Invoice`, `Invoice`, `invoices`, `sales_orders` → forma comparable. */
|
|
219
|
+
function modelToken(m) {
|
|
220
|
+
const last = m.split('.').pop() ?? m;
|
|
221
|
+
return last.replace(/[^a-z0-9]/gi, '').toLowerCase();
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* ¿`ref` (FK de un campo) apunta al modelo `model` de la fila? Tolera prefijo de
|
|
225
|
+
* addon, PascalCase vs tabla y el plural de la tabla (`Invoice` ↔ `invoices`,
|
|
226
|
+
* `CustomerAddress` ↔ `customer_addresses`).
|
|
227
|
+
*/
|
|
228
|
+
export function refTargetsModel(ref, model) {
|
|
229
|
+
if (!ref || !model)
|
|
230
|
+
return false;
|
|
231
|
+
const a = modelToken(ref);
|
|
232
|
+
const b = modelToken(model);
|
|
233
|
+
if (!a || !b)
|
|
234
|
+
return false;
|
|
235
|
+
return a === b || a + 's' === b || a + 'es' === b || b + 's' === a || b + 'es' === a;
|
|
236
|
+
}
|
|
237
|
+
function fieldRef(field) {
|
|
238
|
+
if (field.ref)
|
|
239
|
+
return field.ref;
|
|
240
|
+
const opts = field.options;
|
|
241
|
+
if (opts && !Array.isArray(opts) && typeof opts === 'object') {
|
|
242
|
+
const src = opts.source;
|
|
243
|
+
if (typeof src === 'string' && src)
|
|
244
|
+
return src;
|
|
245
|
+
}
|
|
246
|
+
return undefined;
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Scalar seed for one action field from the row being acted on.
|
|
250
|
+
*
|
|
251
|
+
* Sin `default_from_record` declarado, un campo se siembra con la columna
|
|
252
|
+
* homónima de la fila; y un selector que apunta al MISMO modelo de la fila y
|
|
253
|
+
* que la fila no tiene como columna (`invoice_id` → Invoice en «Registrar pago»
|
|
254
|
+
* de una factura) se siembra con el id de esa fila: la acción se abrió desde
|
|
255
|
+
* ella (retest Pitsline 2026-10-05).
|
|
256
|
+
*/
|
|
257
|
+
export function scalarDefaultFromRecord(field, record, model) {
|
|
217
258
|
if (!record)
|
|
218
259
|
return undefined;
|
|
219
260
|
const spec = defaultFromRecordSpec(field);
|
|
@@ -233,6 +274,12 @@ export function scalarDefaultFromRecord(field, record) {
|
|
|
233
274
|
const v = readRecordPath(record, field.key);
|
|
234
275
|
if (v !== undefined && v !== null && v !== '')
|
|
235
276
|
return v;
|
|
277
|
+
// Sólo si la fila NO tiene esa columna: un `parent_id` vacío de una
|
|
278
|
+
// categoría es «sin padre», no «yo mismo».
|
|
279
|
+
const id = unwrapRecordScalar(record.id);
|
|
280
|
+
if (!(field.key in record) && id !== undefined && id !== null && id !== '' && refTargetsModel(fieldRef(field), model)) {
|
|
281
|
+
return id;
|
|
282
|
+
}
|
|
236
283
|
}
|
|
237
284
|
return undefined;
|
|
238
285
|
}
|
|
@@ -248,13 +295,29 @@ export function ActionModalDispatcher({ open, onOpenChange, action, model, recor
|
|
|
248
295
|
// render, and a memoized lookup would pin the fallback forever.
|
|
249
296
|
const readComponent = () => getActionComponent(model, action.key);
|
|
250
297
|
const CustomComponent = useSyncExternalStore(subscribeActionComponents, readComponent, readComponent);
|
|
298
|
+
// Modal federado declarativo (registerFederatedModal). Si el addon dueño
|
|
299
|
+
// del slug consta como NO instalado, el `modal` se ignora y se usa el
|
|
300
|
+
// formulario genérico de la acción de inmediato (sin esperar el timeout).
|
|
301
|
+
useContributionsVersion();
|
|
302
|
+
const installed = useInstalledAddons();
|
|
303
|
+
const slug = action.modal || '';
|
|
304
|
+
const federatedModal = slug ? resolveFederatedModal(slug, installedPredicate(installed)) : null;
|
|
305
|
+
const modalAddonMissing = !!slug && !federatedModal && !!installed && !installed.addons.has(slug.split('.')[0]);
|
|
251
306
|
if (CustomComponent) {
|
|
252
307
|
return (_jsx(CustomComponent, { open: open, onOpenChange: onOpenChange, action: action, model: model, record: record, endpoint: endpoint, onSuccess: onSuccess }));
|
|
253
308
|
}
|
|
309
|
+
if (federatedModal) {
|
|
310
|
+
// Modal por slug del store canónico: lo registró `api.registry.registerModal`
|
|
311
|
+
// (contrato recordId/payload/close) o registerFederatedModal
|
|
312
|
+
// (ActionModalProps). Recibe ambos contratos.
|
|
313
|
+
const FederatedComponent = federatedModalComponent(federatedModal);
|
|
314
|
+
return (_jsx(Suspense, { fallback: _jsx(LoadingActionDialog, { open: open, onOpenChange: onOpenChange, action: action }), children: _jsx(FederatedComponent, { ...adaptActionProps({ open, onOpenChange, action, model, record, endpoint, onSuccess }) }) }));
|
|
315
|
+
}
|
|
254
316
|
// Declarative custom slot (`modal: "addon.action"`). Wait for the remote,
|
|
255
317
|
// then a hard error — confirm/fields generics look "fine" and hide a
|
|
256
|
-
// missing remote.
|
|
257
|
-
|
|
318
|
+
// missing remote. Exception: the slug's addon is known NOT installed — the
|
|
319
|
+
// remote will never come, so fall through to the generic form now.
|
|
320
|
+
if (action.modal && !modalAddonMissing) {
|
|
258
321
|
return (_jsx(PendingCustomActionModal, { open: open, onOpenChange: onOpenChange, action: action, model: model }));
|
|
259
322
|
}
|
|
260
323
|
if (action.steps && action.steps.length > 0) {
|
|
@@ -288,7 +351,6 @@ const MODAL_SLUG_RE = /^[\w-]+(\.[\w-]+)+$/;
|
|
|
288
351
|
* override from another addon that registerAction()s over it.
|
|
289
352
|
*/
|
|
290
353
|
function PendingCustomActionModal({ open, onOpenChange, action, model, }) {
|
|
291
|
-
const { t } = useTranslation();
|
|
292
354
|
const slug = action.modal || '';
|
|
293
355
|
const [timedOut, setTimedOut] = useState(false);
|
|
294
356
|
useEffect(() => {
|
|
@@ -329,6 +391,11 @@ function PendingCustomActionModal({ open, onOpenChange, action, model, }) {
|
|
|
329
391
|
if (timedOut) {
|
|
330
392
|
return _jsx(MissingCustomActionModal, { open: open, onOpenChange: onOpenChange, action: action, model: model });
|
|
331
393
|
}
|
|
394
|
+
return _jsx(LoadingActionDialog, { open: open, onOpenChange: onOpenChange, action: action });
|
|
395
|
+
}
|
|
396
|
+
/** Diálogo «Cargando…» mientras llega el componente federado de la acción. */
|
|
397
|
+
function LoadingActionDialog({ open, onOpenChange, action, }) {
|
|
398
|
+
const { t } = useTranslation();
|
|
332
399
|
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: t(action.label, { defaultValue: action.label }) }), _jsx(DialogDescription, { children: t('dynamic.action_modal_loading', { defaultValue: 'Cargando…' }) })] }), _jsx("div", { className: "flex justify-center py-6", "data-testid": "federated-action-loading", children: _jsx(Loader2, { className: "h-6 w-6 animate-spin text-muted-foreground" }) })] }) }));
|
|
333
400
|
}
|
|
334
401
|
/** Shown when the action declares `modal` but no federated component registered. */
|
|
@@ -657,11 +724,11 @@ function GenericActionModal({ open, onOpenChange, action, model, record, endpoin
|
|
|
657
724
|
}, [open, model, recordId, api]);
|
|
658
725
|
useEffect(() => {
|
|
659
726
|
if (open && action.fields) {
|
|
660
|
-
setFormData(buildFieldDefaults(action.fields, record));
|
|
727
|
+
setFormData(buildFieldDefaults(action.fields, record, model));
|
|
661
728
|
setFieldErrors({});
|
|
662
729
|
setFormError(undefined);
|
|
663
730
|
}
|
|
664
|
-
}, [open, action.fields, record]);
|
|
731
|
+
}, [open, action.fields, record, model]);
|
|
665
732
|
// Lines the list row does not carry: load them from the declared relation
|
|
666
733
|
// (see prefillRelationRequests) and seed the line-items grid.
|
|
667
734
|
useEffect(() => {
|
|
@@ -870,7 +937,7 @@ function GenericActionModal({ open, onOpenChange, action, model, record, endpoin
|
|
|
870
937
|
// buildFieldDefaults seeds formData for a set of action fields, honoring the
|
|
871
938
|
// same line-items prefill spec + boolean/empty rules GenericActionModal uses, so
|
|
872
939
|
// wizard steps and single-page forms initialize identically.
|
|
873
|
-
export function buildFieldDefaults(fields, record) {
|
|
940
|
+
export function buildFieldDefaults(fields, record, model) {
|
|
874
941
|
const defaults = {};
|
|
875
942
|
for (const field of fields) {
|
|
876
943
|
if (isLineItemsField(field)) {
|
|
@@ -882,7 +949,7 @@ export function buildFieldDefaults(fields, record) {
|
|
|
882
949
|
: [];
|
|
883
950
|
continue;
|
|
884
951
|
}
|
|
885
|
-
const fromRecord = scalarDefaultFromRecord(field, record);
|
|
952
|
+
const fromRecord = scalarDefaultFromRecord(field, record, model);
|
|
886
953
|
if (fromRecord !== undefined && fromRecord !== null && fromRecord !== '') {
|
|
887
954
|
defaults[field.key] = fromRecord;
|
|
888
955
|
continue;
|
|
@@ -925,10 +992,10 @@ function WizardActionModal({ open, onOpenChange, action, model, record, endpoint
|
|
|
925
992
|
if (!open)
|
|
926
993
|
return;
|
|
927
994
|
const allFields = steps.flatMap((s) => s.fields ?? []);
|
|
928
|
-
setFormData(buildFieldDefaults(allFields, record));
|
|
995
|
+
setFormData(buildFieldDefaults(allFields, record, model));
|
|
929
996
|
setStepIndex(0);
|
|
930
997
|
setFormError(undefined);
|
|
931
|
-
}, [open, action.steps, record]);
|
|
998
|
+
}, [open, action.steps, record, model]);
|
|
932
999
|
const updateField = (key, value) => setFormData((prev) => ({ ...prev, [key]: value }));
|
|
933
1000
|
const step = steps[stepIndex];
|
|
934
1001
|
const isLast = stepIndex === steps.length - 1;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { ActionPriority } from '@asteby/metacore-sdk';
|
|
2
|
+
import type { InstalledAddonsValue } from './installed-addons-context';
|
|
3
|
+
export type { ActionPriority };
|
|
4
|
+
/** Lo mínimo que se lee de una acción de metadata para clasificarla. */
|
|
5
|
+
export interface PrioritizableAction {
|
|
6
|
+
key: string;
|
|
7
|
+
priority?: string;
|
|
8
|
+
trigger?: {
|
|
9
|
+
type?: string;
|
|
10
|
+
capability?: string;
|
|
11
|
+
} | null;
|
|
12
|
+
}
|
|
13
|
+
/** "primary" | "secondary" | undefined (= acción normal del menú). */
|
|
14
|
+
export declare function classifyActionPriority(action: PrioritizableAction): ActionPriority | undefined;
|
|
15
|
+
export interface SplitActions<A> {
|
|
16
|
+
/** La única destacada (la primera que se declara `primary`). */
|
|
17
|
+
primary?: A;
|
|
18
|
+
/** El resto de acciones principales, en orden. */
|
|
19
|
+
main: A[];
|
|
20
|
+
/** Compartir / imprimir / correo / chat… */
|
|
21
|
+
secondary: A[];
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Separa primaria, principales y secundarias conservando el orden. Si varias
|
|
25
|
+
* reclaman `primary`, sólo la primera lo es; las demás quedan como principales.
|
|
26
|
+
*/
|
|
27
|
+
export declare function splitActionsByPriority<A extends PrioritizableAction>(actions: readonly A[]): SplitActions<A>;
|
|
28
|
+
/**
|
|
29
|
+
* Una acción que despacha por capacidad (`trigger.type: "capability"`) sólo se
|
|
30
|
+
* ofrece si algún addon instalado la provee. Sin provider = desconocido → se
|
|
31
|
+
* muestra (el servidor revalida igual).
|
|
32
|
+
*/
|
|
33
|
+
export declare function isActionProviderActive(action: PrioritizableAction, installed: InstalledAddonsValue | null): boolean;
|
|
34
|
+
//# sourceMappingURL=action-priority.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"action-priority.d.ts","sourceRoot":"","sources":["../src/action-priority.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA;AAC1D,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,4BAA4B,CAAA;AAEtE,YAAY,EAAE,cAAc,EAAE,CAAA;AAE9B,wEAAwE;AACxE,MAAM,WAAW,mBAAmB;IAChC,GAAG,EAAE,MAAM,CAAA;IACX,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,OAAO,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAA;CAC1D;AAqBD,sEAAsE;AACtE,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,mBAAmB,GAAG,cAAc,GAAG,SAAS,CAS9F;AAED,MAAM,WAAW,YAAY,CAAC,CAAC;IAC3B,gEAAgE;IAChE,OAAO,CAAC,EAAE,CAAC,CAAA;IACX,kDAAkD;IAClD,IAAI,EAAE,CAAC,EAAE,CAAA;IACT,4CAA4C;IAC5C,SAAS,EAAE,CAAC,EAAE,CAAA;CACjB;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,CAAC,SAAS,mBAAmB,EAAE,OAAO,EAAE,SAAS,CAAC,EAAE,GAAG,YAAY,CAAC,CAAC,CAAC,CAW5G;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,mBAAmB,EAAE,SAAS,EAAE,oBAAoB,GAAG,IAAI,GAAG,OAAO,CAInH"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Segmentos de la clave (separados por _ . -) que marcan una acción de
|
|
2
|
+
// compartir/salida. Por segmento: `create_mailbox` no es «mail», `send` solo
|
|
3
|
+
// (enviar una cotización) sigue siendo flujo principal.
|
|
4
|
+
const SECONDARY_SEGMENTS = new Set([
|
|
5
|
+
'print',
|
|
6
|
+
'reprint',
|
|
7
|
+
'share',
|
|
8
|
+
'email',
|
|
9
|
+
'mail',
|
|
10
|
+
'whatsapp',
|
|
11
|
+
'chat',
|
|
12
|
+
'download',
|
|
13
|
+
'pdf',
|
|
14
|
+
'xml',
|
|
15
|
+
'acuse',
|
|
16
|
+
]);
|
|
17
|
+
const CRUD_KEYS = new Set(['view', 'edit', 'delete']);
|
|
18
|
+
/** "primary" | "secondary" | undefined (= acción normal del menú). */
|
|
19
|
+
export function classifyActionPriority(action) {
|
|
20
|
+
if (action.priority === 'primary' || action.priority === 'secondary')
|
|
21
|
+
return action.priority;
|
|
22
|
+
if (CRUD_KEYS.has(action.key))
|
|
23
|
+
return undefined;
|
|
24
|
+
return action.key
|
|
25
|
+
.toLowerCase()
|
|
26
|
+
.split(/[_.-]+/)
|
|
27
|
+
.some((seg) => SECONDARY_SEGMENTS.has(seg))
|
|
28
|
+
? 'secondary'
|
|
29
|
+
: undefined;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Separa primaria, principales y secundarias conservando el orden. Si varias
|
|
33
|
+
* reclaman `primary`, sólo la primera lo es; las demás quedan como principales.
|
|
34
|
+
*/
|
|
35
|
+
export function splitActionsByPriority(actions) {
|
|
36
|
+
let primary;
|
|
37
|
+
const main = [];
|
|
38
|
+
const secondary = [];
|
|
39
|
+
for (const a of actions) {
|
|
40
|
+
const p = classifyActionPriority(a);
|
|
41
|
+
if (p === 'secondary')
|
|
42
|
+
secondary.push(a);
|
|
43
|
+
else if (p === 'primary' && !primary)
|
|
44
|
+
primary = a;
|
|
45
|
+
else
|
|
46
|
+
main.push(a);
|
|
47
|
+
}
|
|
48
|
+
return { primary, main, secondary };
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Una acción que despacha por capacidad (`trigger.type: "capability"`) sólo se
|
|
52
|
+
* ofrece si algún addon instalado la provee. Sin provider = desconocido → se
|
|
53
|
+
* muestra (el servidor revalida igual).
|
|
54
|
+
*/
|
|
55
|
+
export function isActionProviderActive(action, installed) {
|
|
56
|
+
const t = action.trigger;
|
|
57
|
+
if (!installed || !t || t.type !== 'capability' || !t.capability)
|
|
58
|
+
return true;
|
|
59
|
+
return installed.capabilities.has(t.capability);
|
|
60
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { ProductQuery, ProductResult } from './product-search';
|
|
2
|
+
interface ApiLike {
|
|
3
|
+
get: (url: string, cfg?: {
|
|
4
|
+
params?: Record<string, unknown>;
|
|
5
|
+
signal?: AbortSignal;
|
|
6
|
+
}) => Promise<{
|
|
7
|
+
data: any;
|
|
8
|
+
}>;
|
|
9
|
+
}
|
|
10
|
+
export interface CatalogProductSearchOptions {
|
|
11
|
+
/** Modelo de catálogo. Default `products.Product`. */
|
|
12
|
+
model?: string;
|
|
13
|
+
/** Endpoint de listado. Default `/data/<model>`. */
|
|
14
|
+
endpoint?: string;
|
|
15
|
+
limit?: number;
|
|
16
|
+
/** Tasa de impuesto cuando el producto no trae la suya (config fiscal de la org; p. ej. 0.16). */
|
|
17
|
+
defaultTaxRate?: number;
|
|
18
|
+
}
|
|
19
|
+
/** Proyección pura registro del catálogo → ProductResult (exportada para tests). */
|
|
20
|
+
export declare function catalogRecordToProduct(rec: Record<string, any>, defaultTaxRate?: number): ProductResult;
|
|
21
|
+
export declare function createCatalogProductSearch(api: ApiLike, opts?: CatalogProductSearchOptions): (query: Exclude<ProductQuery, {
|
|
22
|
+
kind: "empty";
|
|
23
|
+
}>, signal: AbortSignal) => Promise<ProductResult[]>;
|
|
24
|
+
/**
|
|
25
|
+
* Buscador del host (`searchProducts`) con el IVA de la org como respaldo: un
|
|
26
|
+
* producto que no trae su tasa entra con la de la org en vez de 0 (retest
|
|
27
|
+
* Pitsline 2026-10-05: el buscador de ops no devolvía tasa y el renglón de la
|
|
28
|
+
* factura quedaba sin IVA). Sin tasa de org, el buscador queda tal cual.
|
|
29
|
+
*/
|
|
30
|
+
export declare function withDefaultTaxRate<Q, S>(search: (query: Q, signal: S) => Promise<ProductResult[]>, defaultTaxRate: number | undefined): (query: Q, signal: S) => Promise<ProductResult[]>;
|
|
31
|
+
export {};
|
|
32
|
+
//# sourceMappingURL=catalog-product-search.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"catalog-product-search.d.ts","sourceRoot":"","sources":["../../src/business/catalog-product-search.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAA;AAEnE,UAAU,OAAO;IACb,GAAG,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QAAC,MAAM,CAAC,EAAE,WAAW,CAAA;KAAE,KAAK,OAAO,CAAC;QAAE,IAAI,EAAE,GAAG,CAAA;KAAE,CAAC,CAAA;CACjH;AAED,MAAM,WAAW,2BAA2B;IACxC,sDAAsD;IACtD,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,oDAAoD;IACpD,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,kGAAkG;IAClG,cAAc,CAAC,EAAE,MAAM,CAAA;CAC1B;AA4BD,oFAAoF;AACpF,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,cAAc,CAAC,EAAE,MAAM,GAAG,aAAa,CAkBvG;AAED,wBAAgB,0BAA0B,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,GAAE,2BAAgC,IAI7E,OAAO,OAAO,CAAC,YAAY,EAAE;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,CAAC,EAAE,QAAQ,WAAW,KAAG,OAAO,CAAC,aAAa,EAAE,CAAC,CAShH;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,CAAC,EACnC,MAAM,EAAE,CAAC,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,KAAK,OAAO,CAAC,aAAa,EAAE,CAAC,EACzD,cAAc,EAAE,MAAM,GAAG,SAAS,GACnC,CAAC,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,KAAK,OAAO,CAAC,aAAa,EAAE,CAAC,CAMnD"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
const num = (v) => {
|
|
2
|
+
const n = typeof v === 'string' && v.trim() !== '' ? Number(v) : typeof v === 'number' ? v : NaN;
|
|
3
|
+
return Number.isFinite(n) ? n : undefined;
|
|
4
|
+
};
|
|
5
|
+
const str = (v) => (typeof v === 'string' && v.trim() ? v.trim() : undefined);
|
|
6
|
+
/** Tasa como fracción: acepta 0.16 o 16. */
|
|
7
|
+
const rate = (v) => {
|
|
8
|
+
const n = num(v);
|
|
9
|
+
if (n == null || n < 0)
|
|
10
|
+
return undefined;
|
|
11
|
+
return n > 1 ? n / 100 : n;
|
|
12
|
+
};
|
|
13
|
+
const bag = (v) => {
|
|
14
|
+
if (typeof v === 'string') {
|
|
15
|
+
try {
|
|
16
|
+
const parsed = JSON.parse(v);
|
|
17
|
+
return parsed && typeof parsed === 'object' ? parsed : {};
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return {};
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return v && typeof v === 'object' ? v : {};
|
|
24
|
+
};
|
|
25
|
+
/** Proyección pura registro del catálogo → ProductResult (exportada para tests). */
|
|
26
|
+
export function catalogRecordToProduct(rec, defaultTaxRate) {
|
|
27
|
+
// Campos de extensión de otros addons (p. ej. claves SAT de fiscal_mexico).
|
|
28
|
+
const extensions = Object.fromEntries(Object.entries(bag(rec.fiscal_data)).filter(([, v]) => typeof v === 'string' && v !== ''));
|
|
29
|
+
const pt = String(rec.product_type ?? '');
|
|
30
|
+
return {
|
|
31
|
+
id: String(rec.id ?? rec.value ?? ''),
|
|
32
|
+
name: String(rec.name ?? rec.label ?? ''),
|
|
33
|
+
sku: str(rec.sku),
|
|
34
|
+
barcode: str(rec.barcode),
|
|
35
|
+
kind: pt === 'service' ? 'service' : 'product',
|
|
36
|
+
price: num(rec.unit_price ?? rec.sale_price ?? rec.price),
|
|
37
|
+
cost: num(rec.cost_price ?? rec.cost),
|
|
38
|
+
tax_rate: rate(rec.tax_rate) ?? defaultTaxRate,
|
|
39
|
+
unit: str(rec.unit_of_measure) ?? str(rec.unit),
|
|
40
|
+
extensions: Object.keys(extensions).length ? extensions : undefined,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
export function createCatalogProductSearch(api, opts = {}) {
|
|
44
|
+
const model = opts.model ?? 'products.Product';
|
|
45
|
+
const url = opts.endpoint ?? `/data/${model}`;
|
|
46
|
+
const limit = opts.limit ?? 20;
|
|
47
|
+
return async (query, signal) => {
|
|
48
|
+
const search = query.kind === 'barcode' ? query.barcode : query.raw.trim();
|
|
49
|
+
const res = await api.get(url, { params: { search, per_page: limit }, signal });
|
|
50
|
+
const rows = res?.data?.data;
|
|
51
|
+
if (!Array.isArray(rows))
|
|
52
|
+
return [];
|
|
53
|
+
return rows
|
|
54
|
+
.filter((r) => r && typeof r === 'object' && r.is_active !== false)
|
|
55
|
+
.map((r) => catalogRecordToProduct(r, opts.defaultTaxRate));
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Buscador del host (`searchProducts`) con el IVA de la org como respaldo: un
|
|
60
|
+
* producto que no trae su tasa entra con la de la org en vez de 0 (retest
|
|
61
|
+
* Pitsline 2026-10-05: el buscador de ops no devolvía tasa y el renglón de la
|
|
62
|
+
* factura quedaba sin IVA). Sin tasa de org, el buscador queda tal cual.
|
|
63
|
+
*/
|
|
64
|
+
export function withDefaultTaxRate(search, defaultTaxRate) {
|
|
65
|
+
if (defaultTaxRate == null)
|
|
66
|
+
return search;
|
|
67
|
+
return async (query, signal) => {
|
|
68
|
+
const rows = await search(query, signal);
|
|
69
|
+
return rows.map((p) => (p.tax_rate == null ? { ...p, tax_rate: defaultTaxRate } : p));
|
|
70
|
+
};
|
|
71
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"catalog.d.ts","sourceRoot":"","sources":["../../src/business/catalog.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,qBAAqB;IAClC,+DAA+D;IAC/D,IAAI,EAAE,MAAM,CAAA;IACZ,yCAAyC;IACzC,KAAK,EAAE,MAAM,CAAA;IACb,uCAAuC;IACvC,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,4EAA4E;IAC5E,WAAW,EAAE,MAAM,EAAE,CAAA;IACrB,kDAAkD;IAClD,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,uDAAuD;IACvD,OAAO,EAAE,MAAM,EAAE,CAAA;CACpB;AAED,eAAO,MAAM,mBAAmB,EAAE,SAAS,qBAAqB,
|
|
1
|
+
{"version":3,"file":"catalog.d.ts","sourceRoot":"","sources":["../../src/business/catalog.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,qBAAqB;IAClC,+DAA+D;IAC/D,IAAI,EAAE,MAAM,CAAA;IACZ,yCAAyC;IACzC,KAAK,EAAE,MAAM,CAAA;IACb,uCAAuC;IACvC,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,4EAA4E;IAC5E,WAAW,EAAE,MAAM,EAAE,CAAA;IACrB,kDAAkD;IAClD,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,uDAAuD;IACvD,OAAO,EAAE,MAAM,EAAE,CAAA;CACpB;AAED,eAAO,MAAM,mBAAmB,EAAE,SAAS,qBAAqB,EAwGtD,CAAA"}
|
package/dist/business/catalog.js
CHANGED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { type LineItem } from './line-items';
|
|
2
|
+
import type { Allocation, AllocationIssue, OpenDocument } from '../primitives/allocation';
|
|
3
|
+
import type { ActionFieldDef, DocumentFormLines, DocumentFormSource, DocumentFormOpenDocuments, DocumentFormsManifest, DocumentFormType } from '../types';
|
|
4
|
+
export declare const EXTENSION_PREFIX = "fiscal_data.";
|
|
5
|
+
export type EditorLinesConfig = DocumentFormLines & {
|
|
6
|
+
field: string;
|
|
7
|
+
};
|
|
8
|
+
/** true si el tipo se abre en el DocumentEditor (`layout: "editor"`). */
|
|
9
|
+
export declare function isEditorLayout(type: Pick<DocumentFormType, 'layout' | 'sources'> | null | undefined): boolean;
|
|
10
|
+
/** Paso de renglones efectivo del tipo (o undefined si no tiene). */
|
|
11
|
+
export declare function editorLinesConfig(type: DocumentFormType, forms: Pick<DocumentFormsManifest, 'lines_field'>): EditorLinesConfig | undefined;
|
|
12
|
+
/**
|
|
13
|
+
* Divulgación progresiva: ¿el campo va plegado en «Opciones fiscales»?
|
|
14
|
+
* - campos de extensión (`fiscal_data.*`) que aportan otros addons;
|
|
15
|
+
* - `section: "fiscal" | "advanced"`;
|
|
16
|
+
* - catálogos opcionales que ya traen un default (método, serie, relación…):
|
|
17
|
+
* el usuario no necesita tocarlos para guardar.
|
|
18
|
+
* Lo obligatorio sin default, la contraparte y el monto siempre están a la vista.
|
|
19
|
+
*/
|
|
20
|
+
export declare function isAdvancedField(f: ActionFieldDef, essentialKeys?: ReadonlySet<string>): boolean;
|
|
21
|
+
export interface EditorFieldGroups {
|
|
22
|
+
/** Campo que elige la contraparte (cliente/proveedor), si el tipo la declara. */
|
|
23
|
+
party?: ActionFieldDef;
|
|
24
|
+
/** A la vista: lo esencial del encabezado. */
|
|
25
|
+
essential: ActionFieldDef[];
|
|
26
|
+
/** Plegados en «Opciones fiscales». */
|
|
27
|
+
advanced: ActionFieldDef[];
|
|
28
|
+
/** Notas largas (textarea): al final. */
|
|
29
|
+
notes: ActionFieldDef[];
|
|
30
|
+
}
|
|
31
|
+
/** Reparte los campos del tipo (más los de extensión) en grupos de pantalla. */
|
|
32
|
+
export declare function splitEditorFields(fields: readonly ActionFieldDef[], opts?: {
|
|
33
|
+
partyField?: string;
|
|
34
|
+
essentialKeys?: readonly string[];
|
|
35
|
+
extensionFields?: readonly ActionFieldDef[];
|
|
36
|
+
}): EditorFieldGroups;
|
|
37
|
+
/**
|
|
38
|
+
* Etiqueta clara con el código como dato secundario:
|
|
39
|
+
* `03 · Transferencia electrónica` → `Transferencia electrónica · 03`.
|
|
40
|
+
* Solo reordena el patrón «CÓDIGO · texto»; cualquier otra etiqueta pasa igual.
|
|
41
|
+
*/
|
|
42
|
+
export declare function friendlyOptionLabel(label: string): string;
|
|
43
|
+
/** El mismo campo con sus opciones en formato «texto · código». */
|
|
44
|
+
export declare function withFriendlyOptions(f: ActionFieldDef): ActionFieldDef;
|
|
45
|
+
/**
|
|
46
|
+
* Defaults del encabezado que salen de la contraparte: los campos de extensión
|
|
47
|
+
* con la MISMA clave en ambos modelos (p. ej. `fiscal_data.uso_cfdi` de Customer
|
|
48
|
+
* → `fiscal_data.uso_cfdi` del documento) y la moneda.
|
|
49
|
+
* Solo llena lo que está vacío: nunca pisa lo que el usuario ya capturó.
|
|
50
|
+
*/
|
|
51
|
+
export declare function partyDefaults(party: Record<string, any> | null, documentFieldKeys: readonly string[], current: Record<string, unknown>): Record<string, unknown>;
|
|
52
|
+
/** Filas de la tarjeta de contraparte: resumen declarado + extensiones (máximo `limit`). */
|
|
53
|
+
export declare function partySummaryRows(party: Record<string, any> | null, summary?: readonly string[], labels?: Record<string, string>, limit?: number): Array<{
|
|
54
|
+
key: string;
|
|
55
|
+
label: string;
|
|
56
|
+
value: string;
|
|
57
|
+
}>;
|
|
58
|
+
export interface CreditStatus {
|
|
59
|
+
limit?: number;
|
|
60
|
+
balance?: number;
|
|
61
|
+
overdue?: number;
|
|
62
|
+
available?: number;
|
|
63
|
+
holdReason?: string;
|
|
64
|
+
/** `ok` | `warn` (supera 80 % o hay vencido) | `block` (retenido o sin crédito disponible). */
|
|
65
|
+
level: 'ok' | 'warn' | 'block';
|
|
66
|
+
}
|
|
67
|
+
export declare function creditStatus(party: Record<string, any> | null, cols: {
|
|
68
|
+
limit?: string;
|
|
69
|
+
balance?: string;
|
|
70
|
+
overdue?: string;
|
|
71
|
+
hold_reason?: string;
|
|
72
|
+
} | undefined, documentTotal: number): CreditStatus | null;
|
|
73
|
+
/**
|
|
74
|
+
* Renglones de un documento origen → LineItem. `map` traduce nombres
|
|
75
|
+
* (`description: product_name`, `discount: discount_pct`). Si el origen trae
|
|
76
|
+
* `tax_amount` y `subtotal` pero no la tasa, la deriva. `credit` (NC) topa la
|
|
77
|
+
* cantidad en lo facturado (o en `max_quantity` si el origen la trae).
|
|
78
|
+
*
|
|
79
|
+
* Con `remaining_quantity` (lo sirve el host: cantidad − lo ya facturado o
|
|
80
|
+
* devuelto) la cantidad sugerida y el tope son lo pendiente y los renglones ya
|
|
81
|
+
* cubiertos se omiten. Cada línea guarda `source_line_id` para el vínculo.
|
|
82
|
+
*/
|
|
83
|
+
export declare function linesFromSource(rows: unknown, map?: Record<string, string>, opts?: {
|
|
84
|
+
kind?: DocumentFormLines['kind'];
|
|
85
|
+
discountMode?: DocumentFormLines['discount_mode'];
|
|
86
|
+
}): LineItem[];
|
|
87
|
+
/** Cuántos renglones del origen se cargaron y cuántos se omitieron por estar ya cubiertos. */
|
|
88
|
+
export declare function sourceLoadSummary(rows: unknown): {
|
|
89
|
+
loaded: number;
|
|
90
|
+
covered: number;
|
|
91
|
+
};
|
|
92
|
+
/** Fuente que precarga desde lo pendiente servido por el host (no desde la relación cruda). */
|
|
93
|
+
export declare function sourceTracksRemaining(src: Pick<DocumentFormSource, 'line_link_field' | 'remaining_qty_field' | 'remaining_endpoint'>): boolean;
|
|
94
|
+
export interface EditorIssue {
|
|
95
|
+
field?: string;
|
|
96
|
+
severity: 'error' | 'warning';
|
|
97
|
+
message: string;
|
|
98
|
+
}
|
|
99
|
+
/** Revisión local (instantánea) de los renglones. Sin ruido: solo lo que hay que mirar. */
|
|
100
|
+
export declare function localIssues(lines: LineItem[], opts?: {
|
|
101
|
+
requireLines?: boolean;
|
|
102
|
+
requireTax?: boolean;
|
|
103
|
+
}): EditorIssue[];
|
|
104
|
+
/** Documento abierto (fila de /data) → OpenDocument para PaymentAllocator. */
|
|
105
|
+
export declare function toOpenDocuments(rows: unknown, cfg: DocumentFormOpenDocuments, currency: string): OpenDocument[];
|
|
106
|
+
/** Monto actual de cada documento, para pasar a modo manual sin perder lo repartido. */
|
|
107
|
+
export declare function manualFromAllocations(allocations: readonly Allocation[]): Record<string, number>;
|
|
108
|
+
/** Renglones del payload: solo los dos campos que declara la acción (`invoice_id`, `amount`). */
|
|
109
|
+
export declare function allocationPayload(allocations: readonly Allocation[], cfg: DocumentFormOpenDocuments): Array<Record<string, unknown>>;
|
|
110
|
+
/**
|
|
111
|
+
* Campo del encabezado con el monto recibido: `amount` si existe; si no, el
|
|
112
|
+
* primer campo numérico/moneda. Convención documentada (el schema no lo nombra).
|
|
113
|
+
*/
|
|
114
|
+
export declare function allocationAmountField(fields: readonly ActionFieldDef[]): string | undefined;
|
|
115
|
+
/** AllocationIssue → mensaje en español claro (con el número del documento). */
|
|
116
|
+
export declare function allocationIssueMessages(issues: readonly AllocationIssue[], docs: readonly OpenDocument[]): EditorIssue[];
|
|
117
|
+
/** Días de atraso de un documento abierto (0 si no está vencido o no tiene vencimiento). */
|
|
118
|
+
export declare function overdueDays(doc: Pick<OpenDocument, 'due_at'>, today?: Date): number;
|
|
119
|
+
//# sourceMappingURL=document-editor-model.d.ts.map
|