feat(WP-67): merge behandelportal into this repo as a monorepo
Restructures into apps/ssp + apps/behandelportal (two Angular projects) plus libs/shared + libs/beheer (cross-app libraries), replacing WP-61's separate sibling repo. That split had already produced real drift: a hand-vendored copy of the backend's OpenAPI doc, a shared/ui+layout tree forked and silently diverging (7 files), and beheer + the styles.scss token bridge duplicated byte-for-byte across both repos. - git mv the SSP's src/app/* into apps/ssp/; fold shared/, beheer/, environments/, the Storybook docs/*.mdx, and styles.scss into libs/shared + libs/beheer (all confirmed identical between the two repos before merging). auth stays deliberately duplicated per ADR-0002 (actor-specific, expected to diverge) - amended there. - One generated API client (libs/shared), no more vendored swagger.json. - .dependency-cruiser split into a base factory + one config per app, and Storybook into .storybook-ssp/.storybook-behandelportal - both forced by the @auth/* alias resolving to different directories per app. - SiteHeaderComponent/ShellComponent gained HEADER_NAV_ITEMS/ HEADER_ADMIN_LINKS/DEBUG_PANEL injection tokens so each app supplies its own nav/admin-links/dev-panel instead of one being hardcoded. - CLAUDE.md, ARCHITECTURE.md, dependencies.md, and ADR-0002 updated; WP-67 backlog entry documents the full decision trail. npm run ci green (lint, dep:check x2, 360 tests across ssp/ behandelportal/shared/beheer, both localized builds, backend tests, snippet + api-client drift); both dev servers, both Storybook instances, and docker compose verified working. The old sibling repo (/home/eho/repos/behandelportal) is left untouched, not deleted. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { Result, ok, err } from '@shared/kernel/fp';
|
||||
import { runSubmit } from '@shared/application/submit';
|
||||
import { currentRole } from '@shared/infrastructure/role';
|
||||
import { problemDetail } from '@shared/infrastructure/api-error';
|
||||
import { environment } from '@shared/environments/environment';
|
||||
import {
|
||||
ApiClient,
|
||||
OrgTemplateAdminViewDto,
|
||||
OrgTemplateDto,
|
||||
OrgTemplateVersionDto,
|
||||
PublishOrgTemplateResponse,
|
||||
SubOrgSummaryDto,
|
||||
} from '@shared/infrastructure/api-client';
|
||||
import {
|
||||
OrgTemplate,
|
||||
OrgTemplateAdminView,
|
||||
OrgTemplateVersion,
|
||||
PublishResult,
|
||||
SubOrgSummary,
|
||||
} from '@brief/domain/org-template';
|
||||
import { parseOrgTemplate } from '@brief/infrastructure/brief.adapter';
|
||||
|
||||
/**
|
||||
* The only place admin org-template HTTP lives (ADR-0001 boundary). CRUD/publish/
|
||||
* rollback go through the generated client (X-Role added by `roleInterceptor`);
|
||||
* `parse*` narrows the untrusted wire shape. The proefbrief is `text/html` and
|
||||
* `ExcludeFromDescription`'d — a hand-written fetch, same seam as `letter-preview.adapter`.
|
||||
*/
|
||||
|
||||
const FAILED = $localize`:@@orgTemplate.action.failed:De actie is niet gelukt. Probeer het later opnieuw.`;
|
||||
const PROEFBRIEF_FAILED = $localize`:@@orgTemplate.proefbrief.failed:De proefbrief kon niet worden geopend.`;
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class OrgTemplateAdapter {
|
||||
private client = inject(ApiClient);
|
||||
|
||||
async list(): Promise<Result<string, SubOrgSummary[]>> {
|
||||
const r = await runSubmit(() => this.client.orgTemplates(), FAILED);
|
||||
if (!r.ok) return r;
|
||||
const out: SubOrgSummary[] = [];
|
||||
for (const s of r.value ?? []) {
|
||||
const parsed = parseSubOrg(s);
|
||||
if (!parsed.ok) return parsed;
|
||||
out.push(parsed.value);
|
||||
}
|
||||
return ok(out);
|
||||
}
|
||||
|
||||
async load(subOrgId: string): Promise<Result<string, OrgTemplateAdminView>> {
|
||||
const r = await runSubmit(() => this.client.orgTemplateGET(subOrgId), FAILED);
|
||||
return r.ok ? parseAdminView(r.value) : r;
|
||||
}
|
||||
|
||||
async save(subOrgId: string, draft: OrgTemplate): Promise<Result<string, OrgTemplateAdminView>> {
|
||||
const r = await runSubmit(
|
||||
() => this.client.orgTemplatePUT(subOrgId, { draft: toDto(draft) }),
|
||||
FAILED,
|
||||
);
|
||||
return r.ok ? parseAdminView(r.value) : r;
|
||||
}
|
||||
|
||||
async publish(subOrgId: string): Promise<Result<string, PublishResult>> {
|
||||
const r = await runSubmit(() => this.client.orgTemplatePublish(subOrgId), FAILED);
|
||||
return r.ok ? parsePublish(r.value) : r;
|
||||
}
|
||||
|
||||
async rollback(subOrgId: string, version: number): Promise<Result<string, OrgTemplateAdminView>> {
|
||||
const r = await runSubmit(() => this.client.orgTemplateRollback(subOrgId, version), FAILED);
|
||||
return r.ok ? parseAdminView(r.value) : r;
|
||||
}
|
||||
|
||||
/** Proefbrief: the unpublished draft rendered over a fixture letter, opened as a Blob. */
|
||||
async proefbrief(subOrgId: string): Promise<Result<string, Blob>> {
|
||||
let res: Response;
|
||||
try {
|
||||
res = await fetch(
|
||||
`${environment.apiBaseUrl}/api/v1/admin/org-template/${encodeURIComponent(subOrgId)}/preview`,
|
||||
{ headers: { 'X-Role': currentRole() } },
|
||||
);
|
||||
} catch {
|
||||
return err(PROEFBRIEF_FAILED);
|
||||
}
|
||||
if (!res.ok) {
|
||||
try {
|
||||
return err(problemDetail(await res.json(), PROEFBRIEF_FAILED));
|
||||
} catch {
|
||||
return err(PROEFBRIEF_FAILED);
|
||||
}
|
||||
}
|
||||
return ok(await res.blob());
|
||||
}
|
||||
}
|
||||
|
||||
// --- parse: wire → domain, validating at the boundary ---
|
||||
|
||||
function parseSubOrg(dto: SubOrgSummaryDto): Result<string, SubOrgSummary> {
|
||||
if (typeof dto.subOrgId !== 'string' || typeof dto.orgName !== 'string')
|
||||
return err('sub-org: bad shape');
|
||||
return ok({
|
||||
subOrgId: dto.subOrgId,
|
||||
orgName: dto.orgName,
|
||||
publishedVersion: dto.publishedVersion ?? 0,
|
||||
});
|
||||
}
|
||||
|
||||
function parseVersion(dto: OrgTemplateVersionDto): Result<string, OrgTemplateVersion> {
|
||||
if (typeof dto.version !== 'number' || typeof dto.publishedAt !== 'string')
|
||||
return err('version: bad shape');
|
||||
const template = parseOrgTemplate(dto.template);
|
||||
if (!template.ok) return template;
|
||||
return ok({ version: dto.version, publishedAt: dto.publishedAt, template: template.value });
|
||||
}
|
||||
|
||||
export function parseOrgTemplateAdminView(
|
||||
dto: OrgTemplateAdminViewDto,
|
||||
): Result<string, OrgTemplateAdminView> {
|
||||
const draft = parseOrgTemplate(dto.draft);
|
||||
if (!draft.ok) return draft;
|
||||
if (typeof dto.publishedVersion !== 'number' || typeof dto.unsentBriefs !== 'number')
|
||||
return err('admin-view: bad shape');
|
||||
const history: OrgTemplateVersion[] = [];
|
||||
for (const v of dto.history ?? []) {
|
||||
const parsed = parseVersion(v);
|
||||
if (!parsed.ok) return parsed;
|
||||
history.push(parsed.value);
|
||||
}
|
||||
return ok({
|
||||
draft: draft.value,
|
||||
publishedVersion: dto.publishedVersion,
|
||||
history,
|
||||
unsentBriefs: dto.unsentBriefs,
|
||||
});
|
||||
}
|
||||
|
||||
const parseAdminView = parseOrgTemplateAdminView;
|
||||
|
||||
function parsePublish(dto: PublishOrgTemplateResponse): Result<string, PublishResult> {
|
||||
if (typeof dto.version !== 'number' || typeof dto.affectedUnsentBriefs !== 'number')
|
||||
return err('publish: bad shape');
|
||||
return ok({ version: dto.version, affectedUnsentBriefs: dto.affectedUnsentBriefs });
|
||||
}
|
||||
|
||||
// --- toDto: domain → wire (for save) ---
|
||||
|
||||
function toDto(t: OrgTemplate): OrgTemplateDto {
|
||||
return {
|
||||
subOrgId: t.subOrgId,
|
||||
orgName: t.orgName,
|
||||
returnAddress: t.returnAddress,
|
||||
...(t.logoDocumentId != null ? { logoDocumentId: t.logoDocumentId } : {}),
|
||||
footerContact: t.footerContact,
|
||||
footerLegal: t.footerLegal,
|
||||
signatureName: t.signatureName,
|
||||
signatureRole: t.signatureRole,
|
||||
signatureClosing: t.signatureClosing,
|
||||
margins: { ...t.margins },
|
||||
version: t.version,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user