feat(web): maestros de clientes, proveedores y artículos (lista, formulario con pestañas, extensiones por cliente)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
GeorgeandClaude Sonnet 5.5 committed 2026-10-16 10:24:39 -05:00
1 parent b759c7bce7
commit 9f003ee4bf
30 files changed
+1553 -23

No files matched your search

@@ -12,9 +12,22 @@ export type ComputedSource = 'docNum' | 'partnerName' | 'partnerTaxId' | 'subtot
* de campos y el formulario base los dibuja: no hay HTML por documento.
*/
export type HeaderField =
| { type: 'text'; key: string; label: string; readonly?: boolean }
| {
type: 'text';
key: string;
label: string;
readonly?: boolean;
/** El código de un maestro no se cambia una vez creado. */
immutable?: boolean;
required?: boolean;
missing?: string;
}
| { type: 'date'; key: string; label: string; required?: boolean; missing?: string }
| { type: 'number'; key: string; label: string; decimals?: number }
| { type: 'number'; key: string; label: string; decimals?: number; readonly?: boolean }
/** Casilla: en SAP un campo `tYES` / `tNO`. */
| { type: 'checkbox'; key: string; label: string; readonly?: boolean }
/** Campo de código con su ventana de selección (grupo, moneda, lista de precios…). */
| { type: 'lookup'; key: string; label: string; lookup: VhLookup; required?: boolean; missing?: string }
| { type: 'textarea'; key: string; label: string }
/** Lista de valores válidos (típico de un campo de usuario con valores válidos en SAP). */
| { type: 'select'; key: string; label: string; options: readonly { value: string | number; label: string }[]; required?: boolean; missing?: string }
@@ -221,28 +234,35 @@ const allFields = (layout: DocumentLayout): HeaderField[] => [
...layout.footer.right,
];
/** Campos de cabecera que se editan (y se envían a SAP). */
export const headerKeysOf = (layout: DocumentLayout): readonly string[] =>
allFields(layout).flatMap((f) => {
/** Campos de SAP que se editan (y se envían) de una lista de campos. */
export const keysOfFields = (fields: readonly HeaderField[]): string[] =>
fields.flatMap((f) => {
switch (f.type) {
case 'computed':
return [];
case 'freight':
return ['FreightExpense', 'FreightAmount'];
case 'folio':
return [f.prefixKey, f.numberKey];
case 'freight':
return ['FreightExpense', 'FreightAmount'];
default:
return [f.key];
}
});
/** Campos obligatorios de una lista de campos, con el texto para el aviso ("Indique el cliente."). */
export const requiredOfFields = (fields: readonly HeaderField[]): { key: string; label: string }[] =>
fields.flatMap((f) => (isRequired(f) ? [{ key: f.key, label: f.missing ?? f.label }] : []));
/** Campos de cabecera que se editan (y se envían a SAP). */
export const headerKeysOf = (layout: DocumentLayout): readonly string[] => keysOfFields(allFields(layout));
/** Campos obligatorios de la cabecera, con el texto para el aviso ("Indique el cliente."). */
export const requiredHeaderOf = (layout: DocumentLayout): { key: string; label: string }[] =>
allFields(layout).flatMap((f) =>
(f.type === 'date' || f.type === 'warehouse' || f.type === 'partner' || f.type === 'select') && f.required
? [{ key: f.key, label: f.missing ?? f.label }]
: [],
);
export const requiredHeaderOf = (layout: DocumentLayout): { key: string; label: string }[] => requiredOfFields(allFields(layout));
/** Campos con clave y marca de obligatorio. */
export function isRequired(f: HeaderField): f is HeaderField & { key: string; required: true; missing?: string } {
return 'required' in f && f.required === true && 'key' in f;
}
export const hasFreight = (layout: DocumentLayout): boolean => allFields(layout).some((f) => f.type === 'freight');