feat(web): tablas en los maestros (direcciones, personas de contacto, precios por lista, stock por almacén)
sbo-rows-table reutilizable (también en la ventana de lotes). Direcciones y contactos se envían completos con reemplazo; los precios solo por filas cambiadas. Probado contra SBODemoCL. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e853b1b76c
commit
4e9d6a757d
14 files changed
+679
-96
No files matched your search
@@ -5,19 +5,25 @@ import { ActivatedRoute, Router, RouterLink } from '@angular/router';
|
||||
import { map } from 'rxjs';
|
||||
import { API_URL, apiErrorMessage } from '../../core/api';
|
||||
import { SlMasterApi } from '../../core/sl/sl-master';
|
||||
import { RowsTable } from '../../shared/grid/rows-table';
|
||||
import { Icon, SboWindow, Tab, Tabs, Toolbar, type StatusMessage, type WindowBadge } from '../../shared/window';
|
||||
import { updatePayload, type DocRecord } from '../documents/document-model';
|
||||
import type { HeaderField } from '../documents/document-layouts';
|
||||
import { MasterField } from './master-field';
|
||||
import {
|
||||
type CollectionRow,
|
||||
type MasterNavTarget,
|
||||
collectionPayload,
|
||||
collectionRowsOf,
|
||||
collectionsCreatePayload,
|
||||
createMasterPayload,
|
||||
masterCollectionsOf,
|
||||
masterKeysOf,
|
||||
masterRequiredOf,
|
||||
pickMaster,
|
||||
validateMaster,
|
||||
} from './master-model';
|
||||
import type { MasterScreen } from './master-types';
|
||||
import type { MasterCollection, MasterScreen } from './master-types';
|
||||
|
||||
const text = (v: unknown): string => (v === null || v === undefined ? '' : String(v));
|
||||
|
||||
@@ -30,8 +36,9 @@ const text = (v: unknown): string => (v === null || v === undefined ? '' : Strin
|
||||
@Component({
|
||||
selector: 'sbo-master-form-page',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [Icon, MasterField, RouterLink, SboWindow, Tab, Tabs, Toolbar],
|
||||
imports: [Icon, MasterField, RouterLink, RowsTable, SboWindow, Tab, Tabs, Toolbar],
|
||||
templateUrl: './master-form.page.html',
|
||||
styles: '.addrow { margin: 6px 0 0; }',
|
||||
})
|
||||
export class MasterFormPage {
|
||||
private readonly route = inject(ActivatedRoute);
|
||||
@@ -43,6 +50,7 @@ export class MasterFormPage {
|
||||
protected readonly layout = this.screen.layout;
|
||||
private readonly keys = masterKeysOf(this.layout);
|
||||
private readonly required = masterRequiredOf(this.layout);
|
||||
private readonly collections = masterCollectionsOf(this.layout);
|
||||
|
||||
protected readonly key = toSignal(this.route.paramMap.pipe(map((p) => p.get('key'))), {
|
||||
initialValue: this.route.snapshot.paramMap.get('key'),
|
||||
@@ -68,6 +76,22 @@ export class MasterFormPage {
|
||||
return pickMaster(doc, this.keys, this.layout.newHeader());
|
||||
}
|
||||
|
||||
/** Filas de las tablas (direcciones, contactos, precios…); se reinician al cargar o recargar el registro. */
|
||||
protected readonly tables = linkedSignal<Record<string, CollectionRow[]>>(() =>
|
||||
Object.fromEntries(this.collections.map((c) => [c.key, collectionRowsOf(this.doc(), c)])),
|
||||
);
|
||||
|
||||
/** Lo que cambió en cada tabla (colección → filas a enviar). */
|
||||
private tableChanges(): Record<string, CollectionRow[]> {
|
||||
const d = this.doc();
|
||||
return Object.fromEntries(
|
||||
this.collections.flatMap((c) => {
|
||||
const changed = collectionPayload(c, collectionRowsOf(d, c), this.tables()[c.key] ?? []);
|
||||
return changed ? [[c.key, changed] as const] : [];
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// --- estado --------------------------------------------------------------------------------
|
||||
|
||||
protected readonly saving = signal(false);
|
||||
@@ -82,7 +106,7 @@ export class MasterFormPage {
|
||||
protected readonly dirty = computed(() => {
|
||||
const d = this.doc();
|
||||
if (!d) return false;
|
||||
return Object.keys(updatePayload(this.pick(d), this.header(), [], []).body).length > 0;
|
||||
return Object.keys(updatePayload(this.pick(d), this.header(), [], []).body).length > 0 || Object.keys(this.tableChanges()).length > 0;
|
||||
});
|
||||
|
||||
protected readonly status = computed<StatusMessage>(() => {
|
||||
@@ -123,6 +147,24 @@ export class MasterFormPage {
|
||||
return this.lockedKeys.has(key);
|
||||
}
|
||||
|
||||
protected rowsOf(c: MasterCollection): CollectionRow[] {
|
||||
return this.tables()[c.key] ?? [];
|
||||
}
|
||||
|
||||
protected setRows(c: MasterCollection, rows: CollectionRow[]): void {
|
||||
this.tables.update((t) => ({ ...t, [c.key]: rows }));
|
||||
this.message.set(null);
|
||||
}
|
||||
|
||||
protected addRow(c: MasterCollection): void {
|
||||
this.setRows(c, [...this.rowsOf(c), { ...(c.newRow?.() ?? {}) }]);
|
||||
}
|
||||
|
||||
/** La tabla permite añadir y quitar filas. */
|
||||
protected growable(c: MasterCollection): boolean {
|
||||
return !c.readonly && c.mode === 'replace' && c.growable !== false;
|
||||
}
|
||||
|
||||
protected value(key: string): unknown {
|
||||
return this.header()[key];
|
||||
}
|
||||
@@ -153,17 +195,22 @@ export class MasterFormPage {
|
||||
try {
|
||||
const d = this.doc();
|
||||
if (!d) {
|
||||
const created = await this.api.create(this.screen.resource, createMasterPayload(this.header(), this.layout.fixed));
|
||||
const body = { ...createMasterPayload(this.header(), this.layout.fixed), ...collectionsCreatePayload(this.layout, this.tables()) };
|
||||
const created = await this.api.create(this.screen.resource, body);
|
||||
const key = text(created[this.screen.keyField]);
|
||||
await this.router.navigate(['..', key], { relativeTo: this.route, state: { created: key } });
|
||||
return;
|
||||
}
|
||||
const { body } = updatePayload(this.pick(d), this.header(), [], []);
|
||||
const changes = this.tableChanges();
|
||||
Object.assign(body, changes);
|
||||
if (!Object.keys(body).length) {
|
||||
this.message.set({ kind: 'info', text: 'No hay cambios que guardar.' });
|
||||
return;
|
||||
}
|
||||
await this.api.update(this.screen.resource, this.key()!, body, text(d['odata.etag']) || undefined);
|
||||
// Reemplazar la colección solo cuando se manda completa (direcciones, contactos), nunca en las que van por filas.
|
||||
const replace = this.collections.some((c) => c.mode === 'replace' && c.key in changes);
|
||||
await this.api.update(this.screen.resource, this.key()!, body, text(d['odata.etag']) || undefined, replace);
|
||||
this.message.set({ kind: 'ok', text: `${this.screen.singular} actualizado.` });
|
||||
this.resource.reload();
|
||||
} catch (e) {
|
||||
|
||||
Reference in new issue
Block a user