feat(portal-self-service): tell the citizen why a diploma upload was refused (refs #192)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -14,10 +14,8 @@
|
|||||||
@if (documentsProvided()) {
|
@if (documentsProvided()) {
|
||||||
<p utrecht-paragraph role="status">Uw documenten zijn aangeleverd.</p>
|
<p utrecht-paragraph role="status">Uw documenten zijn aangeleverd.</p>
|
||||||
} @else {
|
} @else {
|
||||||
@if (provideDocumentsFailed()) {
|
@if (provideDocumentsError(); as error) {
|
||||||
<p utrecht-paragraph role="alert">
|
<p utrecht-paragraph role="alert">{{ error }}</p>
|
||||||
Het aanleveren van uw documenten is niet gelukt. Probeer het opnieuw.
|
|
||||||
</p>
|
|
||||||
}
|
}
|
||||||
<p utrecht-paragraph>Lever uw diploma aan (PDF).</p>
|
<p utrecht-paragraph>Lever uw diploma aan (PDF).</p>
|
||||||
<label utrecht-form-label for="diploma">Diploma</label>
|
<label utrecht-form-label for="diploma">Diploma</label>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Component, inject, type OnInit, signal } from '@angular/core';
|
import { Component, inject, type OnInit, signal } from '@angular/core';
|
||||||
import { BffApiV1Service, type CurrentRegistration, type SubmitAccepted } from 'api-client';
|
import { HttpErrorResponse } from '@angular/common/http';
|
||||||
|
import { BffApiV1Service, type CurrentRegistration, type Refusal, type SubmitAccepted } from 'api-client';
|
||||||
import { AuthService } from 'auth';
|
import { AuthService } from 'auth';
|
||||||
import { UtrechtComponentsModule } from 'ui';
|
import { UtrechtComponentsModule } from 'ui';
|
||||||
|
|
||||||
@@ -31,7 +32,8 @@ export class RegistrationPage implements OnInit {
|
|||||||
protected readonly withdrawFailed = signal(false);
|
protected readonly withdrawFailed = signal(false);
|
||||||
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);
|
/** Why the last upload failed, worded for the citizen; undefined while there is nothing to report. */
|
||||||
|
protected readonly provideDocumentsError = signal<string | undefined>(undefined);
|
||||||
protected readonly selectedFile = signal<File | undefined>(undefined);
|
protected readonly selectedFile = signal<File | undefined>(undefined);
|
||||||
|
|
||||||
/** Resume an existing in-flight registration after a refresh (S-26): the BFF returns the caller's
|
/** Resume an existing in-flight registration after a refresh (S-26): the BFF returns the caller's
|
||||||
@@ -80,12 +82,12 @@ export class RegistrationPage implements OnInit {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.providingDocuments.set(true);
|
this.providingDocuments.set(true);
|
||||||
this.provideDocumentsFailed.set(false);
|
this.provideDocumentsError.set(undefined);
|
||||||
let contentBase64: string;
|
let contentBase64: string;
|
||||||
try {
|
try {
|
||||||
contentBase64 = await readAsBase64(file);
|
contentBase64 = await readAsBase64(file);
|
||||||
} catch {
|
} catch {
|
||||||
this.provideDocumentsFailed.set(true);
|
this.provideDocumentsError.set(uploadFailure());
|
||||||
this.providingDocuments.set(false);
|
this.providingDocuments.set(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -101,8 +103,8 @@ export class RegistrationPage implements OnInit {
|
|||||||
this.providingDocuments.set(false);
|
this.providingDocuments.set(false);
|
||||||
},
|
},
|
||||||
// Surface the failure instead of swallowing it: keep the action so the user can retry.
|
// Surface the failure instead of swallowing it: keep the action so the user can retry.
|
||||||
error: () => {
|
error: (err: unknown) => {
|
||||||
this.provideDocumentsFailed.set(true);
|
this.provideDocumentsError.set(uploadFailure(err));
|
||||||
this.providingDocuments.set(false);
|
this.providingDocuments.set(false);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -129,6 +131,20 @@ export class RegistrationPage implements OnInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Word a failed upload for the citizen: the BFF says why a file was refused (422 + reason) or that
|
||||||
|
* the virus scanner was unreachable (503, S-29); anything else is a generic retry. */
|
||||||
|
function uploadFailure(err?: unknown): string {
|
||||||
|
if (err instanceof HttpErrorResponse && err.status === 422) {
|
||||||
|
return (err.error as Refusal | null)?.reason === 'infected'
|
||||||
|
? 'Er is een virus gevonden in dit bestand. Het is niet opgeslagen; lever een ander bestand aan.'
|
||||||
|
: 'Dit bestand is geen PDF. Lever uw diploma aan als PDF-bestand.';
|
||||||
|
}
|
||||||
|
if (err instanceof HttpErrorResponse && err.status === 503) {
|
||||||
|
return 'Uw bestand kan tijdelijk niet worden gecontroleerd. Probeer het later opnieuw.';
|
||||||
|
}
|
||||||
|
return 'Het aanleveren van uw documenten is niet gelukt. Probeer het opnieuw.';
|
||||||
|
}
|
||||||
|
|
||||||
/** Read a file's bytes as a base64 string (without the `data:...;base64,` prefix). */
|
/** Read a file's bytes as a base64 string (without the `data:...;base64,` prefix). */
|
||||||
function readAsBase64(file: File): Promise<string> {
|
function readAsBase64(file: File): Promise<string> {
|
||||||
return new Promise<string>((resolve, reject) => {
|
return new Promise<string>((resolve, reject) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user