S16 postgresql opslag added
All checks were successful
Build, test and publish / verify (push) Successful in 7m31s

This commit is contained in:
2026-11-04 17:10:25 +01:00
parent f59cad115a
commit 694bf3fa86
89 changed files with 3087 additions and 252 deletions

View File

@@ -50,11 +50,33 @@ describe('EntityEditor', () => {
const saved = vi.fn();
component.saved.subscribe(saved);
await component.save();
expect(api.saveSchool).toHaveBeenCalledWith({ name: 'Studio' }, undefined);
expect(api.saveSchool).toHaveBeenCalledWith({ name: 'Studio' }, undefined, undefined);
expect(saved).toHaveBeenCalledOnce();
expect(component.busy()).toBe(false);
});
it('retains the revision from opening the editor across background refreshes', async () => {
api.saveDancer.mockReturnValue(of({ id: 3 }));
const fixture = TestBed.createComponent(EntityEditor);
fixture.componentRef.setInput('target', {
kind: 'dancer',
value: { id: 3, name: 'Sam', birthDate: '2010-01-01' },
});
fixture.componentRef.setInput('schoolId', 7);
fixture.componentRef.setInput('revision', 4);
const component = fixture.componentInstance;
component.ngOnInit();
fixture.componentRef.setInput('revision', 5);
component.name = 'Sam edited';
await component.save();
expect(api.saveDancer).toHaveBeenCalledWith(
7,
{ name: 'Sam edited', birthDate: '2010-01-01' },
3,
4,
);
});
it('keeps the editor open and preserves input when saving fails', async () => {
api.saveSchool.mockReturnValue(throwError(() => new HttpErrorResponse({ status: 0 })));
const component = editor({ kind: 'school' });
@@ -116,6 +138,7 @@ describe('EntityEditor', () => {
7,
expect.objectContaining({ preferredDayParts: 1 }),
undefined,
undefined,
);
});

View File

@@ -174,6 +174,8 @@ import { DialogFrame } from './dialog-frame';
})
export class EntityEditor implements OnInit {
readonly target = input.required<EditorTarget>();
readonly revision = input<number>();
private originalRevision?: number;
readonly schoolId = input<number>();
readonly shows = input<Show[]>([]);
readonly closed = output<void>();
@@ -207,6 +209,7 @@ export class EntityEditor implements OnInit {
return `${labels[this.target().kind]} ${this.target().value ? 'bewerken' : 'toevoegen'}`;
}
ngOnInit(): void {
this.originalRevision = this.revision();
const target = this.target();
if (!target.value) return;
if (target.kind === 'group') {
@@ -298,13 +301,14 @@ export class EntityEditor implements OnInit {
let request: Observable<unknown>;
switch (target.kind) {
case 'school':
request = this.api.saveSchool({ name: this.name.trim() }, id);
request = this.api.saveSchool({ name: this.name.trim() }, id, this.originalRevision);
break;
case 'dancer':
request = this.api.saveDancer(
schoolId!,
{ name: this.name.trim(), birthDate: this.date },
id,
this.originalRevision,
);
break;
case 'group':
@@ -318,6 +322,7 @@ export class EntityEditor implements OnInit {
styles: this.selectedStyles,
},
id,
this.originalRevision,
);
break;
case 'show':
@@ -329,6 +334,7 @@ export class EntityEditor implements OnInit {
preferredDayParts: this.preferredDayParts,
},
id,
this.originalRevision,
);
break;
}

View File

@@ -57,6 +57,8 @@ import { DialogFrame } from './dialog-frame';
`,
})
export class GroupMembers implements OnInit {
readonly revision = input<number>();
private originalRevision?: number;
readonly schoolId = input.required<number>();
readonly group = input.required<DanceGroup>();
readonly dancers = input.required<Dancer[]>();
@@ -70,6 +72,7 @@ export class GroupMembers implements OnInit {
);
private readonly api = inject(SchoolApi);
ngOnInit(): void {
this.originalRevision = this.revision();
this.memberIds.set([...this.group().dancerIds]);
}
async toggle(dancerId: number, event: Event): Promise<void> {
@@ -82,7 +85,10 @@ export class GroupMembers implements OnInit {
this.busy.set(true);
this.error.set('');
try {
await firstValueFrom(this.api.membership(this.schoolId(), this.group().id, dancerId, add));
await firstValueFrom(
this.api.membership(this.schoolId(), this.group().id, dancerId, add, this.originalRevision),
);
if (this.originalRevision !== undefined) this.originalRevision++;
this.memberIds.update((ids) =>
add ? [...ids, dancerId] : ids.filter((id) => id !== dancerId),
);

View File

@@ -57,7 +57,7 @@ describe('ScheduleEditor', () => {
const draft = { ...show, finalized: false, violations: ['Group 1 requires 1 dayparts.'] };
api.saveAssignments.mockReturnValue(of(draft));
await component.save();
expect(api.saveAssignments).toHaveBeenCalledWith(7, 4, [[2], [3]]);
expect(api.saveAssignments).toHaveBeenCalledWith(7, 4, [[2], [3]], undefined);
expect(component.current()).toEqual(draft);
expect(component.dirty()).toBe(false);
expect(api.finalize).not.toHaveBeenCalled();

View File

@@ -113,6 +113,8 @@ import { DialogFrame } from './dialog-frame';
export class ScheduleEditor implements OnInit {
readonly show = input.required<Show>();
readonly groups = input.required<DanceGroup[]>();
readonly revision = input<number>();
private originalRevision?: number;
readonly schoolId = input.required<number>();
readonly closed = output<void>();
readonly saved = output<void>();
@@ -124,6 +126,7 @@ export class ScheduleEditor implements OnInit {
assignments: number[][] = [];
ngOnInit(): void {
this.originalRevision = this.revision();
this.current.set(this.show());
this.assignments = this.show().dayParts.map((p) => [...p.danceGroupIds]);
}
@@ -162,9 +165,15 @@ export class ScheduleEditor implements OnInit {
try {
const result = await firstValueFrom(
finalize
? this.api.finalize(this.schoolId(), this.show().id)
: this.api.saveAssignments(this.schoolId(), this.show().id, this.assignments),
? this.api.finalize(this.schoolId(), this.show().id, this.originalRevision)
: this.api.saveAssignments(
this.schoolId(),
this.show().id,
this.assignments,
this.originalRevision,
),
);
if (this.originalRevision !== undefined) this.originalRevision++;
this.current.set(result);
this.assignments = result.dayParts.map((p) => [...p.danceGroupIds]);
this.dirty.set(false);

View File

@@ -10,6 +10,7 @@ interface Preview {
shows: number;
existingId: number | null;
existingName: string | null;
existingRevision: number | null;
}
@Component({
selector: 'app-school-transfer',
@@ -117,7 +118,12 @@ export class SchoolTransfer {
const query = p.existingId === null ? '' : `?replaceId=${p.existingId}`;
await firstValueFrom(
this.http.post(`${this.base}/import${query}`, this.json, {
headers: { 'Content-Type': 'application/json' },
headers: {
'Content-Type': 'application/json',
...(p.existingRevision === null
? {}
: { 'X-School-Revision': String(p.existingRevision) }),
},
}),
);
this.cancel();