Files
emaui/apps/portal/src/app/features/licensing/components/ApplicationSummary.tsx

142 lines
4.3 KiB
TypeScript

import {
Badge,
Divider,
Group,
Grid,
Paper,
Stack,
Text,
Title,
} from "@mantine/core";
import {
conditionHolds,
displayFieldValue,
type Attachment,
type FormFieldConfig,
type FormSectionConfig,
type LicenseTypeRequirements,
} from "@ema-platform/api";
import { useDateDisplayer } from "@ema-platform/shared";
import { useTranslation } from "react-i18next";
import { DocumentSlots } from "./DocumentSlots";
interface Props {
/** Every form section (not just the "review" group) in wizard-step order. */
sections: FormSectionConfig[];
formData: Record<string, Record<string, unknown>>;
localized: (value: { en?: string; am?: string } | undefined) => string;
config: LicenseTypeRequirements;
attachments: Attachment[];
applicationId: string;
}
/**
* Read-only "what was filed" view for a submitted application: every
* answered section as a labelled table, then the uploaded documents.
*
* Shown instead of the wizard once there is nothing left to step through —
* the stepper is for filling a form in, not for re-reading one that is
* already someone else's decision to make.
*/
export function ApplicationSummary({
sections,
formData,
localized,
config,
attachments,
applicationId,
}: Props) {
const showDate = useDateDisplayer();
const { i18n } = useTranslation();
// Shared with the officer's review screen, so the applicant and the reviewer
// never read the same answer two different ways.
const display = (field: FormFieldConfig, raw: unknown) =>
displayFieldValue(field, raw, {
language: i18n.language,
showDate,
currency: config.feeCurrency,
}) || "—";
return (
<Stack gap="md">
{sections.map((section) => {
const fields = (section.fields ?? []).filter((f) =>
conditionHolds(f.showWhen, formData),
);
if (fields.length === 0) return null;
return (
<Paper withBorder p="lg" radius="md" key={section.key}>
<Group justify="space-between" align="center" mb="xs">
<Title order={5}>{localized(section.title)}</Title>
<Badge variant="light" color="gray" size="sm">
{fields.length} {fields.length === 1 ? "detail" : "details"}
</Badge>
</Group>
{localized(section.description) && (
<Text fz="xs" c="dimmed" mb="sm">
{localized(section.description)}
</Text>
)}
<Divider mb="md" />
{/* Label above value in two columns — a definition list reads far
better than a bordered grid when most answers are short. */}
<Grid gutter="md">
{fields.map((field) => {
const value = display(
field,
formData[section.key]?.[field.key],
);
const answered = value !== "—";
return (
<Grid.Col
span={{
base: 12,
sm: field.type === "TEXTAREA" ? 12 : 6,
}}
key={field.key}
>
<Text fz="xs" c="dimmed" tt="uppercase" fw={600}>
{localized(field.label)}
</Text>
<Text
fz="sm"
mt={2}
c={answered ? undefined : "dimmed"}
fs={answered ? undefined : "italic"}
style={{ wordBreak: "break-word" }}
>
{answered ? value : "Not provided"}
</Text>
</Grid.Col>
);
})}
</Grid>
</Paper>
);
})}
<Paper withBorder p="lg" radius="md">
<Title order={5} mb="sm">
Documents
</Title>
<Divider mb="md" />
<DocumentSlots
requirements={config.documentRequirements}
attachments={attachments}
formData={formData}
ownerType="APPLICATION"
ownerId={applicationId}
readOnly
onUploaded={() => {
// Read-only here — nothing to react to, but DocumentSlots
// requires the callback.
}}
/>
</Paper>
</Stack>
);
}