import type { ReactNode } from 'react'; import type { FieldCondition } from './fieldCondition'; export type ControlType = 'text' | 'number' | 'currency' | 'percentage' | 'date' | 'select' | 'searcher' | 'autocomplete' | 'readonly' | 'textarea' | 'toggle' | 'checkbox' | 'display'; export type FieldContext = 'header' | 'line' | 'line-detail'; export type DocumentGroup = 'PURCHASES' | 'SALES' | 'INVENTORY'; export type ItemLayout = 'PRODUCTS_ONLY' | 'SERVICES_ONLY' | 'MIXED'; export type ActionVariant = 'primary' | 'secondary' | 'danger' | 'ghost'; export type ColumnFormat = 'currency' | 'number' | 'percentage' | 'text' | 'date'; export type ColumnAlign = 'left' | 'center' | 'right'; export type ColumnAggregate = 'sum' | 'count'; export type HeaderMode = 'accordion' | 'tabs'; export type Breakpoint = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl'; export type ValidationSeverity = 'error' | 'warning'; export interface LineModalSectionConfig { key: string; label: string; icon?: string; visibleWhen?: import('./fieldCondition').FieldCondition; hiddenWhen?: import('./fieldCondition').FieldCondition; } export interface SelectOption { label: string; value: string | number; } /** * Validador que el consumidor registra para las reglas `type: 'custom'`. Devuelve * `true` si el valor es válido. El kit no trae ninguno: una regla `custom` sin * validador registrado se considera cumplida (no puede bloquear por lo que no sabe * evaluar). */ export type CustomFieldValidator = (value: unknown, context: { fieldDef: FieldDescriptor; values?: Record; }) => boolean; /** Registro de validadores `custom`, indexado por el `value` (string) de la regla. */ export type CustomFieldValidators = Record; export interface ValidationRule { type: 'required' | 'min' | 'max' | 'minLength' | 'maxLength' | 'pattern' | 'custom'; value?: unknown; message: string; severity: ValidationSeverity; } /** * Bloqueo condicional de un campo. A diferencia de `editableOn` (que decide si el * campo es editable por estado), un campo bloqueado permanece visible y editable * por estado pero se renderiza con un candado + tooltip y cursor `not-allowed`, * indicando que existe una condición de negocio que impide cambiarlo en este * momento (p.ej. ya hay líneas registradas). */ export interface FieldLockCondition { /** * Bloquea cuando el documento tiene >= 1 línea. El estado `hasLines` lo deriva * el kit internamente (`lines.length > 0`); el consumidor no lo inyecta. */ hasLines?: boolean; /** Estados del documento en los que aplica el bloqueo. Si se omite, aplica en cualquier estado. */ states?: string[]; } export interface FieldDescriptor { key: string; section: string; span: 1 | 2 | 3 | 4; order: number; control: ControlType; /** * Formato de PRESENTACIÓN del valor, independiente del control. * * Existe porque `control` responde a «cómo se captura» y no a «qué es el dato»: * un importe calculado por el servidor se declara `control: 'readonly'` —no se * teclea— y sin este campo el kit no tenía forma de saber que era dinero, así * que lo pintaba crudo (`500000`) mientras el MISMO importe salía `$ 500.000,00` * en la grilla de líneas. Es la misma escala que ya usan las columnas * (`LineColumnConfig.format`), a propósito. * * Manda sobre lo que insinúe `control`. Sólo afecta a la lectura: un control * editable sigue capturando el número en crudo. */ format?: ColumnFormat; label: string; placeholder?: string; required?: boolean; options?: SelectOption[]; /** * Nombre del catálogo de entidades para cargar opciones dinámicamente. * Cuando está presente, el componente llama: * GET /api/v1/entities/{optionsSource}/options?companyId=X&operationCenterId=Y * y reemplaza el array `options` estático con el resultado. * Si la llamada falla, se usa el array `options` como fallback. */ optionsSource?: string; lookup?: Record; defaultValue?: unknown; editableOn: string[]; visibleOn: string[]; visibleWhen?: FieldCondition; hiddenWhen?: FieldCondition; /** Permiso requerido para que el campo sea visible. Sin él → campo oculto. */ permissionVisibility?: string; /** Permiso requerido para que el campo sea editable. Sin él → visible pero deshabilitado. */ permissionEnable?: string; /** * Bloqueo declarativo del campo (candado visual + tooltip). El kit evalúa la * condición contra el estado del documento y `hasLines` (derivado de las líneas). */ lockedWhen?: FieldLockCondition; /** Texto literal del tooltip mostrado al pasar el mouse sobre el candado. */ lockTooltip?: string; /** * Clave i18n del tooltip del candado, resuelta con el traductor del kit * (`useMasterDocumentTranslation`): sirve tanto una clave `master_document.*` * del catálogo propio como cualquier clave que el host haya registrado. * * Tiene prioridad sobre `lockTooltip`; si no se pasa ninguna de las dos, se usa * el texto por defecto `master_document.field.lockTooltip`. */ lockTooltipKey?: string; validations: ValidationRule[]; context: FieldContext; hideOnBreakpoints?: Breakpoint[]; } export interface SectionConfig { key: string; label: string; subtitle?: string; defaultOpen: boolean; accent?: string; /** docStates en los que la sección es visible. Si está vacío o ausente, siempre visible. */ visibleOn?: string[]; /** Condición sobre headerValues para mostrar la sección. */ visibleWhen?: FieldCondition; /** Condición sobre headerValues para ocultar la sección. */ hiddenWhen?: FieldCondition; /** Permiso RBAC requerido. Sin él la sección se oculta. */ permissionVisibility?: string; } export interface HeaderLayoutConfig { mode: HeaderMode; columns: 1 | 2 | 3 | 4; sections: SectionConfig[]; } export interface LineColumnConfig { key: string; header: string; width?: string; /** * Si la columna debe quedarse EXACTAMENTE en `width` y no crecer para repartirse * el espacio sobrante. * * Sin declararlo, se deduce del ancho: por debajo de 100px una columna es, por * construcción, de código, consecutivo o unidad de medida —a una columna de texto * nadie le pone 60px—, y estirarla dejaba el consecutivo tan ancho como la * descripción. Declararlo permite fijar también una columna más ancha, o forzar a * crecer una estrecha con `fixedWidth: false`. */ fixedWidth?: boolean; align?: ColumnAlign; format?: ColumnFormat; aggregate?: ColumnAggregate; hideOn?: Breakpoint[]; showInCard?: boolean; /** Condición sobre headerValues para mostrar la columna. */ visibleWhen?: FieldCondition; /** Condición sobre headerValues para ocultar la columna. */ hiddenWhen?: FieldCondition; /** Permiso RBAC requerido. Sin él la columna se oculta. */ permissionVisibility?: string; } export interface DetailLayoutConfig { mode: HeaderMode; columns: LineColumnConfig[]; } export interface LayoutConfig { header: HeaderLayoutConfig; detail: DetailLayoutConfig; itemLayout: ItemLayout; } export interface StateBadgeConfig { label: string; bg: string; color: string; } export interface TransitionConfig { to: string; /** Clave de la acción que origina esta transición (ej: "send_to_approval"). */ actionKey?: string; label: string; icon?: ReactNode; style: ActionVariant; hasGuard?: boolean; shortcut?: string; disabled?: boolean; loading?: boolean; } export interface ActionConfig { key: string; label: string; icon?: ReactNode; variant?: ActionVariant; onClick: () => void; disabled?: boolean; /** * Texto del tooltip (atributo nativo `title`) que explica por qué la acción * está deshabilitada. Solo se muestra cuando `disabled` es true. */ disabledReason?: string; /** * La acción está ejecutándose: su botón muestra un indicador en lugar del * icono. Espejo de `TransitionConfig.loading`, que ya existía. * * Va aparte de `disabled` a propósito: durante una mutación TODAS las * acciones se deshabilitan (gating in-flight), pero solo la que el usuario * pulsó debe girar. */ loading?: boolean; visibleOn?: string[]; position?: 'toolbar' | 'actionBar' | 'moreMenu'; requiresConfirmation?: boolean; confirmationMessage?: string; /** Permiso RBAC requerido. Sin él la acción se oculta. */ permissionVisibility?: string; } export interface FactBoxSectionConfig { key: string; label: string; defaultOpen: boolean; badge?: ReactNode; children: ReactNode; } export interface TotalItem { key: string; label: string; value: string | number; highlight?: boolean; color?: string; } export type CfdeStatus = 'PENDING' | 'SENT' | 'ACCEPTED' | 'REJECTED' | 'ERROR'; export interface CfdeData { status: CfdeStatus; number?: string; cufe?: string; issueDate?: string; qrUrl?: string; printCount?: number; isTransmitted?: boolean; } export type PeriodMode = 'days' | 'months' | 'custom'; export interface PaymentTermsData { code: string; name: string; installmentCount: number; periodMode: PeriodMode; dueDays: number; earlyPaymentEnabled?: boolean; earlyPaymentDays?: number; earlyPaymentDiscountPct?: number; advancePaymentPct?: number; advancePaymentAmount?: number; } export interface FinancialData { receivableAmount?: number; payableAmount?: number; advanceAmount?: number; cashAmount?: number; receivableAmountLocal?: number; payableAmountLocal?: number; currencyCode?: string; } export type DiscountMode = 'percentage' | 'amount'; export interface GlobalDiscountData { mode: DiscountMode; percentage?: number; amount?: number; } export interface DocumentHeaderData { [key: string]: unknown; } export interface DocumentLineData { id: string; line_number: number; [key: string]: unknown; } export interface ComputedRule { targetField: string; watch: string[]; compute: (formData: Record) => unknown; } export interface FieldError { message: string; severity: ValidationSeverity; } export type FieldErrorMap = Record; export interface EmptyStateConfig { icon?: ReactNode; title: string; description?: string; action?: { label: string; onClick: () => void; }; } export interface ContextStripItem { label: string; value: string; mono?: boolean; accent?: boolean; /** * Breakpoints en los que el item se oculta (análogo a `FieldDescriptor.hideOnBreakpoints`). * Útil para esconder campos secundarios en anchos reducidos y evitar que la franja * se sature. La franja completa sólo es visible en `md+`, por lo que `xs`/`sm` rara * vez aplican. Ej: `['md', 'lg']` oculta el item en ventanas de ~768–1279px y lo * muestra de nuevo en `xl+`. El ocultamiento es puramente CSS (responde al resize * sin recalcular en JS). */ hideOnBreakpoints?: Breakpoint[]; } export interface DocumentListColumn { key: string; /** Campo real en la fila. Si omitido, se usa `key`. */ dataKey?: string; header: string; align?: ColumnAlign; render?: (row: Record) => ReactNode; mono?: boolean; width?: string; /** * Filtro propio de la columna: pinta un embudo en su encabezado que despliega * el control. Requiere `onColumnFilterChange` en el listado; sin él la columna * no ofrece embudo (un afordance sin destino no se muestra). */ filter?: DocumentListColumnFilter; } /** * Filtro de una columna de la grilla (el embudo del encabezado). * * `'lookup'` es el caso de una columna FORÁNEA: despliega el mismo searcher que * usa el formulario del documento y filtra por el id del registro elegido, en * vez de obligar a teclear el código. Es el motivo por el que existe esta * capacidad: una columna foránea muestra un nombre pero filtra por un id. */ export interface DocumentListColumnFilter { type: 'text' | 'date' | 'select' | 'lookup'; /** * Campo por el que se filtra en el backend. Por defecto `dataKey ?? key` de la * columna, que sirve para las columnas de valor simple; una columna foránea * suele mostrar `cliente_nombre` y filtrar por `cliente_id`. */ field?: string; options?: SelectOption[]; placeholder?: string; /** Config del searcher para `type: 'lookup'` (la misma que usan los campos del detalle). */ searcher?: LookupFieldConfig; } export interface FilterPill { key: string; label: string; count?: number; active?: boolean; bg?: string; color?: string; } export interface SearchField { key: string; label: string; type: 'text' | 'date' | 'select'; options?: SelectOption[]; placeholder?: string; } export interface DocumentListCompany { uuid: string | number; code: string | number; name: string; isSelectedByDefault?: boolean; } export interface WizardOption { id: string; label: string; description?: string; icon?: ReactNode; color?: string; } export interface ApprovalStep { label: string; status: 'completed' | 'current' | 'pending'; } export interface ActionBarProps { breadcrumbLabel: string; /** * Título de la cabecera: el rótulo de la CLASE de documento * (`identity.label`, p.ej. «Devolución de Venta»), que es el análogo del * nombre del maestro en MasterCrud. Ausente ⇒ cae a `breadcrumbLabel`. */ documentTitle?: string; /** * Nombre de la compañía, pintado como badge bajo el título. Ausente ⇒ no se * pinta: no toda plantilla lo declara. */ companyName?: string; onBreadcrumbClick: () => void; /** UUID interno del documento. Se usa para navegación/refetch, NO para display. */ documentId?: string; /** * Número de documento legible (p.ej. "PV-2026-004821" o una etiqueta * "pendiente"). Es lo que se muestra como título en negrita; cae a * `documentId` solo si no hay número disponible. */ documentNumber?: string; stateBadge?: StateBadgeConfig; docTypeBadge?: string; transitions: TransitionConfig[]; onTransition: (to: string) => void; actions: ActionConfig[]; onToggleFactBox?: () => void; factBoxOpen?: boolean; className?: string; } export interface ContextStripProps { items: ContextStripItem[]; currencyPanel?: ReactNode; className?: string; } export interface HeaderRendererProps { mode: HeaderMode; columns: 1 | 2 | 3 | 4; sections: SectionConfig[]; fieldsBySection: Record; values: DocumentHeaderData; errors?: FieldErrorMap; onFieldChange: (key: string, value: unknown) => void; docState: string; mode_form: 'create' | 'edit' | 'view'; /** * Si el documento tiene al menos una línea. Lo deriva el kit (`lines.length > 0`) * y se usa para evaluar `FieldDescriptor.lockedWhen.hasLines`. */ hasLines?: boolean; className?: string; /** Mapa de configs LookupField por key de campo searcher. */ lookupConfigs?: Record; /** Fetcher del adapter para campos searcher. */ fetcher?: unknown; /** Callback cuando un LookupField selecciona un registro (para bindFields). */ onRecordSelect?: (fieldKey: string, record: Record | null) => void; } export interface LinesSectionProps { title: string; lines: DocumentLineData[]; columns: LineColumnConfig[]; lineFields: FieldDescriptor[]; alerts?: Array<{ label: string; bg: string; color: string; }>; canAddLine: boolean; onAddLine: () => void; onEditLine: (lineId: string) => void; /** Opcional desde que existe `lineDeletable`: un documento que no borra no debería * verse obligado a inventar un callback muerto para satisfacer el tipo. */ onDeleteLine?: (lineId: string) => void; onDuplicateLine?: (lineId: string) => void; /** * Indica si las líneas son editables. Por defecto `true`. * * Cuando es `false`, no se renderiza el botón lápiz (`aria-label="Editar línea"`) * ni el ítem "Editar" del menú contextual. En su lugar, si se provee * `onViewLine`, se muestra la acción "Ver" (icono ojo) que abre la línea en * solo-lectura. No afecta agregar, eliminar ni duplicar líneas. */ lineEditable?: boolean; /** * Indica si las líneas 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. Simétrico a * `lineEditable`. * * Existe porque eliminar era la única de las cuatro operaciones de línea sin * gobierno —agregar tiene `canAddLine`, editar `lineEditable` y duplicar es * opcional—, así que un documento de sólo consulta no tenía forma de declarar * que no borra. Y desde 1.0.333 el botón salió del menú a la fila, con lo que * el hueco pasó de invisible a estar a la vista. */ lineDeletable?: boolean; /** * Abre la línea en modo solo-lectura ("Ver"). Solo se usa cuando * `lineEditable === false`: la acción "Ver" (icono ojo) reemplaza a "Editar" * en el lápiz inline, el ítem del menú kebab y la tarjeta móvil. */ onViewLine?: (lineId: string) => void; activeLineId?: string | null; /** * Id de la línea con una mutación en vuelo (guardar, borrar o duplicar). * Esa fila sustituye su grupo de acciones por un indicador y lo mantiene * visible, en vez de esconderlo hasta el hover como en reposo. * * Es UNA línea y no una lista: a diferencia de las acciones de fila del * listado, las del documento sí están serializadas por `state.saving`, que * deshabilita la barra mientras dure la mutación. */ busyLineId?: string | null; currentBreakpoint?: Breakpoint; className?: string; /** Activates checkbox column and fulfillment badge column. */ selectionMode?: boolean; /** IDs of currently selected lines (controlled). */ selectedLineIds?: string[]; /** Called when the user toggles a line checkbox. */ onLineSelect?: (lineId: string, selected: boolean) => void; /** * Fulfillment status per line ID. * Lines with Fulfilled or Closed status get a disabled checkbox. */ lineStatuses?: Record; /** * Inline error message per line ID shown when user attempts * to select a Fulfilled or Closed line. */ lineSelectionErrors?: Record; /** Opens the close/reopen modal for a specific line. */ onOpenLineCloseModal?: (lineId: string, description: string, mode: 'close' | 'reopen') => void; /** Which line-level actions are available (from template.crossing.lineActions). */ lineActions?: { close?: boolean; reopen?: boolean; }; /** * Acciones de línea ADICIONALES (declarativas) a ANEXAR al menú de fila, junto * a las acciones quemadas. Las construye `SmartMasterDocument` desde * `template.lineActionRegistry` (gateadas por estado de línea/documento). Si se * omite, el menú es idéntico al actual. No reemplaza a `onOpenLineCloseModal` * ni a `lineActions` (mecanismo de las acciones quemadas). */ getLineActions?: (line: DocumentLineData) => RowAction[]; /** * Oculta el título de la sección (no las alertas ni el botón "Nueva línea"). * Lo usa el contenedor de pestañas: la barra ya nombra la pestaña activa y el * título quedaría duplicado. Ausente ⇒ `false` (comportamiento sin cambios). */ hideTitle?: boolean; } /** * Pestaña del área de detalle, YA RESUELTA para el nivel presentacional. * * `MasterDocument` es tonto por diseño: no conoce el template, los permisos ni el * registro de renderers del host. El contenedor smart resuelve todo eso y entrega * cada pestaña con su etiqueta ya traducida y, para los tipos 2 y 3, un `render` * que ya cerró sobre el contexto del documento. * * @see DetailTabTemplate — la forma declarativa en el template * @see LineTabRenderContext — lo que recibe el renderer del host */ export interface LineTabConfig { /** Identificador único dentro del documento. */ key: string; /** Etiqueta visible, ya resuelta (`labelKey` → catálogo → `label` → default). */ label: string; /** Icono ya resuelto a nodo. */ icon?: ReactNode; /** Tipo de contenido. `'lines'` monta la grilla nativa; el resto usa `render`. */ type: 'lines' | 'flexQuery' | 'render'; /** * Contenido de la pestaña. Requerido para `flexQuery` y `render`; ignorado para * `lines`, que monta `LinesSection` con todos los props del documento. * * Se invoca solo mientras la pestaña está activa: al cambiar de pestaña el * contenido se DESMONTA en vez de ocultarse, porque la grilla nativa mide su * contenedor de scroll para virtualizar y con `display:none` mediría cero. * * Recibe los ayudantes que solo el nivel presentacional puede dar, porque es * quien tiene el estado: hoy, abrir el modal de una línea. */ render?: (helpers: LineTabHelpers) => ReactNode; } /** * Ayudantes que `MasterDocument` inyecta al contenido de una pestaña. * * Existen porque el modal de línea es estado interno del componente * presentacional: el contenedor smart construye el contexto del renderer, pero no * puede abrir un modal que no controla. `LineTabsRegion` hace de correa. */ export interface LineTabHelpers { /** Abre la línea en el modal de solo lectura del kit. */ openLineDetail: (lineId: string) => void; } /** * Filtro externo resuelto, listo para la prop `externalFilters` de * ``. Misma forma que su `FlexFilter`, declarada aquí porque el kit no * depende del paquete. */ export interface LineTabExternalFilter { /** Columna del modelo EF; admite navegación con puntos. */ column: string; /** Operador ya resuelto (el template omite `op` para decir `'eq'`). */ op: import('./template/MasterDocumentTemplate.types').DetailTabFilterOp; /** Valor de la comparación. Irrelevante para `isNull`/`isNotNull`. */ value: unknown; } /** * Contexto que el kit inyecta en el renderer del host de una pestaña Tipo 2 o * Tipo 3. Es el contrato real de la capacidad: todo lo que el host necesita para * pintar su listado sin re-derivar estado del documento ni duplicar el gateo de * acciones. * * Los nombres siguen los de las props de `` * (`@SiesaTeams/flex-query` npm 0.4.0) para que el cableado del host sea un * paso-a-través y no una traducción. */ export interface LineTabRenderContext { /** Pestaña que se está renderizando. */ tabKey: string; documentId: string; documentClass?: string; docState: string; mode: 'create' | 'edit' | 'view'; headerValues: DocumentHeaderData; /** * Locale BCP 47 del idioma activo (`'es-CO'`, `'en-US'`), resuelto por el kit. * Son exactamente los valores que acepta la prop `locale` de ``: * pásalo tal cual en vez de fijar un idioma, o la consulta quedará en español * dentro de un documento en inglés. */ locale: string; /** * Tipo 2: el descriptor declarado en el template, sin interpretar por el kit. * Sus nombres son los de las props de ``. Ausente en Tipo 3. */ flexQuery?: { reportCode: string; contractBaseUrl?: string; labelNamespace?: string; /** Presentación y modo de vista: pivot, árbol, virtual, autoLoad, selección. */ view?: import('./template/MasterDocumentTemplate.types').DetailTabFlexQueryView; params?: Record; }; /** * Qué declaró el template para el clic en fila. `'openLineDetail'` significa * que debes cablear `onRowClick` a `openLineDetail(row.id)`. */ rowClick?: import('./template/MasterDocumentTemplate.types').DetailTabRowClick; /** * Abre una línea del documento en el modal de solo lectura del kit, con las * secciones y campos que el template ya declara. Si el id no corresponde a * ninguna línea cargada, no hace nada. * * Es la contrapartida de `rowClick: 'openLineDetail'`: la consulta lista, y el * detalle de la línea lo sigue pintando el kit — sin que el host tenga que * reconstruir un formulario que ya existe. */ openLineDetail: (lineId: string) => void; /** * Filtros externos ya resueltos contra el documento, con la forma exacta de la * prop `externalFilters` de `` (`FlexFilter[]`). Los filtros cuyo * `headerField` no tiene valor no aparecen. Vacío o ausente ⇒ no pases la prop. */ externalFilters?: LineTabExternalFilter[]; /** Columnas del template ya filtradas por permiso y condición. */ columns: LineColumnConfig[]; /** * Acciones de fila ya gateadas por estado de línea, `docState` y permisos. * Construidas por el kit desde `template.lineActionRegistry`: ejecutarlas por * aquí conserva el manejo de `ActionResult`, refetch y toast, y el round-trip * de `row_version`. Devuelve `[]` cuando el documento no declara acciones. */ rowActions: (row: Record) => RowAction[]; /** * Se incrementa tras cada mutación de línea del kit. Úsalo como dependencia * del fetch para refrescar la consulta sin exponer un ref imperativo. */ refreshToken: number; /** Hay una mutación del documento en vuelo: deshabilita tus acciones. */ busy: boolean; formatCurrency: (value: number) => string; formatDate?: (value: string) => string; formatNumber?: (value: number) => string; } export interface LineTabsBarProps { tabs: LineTabConfig[]; activeKey: string; onChange: (key: string) => void; className?: string; } export interface FactBoxProps { sections: FactBoxSectionConfig[]; className?: string; /** * @deprecated DEPRECADO y sin efecto. El FactBox dejó de ser un cajón modal por * debajo de `xl`: los widgets van en el flujo del documento (ver `FactBoxInline`), * así que no hay nada que abrir ni cerrar. Se conservan para no romper la * compilación de los hosts que aún los pasan; se retiran en el próximo major. */ open?: boolean; /** @deprecated Sin efecto. Ver `open`. */ onClose?: () => void; } export interface FactBoxTotalsProps { grandTotal: { label: string; value: string; subtitle?: string; }; kpis: Array<{ label: string; value: string; color?: string; }>; breakdown: Array<{ label: string; value: string; color?: string; isSummary?: boolean; }>; className?: string; } /** * Acción de fila del listado (DEF-4). * * Describe una acción contextual (ej: Borrar, Anular) disponible para una fila * concreta del listado según su estado. La construye el contenedor smart a * partir del `actionRegistry` + la disponibilidad por estado, y la consume * `DocumentList` para renderizar el ítem en el menú de la fila. */ export interface RowAction { /** Clave de la acción (ej: 'delete', 'cancel'). */ key: string; /** Etiqueta visible en el menú. */ label: string; /** Ícono opcional ya resuelto a ReactNode. */ icon?: ReactNode; /** Variante visual; `danger` se renderiza en rojo. */ variant?: ActionVariant; /** Ejecuta la acción sobre la fila. El error se propaga vía el contenedor smart. */ onClick: (row: Record) => void; /** Si true, `DocumentList` pide confirmación con `ConfirmDialog` antes de ejecutar. */ requiresConfirmation?: boolean; /** Mensaje mostrado en el `ConfirmDialog` cuando `requiresConfirmation` es true. */ confirmationMessage?: string; /** * Si true, la acción se muestra VISIBLE pero DESHABILITADA (no ejecutable). Lo * usan las acciones de línea declarativas para el gateo "visible pero * deshabilitada" (D2). `DocumentList` no lo lee; el menú de fila de * `LinesSection` sí. Ausente ⇒ habilitada (comportamiento sin cambios). */ disabled?: boolean; } export interface DocumentListProps { title: string; subtitle?: string; /** Lista de compañías para el selector desplegable. Si se provee, muestra un dropdown. */ companies?: DocumentListCompany[]; /** Compañía seleccionada actualmente. */ selectedCompany?: DocumentListCompany; /** Texto de la compañía como fallback cuando no se usa el array `companies`. */ companyName?: string; onCompanyChange?: (company: DocumentListCompany) => void; onExport?: () => void; /** * Acción de crear. Omitirla deja el botón "+ Nuevo" **ausente**: un listado de * solo consulta no tiene por qué inventar un afordance sin destino. */ onCreateNew?: () => void; createNewLabel?: string; /** * ¿Se ofrece la acción de crear? Ausente ⇒ `true` (comportamiento sin cambios). * * `false` deja el botón "+ Nuevo" **ausente**, no deshabilitado. El listado es * presentacional y no resuelve permisos por su cuenta: el contenedor smart * (`MasterDocumentPage`) calcula este booleano a partir de * `template.list.canCreate` y `template.list.createPermission` contra los * permisos del usuario. */ canCreate?: boolean; pills: FilterPill[]; onPillClick: (key: string) => void; searchFields: SearchField[]; searchValues: Record; onSearchChange: (key: string, value: unknown) => void; /** * Fetcher del adapter (`Fetcher` de LookupField). Lo necesita el embudo de una * columna FORÁNEA (`filter.type: 'lookup'`); sin él ese embudo degrada al * input de texto con lupa. */ fetcher?: unknown; /** * Valores activos de los filtros por columna, indexados por el campo que * filtra cada uno (`filter.field ?? dataKey ?? key`). */ columnFilters?: Record; /** * Cambio de un filtro de columna. Sin este callback las columnas NO pintan el * embudo. El valor llega `undefined` cuando el filtro se limpia. * * El listado es presentacional: no acumula estado ni decide cuándo recargar; * eso vive en el contenedor, que ya recarga desde la página 1. */ onColumnFilterChange?: (field: string, value: unknown) => void; onSearch: () => void; onClearSearch: () => void; searchOpen?: boolean; onToggleSearch?: () => void; columns: DocumentListColumn[]; rows: Record[]; totalCount: number; /** * Destino de un clic en la fila. **Opcional a propósito**: sin él la fila NO es un * enlace, que es el canon de MasterCrud (`tabindex: null`, `cursor: auto`). Sólo * los listados con un destino de fila distinto del botón «Editar» lo declaran. */ onRowClick?: (row: Record) => void; /** * Acción «Ver» de la fila. **Opcional a propósito**: sólo aparece si el host la * declara, porque resuelta por defecto llevaba al mismo detalle que «Editar». */ onViewRow?: (row: Record) => void; onEditRow?: (row: Record) => void; /** * URL del detalle de una fila, para que «Ver»/«Editar» se pinten como ENLACE * (``) en vez de botón. * * Es lo que devuelve al usuario los gestos que el navegador ya trae: Ctrl/⌘ + * clic y clic con la rueda abren el detalle en otra pestaña, el menú contextual * ofrece «Abrir en pestaña nueva» y la barra de estado muestra el destino. Un * `