import type { DocumentGroup, ItemLayout, ActionVariant, HeaderLayoutConfig, LineColumnConfig, LineModalSectionConfig, FieldDescriptor, StateBadgeConfig, ValidationSeverity } from '../MasterDocument.types'; import type { SerialStatus } from '../adapter/MasterDocumentAdapter.types'; export interface FieldRef { field: string; } export interface NumberLiteral { value: number; } export interface StringLiteral { value: string; } export type ExpressionNode = ComputedExpression | FieldRef | NumberLiteral; export type ComputedExpression = { op: 'add'; args: ExpressionNode[]; } | { op: 'subtract'; args: [ExpressionNode, ExpressionNode]; } | { op: 'multiply'; args: ExpressionNode[]; } | { op: 'divide'; args: [ExpressionNode, ExpressionNode]; } | { op: 'round'; args: [ExpressionNode, NumberLiteral]; } | { op: 'coalesce'; args: ExpressionNode[]; } | { op: 'if'; condition: ConditionExpression; then: ExpressionNode; else: ExpressionNode; } | { op: 'custom'; ruleId: string; }; export type ConditionExpression = { op: 'eq'; left: ExpressionNode | StringLiteral; right: ExpressionNode | StringLiteral; } | { op: 'neq'; left: ExpressionNode | StringLiteral; right: ExpressionNode | StringLiteral; } | { op: 'gt' | 'gte' | 'lt' | 'lte'; left: ExpressionNode; right: ExpressionNode; } | { op: 'and'; conditions: ConditionExpression[]; } | { op: 'or'; conditions: ConditionExpression[]; } | { op: 'not'; condition: ConditionExpression; }; export interface SerializableComputedRule { targetField: string; watch: string[]; expression: ComputedExpression; } export interface SerializableTransitionConfig { from: string[]; to: string; label: string; icon?: string; style: ActionVariant; hasGuard?: boolean; shortcut?: string; requiresConfirmation?: boolean; confirmationMessage?: string; } export interface SerializableActionConfig { key: string; label: string; icon?: string; variant?: ActionVariant; visibleOn?: string[]; position?: 'actionBar' | 'moreMenu' | 'toolbar'; disabledOn?: string[]; requiresConfirmation?: boolean; confirmationMessage?: string; /** Permiso RBAC requerido. Sin él la acción se oculta. */ permissionVisibility?: string; } export interface ActionEndpointConfig { method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'; path: string; } /** Entry in `actionRegistry` — defines how each action is rendered and executed. */ export interface ActionRegistryEntry { label: string; icon?: string; variant?: ActionVariant; position?: 'toolbar' | 'moreMenu' | 'actionBar'; requiresConfirmation?: boolean; confirmationMessage?: string; /** Permiso RBAC requerido. Sin él la acción se oculta. */ permissionVisibility?: string; /** For transition actions: the target state key. */ transitionTo?: string; /** If true, the component calls validateEndpoint before executing. */ hasGuard?: boolean; endpoint?: ActionEndpointConfig; createEndpoint?: ActionEndpointConfig; validateEndpoint?: ActionEndpointConfig; /** * Si true, la acción se muestra siempre independientemente de lo que retorne * el endpoint de procesos disponibles (availableActionKeys). * Útil para acciones de consulta/navegación (ej: "Ver entregas") que no son * parte del flujo de trabajo pero siempre deben estar visibles. * Solo aplica cuando el documento ya existe (documentId presente). */ alwaysVisible?: boolean; /** * Estados del documento en los que la acción se muestra VISIBLE pero * DESHABILITADA, en lugar de oculta. Útil para comunicar que la acción * existe pero no está disponible en el estado actual (ej: "Dar por cumplida" * mientras el documento está en `RETAINED`), evitando que el usuario crea que * la acción desapareció. * * Cuando el `docState` actual está en esta lista, la acción se renderiza * deshabilitada aunque NO venga en `getAvailableActions`. En cualquier otro * estado, la acción se comporta como hoy (habilitada si está disponible, * oculta si no). Sin este campo el comportamiento es idéntico al actual. * * @see disabledTooltip */ disabledStates?: string[]; /** * Texto (o clave i18n) mostrado como tooltip (atributo nativo `title`) cuando * la acción está deshabilitada por `disabledStates`. Debe explicar por qué la * acción no está disponible y qué debe hacer el usuario para habilitarla. */ disabledTooltip?: string; /** * Determines how the action is executed. * - 'endpoint': calls the configured endpoint (default) * - 'transition': triggers a state transition * - 'modalList': opens a DocumentModalList modal (requires `modalList` config) * - 'createFromSource': opens the CreateFromSourceModal (requires * `template.createFromSource` config + the `createFromSource` field below). */ actionType?: 'endpoint' | 'transition' | 'modalList' | 'createFromSource'; /** Configuration for 'modalList' action type. */ modalList?: DocumentModalListConfig; /** * Configuración para `actionType: 'createFromSource'`. `mode: 'create'` (caso A, * desde el listado) crea un documento hijo nuevo en DRAFT; `mode: 'append'` * (caso B, desde el documento) agrega líneas cruzadas al DRAFT actual. */ createFromSource?: { mode: 'create' | 'append'; }; } /** Campo individual del formulario de una acción de línea (`actionType: 'formModal'`). */ export interface LineActionField { /** Clave del campo en el payload emitido. */ name: string; /** Etiqueta visible. */ label: string; /** Primitivo de form del kit usado para renderizar el campo. */ type: 'select' | 'textarea' | 'text' | 'number'; /** Si true, el modal exige un valor antes de permitir el submit. */ required?: boolean; /** Opciones para `type: 'select'`. */ options?: { value: string; label: string; }[]; /** Placeholder del control. */ placeholder?: string; } /** * Entrada del `lineActionRegistry` — describe una acción de línea ADICIONAL: cómo * se renderiza, cómo se gatea (sin asumir un `line.status` canónico) y cómo se * ejecuta. * * Gateo (D2), sin asumir estado de línea: * - `statusField` + `visibleStates`/`disabledStates` → contra `line[statusField]`. * - `visibleDocStates`/`disabledDocStates` → contra el `docState` del documento * (el cierre suele depender del estado del documento, p.ej. solo tras APPROVED). * - Sin ninguna declaración → la acción es visible y habilitada siempre. */ export interface LineActionEntry { label: string; icon?: string; variant?: ActionVariant; /** D2: campo de la línea contra el que se evalúa `visibleStates`/`disabledStates`. */ statusField?: string; /** Valores de `line[statusField]` en los que la acción es VISIBLE. */ visibleStates?: string[]; /** Valores de `line[statusField]` en los que la acción se muestra DESHABILITADA. */ disabledStates?: string[]; /** D2: `docState`s del documento en los que la acción es VISIBLE. */ visibleDocStates?: string[]; /** `docState`s del documento en los que la acción se muestra DESHABILITADA. */ disabledDocStates?: string[]; /** Permiso RBAC requerido. Sin él la acción se oculta (paridad con ActionRegistryEntry). */ permissionVisibility?: string; /** * Tipo de acción. Extensible a 'endpoint' | 'navigate'. * * - `'formModal'`: al clicar, el kit abre su modal de formulario genérico * (`LineActionFormModal`) con los `formModal.fields` y, tras el submit, * ejecuta la acción vía `adapter.executeLineAction` o el `endpoint` genérico. * - `'custom'`: al clicar, el kit NO abre ningún modal propio: delega * directamente en `adapter.executeLineAction(actionKey, lineId, {}, state)` * con `payload` vacío. El consumidor decide la UX (p.ej. abrir su propio * modal con un picker) dentro de ese override y devuelve el `ActionResult`. */ actionType: 'formModal' | 'custom'; /** * Configuración del modal de formulario. Requerido para `actionType: 'formModal'`; * ignorado para `actionType: 'custom'` (el consumidor aporta su propia UI). */ formModal?: { title: string; submitLabel?: string; fields: LineActionField[]; }; /** * Endpoint bespoke por documento que CIERRA el flujo. El `path` soporta los * placeholders `{id}`/`{documentId}` (documento) y `{lineId}` (línea). * * Es FUNCIONAL, no solo informativo: por defecto el kit lo ejecuta vía * `adapter.genericFetch(method, path, payload)` (sustituyendo los placeholders), * de modo que el caso simple no requiere código de adapter. Si el adapter * implementa `executeLineAction`, ese override toma precedencia y este endpoint * queda como fuente de verdad declarativa de la ruta. * * Opcional: para `actionType: 'custom'` no aplica, y para `formModal` es * innecesario si el adapter implementa `executeLineAction`. */ endpoint?: { method: string; path: string; }; } /** Nivel del documento desde el cual se abre el modal. */ export type DocumentModalListContext = 'document' | 'linesHeader' | 'line'; /** Modo de visualización de los registros en el modal. */ export type DocumentModalListDisplayMode = 'list' | 'card'; /** Configuración de endpoint para el modal list (data fetching, toolbar, menú). */ export interface DocumentModalListEndpointConfig { method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'; /** * Path del endpoint. Soporta interpolación de variables entre llaves. * Variables disponibles: {documentId}, {lineId}, {recordId} (en menú de contexto). * Ej: "/documents/{documentId}/deliveries" */ path: string; /** Parámetros estáticos adicionales enviados en el cuerpo o query. */ params?: Record; } /** Botón del toolbar del modal. */ export interface DocumentModalListButtonConfig { key: string; label: string; icon?: string; variant?: ActionVariant; /** Endpoint que se llama al hacer clic en el botón. */ endpoint: DocumentModalListEndpointConfig; /** Si true, recarga la lista principal después de la acción. */ reloadAfter?: boolean; requiresConfirmation?: boolean; confirmationMessage?: string; /** Si true, el botón se deshabilita mientras la acción está en progreso. */ disableWhileLoading?: boolean; } /** Ítem del menú de contexto de cada registro. */ export interface DocumentModalListMenuItemConfig { key: string; label: string; icon?: string; /** Si true, se muestra en rojo (acción destructiva). */ danger?: boolean; /** Si true, se dibuja un separador antes del ítem. */ separator?: boolean; /** Endpoint que se llama al seleccionar el ítem. {recordId} se reemplaza con el ID del registro. */ endpoint: DocumentModalListEndpointConfig; /** Si true, recarga la lista principal después de la acción. */ reloadAfter?: boolean; requiresConfirmation?: boolean; confirmationMessage?: string; } /** Columna para el modo de visualización 'list'. */ export interface DocumentModalListColumnConfig { key: string; /** Campo real en los datos del registro. Si omitido, se usa `key`. */ dataKey?: string; header: string; align?: 'left' | 'center' | 'right'; mono?: boolean; width?: string; format?: 'currency' | 'number' | 'date' | 'text'; /** Plantilla con variables entre llaves: "{codigo} — {nombre}" */ displayTemplate?: string; /** Mapa de valores → configuración visual de badge. */ badgeMap?: Record; } /** Campo individual dentro de una card. */ export interface DocumentModalListCardFieldConfig { key: string; /** Campo real en los datos del registro. Si omitido, se usa `key`. */ dataKey?: string; /** Etiqueta del campo. Si omitido, no se muestra label. */ label?: string; format?: 'currency' | 'number' | 'date' | 'text'; /** Si true, el valor se muestra en negrita. */ bold?: boolean; /** Si true, el valor se muestra con el color primario. */ accent?: boolean; } /** Configuración de la visualización en modo card. */ export interface DocumentModalListCardConfig { /** Campo usado como título principal de la card. */ titleField: string; /** Campo usado como subtítulo (opcional). */ subtitleField?: string; /** Campo cuyo valor se muestra como badge. */ badgeField?: string; /** Mapa de valores del badgeField → configuración visual. */ badgeMap?: Record; /** Campos adicionales mostrados en el cuerpo de la card. */ fields?: DocumentModalListCardFieldConfig[]; } /** * Configuración completa del modal de lista interna del documento. * Se define dentro de `ActionRegistryEntry.modalList` en el JSON template. * * @example * ```json * { * "actionRegistry": { * "ver_entregas": { * "label": "Ver entregas", * "icon": "DocumentIcon", * "variant": "secondary", * "position": "moreMenu", * "actionType": "modalList", * "modalList": { * "title": "Entregas del pedido", * "context": "document", * "displayMode": "list", * "dataEndpoint": { "method": "GET", "path": "/documents/{documentId}/deliveries" }, * "columns": [ * { "key": "number", "header": "N°", "mono": true }, * { "key": "date", "header": "Fecha", "format": "date" }, * { "key": "state", "header": "Estado", "badgeMap": { "PENDING": {...} } } * ], * "contextMenu": [ * { * "key": "cancel", * "label": "Anular", * "danger": true, * "endpoint": { "method": "POST", "path": "/deliveries/{recordId}/cancel" }, * "reloadAfter": true * } * ] * } * } * } * } * ``` */ export interface DocumentModalListConfig { /** Título mostrado en el header del modal. */ title: string; /** * Si true, el modal se abre maximizado (pantalla completa). * Si false (default), ocupa el 75% de la pantalla. */ maximized?: boolean; /** * Modo de visualización de los registros. * - 'list': tabla con columnas (default) * - 'card': grilla de cards */ displayMode?: DocumentModalListDisplayMode; /** * Nivel del documento desde el cual se abre el modal. * Determina qué variables de contexto están disponibles para interpolación. * - 'document': cabecera del documento (provee {documentId}) * - 'linesHeader': cabecera de la sección de líneas (provee {documentId}) * - 'line': una línea específica (provee {documentId} y {lineId}) */ context: DocumentModalListContext; /** * Nombre del campo en cada registro que contiene el ID único. * Se usa para reemplazar {recordId} en los endpoints del menú de contexto. * Default: 'id' */ recordIdField?: string; /** Endpoint para cargar los datos del listado principal. */ dataEndpoint: DocumentModalListEndpointConfig; /** Configuración del toolbar de botones. */ toolbar?: { buttons: DocumentModalListButtonConfig[]; }; /** Configuración de columnas para displayMode: 'list'. */ columns?: DocumentModalListColumnConfig[]; /** Configuración de cards para displayMode: 'card'. */ card?: DocumentModalListCardConfig; /** Ítems del menú de contexto de cada registro. */ contextMenu?: DocumentModalListMenuItemConfig[]; /** Mensaje mostrado cuando no hay registros. */ emptyMessage?: string; /** Tamaño de página para paginación del listado. Default: 20. */ pageSize?: number; } /** Configures the DocumentProcess endpoint that returns the action whitelist. */ export interface ActionsSourceConfig { /** Endpoint path — e.g. "api/v1/documents/{id}/processes" */ endpoint: string; /** When true the component appends ?docState=X to the request. */ sendDocState?: boolean; /** Action keys shown when the endpoint is unavailable. */ fallbackKeys?: string[]; } export interface ContextStripMapping { label: string; field: string; mono?: boolean; accent?: boolean; } export interface SearcherFieldConfig { entity: string; displayFields: string[]; searchFields?: string[]; displayTemplate?: string; /** Campo del registro que se usa como valor (ID). Default: 'Id' */ valueField?: string; /** Mapeo de campos del registro seleccionado a campos del formulario. * Ej: { "item_name": "nombre", "udm": "udm" } → al seleccionar un producto, * se auto-llenan item_name y udm con los valores del registro. */ bindFields?: Record; filters?: Record; filterDependencies?: Record; pageSize?: number; } /** * Define cómo calcular un campo de totales a partir de las líneas del documento. * Las aggregations se evalúan en orden; las últimas pueden referenciar las anteriores. */ export type TotalsAggregation = { op: 'sum'; field: string; } | { op: 'sumProduct'; fields: string[]; } | { op: 'sumDiscount'; qtyField: string; priceField: string; discountField: string; } | { op: 'diff'; a: string; b: string; } | { op: 'add'; fields: string[]; } | { op: 'ref'; field: string; multiplier?: number; }; export interface TotalsWidgetConfig { grandTotalField: string; grandTotalLabel: string; subtitleTemplate?: string; /** * Agrega campos calculados a partir de state.lines antes de renderizar. * Solo tiene efecto cuando la sección usa dataSource: "documentState". * El orden de las claves determina el orden de evaluación. */ aggregations?: Record; kpis: Array<{ label: string; field: string; color?: string; negated?: boolean; }>; breakdown: Array<{ label: string; field: string; color?: string; isSummary?: boolean; negated?: boolean; }>; } export interface KeyValueWidgetConfig { fields: Array<{ label: string; field: string; format?: 'currency' | 'date' | 'text' | 'number' | 'percentage'; }>; } export interface ProgressBarWidgetConfig { label: string; valueField: string; maxField: string; format?: 'currency' | 'number' | 'percentage'; color?: string; } export type FactBoxWidgetType = { type: 'totals'; config: TotalsWidgetConfig; } | { type: 'cfde'; } | { type: 'paymentTerms'; } | { type: 'financial'; } | { type: 'keyValue'; config: KeyValueWidgetConfig; } | { type: 'progressBar'; config: ProgressBarWidgetConfig; } /** * Muestra el historial de transiciones de estado (OP1 — document_status_history). * Los datos se cargan via `adapter.getStatusHistory(documentId)`. * Solo visible cuando el documento ya existe (documentId presente). */ | { type: 'statusHistory'; } | { type: 'custom'; widgetKey: string; }; export interface FactBoxSectionTemplate { key: string; label: string; defaultOpen: boolean; widget: FactBoxWidgetType; dataSource: 'adapter' | 'documentState'; visibleOn?: string[]; /** Condición sobre headerValues para mostrar la sección. */ visibleWhen?: import('../fieldCondition').FieldCondition; /** Condición sobre headerValues para ocultar la sección. */ hiddenWhen?: import('../fieldCondition').FieldCondition; /** Permiso RBAC requerido. Sin él la sección se oculta. */ permissionVisibility?: string; badgeField?: string; badgeMap?: Record; } export interface ApprovalTemplateConfig { /** v1.1 key — states in which the approval panel is shown. */ approvalStates?: string[]; /** v1.0 legacy alias — same meaning as approvalStates. */ states?: string[]; message: string; approveLabel?: string; rejectLabel?: string; } export interface CrossingLineActionsConfig { close?: boolean; reopen?: boolean; } export interface CrossingTraceabilityConfig { enabled: boolean; label?: string; } export interface CrossingGenerateField { key: string; label: string; type: 'text' | 'date' | 'number'; required?: boolean; } /** * Textos (i18n) sobreescribibles del panel de cruce. Permiten que la app * consumidora inyecte sus traducciones / textos PRD-verbatim sin tocar el kit. * Los defaults están en español. `quantityExceedsPending` admite los * placeholders `{pending}` y `{uom}`. */ export interface CrossingMessagesConfig { quantityRequired?: string; quantityPositive?: string; quantityExceedsPending?: string; terminalLineNotSelectable?: string; } export interface CrossingTemplateConfig { enabled: boolean; /** States in which child document generation is allowed. */ generateOn: string[]; allowBulkConvert?: boolean; lineActions?: CrossingLineActionsConfig; traceability?: CrossingTraceabilityConfig; generateHeaderFields?: CrossingGenerateField[]; messages?: CrossingMessagesConfig; } /** * Config del motor de cruce WHOLE-DOCUMENT (Épica 105-5). Independiente de * `crossing` (per-línea, FEAT-094 94-09/94-10): activa la selección de * documentos ENTEROS (ECD/DCD), el subtotal `Σ(ECD)−Σ(DCD)`, "Desvincular" por * documento y el widget FactBox "Documentos origen". NO debe activarse junto al * motor per-línea para el mismo documento. */ export interface CrossDocumentsTemplateConfig { enabled: boolean; /** Estados en los que el panel es EDITABLE (selección + cruzar + desvincular). En el resto: histórico read-only. */ crossOn: string[]; /** * Permiso requerido para editar (p. ej. `CROSS_FROM_SOURCE_DOCUMENT`). El kit * solo declara la clave; el consumidor resuelve si el usuario la tiene y la * pasa como `canCross` al montar el hook. */ requiresPermission?: string; /** Etiqueta de la sección/FactBox. Default: "Documentos origen". */ factBoxLabel?: string; /** * Override de rutas canónicas del kit. El consumidor normalmente remapea vía * `adapter.genericFetch`; estos overrides son el escape hatch. `{id}` se * sustituye por el documentId. */ routes?: { available?: string; crossed?: string; cross?: string; uncross?: string; }; messages?: import('../MasterDocument.types').CrossDocumentsMessagesConfig; } /** Campo del paso "datos por defecto" (`defaultsStep`). Valores aplicados como default por línea, editables luego. */ export interface CreateFromSourceField { key: string; label: string; type: 'text' | 'number' | 'date' | 'select'; /** Opciones estáticas para `type: 'select'`. */ options?: Array<{ label: string; value: string; }>; /** * Entidad de catálogo para `type: 'select'` cuando las opciones se cargan del * backend vía `adapter.getSelectOptions(entity, companyId, operationCenterId)`. * Tiene precedencia sobre `options` si el adapter la implementa. */ optionsEntity?: string; placeholder?: string; required?: boolean; } /** Columna de la tabla de selección de líneas. `key` lee el campo homónimo de `PendingSourceLine` (o su `extra`). */ export interface CreateFromSourceLineColumn { key: string; header: string; align?: 'left' | 'center' | 'right'; /** Formato de presentación. `number`/`currency` usan `formatNumber`/`formatCurrency`. */ format?: 'text' | 'number' | 'currency' | 'date'; /** Si true, la columna se OCULTA (no colapsa) sin `lineSelection.financialPermission`. */ financial?: boolean; mono?: boolean; } /** Un efecto listado en el paso de confirmación. Solo presentación (i18n en `label`). */ export interface CreateFromSourceEffectPreview { key: string; label: string; description?: string; } /** * Config de la capacidad "crear/continuar desde origen". * @see CreateFromSourceField, CreateFromSourceLineColumn */ export interface CreateFromSourceTemplateConfig { /** `child_class` de los endpoints de cruce (p.ej. 'PURCHASE_RETURN'). */ childClass: string; /** Naturaleza del tercero, para etiquetar/enrutar el filtro (proveedor vs cliente). */ thirdPartyKind: 'supplier' | 'customer'; /** * Clave de `headerValues` que porta el ID del tercero. En modo `append` (caso B) * el kit la lee para filtrar los orígenes por el tercero del documento y llega * read-only (BV-X06). Opcional; si se omite, no se pre-filtra por tercero. */ thirdPartyHeaderField?: string; /** * Claves que deben coincidir entre orígenes para poder combinarlos (BV-X04). * Se comparan contra el PRIMER origen seleccionado; los divergentes se * deshabilitan mostrando el detalle de la diferencia. Ej: * `['supplierId','supplierBranchId','currencyId']`. */ compatibilityKeys?: string[]; /** Estados del documento origen elegibles (BV-X02). Ej: `['APPROVED','POSTED','INVOICED']`. */ eligibleSourceStates?: string[]; /** Multi-selección: permiso para combinar >1 origen (ausente ⇒ solo 1). */ multiSelect?: { permission?: string; maxSources?: number; }; /** * Camino de captura libre (sin origen). Si se define, el modo `create` muestra * un paso 0 "selector de origen" (from-source vs captura libre). La opción * libre navega a `route` y se deshabilita si el usuario no tiene `permission`. */ allowFreeCapture?: { permission: string; route: string; }; /** Paso de "datos por defecto" (p.ej. motivo). Aplicados como default por línea, EDITABLES luego. */ defaultsStep?: { titleKey?: string; fields: CreateFromSourceField[]; }; /** * Campos del ENCABEZADO del documento hijo capturados al crear (caso A), p.ej. * `referenceNumber`, `notes`. Config-driven: sustituyen a los campos quemados * "Referencia"/"Notas". Cada `key` viaja en `request.header[key]`; los alias * `referenceNumber`/`notes` se mapean también a sus campos nombrados. Solo se * muestran en modo `create`. */ createHeaderFields?: CreateFromSourceField[]; /** Selección de líneas. */ lineSelection: { /** Campo del payload que porta la cantidad a cruzar. Default `quantityToCross`. */ quantityField?: string; /** Campo de `PendingSourceLine` con la cantidad pendiente (tope ≤, BV-X03). Default `quantityPending`. */ pendingField?: string; columns: CreateFromSourceLineColumn[]; /** Permiso para ver columnas `financial` (P15, hide-not-collapse). */ financialPermission?: string; maxLines?: number; }; /** Paso opcional de confirmación de efectos antes de persistir. */ confirmationStep?: { enabled: boolean; effects?: CreateFromSourceEffectPreview[]; }; /** Textos sobreescribibles (i18n). Todos opcionales; el kit trae defaults en español. */ i18n?: { newButton?: string; originSelectorTitle?: string; fromSourceOption?: string; freeCaptureOption?: string; step1Title?: string; step2Title?: string; confirmTitle?: string; emptyState?: string; incompatibleReason?: string; quantityRequired?: string; quantityPositive?: string; quantityExceedsPending?: string; backButton?: string; cancelButton?: string; nextButton?: string; confirmButton?: string; loadingSources?: string; loadingLines?: string; emptyLines?: string; quantityColumn?: string; singleSelectHint?: string; /** Encabezados de la tabla de documentos origen. */ sourceColumns?: { document?: string; thirdParty?: string; date?: string; state?: string; total?: string; }; /** Resumen del paso de confirmación. Placeholders `{lines}` y `{sources}`. */ confirmSummary?: string; }; } export interface DocumentListColumnTemplate { key: string; /** Campo real en las filas devueltas por listDocuments. Si omitido, se usa `key`. */ dataKey?: string; header: string; align?: 'left' | 'center' | 'right'; mono?: boolean; width?: string; format?: 'currency' | 'number' | 'date' | 'text'; displayTemplate?: string; badgeMap?: Record; /** * Embudo de filtro en el encabezado de ESTA columna. Ausente ⇒ la columna no * ofrece embudo (comportamiento previo). */ filter?: DocumentListColumnFilterTemplate; } /** * Filtro por columna de la grilla del listado. * * `'lookup'` es el caso de una columna FORÁNEA: el embudo despliega el mismo * searcher que el formulario del documento y filtra por el id del registro * elegido. Una columna así muestra un nombre pero se filtra por un id, y por eso * `field` casi siempre difiere de la key de la columna. */ export interface DocumentListColumnFilterTemplate { type: 'text' | 'date' | 'select' | 'lookup'; /** Campo que espera el backend. Default: `dataKey ?? key` de la columna. */ field?: string; options?: Array<{ label: string; value: string | number; }>; placeholder?: string; /** Config del searcher para `type: 'lookup'`, declarada inline. */ searcher?: SearcherFieldConfig; /** * Key dentro de `template.searchers` a reusar. Resolución: `searcher` → * `searchers[searcherKey]` → `searchers[field]` → `searchers[key de la columna]`. */ searcherKey?: string; } export interface DocumentListFilterTemplate { key: string; label: string; type: 'text' | 'date' | 'select'; options?: Array<{ label: string; value: string | number; }>; placeholder?: string; } export interface DocumentListPillTemplate { key: string; label: string; stateFilter?: string; bg?: string; color?: string; } export interface DocumentListTemplateConfig { title: string; subtitle?: string; createNewLabel?: string; /** * Permiso RBAC requerido para que el listado ofrezca la acción de crear. * * Cuando se declara y el usuario NO tiene el permiso, el botón "+ Nuevo" * queda **ausente** (no deshabilitado): una acción que el usuario no puede * ejecutar no debe ofrecerse. Aplica por igual al wizard de documento en * blanco y al flujo `createFromSource`, porque lo que se gatea es el * afordance de crear, no la ruta que toma. * * Ausente ⇒ el botón se muestra siempre (comportamiento sin cambios). * * Sigue la convención de permisos del kit: si no hay sistema de permisos * activo (`userPermissions === null`, es decir sin `userPermissions` en el * ``), el botón se muestra — el fallback es * permisivo, igual que `permissionVisibility` en el `actionRegistry`. * * Caso de uso: una pantalla que unifica varias clases de una misma familia * documental, donde solo algunas se crean a mano y las demás nacen de un * proceso de generación. */ createPermission?: string; /** * ¿Esta clase de documento se crea a mano? * * `false` deja el botón "+ Nuevo" **ausente** del DOM para todo el mundo, sin * importar los permisos: hay clases que nunca nacen de una acción del usuario * sino de un proceso de generación, y para ellas no existe permiso de crear * que declarar. Ausente ⇒ `true` (comportamiento sin cambios). * * Es ortogonal a `createPermission` y se evalúan en conjunto: el afordance se * ofrece solo si la clase se crea a mano **y** el usuario tiene el permiso. * `canCreate: false` gana siempre — no es una condición de autorización sino * de existencia, así que ningún permiso la revierte. * * | `canCreate` | `createPermission` | Resultado | * |---|---|---| * | ausente/`true` | ausente | se ofrece (retrocompat) | * | ausente/`true` | declarado | se ofrece solo con el permiso | * | `false` | cualquiera | **ausente siempre** | */ canCreate?: boolean; columns: DocumentListColumnTemplate[]; filters: DocumentListFilterTemplate[]; pills: DocumentListPillTemplate[]; defaultPageSize?: number; /** * Búsqueda básica: la caja visible de la barra del listado, con el embudo del * panel avanzado dentro — la disposición de MasterCrud. * * Es **opt-in a propósito**. Lo que teclea el usuario viaja como * `request.search` del `DocumentListRequest`, y ese campo lo mapea el adapter * de cada superficie: hay adapters que lo traducen (a `consecutive`, a * `supplierReferenceNumber`…) y otros que lo ignoran. Encenderla en una * superficie cuyo adapter no lo mapea pintaría una caja que no filtra nada y * no avisa, así que la decisión es de quien conoce el adapter —la plantilla— * y no del kit. * * Ausente ⇒ no se pinta la caja, y el panel avanzado se abre desde un botón * de embudo propio en la barra (comportamiento sin cambios en la práctica). */ quickSearch?: { /** Ausente ⇒ `true` cuando el objeto se declara. */ enabled?: boolean; /** Ausente ⇒ el rótulo genérico «Buscar». */ placeholder?: string; }; /** * Campo de cada fila que contiene el estado del documento (`docState`), * usado para gatear las acciones de fila (DEF-4: Borrar en DRAFT, Anular en * estados posteriores). * * Si se omite, el contenedor smart intenta `docState`, luego `state` y por * último `estado`. Declararlo explícitamente evita ambigüedad cuando la fila * usa otra clave. */ rowStateField?: string; /** * Campo de cada fila que contiene el token de concurrencia optimista * (`row_version` de la cabecera). Por defecto `"rowVersion"`. * * Al disparar una acción de fila desde el listado (Anular/Aprobar/…), el kit * propaga este valor al `DocumentState` que entrega a `executeAction`, usando * las MISMAS claves que el flujo de detalle (`serverVersion` y * `headerValues[versionKeys.header]`, por defecto `_rowVersion`). Así el * backend detecta el conflicto (409 `OPTIMISTIC_LOCK_VIOLATION`) sin que el * consumidor tenga que re-leer el documento antes de la transición. * * Es la contraparte, a nivel de transición desde la fila, del row_version de * cabecera que ya viaja en `saveLine`/`deleteLine`. * * El valor se propaga como STRING (`serverVersion: String(token)` y * `headerValues[_rowVersion]: String(token)`), idéntico al flujo de detalle del * kit (que también usa string en `executeTransition`/`saveLine`/`deleteLine`). * * Si la fila no expone este campo, el token queda `undefined` (NO se fuerza 0), * para que el adapter distinga "no hay token" de "token = 0" y se preserve la * retrocompatibilidad con documentos que no lo configuren. * * CONTRATO PARA LA APP: cuando se quiere detección de concurrencia, el listado * (`listDocuments`) debe devolver el `row_version` de cada fila en este campo, y * el `executeAction` del adapter debe leer `documentState.serverVersion` (o * `headerValues._rowVersion`) y enviarlo SIN colapsar `undefined` a `0` * (`?? 0`). Es decir: el token debe ser NULLABLE de punta a punta; mandar `0` * cuando no hay token reintroduce el 409 falso que este mecanismo evita. */ concurrencyTokenField?: string; } export interface WizardStepTemplate { key: string; label: string; entity: string; displayFields: string[]; } export interface WizardTemplateConfig { steps: WizardStepTemplate[]; } export interface CurrencyConfig { /** Campo en headerValues que contiene el código de moneda del documento */ codeField: string; /** Campo en headerValues que contiene la tasa de cambio */ rateField?: string; /** Etiqueta base (ej: 'TRM') */ rateLabel?: string; /** Si true, permite al usuario cambiar la moneda vía adapter.changeCurrency() */ editable?: boolean; /** * Código de moneda que se muestra cuando `codeField` está vacío. * Garantiza que el CurrencyPanel sea siempre visible en el editor, * incluso antes de que el backend resuelva la moneda del documento. * Ej: 'COP', 'USD' */ defaultCode?: string; } export interface LineModalWidgetConfig { /** Identificador único — se pasa al adapter.getLineWidgetData. */ widgetKey: string; /** Título del panel en el sidebar. */ title: string; /** Campos del formulario que disparan la recarga del widget. */ watchFields: string[]; /** Milisegundos de debounce tras el último cambio. Default: 300. */ debounceMs?: number; /** Condición (motor G2) para mostrar el widget. */ visibleWhen?: import('../fieldCondition').FieldCondition; /** Condición (motor G2) para ocultar el widget. */ hiddenWhen?: import('../fieldCondition').FieldCondition; } /** * Modo de manejo de seriales del documento. * - `capture`: documentos de ENTRADA (`NatureStock = Add`). El operador ingresa * los seriales uno a uno (teclado o escáner HID). Los seriales aún no existen * en el destino o vienen de estado `Unavailable`. * - `select`: documentos de SALIDA (`NatureStock = Subtract`). El operador * selecciona seriales que ya existen en inventario en estado `Available`. * - `none`: sin manejo de seriales (equivale a omitir `serialSupport`). */ export type SerialMode = 'capture' | 'select' | 'none'; /** * Activa el `SerialPanel` nativo del kit dentro del modal de línea. * Se declara en `layout.detail.serialSupport`. * * El panel solo se muestra cuando el campo `triggerField` del form state de la * línea es `true` (poblado típicamente vía `bindFields` desde el searcher de * ítem, p.ej. `{ "_itemIsSerialized": "isSerialized" }`). El kit gestiona un * campo virtual `serial_ids: string[]` en el form state y lo inyecta en el * payload de `saveLine`. La cantidad requerida (`M`) se lee de `quantityField`. * * @example * ```json * "detail": { * "serialSupport": { * "mode": "capture", * "triggerField": "_itemIsSerialized", * "quantityField": "quantity_base" * } * } * ``` */ export interface SerialSupportConfig { /** `capture` para entradas, `select` para salidas, `none` para desactivar. */ mode: SerialMode; /** * Campo booleano del form state de la línea que activa el panel cuando es * `true`. Default: `_itemIsSerialized`. */ triggerField?: string; /** * Campo numérico del form state que define la cantidad de seriales requerida * (`M`). El botón Guardar permanece bloqueado hasta que `serial_ids.length` * iguale a este valor. Default: `quantity_base`. */ quantityField?: string; /** * Clave del campo virtual donde el kit acumula los IDs de seriales y que viaja * en el payload de `saveLine`. Default: `serial_ids`. */ serialIdsField?: string; /** * Campo del form state que contiene el ID de la extensión de ítem * (`itemExtensionId`). El kit lo envía al adapter en `validateSerial` / * `fetchAvailableSerials` y, además, lo usa para detectar el CAMBIO de ítem: * cuando su valor cambia, el kit limpia `serial_ids`. Default: `item_extension_id`. */ itemField?: string; /** * Campo del form state con el ID de la bodega/almacén origen (`storageId`), * enviado al adapter cuando está presente. Default: `storage_id`. */ storageField?: string; /** * Campo del form state con el ID del lote (`lotId`), enviado al adapter cuando * está presente. Default: `lot_id`. */ lotField?: string; /** * Modo `select`. Estados que, al MARCAR una fila, exigen una confirmación Sí/No * antes de agregar el serial a la selección. * * **Ausente o vacío ⇒ comportamiento idéntico al actual** (nunca se confirma). * * Reglas del guard (el orden importa): * - DESmarcar nunca confirma — ni desde la fila ni desde el chip del resumen. * - El tope de sobre-selección (`n >= required`) se evalúa ANTES: no tiene * sentido preguntar por un serial que de todos modos sería rechazado. * - Cancelar no muta nada: ni la selección ni el mensaje de error visible. * * @example Confirmar solo las unidades que ya tuvieron movimientos: * ```ts * serialSupport: { * mode: 'select', * triggerField: 'item_tracks_serials', * confirmOnStatuses: ['Unavailable'], * } * ``` */ confirmOnStatuses?: SerialStatus[]; /** * Clave i18n del mensaje de esa confirmación. Recibe la variable `{serialCode}`. * Default: `serial_panel.confirm_selection`. * * El texto del catálogo debe usar exactamente el token `{serialCode}`: el * factory de i18n solo sustituye las variables que el call site pasa, así que * cualquier otro token se renderiza literal en pantalla. */ confirmMessageKey?: string; } export interface ServerRuleConfig { /** Identificador único de la regla — se pasa al adapter.evaluateServerRule. */ ruleId: string; /** Campos que disparan la evaluación cuando el usuario los modifica. */ triggerFields: string[]; /** Campos que se actualizan con el resultado del adapter. */ targetFields: string[]; /** * Cuándo ejecutar la regla. * - 'change' (default): solo cuando el usuario modifica un triggerField. * - 'always': también al inicializar el formulario (útil para precarga en edición). */ triggerOn?: 'change' | 'always'; /** Milisegundos de debounce tras el último cambio. Default: 300. */ debounceMs?: number; /** Condición (motor G2) que debe cumplirse para ejecutar la llamada. */ condition?: import('../fieldCondition').FieldCondition; } export interface DocumentRuleConfig { key: string; message: string; severity: ValidationSeverity; condition: ConditionExpression; } /** * Tipo de pestaña del área de detalle. * * - `lines` — Tipo 1: la grilla nativa (`LinesSection`) tal cual está hoy. * - `flexQuery` — Tipo 2: consulta del host, de solo lectura, con las acciones * de fila declarativas del `lineActionRegistry`. * - `render` — Tipo 3: componente libre del host. */ export type DetailTabType = 'lines' | 'flexQuery' | 'render'; /** Campos comunes a las tres variantes de pestaña. */ export interface DetailTabBase { /** Identificador único de la pestaña dentro del documento. */ key: string; /** * Etiqueta visible, cruda. Mismo criterio que `LineActionEntry.label` y * `layout.detail.title`: es dato del host, el kit no la traduce. */ label?: string; /** * Clave del catálogo i18n. Tiene prioridad sobre `label` cuando resuelve a un * texto distinto de la propia clave. Sin `label` ni `labelKey`, una pestaña * `lines` cae a `master_document.lines.tab`. */ labelKey?: string; /** Nombre del icono en el registro del provider (`iconRegistry`). */ icon?: string; /** Permiso RBAC requerido. Sin él la pestaña se oculta. */ permissionVisibility?: string; /** Condición (motor G2) sobre `headerValues` para mostrar la pestaña. */ visibleWhen?: import('../fieldCondition').FieldCondition; /** Condición (motor G2) sobre `headerValues` para ocultar la pestaña. */ hiddenWhen?: import('../fieldCondition').FieldCondition; } /** * Tipo 1 — la grilla nativa de líneas. `type` ausente ⇒ `'lines'`, de modo que * una pestaña sin tipo declarado es siempre la de siempre. * * Es el único tipo con CRUD (agregar / editar / duplicar / borrar, seriales y * concurrencia `row_version`). No se oculta por `permissionVisibility` cuando es * la última pestaña visible: el documento no puede quedarse sin sus líneas. */ export interface DetailTabLines extends DetailTabBase { type?: 'lines'; } /** * Operador de comparación de un filtro externo. * * Es el mismo conjunto cerrado que `FlexFilterOp` del paquete * `@SiesaTeams/flex-query` (npm 0.4.0). Se declara aquí en vez de importarse a * propósito: el kit **no depende** de FlexQuery —de hecho no puede, ver * `DetailTabFlexQuery`— y este tipo solo describe la forma del dato que viaja. */ export type DetailTabFilterOp = 'eq' | 'neq' | 'gt' | 'gte' | 'lt' | 'lte' | 'contains' | 'startsWith' | 'endsWith' | 'isNull' | 'isNotNull'; /** * Filtro externo declarado por el template y resuelto por el kit contra el * documento. Se entrega al renderer en `ctx.externalFilters`, con la forma exacta * que espera la prop `externalFilters` de ``. * * «Externo» es el término de FlexQuery: el usuario no los ve ni los puede editar * dentro de la consulta. Es lo correcto aquí — una pestaña de líneas de ESTE * documento no debe dejar que el usuario borre el filtro que la acota al * documento. */ export interface DetailTabExternalFilter { /** * Columna del modelo EF del backend. **No** necesita estar declarada en la * definición del reporte y admite navegación con puntos * (`'ciudad.id_region'`), porque FlexQuery valida los filtros externos contra * el modelo, no contra el reporte. */ column: string; /** Operador de comparación. Ausente ⇒ `'eq'`. */ op?: DetailTabFilterOp; /** * Campo de `headerValues` del que se toma el valor. El pseudo-campo * `'_documentId'` resuelve al id del documento, que es el filtro que casi * siempre se quiere. Si el campo no existe o vale `null`/`undefined`, el filtro * **no viaja**: un filtro sin valor no es un filtro. */ headerField?: string; /** * Valor literal, para condiciones fijas del template (`{ column: 'anulada', * op: 'eq', value: false }`). `headerField` tiene prioridad si ambos están. * Los operadores `isNull`/`isNotNull` no necesitan ninguno de los dos. */ value?: unknown; } /** Modo de vista inicial de la consulta. Es la prop `viewMode` de ``. */ export type DetailTabViewMode = 'flat' | 'tree' | 'pivot'; /** Cómo se dibuja la matriz de pivot. Es `PivotView` de FlexQuery. */ export type DetailTabPivotView = 'table' | 'bar' | 'stackedBar' | 'line' | 'donut'; /** Función de agregación de una métrica de pivot. Es `FlexAggregateFn`. */ export type DetailTabAggregateFn = 'sum' | 'avg' | 'min' | 'max' | 'count' | 'countDistinct'; /** * Métrica de una celda del pivot. Mismos nombres que `FlexPivotValueField`. */ export interface DetailTabPivotValue { /** Columna del reporte; debe ser numérica y agregable. */ column: string; /** Función de agregación. Ausente ⇒ `'sum'`. */ fn?: DetailTabAggregateFn; /** Etiqueta propia; ausente ⇒ la del reporte. */ label?: string; } /** * Configuración de pivot precargada. Es la prop `defaultPivotConfig` de * `` y usa **sus mismos nombres de campo** (`rowFields`, * `columnFields`, `valueFields`…) para que el host la pase tal cual sin traducir. * * Ojo con lo que significa «default»: FlexQuery la trata como **semilla** del * estado inicial, no como override reactivo. Cambiarla después de montar no tiene * efecto, y el usuario puede reconfigurar el pivot desde el toolbar y guardarlo * como vista. El template propone; el usuario dispone. */ export interface DetailTabPivotConfig { /** Campos que forman las filas (eje Y). */ rowFields: string[]; /** Campos que forman las columnas (eje X). */ columnFields: string[]; /** Métricas calculadas en cada intersección. */ valueFields: DetailTabPivotValue[]; /** Subtotales por nivel de fila. Ausente ⇒ true en FlexQuery. */ showSubtotals?: boolean; /** Fila de gran total al pie. Ausente ⇒ true en FlexQuery. */ showGrandTotal?: boolean; /** Orden global de los valores de columna. */ columnSort?: 'asc' | 'desc'; /** Cómo se dibuja: tabla cruzada o gráfica. Ausente ⇒ `'table'`. */ view?: DetailTabPivotView; /** Título editorial de la gráfica (solo cuando `view !== 'table'`). */ title?: string; } /** * Opciones de presentación y de vista de la consulta. Son un subconjunto tipado * de las props de ``: se declaran aquí para que el template las lleve * con nombre y documentación, en vez de esconderlas en el saco sin tipar de * `params`, que es donde acabarían si no. */ export interface DetailTabFlexQueryView { /** Título en el toolbar de la consulta. Sin él, el bloque no se dibuja. */ reportTitle?: string; /** Eyebrow de contexto encima del título (p.ej. `'Consulta · Compras'`). */ reportModule?: string; /** Arrancar con el toolbar contraído. Útil dentro de una pestaña estrecha. */ defaultCollapsed?: boolean; /** Ocultar el título en el toolbar; sigue usándose en los archivos exportados. */ hideTitleInToolbar?: boolean; /** * Modo inicial. `'tree'` exige que la definición del reporte declare su * `FlexTreeConfig` (`parentKey`/`parentIdKey`); el template no puede suplirlo. */ viewMode?: DetailTabViewMode; /** Configuración de pivot. Solo se usa cuando `viewMode === 'pivot'`. */ pivot?: DetailTabPivotConfig; /** * `'paged'` pide página a página; `'virtual'` trae todo y virtualiza, y mueve * orden, agrupamiento y agregación al cliente. Ausente ⇒ lo que diga el reporte. */ paginationMode?: 'paged' | 'virtual'; /** * `false` monta la consulta en reposo: dibuja el chrome pero no pide datos * hasta que el usuario lo pida. Es lo correcto para consultas caras o con * filtros obligatorios que el documento no puede resolver solo. */ autoLoad?: boolean; /** Columna de selección. Qué hacer con la selección es cosa del host. */ enableRowSelection?: boolean; /** * Techos de sobrecarga del motor de pivot y de la gráfica. Se pasan tal cual a * `maxPivotRows` / `maxPivotColumns` / `maxPivotCells` / `maxChartCategories`. * Ausentes ⇒ los defaults de FlexQuery (100 000 / 500 / 2 000 000 / 60). */ limits?: { maxPivotRows?: number; maxPivotColumns?: number; maxPivotCells?: number; maxChartCategories?: number; }; } /** * Qué hace el clic en una fila de la consulta. * * - `'none'` (por defecto): nada. La consulta es de lectura y no navega. * - `'openLineDetail'`: el kit abre esa línea en su modal de solo lectura, con * las secciones y los campos que el template ya declara para la línea. Requiere * que la fila traiga el `id` de la línea del documento. */ export type DetailTabRowClick = 'none' | 'openLineDetail'; /** * Tipo 2 — listado en FlexQuery. La consulta la renderiza el HOST: el kit no * conoce el componente, solo transporta el descriptor y monta el renderer * registrado en `MasterDocumentProvider.lineTabRenderers[renderKey]`. * * **Por qué el kit no puede importar FlexQuery.** `@SiesaTeams/flex-query` * declara `siesa-ui-kit` entre sus dependencias —construye toda su UI con este * kit—, así que una dependencia en sentido contrario cerraría un ciclo * `kit → flex-query → kit`. La inyección por registro no es una preferencia de * diseño: es la única forma que no rompe el grafo de paquetes. * * **Es de solo lectura.** No escribe en `state.lines`, así que no alimenta * `lineTotals`, `hasLines` ni los totales del FactBox: esas derivaciones siguen * colgando de la grilla nativa. Las mutaciones van por `rowActions`, que las * construye el kit desde `lineActionRegistry` con su gateo por estado y permisos * — el host no debe mutar por su cuenta, o se salta el round-trip de * `row_version` que el kit garantiza. * * @see toFlexRowActions — mapea `RowAction[]` del kit a `FlexRowAction[]` */ export interface DetailTabFlexQuery extends DetailTabBase { type: 'flexQuery'; /** Clave del renderer registrado en `MasterDocumentProvider.lineTabRenderers`. */ renderKey: string; /** * Descriptor de la consulta, alineado con las props de `` * (`@SiesaTeams/flex-query` npm 0.4.0). El kit resuelve `externalFilters` * contra el documento y pasa el resto sin interpretar. */ flexQuery: { /** * Código del reporte, en snake_case. Debe coincidir con el campo `code` de * `flex_factory_report_definitions` en el backend del microservicio. Es la * prop `reportCode` de ``. */ reportCode: string; /** * URL base del API FlexQuery (prop `contractBaseUrl`). Omitirla es lo normal: * el renderer del host conoce su propio API. Declararla sirve para apuntar * una pestaña a OTRO microservicio. */ contractBaseUrl?: string; /** * Namespace i18next del host para las etiquetas de columna (prop * `labelNamespace`). Requiere que el host tenga `I18nextProvider` en el árbol * con ese namespace cargado; sin él FlexQuery muestra el `labelKey` crudo. */ labelNamespace?: string; /** * Filtros que acotan la consulta al documento. Se resuelven contra * `headerValues` y llegan al renderer listos para la prop `externalFilters`. * * @example * ```ts * externalFilters: [ * { column: 'document_id', headerField: '_documentId' }, * { column: 'anulada', op: 'eq', value: false }, * ] * ``` */ externalFilters?: DetailTabExternalFilter[]; /** * Presentación y modo de vista: pivot, árbol, paginación virtual, carga bajo * demanda, selección de filas y techos de sobrecarga. * * @see DetailTabFlexQueryView */ view?: DetailTabFlexQueryView; /** * Escotilla para props de `` que este descriptor todavía no * tipa. El kit no las interpreta ni las valida: las pasa tal cual. Si algo de * aquí se vuelve común, promuévelo a `view` con su documentación. */ params?: Record; }; /** * Acotar las acciones de fila a estas claves del `lineActionRegistry`. * Ausente ⇒ todas las del registro. `false` ⇒ ninguna. */ rowActions?: string[] | false; /** * Qué hace el clic en una fila. Ausente ⇒ `'none'`. * * @see DetailTabRowClick */ rowClick?: DetailTabRowClick; } /** * Tipo 3 — listado Render: un componente libre del host, montado por clave. * Mismo mecanismo que los widgets custom del FactBox (`factBoxWidgets`). */ export interface DetailTabRender extends DetailTabBase { type: 'render'; /** Clave del renderer registrado en `MasterDocumentProvider.lineTabRenderers`. */ renderKey: string; } export type DetailTabTemplate = DetailTabLines | DetailTabFlexQuery | DetailTabRender; /** * Un grano del documento al que se le pueden colgar campos adicionales. * * El kit no inventa el catálogo: consume los **Features** que el backend del host * ya siembra, uno por cada (clase de documento × grano). El código se deriva por * convención — `{documentClass}_{suffix}` — porque es exactamente como los siembra * el servicio Core (`PURCHASE_ORDER_HEADER`, `SALES_ORDER_LINE`, …), así que un * template que solo dice `header: true` ya queda cableado. * * `featureCode` está para el host cuyo catálogo NO sigue esa convención; * declararlo desactiva la derivación por completo. */ export interface DynamicEntitiesGrainConfig { /** * Código literal del Feature. Ausente ⇒ se deriva como * `{documentClass}_{suffix}`, donde la clase es la del ESTADO del documento (la * que resuelve el wizard) y, en su defecto, `identity.documentClass`. */ featureCode?: string; /** Sufijo del grano cuando el código se deriva. Defaults: `HEADER` y `LINE`. */ suffix?: string; /** Título del bloque, crudo. Es dato del host: el kit no lo traduce. */ label?: string; /** * Clave del catálogo i18n para el título. Gana sobre `label` cuando resuelve a * un texto distinto de la propia clave. Sin ninguna de las dos se usa * `master_document.dynamicEntities.headerTitle` / `.lineTitle`. */ labelKey?: string; } /** * Campos adicionales (entidades dinámicas / EAV) del documento. * * Ausente ⇒ el documento no pinta ningún bloque de campos adicionales, que es el * comportamiento de siempre. **Declararlo no basta**: el host debe además envolver * la pantalla en un `DynamicEntitiesProvider`, que es quien aporta `baseUrl` y el * `fetcher` con las cabeceras de la sesión. Sin provider el kit se queda inerte en * silencio en lugar de reventar — un documento no puede dejar de funcionar porque * falte una capacidad opcional. * * El guardado es en DOS FASES, igual que en `MasterCrud`: primero se persiste el * documento o la línea y, solo cuando el backend devuelve su id, se hace POST de * los valores EAV contra ese id. Por eso funciona también en creación, donde al * abrir el formulario todavía no existe ningún id al que colgarlos. * * @example * ```ts * // Derivado por convención desde identity.documentClass * dynamicEntities: { header: true, line: true } * * // Código explícito y título propio * dynamicEntities: { * header: { featureCode: 'PURCHASE_ORDER_HEADER', labelKey: 'ocd.additional_fields' }, * line: false, * } * ``` */ export interface DynamicEntitiesTemplateConfig { /** Campos adicionales del encabezado. `true` ⇒ configuración por defecto. */ header?: boolean | DynamicEntitiesGrainConfig; /** * Campos adicionales de la línea, dentro del modal de línea. * * Una entidad multi-registro solo se puede capturar sobre una línea que ya * existe: en una línea nueva el bloque lo avisa y se habilita al reabrirla. */ line?: boolean | DynamicEntitiesGrainConfig; } export interface MasterDocumentTemplate { version: '1.0' | '1.1'; identity: { documentGroup: DocumentGroup; documentClass: string; label: string; breadcrumbLabel: string; }; stateMachine: { initial: string; /** v1.1: states and transitions come from API. When 'api', states/transitions below are optional. */ source?: 'api' | 'template'; /** v1.1: API endpoint path for states. Resolved by SmartMasterDocument at runtime. */ endpoint?: string; /** v1.0 legacy: states embedded in template. Used as fallback when API is unavailable. */ states?: Record; /** v1.0 legacy: transitions embedded in template. Used as fallback when API is unavailable. */ transitions?: SerializableTransitionConfig[]; }; layout: { header: HeaderLayoutConfig; detail: { columns: LineColumnConfig[]; title?: string; lineModal?: { sections?: LineModalSectionConfig[]; widgets?: LineModalWidgetConfig[]; /** * Indica si las líneas del documento son editables. Por defecto `true`. * * Cuando es `false`, la acción por línea NO se oculta: se DEGRADA. Pasa a * rotularse "Ver" con el icono de ojo y abre el modal en solo-lectura, que * es lo que hacen el lápiz inline, el ítem del menú y la tarjeta móvil, y lo * mismo que anuncia el título del modal («Ver línea»). Este texto decía * «se oculta por completo», que era una TERCERA descripción del mismo prop y * no coincidía ni con el código ni con los otros dos JSDoc. Útil para * documentos que mueven * inventario (entradas/salidas/ajustes/transferencias) donde, por diseño * de backend (ADR-017 R3-Lines), las líneas son solo agregar/eliminar y * no existe `PUT /lines/{lineId}`. * * No afecta agregar, eliminar ni duplicar líneas, ni el modo creación. */ editable?: boolean; /** * Indica si las líneas del documento se pueden ELIMINAR. Por defecto `true`. * * Cuando es `false`, la afordancia de borrado por línea no se renderiza: ni * el botón de la fila ni el ítem del menú de más opciones. * * Cierra la asimetría que tenía el kit: agregar (`canAddLine`), editar * (`editable`) y duplicar (`onDuplicateLine` opcional) ya se podían apagar * desde el documento; eliminar era la única de las cuatro sin gobierno, así * que una clase de sólo consulta podía declarar que no se agrega, no se * edita y no se duplica, pero no que no se borra. */ deletable?: boolean; /** * Estados del documento en los que la línea se puede EDITAR. Ausente ⇒ sin * restricción por estado. `'*'` vale como comodín, igual que en * `FieldDescriptor.editableOn`, cuyo vocabulario se reutiliza a propósito. * * El encabezado tenía eje de estado desde siempre (`editableOn`/`visibleOn`) * y las líneas no, así que la única salida era construir el template ya * conociendo el estado: un canal paralelo por el que el host se entera del * estado, y un remount del árbol en cada carga. Esto lo sustituye. * * La regla que describe NO es estética: en los documentos que mueven stock el * backend rechaza toda operación de línea fuera de DRAFT * (`document.lines_only_allowed_in_draft`), así que una afordancia ofrecida * fuera de ese estado sólo puede terminar en error. */ editableOn?: string[]; /** Estados en los que la línea se puede ELIMINAR. Ver `editableOn`. */ deletableOn?: string[]; /** Estados en los que se pueden AGREGAR líneas. Ver `editableOn`. */ addableOn?: string[]; }; /** * Activa el soporte nativo de seriales (`SerialPanel`) en el modal de línea. * Omitir (o `mode: "none"`) deja el comportamiento actual sin cambios. */ serialSupport?: SerialSupportConfig; /** * Cuando la condición se cumple contra `headerValues`, el botón "Nueva * línea" se deshabilita — ADEMÁS del gate existente (`mode !== 'view'` y * campos required del encabezado llenos). Reutiliza el MISMO motor * `FieldCondition` / `evaluateCondition` que ya usan * `FieldDescriptor.visibleWhen` y `SectionConfig.visibleWhen`. * * Ausente ⇒ comportamiento idéntico al actual (retrocompatible: ningún * consumidor existente se ve afectado). * * @example Bloquear al agregar línea cuando hay descuento global activo: * ```ts * addLineLockedWhen: { * $or: [ * { globalDiscountPct: { $ne: null } }, * { globalDiscountAmount: { $ne: null } }, * ], * } * ``` */ addLineLockedWhen?: import('../fieldCondition').FieldCondition; /** * Multi-listados de líneas en pestañas. **Ausente ⇒ comportamiento idéntico * al actual**: se renderiza `LinesSection` sin barra ni contenedor extra. * * Reglas: * - `type` ausente en una pestaña ⇒ `'lines'` (Tipo 1). * - Una sola pestaña de tipo `lines` ⇒ misma grilla y barra OCULTA: una * pestaña sola no es navegación. * - El orden del array es el orden de la barra; la primera pestaña visible * es la activa por defecto. * * @example * ```ts * tabs: [ * { key: 'lines' }, // Tipo 1 * { key: 'analysis', type: 'flexQuery', label: 'Análisis', * renderKey: 'flexQuery', flexQuery: { queryCode: 'DOC_LINES_ANALYSIS', * bindFilters: { documentId: '_documentId' } } }, // Tipo 2 * { key: 'attachments', type: 'render', label: 'Adjuntos', * renderKey: 'attachmentsPanel' }, // Tipo 3 * ] * ``` */ tabs?: DetailTabTemplate[]; }; itemLayout: ItemLayout; }; fields: FieldDescriptor[]; searchers: Record; /** * v1.1 — action definitions keyed by action key. * Replaces the `actions` array. Used together with `actionsSource`. */ actionRegistry?: Record; /** * Acciones de línea ADICIONALES (declarativas), keyed by action key. Se ANEXAN * al menú de fila de `LinesSection` junto a las acciones quemadas * (Editar/Ver/Duplicar/Cerrar-reabrir/Eliminar), que NO pasan por aquí y no * cambian. Ausente ⇒ comportamiento idéntico al actual. * * @see LineActionEntry */ lineActionRegistry?: Record; /** * v1.1 — configures the DocumentProcess endpoint that returns the action whitelist. */ actionsSource?: ActionsSourceConfig; /** * v1.0 legacy — action list with visibleOn filters. * Still supported for backward compatibility with local JSON templates. * @deprecated Use actionRegistry + actionsSource instead. */ actions?: SerializableActionConfig[]; contextStrip: ContextStripMapping[]; /** Configuración de moneda / multimoneda (aparece en el context strip) */ currency?: CurrencyConfig; factBox: { sections: FactBoxSectionTemplate[]; }; computedRules: SerializableComputedRule[]; serverRules?: ServerRuleConfig[]; /** * Agrega totales desde líneas → headerValues automáticamente al mutar líneas. * Las claves son nombres de campos en headerValues; los valores definen cómo * se computan desde state.lines. Ideal para subtotal, iva, total_document, etc. */ lineTotals?: Record; documentRules?: DocumentRuleConfig[]; approval?: ApprovalTemplateConfig; conflictDetection?: { enabled: boolean; pollIntervalMs?: number; }; list?: DocumentListTemplateConfig; wizard?: WizardTemplateConfig; crossing?: CrossingTemplateConfig; /** Motor de cruce whole-document (Épica 105-5). Independiente de `crossing`. */ crossDocuments?: CrossDocumentsTemplateConfig; /** * Capacidad "crear/continuar documento desde origen" (caso A create / caso B * append). Se dispara vía `actionRegistry` con `actionType: 'createFromSource'`. * Requiere que el adapter implemente `fetchAvailableSources`/`fetchPendingLines` * y `createFromSource`/`appendFromSource` según el/los modo(s) usados. */ createFromSource?: CreateFromSourceTemplateConfig; /** * Campos adicionales (entidades dinámicas / EAV) del encabezado y de la línea. * Requiere que el host envuelva la pantalla en `DynamicEntitiesProvider`. * * @see DynamicEntitiesTemplateConfig */ dynamicEntities?: DynamicEntitiesTemplateConfig; /** * Override de las claves reservadas de versión (concurrencia optimista) que el * kit re-mergea automáticamente en los payloads del adapter. * * El kit retiene el token de versión leyéndolo del ESTADO del documento (no del * formulario del modal) y lo reinyecta en `saveLine`/`deleteLine` de la línea y * en `saveHeader` de la cabecera, eliminando el `Map` manual * que cada app consumidora mantenía. * * Defaults (no es necesario declarar este campo si se usan estos nombres): * - `line`: `_row_version` — vive en cada `DocumentLineData`. * - `header`: `_rowVersion` — vive en `DocumentState.headerValues`. */ versionKeys?: { /** Clave del rowVersion de cada línea. Default `_row_version`. */ line?: string; /** Clave del rowVersion de la cabecera en `headerValues`. Default `_rowVersion`. */ header?: string; }; /** * @deprecated NO TIENE EFECTO. Está declarado desde el inicio y nunca se leyó: * ningún componente del kit lo consulta, así que asignarlo no cambia nada. * * No se implementa a propósito. Sería un cuarto canal de traducción con peores * garantías que los tres que ya existen: el template es un objeto JS estático, * de modo que un host que cambie de idioma en runtime NO vería re-renderizar * estos textos. Las alternativas correctas, por orden de preferencia: * * 1. Registrar las claves `master_document.*` en el catálogo del backend o vía * `TranslationProvider.staticResources` — es reactivo al cambio de idioma. * 2. Usar los canales de override tipados que sí funcionan: * `CreateFromSourceTemplateConfig.i18n`, `CrossingMessagesConfig`, * `CrossDocumentsMessagesConfig`, o `FieldDescriptor.lockTooltipKey`. * * Se mantiene el campo en vez de borrarlo para no romper la compilación de los * hosts que lo tengan asignado; se eliminará en la próxima versión mayor. * * @see src/components/MasterDocument/i18n.ts - Catálogo del kit */ i18n?: Record; } //# sourceMappingURL=MasterDocumentTemplate.types.d.ts.map