feat(portal): real diploma file upload on the self-service page (refs #103)

Replaces the S-10a stub button with a labelled file input (accept application/pdf);
the component base64-encodes the chosen file client-side and posts it (with its name
and type) keyed by the reference, confirming on success and surfacing a retryable
failure. The upload button stays disabled until a file is chosen.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
not
2026-07-20 12:15:49 +02:00
co-authored by Claude Opus 4.8
parent d354fe507a
commit 1abd4b6472
2 changed files with 54 additions and 14 deletions
@@ -19,11 +19,20 @@
Het aanleveren van uw documenten is niet gelukt. Probeer het opnieuw. Het aanleveren van uw documenten is niet gelukt. Probeer het opnieuw.
</p> </p>
} }
<p utrecht-paragraph>Lever uw diploma aan (PDF).</p>
<label utrecht-form-label for="diploma">Diploma</label>
<input
id="diploma"
type="file"
accept="application/pdf"
[disabled]="providingDocuments()"
(change)="onFileSelected($event)"
/>
<button <button
utrecht-button utrecht-button
appearance="primary-action-button" appearance="primary-action-button"
type="button" type="button"
[disabled]="providingDocuments()" [disabled]="providingDocuments() || !selectedFile()"
(click)="provideDocuments()" (click)="provideDocuments()"
> >
Documenten aanleveren Documenten aanleveren
@@ -29,6 +29,7 @@ export class RegistrationPage {
protected readonly providingDocuments = signal(false); protected readonly providingDocuments = signal(false);
protected readonly documentsProvided = signal(false); protected readonly documentsProvided = signal(false);
protected readonly provideDocumentsFailed = signal(false); protected readonly provideDocumentsFailed = signal(false);
protected readonly selectedFile = signal<File | undefined>(undefined);
submit(): void { submit(): void {
this.submitting.set(true); this.submitting.set(true);
@@ -47,14 +48,34 @@ export class RegistrationPage {
}); });
} }
provideDocuments(): void { onFileSelected(event: Event): void {
const input = event.target as HTMLInputElement;
this.selectedFile.set(input.files?.[0] ?? undefined);
}
async provideDocuments(): Promise<void> {
const reference = this.reference(); const reference = this.reference();
if (!reference) { const file = this.selectedFile();
if (!reference || !file) {
return; return;
} }
this.providingDocuments.set(true); this.providingDocuments.set(true);
this.provideDocumentsFailed.set(false); this.provideDocumentsFailed.set(false);
this.bff.postSelfServiceRegistrationsIdDocuments(reference).subscribe({ let contentBase64: string;
try {
contentBase64 = await readAsBase64(file);
} catch {
this.provideDocumentsFailed.set(true);
this.providingDocuments.set(false);
return;
}
this.bff
.postSelfServiceRegistrationsIdDocuments(reference, {
contentBase64,
fileName: file.name,
contentType: file.type || 'application/pdf',
})
.subscribe({
next: () => { next: () => {
this.documentsProvided.set(true); this.documentsProvided.set(true);
this.providingDocuments.set(false); this.providingDocuments.set(false);
@@ -87,3 +108,13 @@ export class RegistrationPage {
}); });
} }
} }
/** Read a file's bytes as a base64 string (without the `data:...;base64,` prefix). */
function readAsBase64(file: File): Promise<string> {
return new Promise<string>((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(((reader.result as string) ?? '').split(',', 2)[1] ?? '');
reader.onerror = () => reject(reader.error ?? new Error('Could not read the file.'));
reader.readAsDataURL(file);
});
}