import type { Fetcher } from '../../LookupField/services/api.types'; import type { DocumentHeaderData, DocumentLineData, FieldErrorMap, DocumentListCompany } from '../MasterDocument.types'; export interface DocumentState { /** * UUID v7 del documento. Es la PK globalmente única de document_headers.id. * Se usa para todas las operaciones sobre un documento existente. * El número legible ("PV-2026-004821") se retorna en headerValues._documentNumber. * Vacío ("") en documentos nuevos que aún no se han guardado. */ documentId?: string; /** * Clase del documento (p.ej. `'CREDIT_NOTE_SALES'`). * * Solo hace falta cuando una misma superficie sirve a más de una clase: el kit * la usa para `createDocument` (POST del primer guardado) y para las consultas * por clase del detalle (`getDocumentStates`, `getFieldRules`) en lugar de * `template.identity.documentClass`, que es una constante por superficie. * * La puebla `MasterDocumentPage` desde el paso de clase del wizard. **Ausente ⇒ * se usa la del template**, que es el comportamiento de siempre. * * ⚠️ En una superficie multi-clase, el adapter DEBE devolverla también en * `getDocument`: al abrir un documento existente el kit no tiene de dónde * deducirla, y sin ella el detalle consultaría estados y reglas de campo con la * clase pineada del template. El síntoma es traicionero — el documento se * comporta bien recién creado y distinto al reabrirlo. */ documentClass?: string; mode: 'create' | 'edit' | 'view'; docState: string; headerValues: DocumentHeaderData; lines: DocumentLineData[]; headerErrors: FieldErrorMap; lineErrors: Record; loading: boolean; saving: boolean; error?: string; serverVersion?: string; factBoxData: Record>; /** * Conflicto de edición concurrente. La fuente de verdad es el 409 del servidor * (`OPTIMISTIC_LOCK_VIOLATION`) en CUALQUIER mutación (saveHeader, saveLine, * execute-process), ya sea lanzado como error o devuelto en la clave reservada * `_conflict`. `null`/ausente = sin conflicto. * * - `hard: true` ⇒ conflicto duro del servidor: el modal NO es descartable, la * única acción es recargar/reconciliar. La edición en vuelo se descarta. * - `hard` ausente/false ⇒ aviso blando (legacy, p.ej. polling); descartable. * - `expected`/`actual` provienen de las extensiones del problem-details * (`row_version_expected` / `row_version_actual`) para log y diagnóstico. */ conflict?: { modifiedBy?: string; modifiedAgo?: string; message?: string; hard?: boolean; expected?: string | number; actual?: string | number; } | null; /** * Acceso denegado disparado por un `SaveResult` con la clave reservada * `_forbidden` (HTTP 403). Activa un banner 403. `null`/ausente = sin error. */ forbidden?: { message: string; } | null; /** * Endpoint para el primer guardado (documentId === ''). * El backend lo resuelve: e.g. "/api/v1/documents/SALES_ORDER" */ createEndpoint?: string; /** * Endpoint para guardados subsiguientes (documentId !== ''). * Puede contener {id} como placeholder: e.g. "/api/v1/documents/{id}/actions/save" */ endpoint?: string; } export interface SaveResult { success: boolean; documentId: string; errors?: FieldErrorMap; serverVersion?: string; } export interface SaveLineResult { success: boolean; lineId: string; /** * Retornado solo cuando la primera línea crea el documento al mismo tiempo. * El reducer usará este ID para actualizar el estado y pasar a modo 'edit'. */ documentId?: string; serverVersion?: string; lineData?: Record; /** * Campos del header resueltos por el backend al crear el documento: * consecutivo (_documentNumber), fecha (_fecha), vencimiento (_vencimiento), etc. * El reducer los mergea sobre state.headerValues sin pisar los valores ingresados por el usuario. */ headerValues?: DocumentHeaderData; errors?: FieldErrorMap; } export interface DeleteLineResult { serverVersion?: string; /** Campos del header recalculados por el backend (ej: total_document, subtotal). */ headerValues?: Record; } /** * Opciones de `deleteLine`. Aditivo y opcional para no romper implementaciones * existentes que solo declaran `(id, lineId)`. */ export interface DeleteLineOptions { /** * Token de concurrencia optimista (`rowVersion`) de la línea, leído por el kit * desde el estado del documento (`line._row_version`, o la clave configurada en * `template.versionKeys.line`). Algunos backends lo exigen en el DELETE para * evitar borrar una versión obsoleta de la línea. Ausente cuando la línea del * estado no tiene versión registrada. */ rowVersion?: string; /** * Token de concurrencia optimista de la CABECERA (`serverVersion`), leído por el * kit desde el estado del documento. Viaja en todas las mutaciones de línea para * que el backend valide el `row_version` del header al eliminar una línea (mismo * contrato que `saveLine`). Aditivo y opcional; ausente cuando el estado aún no * tiene versión de cabecera. */ headerRowVersion?: string; } export interface TransitionValidationResult { allowed: boolean; errors?: string[]; warnings?: string[]; } export interface TransitionResult { success: boolean; newState: string; errors?: string[]; /** * Nuevo `row_version` del documento tras el execute-process. El backend lo * incluye en la respuesta de aprobar/anular/transición; el kit lo usa para * resincronizar `serverVersion` y evitar un falso conflicto en la próxima * mutación. Ausente ⇒ se conserva la versión previa del estado. */ serverVersion?: string; } export interface ActionResult { success: boolean; message?: string; updatedData?: Partial; /** * Indica que la acción ELIMINÓ el documento (p.ej. `delete`, o un `cancel` * que lo borra). Cuando es `true` junto a `success: true`, el detalle navega * de vuelta al listado en lugar de intentar mezclar `updatedData` sobre un * documento inexistente. Ausente o `false` ⇒ comportamiento sin cambios. */ navigateToList?: boolean; } export interface ConflictInfo { modifiedBy: string; modifiedAt: string; modifiedAgo: string; } export interface DocumentListRequest { page: number; pageSize: number; /** Filtro de compañía. Ausente o vacío = todas las compañías (Global). */ companyId?: string; /** * Filtros del listado. Además de los del template, el kit inyecta aquí claves * reservadas: * * - `operationCenterId: string` — el CO del contexto. * - `documentClass: string` — la clase de la superficie (caso por defecto). * - `documentClasses: string[]` — presente **en lugar de** `documentClass` * cuando la superficie declara varias clases * (`MasterDocumentPage.documentClasses`), porque un visor compartido lista los * documentos de todas ellas. * * Las dos claves de clase nunca viajan juntas: un request no debe contradecirse * a sí mismo ni dejarle al adapter la decisión de cuál gana. */ filters?: Record; search?: string; stateFilter?: string; orderBy?: { field: string; direction: 'asc' | 'desc'; }; } export interface DocumentListResponse { rows: Record[]; totalCount: number; page: number; pageSize: number; } /** * Entrada del historial de transiciones de estado de un documento. * Corresponde a una fila de `document_status_history` (OP1) en el backend. * Los registros son append-only: nunca se eliminan. */ export interface StatusHistoryEntry { /** UUID de la entrada en document_status_history. */ id: string; /** state_code del estado anterior. null en la creación inicial del documento. */ previousState: string | null; /** Etiqueta legible del estado anterior (opcional, resuelta por el backend). */ previousStateLabel?: string; /** state_code del nuevo estado. */ newState: string; /** Etiqueta legible del nuevo estado (opcional, resuelta por el backend). */ newStateLabel?: string; /** * UUID del usuario en la BD (FK a la tabla de usuarios). * No usar directamente en la UI — usar `changedByName`. */ changedBy: string; /** * Nombre legible del usuario resuelto por el endpoint. * El backend siempre lo incluye (ej: "Sistema" mientras no haya auth real). * La UI usa `changedByName ?? changedBy` sin necesidad de detectar UUIDs. */ changedByName?: string; /** ISO 8601 datetime de la transición. */ changedAt: string; /** Notas opcionales registradas al momento de la transición. */ notes?: string; /** * Clave del proceso C7 ejecutado (ej: "send_to_approval", "approve", "cancel"). * El componente la resuelve a un label legible usando un diccionario local. * Gap: C7 aún no está implementado en el backend — se usa action_key directamente. */ actionKey?: string; /** * Nombre legible del proceso C7 (opcional). * Cuando el backend lo resuelva, sobreescribe la resolución local de actionKey. */ processName?: string; } export interface DocumentStateEntry { key: string; label: string; bg: string; color: string; } export interface DocumentStateTransition { from: string; to: string; actionKey: string; /** Label legible enviado directamente por el backend. Tiene prioridad sobre actionRegistry. */ label?: string; icon?: string; variant?: string; hasGuard?: boolean; } export interface DocumentStatesResponse { initial: string; states: DocumentStateEntry[]; transitions: DocumentStateTransition[]; } /** * Estado de un serial relevante para la UI. * * - `Created` — la unidad existe pero nunca entró a inventario (sin movimientos). * - `Available` — disponible para seleccionar (modo `select` de salida). * - `Active` — ya en inventario / asignado a una línea (vista solo-lectura). * - `Committed` — comprometido en otro documento, no disponible aún. * - `Unavailable` — no disponible en la bodega origen (la unidad YA tuvo movimientos). * * `Created` se añadió sin quitar ni renombrar nada: es una AMPLIACIÓN de la unión, * así que un host que solo emita los cuatro valores previos sigue compilando igual. */ export type SerialStatus = 'Created' | 'Available' | 'Active' | 'Committed' | 'Unavailable'; /** Una fila de serial retornada por `fetchAvailableSerials` (modo `select`). */ export interface SerialRow { /** UUID del serial (cmmd_serials_prj.id). Es lo que viaja en `serial_ids`. */ serialId: string; /** Código legible del serial (solo para display). */ serialCode: string; lotCode?: string; locationCode?: string; status: SerialStatus; } /** Resultado de validar un serial escaneado/tecleado (modo `capture`). */ export interface SerialValidationResult { available: boolean; /** * UUID del serial resuelto por el backend. El kit lo agrega a `serial_ids` * cuando `available === true`. Si se omite, el kit usa el `serialCode` crudo. */ serialId?: string; /** Mensaje para mostrar al usuario cuando `available === false`. */ reason?: string; } /** Resultado paginado de `fetchAvailableSerials` (modo `select`). */ export interface SerialListResult { items: SerialRow[]; total: number; page: number; pageSize: number; } /** Parámetros de `validateSerial` (modo `capture`). */ export interface ValidateSerialParams { serialCode: string; itemExtensionId: string; storageId?: string; lotId?: string; } /** Parámetros de `fetchAvailableSerials` (modo `select`). */ export interface FetchAvailableSerialsParams { itemExtensionId: string; storageId?: string; lotId?: string; status?: SerialStatus; page: number; pageSize: number; /** Búsqueda server-side por código de serial. */ q?: string; } /** * Documento ORIGEN candidato (paso 1). Espeja `AvailableSourceDocumentDto` del * backend. Además de los campos estándar, `extra` transporta las claves de * compatibilidad declaradas en `compatibilityKeys` (p.ej. `supplierId`, * `supplierBranchId`, `currencyId`) y cualquier dato adicional para el grid. */ export interface AvailableSourceItem { /** UUID del documento origen (viaja como `documentId` en el request). */ documentId: string; /** Número visible (prefijo + consecutivo). */ documentNumber: string; documentDate?: string; documentClass?: string; state?: string; /** Nombre del tercero (proveedor/cliente) para mostrar y filtrar. */ thirdPartyName?: string; /** ID del tercero — usado para el filtro de compatibilidad y el bloqueo del header. */ thirdPartyId?: string; netTotal?: number; currencyCode?: string; /** * Claves de compatibilidad y campos adicionales del DTO. El kit lee de aquí los * valores de `compatibilityKeys` para deshabilitar orígenes incompatibles. */ extra?: Record; } /** * Línea pendiente de un documento origen (paso 2). Espeja `PendingLineDto`. * `extra` transporta columnas adicionales (lote, bodega, CPP, etc.) que las * columnas del template pueden referenciar por `key`. */ export interface PendingSourceLine { lineId: string; documentId: string; itemCode?: string; itemDescription?: string; /** Cantidad aún pendiente de cruzar (tope de "cantidad a cruzar"). */ quantityPending: number; unitOfMeasure?: string; unitPrice?: number | null; lineStatus?: string; /** Columnas adicionales referenciables por las `lineSelection.columns` del template. */ extra?: Record; } /** Parámetros de `fetchAvailableSources`. */ export interface FetchAvailableSourcesParams { childClass: string; thirdPartyId?: string; operationCenterId?: string; currencyId?: string; /** Estados elegibles (de `eligibleSourceStates`). */ states?: string[]; } /** Parámetros de `fetchPendingLines`. */ export interface FetchPendingLinesParams { childClass: string; sourceIds: string[]; } /** Selección de líneas por documento origen para el request de create/append. */ export interface CreateFromSourceLineSelection { sourceLineId: string; quantityToCross: number; } export interface CreateFromSourceSource { documentId: string; lines: CreateFromSourceLineSelection[]; } /** Request de `createFromSource` (caso A). */ export interface CreateFromSourceRequest { childClass: string; header: { operationCenterId?: string; documentDate?: string; referenceNumber?: string; notes?: string; /** ID del tercero del PRIMER origen elegido (se persiste y bloquea, caso A). */ thirdPartyId?: string; /** Campos de header adicionales capturados vía `createHeaderFields`. */ [key: string]: unknown; }; /** Datos por defecto capturados en `defaultsStep` (p.ej. `{ defaultReasonId }`). */ defaults?: Record; sources: CreateFromSourceSource[]; } /** Request de `appendFromSource` (caso B). */ export interface AppendFromSourceRequest { defaults?: Record; sources: CreateFromSourceSource[]; } export interface CurrencyInfo { id: string; code: string; name: string; isBase: boolean; isActive: boolean; } export interface IMasterDocumentAdapter { fetcher: Fetcher; getDocument(id: string): Promise; saveHeader(id: string, header: DocumentHeaderData): Promise; /** * Guarda una línea del documento. * * Cuando `id` es `null` (documento aún no guardado) y se proveen `headerValues`, * el adapter debe crear el documento y la primera línea en una sola operación. * En ese caso la respuesta incluirá `documentId` con el ID generado por el backend. * * Para líneas subsiguientes (documento ya guardado), `id` es el UUID del documento * y `headerValues` no se envía. * * Concurrencia optimista: el kit inyecta automáticamente en `data` DOS tokens * leídos del estado del documento (salvo que `data` ya los traiga): * - el `row_version` de la CABECERA (`_rowVersion`, alias de `serverVersion`), * en TODAS las mutaciones de línea (nuevas y existentes), para que el backend * valide la concurrencia del header al guardar la línea; * - el `row_version` de la LÍNEA (`_row_version`) SOLO al editar una línea * existente (las líneas nuevas no tienen versión previa). * El adapter puede leer ambos de `data` sin mantener su propio cache. Las claves * son configurables vía `template.versionKeys` (`header`/`line`). Tras una * respuesta exitosa, el kit resincroniza `serverVersion`/`_row_version` con lo * que devuelve el backend (`SaveLineResult.serverVersion`/`lineData`). */ saveLine(id: string | null, lineId: string | null, data: Record, headerValues?: DocumentHeaderData): Promise; /** * Elimina una línea del documento. * * `options.rowVersion` (aditivo) transporta el token de concurrencia optimista * de la línea cuando el estado lo tiene; los backends que lo exijan en el DELETE * pueden usarlo. La firma es retrocompatible: las implementaciones que solo * declaran `(id, lineId)` siguen compilando y funcionando. */ deleteLine(id: string, lineId: string, options?: DeleteLineOptions): Promise; validateTransition?(id: string, to: string): Promise; executeTransition(id: string, to: string, docState: string, serverVersion?: string): Promise; executeAction(actionKey: string, documentState: DocumentState): Promise; /** * OVERRIDE para acciones de LÍNEA adicionales (declaradas en * `template.lineActionRegistry`), p.ej. "cerrar línea". `payload` son los * valores del formulario del modal. Devuelve `ActionResult` y reusa el mismo * contrato de merge/refresh + toast que `executeAction`: `updatedData` puede * traer `docState`, `headerValues` y/o `lines` para que el detalle se * resincronice tras la acción. * * Solo es necesario cuando la acción requiere lógica bespoke (p.ej. recalcular * `lineStatuses`/`docState` tras cerrar la última línea). Si NO se implementa, * el kit ejecuta por defecto el `endpoint` declarado en el `LineActionEntry` * vía {@link genericFetch}, sustituyendo `{id}`/`{documentId}` y `{lineId}` en * el path — de modo que el caso simple no requiere código de adapter. Si no se * implementa ESTE método NI `genericFetch`, la acción queda inerte (no-op) y se * muestra un mensaje en el modal. * * Resincronización tras éxito (en AMBAS vías): si el resultado trae * `updatedData` se mergea; si no, el kit recarga el documento vía `getDocument` * para reflejar cambios que no puede inferir (p.ej. `lineStatuses`/`docState` * tras la vía genérica, donde el endpoint suele responder 204). */ executeLineAction?(actionKey: string, lineId: string, payload: Record, state: DocumentState): Promise; getFactBoxData?(id: string, sectionKey: string): Promise>; getTemplate?(documentClass: string): Promise; getAvailableActions?(documentId: string, docState: string): Promise; getDocumentStates?(documentClass: string): Promise; getFieldRules?(documentClass: string, docState: string): Promise>; getSelectOptions?(entity: string, companyId?: string, operationCenterId?: string): Promise>; getNewDocument?(documentClass: string, companyId: string, operationCenterId: string, documentTypeId: string): Promise; createDocument?(documentClass: string, companyId: string, operationCenterId: string, documentTypeId: string): Promise<{ documentId: string; }>; listDocuments?(request: DocumentListRequest): Promise; listCompanies?(): Promise; deleteDocument?(id: string): Promise; getStatusHistory?(documentId: string): Promise; checkConflict?(id: string, serverVersion: string): Promise; approve?(id: string): Promise; reject?(id: string, reason?: string): Promise; formatCurrency(value: number): string; formatDate?(value: string): string; formatNumber?(value: number): string; listCurrencies?(onlyActive?: boolean): Promise; getExchangeRate?(currencyCode: string): Promise<{ rate: number; label: string; }>; changeCurrency?(documentId: string, currencyCode: string): Promise; evaluateCustomRule?(ruleId: string, formData: Record): unknown; getLineWidgetData?(widgetKey: string, formData: Record, documentId: string | undefined): Promise>; evaluateServerRule?(ruleId: string, formData: Record, documentId: string | undefined): Promise>>; /** * Fetcher genérico para llamadas HTTP arbitrarias. * Usado por DocumentModalList para ejecutar acciones de toolbar y menú de contexto. * El path puede contener el baseUrl completo o ser relativo al baseUrl del adapter. */ genericFetch?(method: string, path: string, body?: unknown): Promise; /** * Valida en tiempo real si un serial está disponible para captura (modo * `capture`). Llamado al escanear/teclear cada serial. NO hace transición de * estado; solo consulta, por lo que es seguro llamarlo múltiples veces. * Endpoint sugerido: GET /api/v1/inventory/serials/{serialCode}/availability */ validateSerial?(params: ValidateSerialParams): Promise; /** * Obtiene la lista paginada de seriales disponibles (modo `select`). * Endpoint sugerido: GET /api/v1/inventory/serials?status=Available&... */ fetchAvailableSerials?(params: FetchAvailableSerialsParams): Promise; /** Paso 1: documentos origen candidatos para el `childClass`, filtrados por tercero/CO/estado. */ fetchAvailableSources?(params: FetchAvailableSourcesParams): Promise; /** Paso 2: líneas pendientes (bulk) de los orígenes seleccionados. */ fetchPendingLines?(params: FetchPendingLinesParams): Promise; /** Modo `create` (caso A): crea el documento hijo en DRAFT y devuelve su id. */ createFromSource?(request: CreateFromSourceRequest): Promise<{ documentId: string; }>; /** Modo `append` (caso B): agrega las líneas cruzadas al documento existente. */ appendFromSource?(documentId: string, request: AppendFromSourceRequest): Promise<{ addedLineIds: string[]; }>; } //# sourceMappingURL=MasterDocumentAdapter.types.d.ts.map