diff --git a/apps/backoffice/src/app/features/license-review/pages/LicenseReviewPage/index.tsx b/apps/backoffice/src/app/features/license-review/pages/LicenseReviewPage/index.tsx
index 4b0351b90..3cd1b1e9c 100644
--- a/apps/backoffice/src/app/features/license-review/pages/LicenseReviewPage/index.tsx
+++ b/apps/backoffice/src/app/features/license-review/pages/LicenseReviewPage/index.tsx
@@ -57,12 +57,14 @@ import {
useResumeApplicationMutation,
useScheduleInspectionMutation,
type RemarkTargetType,
+ type StaffEvidenceRequirement,
} from "@ema-platform/api";
import {
AdvancedTable,
AmharicDatePicker,
ErrorState,
ModalFooter,
+ PdfPreviewModal,
useServerTable,
} from "@ema-platform/ui";
import { useDateDisplayer } from "@ema-platform/shared";
@@ -178,6 +180,19 @@ export function LicenseReviewPage() {
),
[requirements],
);
+ // What each role is *required* to produce (CV, work agreement, ERB
+ // certificate). Without this the tab can only list what was uploaded, so a
+ // missing CV looks identical to a role that never needed one.
+ const evidenceByRole = useMemo(
+ () =>
+ new Map(
+ requirements?.staffRoleRequirements.map((r) => [
+ r.roleKey,
+ r.requiredEvidence ?? [],
+ ]) ?? [],
+ ),
+ [requirements],
+ );
const [completeReview] = useCompleteReviewMutation();
const [requestAdjustment] = useRequestAdjustmentMutation();
@@ -971,6 +986,7 @@ export function LicenseReviewPage() {
renderEvidence: (member) => (
),
@@ -1275,36 +1291,96 @@ export function LicenseReviewPage() {
*/
function StaffEvidenceCell({
staffId,
+ required,
fallback,
}: {
staffId: string;
+ /** What this person's role must produce, from the licence-type config. */
+ required: StaffEvidenceRequirement[];
fallback?: { id: string; documentKey: string; files: { url?: string }[] }[];
}) {
+ const localized = useLocalized();
+ const { t } = useTranslation();
+ const [preview, setPreview] = useState<{ url: string; title: string } | null>(
+ null,
+ );
const { data: attachments } = useGetAttachmentsQuery({
ownerType: "APPLICATION_STAFF",
ownerId: staffId,
});
const docs = attachments?.length ? attachments : (fallback ?? []);
+ const uploadedBy = new Map(docs.map((d) => [d.documentKey, d]));
+
+ // Drive the list off the requirements, not off what happens to have been
+ // uploaded: a mandatory CV that is absent has to be visible as absent, which
+ // is the whole point of the officer looking at this column. Anything
+ // uploaded outside the list still gets shown rather than silently dropped.
+ const extras = docs.filter(
+ (d) => !required.some((r) => r.docKey === d.documentKey),
+ );
+
+ if (!required.length && !extras.length) {
+ return (
+
+ —
+
+ );
+ }
+
+ const badge = (
+ key: string,
+ label: string,
+ url: string | undefined,
+ mandatory: boolean,
+ ) => {
+ const missing = !url;
+ return (
+
+ setPreview({ url: url, title: label })
+ }
+ >
+ {label}
+ {missing && mandatory ? " *" : ""}
+
+
+ );
+ };
return (
- {docs.map((doc) => {
- const url = doc.files?.[0]?.url;
- return (
-
- {doc.documentKey}
-
- );
- })}
+ {required.map((item) =>
+ badge(
+ item.docKey,
+ localized(item.label) || item.docKey,
+ uploadedBy.get(item.docKey)?.files?.[0]?.url,
+ item.mandatory,
+ ),
+ )}
+ {extras.map((doc) =>
+ badge(doc.id, doc.documentKey, doc.files?.[0]?.url, false),
+ )}
+ setPreview(null)}
+ url={preview?.url ?? ""}
+ title={preview?.title}
+ />
);
}