Skip to content

Commit

Permalink
primary identification screen with document type
Browse files Browse the repository at this point in the history
  • Loading branch information
faiza-jahanzeb committed Feb 11, 2025
1 parent 1f1a6cf commit 877a2bf
Show file tree
Hide file tree
Showing 4 changed files with 205 additions and 78 deletions.
13 changes: 12 additions & 1 deletion frontend/app/.server/locales/protected-en.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
"required": "Current status in Canada is required.",
"invalid": "Please select Canadian Citizen born outside Canada.",
"options": {
"select-option": "Select option",
"canadian-citizen-born-in-canada": "Canadian citizen born in Canada",
"canadian-citizen-born-outside-canada": "Canadian citizen born outside Canada",
"registered-indian-born-in-canada": "Registered Indian born in Canada",
Expand All @@ -62,7 +63,17 @@
"no-legal-status-in-canada": "No Legal Status in Canada"
}
},
"please-select": "Please select",
"document-type": {
"title": "Document type",
"required": "Document type is required.",
"options": {
"select-option": "Select option",
"certificate-of-canadian-citizenship": "Certificate of Canadian citizenship",
"certificate-of-registration-of-birth-abroad": "Certificate of Registration of Birth Abroad",
"birth-certificate-and-certificate-of-indian-status": "Birth certificates and Certificates of Indian Status",
"certificate-of-canadian-citizenship-and-certificate-of-indian-status": "Certificate of Canadian Citizenship and Certificate of Indian Status"
}
},
"page-title": "Primary identity document"
},
"request-details": {
Expand Down
13 changes: 12 additions & 1 deletion frontend/app/.server/locales/protected-fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
"required": "Statut actuel au Canada est requis.",
"invalid": "Veuillez sélectionner un citoyen canadien né en dehors du Canada.",
"options": {
"select-option": "Sélectionner une option",
"canadian-citizen-born-in-canada": "Citoyen canadien né au Canada",
"canadian-citizen-born-outside-canada": "Citoyen canadien né à l'extérieur du Canada",
"registered-indian-born-in-canada": "Indien enregistré né au Canada",
Expand All @@ -62,7 +63,17 @@
"no-legal-status-in-canada": "Aucun statut légal au Canada"
}
},
"please-select": "Veuillez sélectionner",
"document-type": {
"title": "Type de document",
"required": "Le type de document est requis.",
"options": {
"select-option": "Sélectionner une option",
"certificate-of-canadian-citizenship": "Certificat de citoyenneté canadienne",
"certificate-of-registration-of-birth-abroad": "Certificat d'enregistrement de naissance à l'étranger",
"birth-certificate-and-certificate-of-indian-status": "Certificats de naissance et certificats de statut indien",
"certificate-of-canadian-citizenship-and-certificate-of-indian-status": "Certificat de citoyenneté canadienne et certificat de statut indien"
}
},
"page-title": "Document d'identité principal"
},
"request-details": {
Expand Down
5 changes: 4 additions & 1 deletion frontend/app/@types/express-session.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,10 @@ declare module 'express-session' {
type: string;
scenario: string;
};
currentStatusInCanada?: string;
primaryDocuments?: {
currentStatusInCanada: string;
documentType: string;
};
};
}
}
Expand Down
252 changes: 177 additions & 75 deletions frontend/app/routes/protected/person-case/primary-docs.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useId } from 'react';
import { useId, useState } from 'react';

import { data, useFetcher } from 'react-router';
import type { RouteHandle } from 'react-router';
import type { RouteHandle, SessionData } from 'react-router';

import { faExclamationCircle, faXmark } from '@fortawesome/free-solid-svg-icons';
import { useTranslation } from 'react-i18next';
Expand All @@ -16,10 +16,23 @@ import { FetcherErrorSummary } from '~/components/error-summary';
import { InputSelect } from '~/components/input-select';
import { PageTitle } from '~/components/page-title';
import { Progress } from '~/components/progress';
import { AppError } from '~/errors/app-error';
import { ErrorCodes } from '~/errors/error-codes';
import { getFixedT } from '~/i18n-config.server';
import { handle as parentHandle } from '~/routes/protected/layout';
import { getLanguage } from '~/utils/i18n-utils';

type PrimaryDocumentsSessionData = NonNullable<SessionData['inPersonSINCase']['primaryDocuments']>;

/**
* Valid current status in Canada for proof of concept
*/
const VALID_CURRENT_STATUS = ['canadian-citizen-born-outside-canada'] as const;
/**
* Valid document type for proof of concept
*/
const VALID_DOCTYPE = ['certificate-of-canadian-citizenship', 'certificate-of-registration-of-birth-abroad'] as const;

export const handle = {
i18nNamespace: [...parentHandle.i18nNamespace, 'protected'],
} as const satisfies RouteHandle;
Expand All @@ -30,9 +43,7 @@ export async function loader({ context, request }: Route.LoaderArgs) {

return {
documentTitle: t('protected:primary-identity-document.page-title'),
defaultFormValues: {
currentStatusInCanada: context.session.inPersonSINCase?.currentStatusInCanada,
},
defaultFormValues: context.session.inPersonSINCase?.primaryDocuments,
};
}

Expand All @@ -46,37 +57,45 @@ export async function action({ context, request }: Route.ActionArgs) {
const t = await getFixedT(request, handle.i18nNamespace);

const formData = await request.formData();
const action = formData.get('action');
const documentTypeAvailable = formData.get('documentType') != null;

if (formData.get('action') === 'back') {
throw i18nRedirect('routes/protected/person-case/privacy-statement.tsx', request); //TODO: change it to redirect to file="routes/protected/person-case/request-details.tsx"
}
switch (action) {
case 'back': {
throw i18nRedirect('routes/protected/person-case/request-details.tsx', request);
}

// submit action
const schema = v.object({
currentStatusInCanada: v.pipe(
v.string(t('protected:primary-identity-document.current-status-in-canada.required')),
v.trim(),
v.nonEmpty(t('protected:primary-identity-document.current-status-in-canada.required')),
v.literal(
'canadian-citizen-born-outside-canada',
t('protected:primary-identity-document.current-status-in-canada.invalid'),
),
),
});

const input = { currentStatusInCanada: formData.get('currentStatusInCanada') as string };
const parsedDataResult = v.safeParse(schema, input, { lang });

if (!parsedDataResult.success) {
return data({ errors: v.flatten<typeof schema>(parsedDataResult.issues).nested }, { status: 400 });
}
case 'next': {
const schema = v.object({
currentStatusInCanada: v.pipe(
v.string(t('protected:primary-identity-document.current-status-in-canada.required')),
v.trim(),
v.nonEmpty(t('protected:primary-identity-document.current-status-in-canada.required')),
v.picklist(VALID_CURRENT_STATUS, t('protected:primary-identity-document.current-status-in-canada.invalid')),
),
documentType: v.picklist(VALID_DOCTYPE, t('protected:primary-identity-document.document-type.required')),
}) satisfies v.GenericSchema<PrimaryDocumentsSessionData>;

context.session.inPersonSINCase = {
...(context.session.inPersonSINCase ?? {}),
...input,
};
const input = {
currentStatusInCanada: formData.get('currentStatusInCanada') as string,
documentType: documentTypeAvailable ? (formData.get('documentType') as string) : undefined,
} satisfies Partial<PrimaryDocumentsSessionData>;

const parseResult = v.safeParse(schema, input, { lang });

if (!parseResult.success) {
return data({ errors: v.flatten<typeof schema>(parseResult.issues).nested }, { status: 400 });
}

throw i18nRedirect('routes/protected/request.tsx', request); //TODO: change it to redirect to file="routes/protected/person-case/secondary-docs.tsx"
context.session.inPersonSINCase ??= {};
context.session.inPersonSINCase.primaryDocuments = parseResult.output;

throw i18nRedirect('routes/protected/request.tsx', request); //TODO: change it to redirect to file="routes/protected/person-case/secondary-docs.tsx"
}
default: {
throw new AppError(`Unrecognized action: ${action}`, ErrorCodes.UNRECOGNIZED_ACTION);
}
}
}

export default function PrimaryDocs({ loaderData, actionData, params }: Route.ComponentProps) {
Expand All @@ -87,41 +106,11 @@ export default function PrimaryDocs({ loaderData, actionData, params }: Route.Co
const isSubmitting = fetcher.state !== 'idle';
const errors = fetcher.data?.errors;

const dummyOption: { label: string; value: string } = {
label: t('protected:primary-identity-document.please-select'),
value: '',
const [currentStatus, setCurrentStatus] = useState(loaderData.defaultFormValues?.currentStatusInCanada);

const handleCurrentStatusChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
setCurrentStatus(event.target.value);
};
const currentStatusInCanadaOptions: { label: string; value: string }[] = [
dummyOption,
{
label: t('protected:primary-identity-document.current-status-in-canada.options.canadian-citizen-born-in-canada'),
value: 'canadian-citizen-born-in-canada',
},
{
label: t('protected:primary-identity-document.current-status-in-canada.options.canadian-citizen-born-outside-canada'),
value: 'canadian-citizen-born-outside-canada',
},
{
label: t('protected:primary-identity-document.current-status-in-canada.options.registered-indian-born-in-canada'),
value: 'registered-indian-born-in-canada',
},
{
label: t('protected:primary-identity-document.current-status-in-canada.options.registered-indian-born-outside-canada'),
value: 'registered-indian-born-outside-canada',
},
{
label: t('protected:primary-identity-document.current-status-in-canada.options.permanent-resident'),
value: 'permanent-resident',
},
{
label: t('protected:primary-identity-document.current-status-in-canada.options.temporary-resident'),
value: 'temporary-resident',
},
{
label: t('protected:primary-identity-document.current-status-in-canada.options.no-legal-status-in-canada'),
value: 'no-legal-status-in-canada',
},
];

return (
<>
Expand All @@ -135,17 +124,23 @@ export default function PrimaryDocs({ loaderData, actionData, params }: Route.Co
</div>
<Progress className="mt-8" label="" value={30} />
<PageTitle subTitle={t('protected:in-person.title')}>{t('protected:primary-identity-document.page-title')}</PageTitle>

<FetcherErrorSummary fetcherKey={fetcherKey}>
<fetcher.Form method="post" noValidate>
<InputSelect
id="currentStatusInCanada"
name="currentStatusInCanada"
errorMessage={errors?.currentStatusInCanada?.at(0)}
defaultValue={loaderData.defaultFormValues.currentStatusInCanada}
required
options={currentStatusInCanadaOptions}
label={t('protected:primary-identity-document.current-status-in-canada.title')}
/>
<div className="space-y-6">
<CurrentStatusInCanada
defaultValue={loaderData.defaultFormValues?.currentStatusInCanada}
errorMessage={errors?.currentStatusInCanada?.at(0)}
onChange={handleCurrentStatusChange}
/>
{currentStatus && (
<DocumentType
currentStatus={currentStatus}
defaultValue={loaderData.defaultFormValues?.documentType}
errorMessage={errors?.documentType?.at(0)}
/>
)}
</div>
<div className="mt-8 flex flex-wrap items-center gap-3">
<Button name="action" value="back" id="back-button" disabled={isSubmitting}>
{t('protected:person-case.previous')}
Expand All @@ -159,3 +154,110 @@ export default function PrimaryDocs({ loaderData, actionData, params }: Route.Co
</>
);
}

interface CurrentStatusInCanadaProps {
defaultValue?: string;
errorMessage?: string;
onChange?: React.ChangeEventHandler<HTMLSelectElement>;
}

function CurrentStatusInCanada({ defaultValue, errorMessage, onChange }: CurrentStatusInCanadaProps) {
const { t } = useTranslation(handle.i18nNamespace);
const CURRENT_STATUS_IN_CANADA = [
'canadian-citizen-born-in-canada',
'canadian-citizen-born-outside-canada',
'registered-indian-born-in-canada',
'registered-indian-born-outside-canada',
'permanent-resident',
'temporary-resident',
'no-legal-status-in-canada',
] as const;

const currentStatusInCanadaOptions = [
{
children: t('protected:request-details.requests.select-option'),
value: '',
},
...CURRENT_STATUS_IN_CANADA.map((value) => ({
value: value,
children: t(`protected:primary-identity-document.current-status-in-canada.options.${value}` as const),
})),
];

return (
<>
<InputSelect
id="currentStatusInCanada"
name="currentStatusInCanada"
errorMessage={errorMessage}
defaultValue={defaultValue}
required
options={currentStatusInCanadaOptions}
label={t('protected:primary-identity-document.current-status-in-canada.title')}
onChange={onChange}
/>
</>
);
}

interface DocumentTypeProps {
currentStatus?: string;
defaultValue?: string;
errorMessage?: string;
}

function DocumentType({ currentStatus, defaultValue, errorMessage }: DocumentTypeProps) {
const { t } = useTranslation(handle.i18nNamespace);

const REGISTERED_CANADAIAN_BORN_OUTSIDE_CANADA_DOCUMENT_TYPE = [
'certificate-of-canadian-citizenship',
'certificate-of-registration-of-birth-abroad',
] as const;

const REGISTERED_INDIAN_BORN_IN_CANADA_DOCUMENT_TYPE = [
'birth-certificate-and-certificate-of-indian-status',
'certificate-of-canadian-citizenship-and-certificate-of-indian-status',
] as const;

const documentTypeOptions = [
{
children: t('protected:request-details.requests.select-option'),
value: '',
hidden: true,
},
...(() => {
switch (currentStatus) {
case 'canadian-citizen-born-outside-canada':
return REGISTERED_CANADAIAN_BORN_OUTSIDE_CANADA_DOCUMENT_TYPE.map((value) => ({
value: value,
children: t(`protected:primary-identity-document.document-type.options.${value}` as const),
}));

case 'registered-indian-born-in-canada':
return REGISTERED_INDIAN_BORN_IN_CANADA_DOCUMENT_TYPE.map((value) => ({
value: value,
children: t(`protected:primary-identity-document.document-type.options.${value}` as const),
}));

default:
return [];
}
})(),
];

return (
<>
{(currentStatus === 'canadian-citizen-born-outside-canada' || currentStatus === 'registered-indian-born-in-canada') && (
<InputSelect
id="documentType"
name="documentType"
errorMessage={errorMessage}
defaultValue={defaultValue}
required
options={documentTypeOptions}
label={t('protected:primary-identity-document.document-type.title')}
/>
)}
</>
);
}

0 comments on commit 877a2bf

Please sign in to comment.