feat(web): lista de documentos con barra de filtros y búsqueda por columna
Cada pantalla de documento del menú abre primero la lista de los últimos documentos; desde ahí se crea uno (/nuevo) o se abre uno (/:docEntry). - sbo-list-view: título, botón de alta, barra de filtros, tabla y paginación (50 por página, más recientes primero). - sbo-filter-bar (como el FilterBar del mini-erp): búsqueda por la columna activa + filtros declarativos text / select (con valor por defecto) / multi (chips + lista de valores) / date (siempre rango [desde – hasta]), con Ir y Limpiar. Ir aplica búsqueda y filtros en una sola consulta. - Búsqueda numérica por igualdad (DocNum eq 314) con aviso si no es número; una columna con texto calculado (format) ya no es buscable por defecto. - Piezas compartidas con sbo-value-help, que se refactoriza sobre ellas: createSlList (estado de búsqueda/paginación), sbo-data-table, sbo-pager, sbo-search-bar. Insignias de estado (tone) y estilos de botón globales. - DOCUMENT_SCREENS: columnas y filtros de 17 documentos (ventas, compras, inventario) según los campos verificados en el Service Layer real (PurchaseRequests no tiene CardCode; StockTransfers no tiene estado). - Rutas generadas desde el registro: lista, nuevo y :docEntry bajo el permiso de la ruta de menú. El formulario es un marcador (paso 9). - Presupuesto: el aviso del bundle inicial sube a 300 kB; httpResource aporta ~18 kB al chunk compartido (278 kB, 78 kB comprimido). - Verificado en Chrome contra SAP real: 49 órdenes abiertas, 314 en total, rango de fechas, chips de monedas, búsqueda por número, navegación y botón atrás, tema oscuro; ofertas (4391), transferencias y entradas. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
68caf401a7
commit
598307960e
36 files changed
+2163
-359
No files matched your search
@@ -0,0 +1,159 @@
|
||||
import { MENU } from '@basic-erp/sap-types';
|
||||
import { formatNumber } from '../../core/format';
|
||||
import type { ListFilter } from '../../core/sl/list-filters';
|
||||
import type { ListColumn, ListRow, Tone } from '../../shared/list/list.types';
|
||||
|
||||
/** Pantalla de un documento: lista de los últimos ingresados + formulario (nuevo / ver-editar). */
|
||||
export interface DocumentScreen {
|
||||
/** Ruta de menú (`/ventas/ordenes`); de ella cuelgan `nuevo` y `:docEntry`. */
|
||||
path: string;
|
||||
/** Recurso del proxy `/api/sl`. */
|
||||
resource: string;
|
||||
createLabel: string;
|
||||
columns: readonly ListColumn[];
|
||||
/** Campos necesarios para pintar que no tienen columna propia. */
|
||||
extraFields?: readonly string[];
|
||||
filters: readonly ListFilter[];
|
||||
/** Los más recientes primero. */
|
||||
orderby: string;
|
||||
}
|
||||
|
||||
export const menuLabel = (path: string): string =>
|
||||
MENU.flatMap((m) => m.items).find((i) => i.path === path)?.label ?? path;
|
||||
|
||||
export function documentStatus(row: ListRow): { label: string; tone: Tone } | undefined {
|
||||
if (row['Cancelled'] === 'tYES') return { label: 'Cancelado', tone: 'err' };
|
||||
switch (row['DocumentStatus']) {
|
||||
case 'bost_Open':
|
||||
return { label: 'Abierto', tone: 'ok' };
|
||||
case 'bost_Close':
|
||||
return { label: 'Cerrado', tone: 'muted' };
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/** Total en la moneda del documento: SAP guarda en `DocTotalFc` el importe de los documentos en moneda extranjera. */
|
||||
export function documentTotal(row: ListRow): string {
|
||||
const fc = Number(row['DocTotalFc'] ?? 0);
|
||||
const amount = fc !== 0 ? fc : row['DocTotal'];
|
||||
if (amount === null || amount === undefined) return '';
|
||||
return `${row['DocCurrency'] ?? ''} ${formatNumber(amount, 2)}`.trim();
|
||||
}
|
||||
|
||||
const statusColumn: ListColumn = {
|
||||
key: 'DocumentStatus',
|
||||
title: 'Estado',
|
||||
width: '100px',
|
||||
format: (r) => documentStatus(r)?.label ?? '',
|
||||
tone: (r) => documentStatus(r)?.tone,
|
||||
};
|
||||
/** Número de documento: sin separador de miles, y buscable por igualdad (es numérico). */
|
||||
export const docNum: ListColumn = {
|
||||
key: 'DocNum',
|
||||
title: 'Número',
|
||||
type: 'number',
|
||||
align: 'left',
|
||||
width: '110px',
|
||||
mono: true,
|
||||
searchable: true,
|
||||
format: (r) => String(r['DocNum'] ?? ''),
|
||||
};
|
||||
const docDate: ListColumn = { key: 'DocDate', title: 'Fecha contab.', type: 'date', align: 'center', width: '115px' };
|
||||
|
||||
const statusFilter: ListFilter = {
|
||||
type: 'select',
|
||||
key: 'DocumentStatus',
|
||||
label: 'Estado',
|
||||
defaultValue: 'bost_Open',
|
||||
options: [
|
||||
{ value: 'bost_Open', label: 'Abierto' },
|
||||
{ value: 'bost_Close', label: 'Cerrado' },
|
||||
],
|
||||
};
|
||||
const dateFilter: ListFilter = { type: 'date', key: 'DocDate', label: 'Fecha contab.' };
|
||||
const currencyFilter: ListFilter = {
|
||||
type: 'multi',
|
||||
key: 'DocCurrency',
|
||||
label: 'Moneda',
|
||||
resource: 'Currencies',
|
||||
valueField: 'Code',
|
||||
labelField: 'Name',
|
||||
};
|
||||
|
||||
/** Ventas y compras: documentos con socio de negocio y total. */
|
||||
const marketing = (path: string, resource: string, createLabel: string, partner: string): DocumentScreen => ({
|
||||
path,
|
||||
resource,
|
||||
createLabel,
|
||||
orderby: 'DocEntry desc',
|
||||
extraFields: ['Cancelled', 'DocTotalFc', 'DocCurrency'],
|
||||
columns: [
|
||||
docNum,
|
||||
statusColumn,
|
||||
{ key: 'CardCode', title: partner, width: '130px', mono: true, searchable: true },
|
||||
{ key: 'CardName', title: 'Nombre' },
|
||||
docDate,
|
||||
{ key: 'DocDueDate', title: 'Fecha venc.', type: 'date', align: 'center', width: '115px' },
|
||||
{ key: 'DocTotal', title: 'Total', align: 'right', width: '140px', format: documentTotal },
|
||||
],
|
||||
filters: [statusFilter, currencyFilter, dateFilter],
|
||||
});
|
||||
|
||||
const inventory = (path: string, resource: string, createLabel: string, withStatus: boolean): DocumentScreen => ({
|
||||
path,
|
||||
resource,
|
||||
createLabel,
|
||||
orderby: 'DocEntry desc',
|
||||
extraFields: withStatus ? ['Cancelled'] : [],
|
||||
columns: [
|
||||
docNum,
|
||||
...(withStatus ? [statusColumn] : []),
|
||||
docDate,
|
||||
...(resource === 'StockTransfers' || resource === 'InventoryTransferRequests'
|
||||
? [
|
||||
{ key: 'FromWarehouse', title: 'Almacén origen', width: '130px', mono: true, searchable: true },
|
||||
{ key: 'ToWarehouse', title: 'Almacén destino', width: '130px', mono: true, searchable: true },
|
||||
]
|
||||
: []),
|
||||
{ key: 'Comments', title: 'Comentarios', searchable: true },
|
||||
],
|
||||
filters: withStatus ? [statusFilter, dateFilter] : [dateFilter],
|
||||
});
|
||||
|
||||
export const DOCUMENT_SCREENS: readonly DocumentScreen[] = [
|
||||
// Ventas
|
||||
marketing('/ventas/ofertas', 'Quotations', 'Nueva oferta', 'Cliente'),
|
||||
marketing('/ventas/ordenes', 'Orders', 'Nueva orden', 'Cliente'),
|
||||
marketing('/ventas/entrega', 'DeliveryNotes', 'Nueva entrega', 'Cliente'),
|
||||
marketing('/ventas/facturas', 'Invoices', 'Nueva factura', 'Cliente'),
|
||||
marketing('/ventas/devoluciones', 'Returns', 'Nueva devolución', 'Cliente'),
|
||||
marketing('/ventas/notas-credito', 'CreditNotes', 'Nueva nota de crédito', 'Cliente'),
|
||||
// Compras
|
||||
{
|
||||
path: '/compras/solicitudes',
|
||||
resource: 'PurchaseRequests',
|
||||
createLabel: 'Nueva solicitud',
|
||||
orderby: 'DocEntry desc',
|
||||
extraFields: ['Cancelled'],
|
||||
columns: [
|
||||
docNum,
|
||||
statusColumn,
|
||||
docDate,
|
||||
{ key: 'RequriedDate', title: 'Fecha requerida', type: 'date', align: 'center', width: '125px' },
|
||||
{ key: 'RequesterName', title: 'Solicitante', searchable: true },
|
||||
],
|
||||
filters: [statusFilter, dateFilter],
|
||||
},
|
||||
marketing('/compras/ofertas', 'PurchaseQuotations', 'Nueva oferta', 'Proveedor'),
|
||||
marketing('/compras/ordenes', 'PurchaseOrders', 'Nueva orden', 'Proveedor'),
|
||||
marketing('/compras/recepcion', 'PurchaseDeliveryNotes', 'Nueva recepción', 'Proveedor'),
|
||||
marketing('/compras/facturas', 'PurchaseInvoices', 'Nueva factura', 'Proveedor'),
|
||||
marketing('/compras/devoluciones', 'PurchaseReturns', 'Nueva devolución', 'Proveedor'),
|
||||
marketing('/compras/notas-credito', 'PurchaseCreditNotes', 'Nueva nota de crédito', 'Proveedor'),
|
||||
// Inventario
|
||||
inventory('/inventario/entradas', 'InventoryGenEntries', 'Nueva entrada', true),
|
||||
inventory('/inventario/salidas', 'InventoryGenExits', 'Nueva salida', true),
|
||||
inventory('/inventario/transferencias', 'StockTransfers', 'Nueva transferencia', false),
|
||||
inventory('/inventario/solicitudes-traslado', 'InventoryTransferRequests', 'Nueva solicitud', true),
|
||||
];
|
||||
Reference in new issue
Block a user