diff --git a/debug-service-in-chrome.sh b/debug-service-in-chrome.sh index 1aa16baa27..0a0f1f0d1a 100755 --- a/debug-service-in-chrome.sh +++ b/debug-service-in-chrome.sh @@ -12,6 +12,7 @@ SERVICES=( "auth:4040" "workflow:5050" + "events:5555" "config:2021" "gateway:7070" "metrics:1050" diff --git a/packages/client/.storybook/default-request-handlers.ts b/packages/client/.storybook/default-request-handlers.ts index 8f96bd0461..8f84b4ffc5 100644 --- a/packages/client/.storybook/default-request-handlers.ts +++ b/packages/client/.storybook/default-request-handlers.ts @@ -8,18 +8,19 @@ * * Copyright (C) The OpenCRVS Authors located at https://github.com/opencrvs/opencrvs-core/blob/master/AUTHORS. */ -import { http } from 'msw' -import { graphql, HttpResponse } from 'msw' +/* eslint-disable import/no-relative-parent-imports */ +import { http, graphql, HttpResponse } from 'msw' +import { createTRPCMsw, httpLink } from '@vafanassieff/msw-trpc' +import superjson from 'superjson' import { mockOfflineData } from '../src/tests/mock-offline-data' import forms from '../src/tests/forms.json' -import superjson from 'superjson' import { AppRouter } from '../src/v2-events/trpc' -import { createTRPCMsw, httpLink } from '@vafanassieff/msw-trpc' import { tennisClubMembershipEvent, tennisClubMembershipEventIndex } from '../src/v2-events/features/events/fixtures' +import { tennisClubMembershipCertifiedCertificateTemplate } from './tennisClubMembershipCertifiedCertificateTemplate' const tRPCMsw = createTRPCMsw({ links: [ @@ -1106,11 +1107,91 @@ export const handlers = { }) ], config: [ + http.get( + 'http://localhost:6006/api/countryconfig/certificates/tennis-club-membership-certificate.svg', + () => { + return HttpResponse.text( + tennisClubMembershipCertifiedCertificateTemplate + ) + } + ), + http.get( + 'http://localhost:6006/api/countryconfig/certificates/tennis-club-membership-certified-certificate.svg', + () => { + return HttpResponse.text( + tennisClubMembershipCertifiedCertificateTemplate + ) + } + ), + + http.get( + 'http://localhost:6006/api/countryconfig/fonts/NotoSans-Regular.ttf', + async () => { + const fontResponse = await fetch( + 'http://localhost:3040/fonts/NotoSans-Regular.ttf' + ) + const fontArrayBuffer = await fontResponse.arrayBuffer() + return HttpResponse.arrayBuffer(fontArrayBuffer) + } + ), + http.get('http://localhost:2021/config', () => { return HttpResponse.json({ systems: [], config: mockOfflineData.config, - certificates: [] + certificates: [ + { + id: 'tennis-club-membership-certificate', + event: 'TENNIS_CLUB_MEMBERSHIP', + label: { + id: 'certificates.tennis-club-membership.certificate.copy', + defaultMessage: 'Tennis Club Membership Certificate copy', + description: 'The label for a tennis-club-membership certificate' + }, + isDefault: false, + fee: { + onTime: 7, + late: 10.6, + delayed: 18 + }, + svgUrl: + '/api/countryconfig/certificates/tennis-club-membership-certificate.svg', + fonts: { + 'Noto Sans': { + normal: '/api/countryconfig/fonts/NotoSans-Regular.ttf', + bold: '/api/countryconfig/fonts/NotoSans-Bold.ttf', + italics: '/api/countryconfig/fonts/NotoSans-Regular.ttf', + bolditalics: '/api/countryconfig/fonts/NotoSans-Regular.ttf' + } + } + }, + { + id: 'tennis-club-membership-certified-certificate', + event: 'TENNIS_CLUB_MEMBERSHIP', + label: { + id: 'certificates.tennis-club-membership.certificate.certified-copy', + defaultMessage: + 'Tennis Club Membership Certificate certified copy', + description: 'The label for a tennis-club-membership certificate' + }, + isDefault: false, + fee: { + onTime: 7, + late: 10.6, + delayed: 18 + }, + svgUrl: + '/api/countryconfig/certificates/tennis-club-membership-certified-certificate.svg', + fonts: { + 'Noto Sans': { + normal: '/api/countryconfig/fonts/NotoSans-Regular.ttf', + bold: '/api/countryconfig/fonts/NotoSans-Bold.ttf', + italics: '/api/countryconfig/fonts/NotoSans-Regular.ttf', + bolditalics: '/api/countryconfig/fonts/NotoSans-Regular.ttf' + } + } + } + ] }) }) ], diff --git a/packages/client/.storybook/tennisClubMembershipCertifiedCertificateTemplate.ts b/packages/client/.storybook/tennisClubMembershipCertifiedCertificateTemplate.ts new file mode 100644 index 0000000000..a6bd1f6d9f --- /dev/null +++ b/packages/client/.storybook/tennisClubMembershipCertifiedCertificateTemplate.ts @@ -0,0 +1,169 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * OpenCRVS is also distributed under the terms of the Civil Registration + * & Healthcare Disclaimer located at http://opencrvs.org/license. + * + * Copyright (C) The OpenCRVS Authors located at https://github.com/opencrvs/opencrvs-core/blob/master/AUTHORS. + */ + +export const tennisClubMembershipCertifiedCertificateTemplate = ` + + + + + 01 April 2012 + + + Date of certification + + + Imbobo District Office, Chiwala State, Farajaland + + + Place of certification + + + 12345678912345678912345678900 + + + Application ID + + + + + Registrar Jude Hopper + + + I certify that this certificate is a true copy of the civil registry and is issued by the mandated authority in + + + pursuance of civil registration and vital statistics law / J’attestation est une copie conforme de l'état civil. et est + + + délivré par l'autorité mandatée conformément à la loi sur l'enregistrement et les statistiques de l'état civil + + + + + No. 2023HSND234S + + + + Membership etails + + + + 1. + + + Full name + + + + + + + {{ "applicant.firstname"}} {{ "applicant.surname"}} + + + + + 2. + + + Date of birth + + + + + + + {{ "applicant.dob" }} + + + + + + 3. + + + Recommender Full name + + + + + + + {{ "recommender.firstname" }} {{ "recommender.surname" }} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Farajaland Countryside Tennis Club + + + Farajaland Tennis Club Membership Certified Certificate + + + + + + + + CAUTION : THERE ARE OFFENCES RELATING TO FALSIFYING OR ALTERING A CERTIFICATE AND USING OR POSSESSING A FALSE CERTIFICATE. A + + + CERTIFICATE IS NOT PROOF OF IDENTITY / ATTENTION : IL EXISTE DES INFRACTIONS RELATIVES À LA FALSIFIATION OU À LA MODIFICATION + + + D'UN CERTIFICAT ET À L'UTILISATION OU LA POSSESSION D'UN FAUX CERTIFICAT. UN CERTIFICAT N'EST PAS UNE PREUVE D'IDENTITÉ + + + + + + + + + + + + + + + + + + +` diff --git a/packages/client/src/offline/selectors.ts b/packages/client/src/offline/selectors.ts index 91e1d5ed75..fbf2413018 100644 --- a/packages/client/src/offline/selectors.ts +++ b/packages/client/src/offline/selectors.ts @@ -47,6 +47,14 @@ export const getOfflineData = (store: IStoreState): IOfflineData => { return data } +export const getLanguage = createSelector( + getOfflineData, + (data) => data.languages[0] +) +export const getCertificateTemplates = createSelector( + getOfflineData, + (data) => data.templates.certificates +) export const getLocations = createSelector(getOfflineData, (data) => ({ ...data.locations, ...data.facilities, diff --git a/packages/client/src/v2-events/STYLEGUIDE.md b/packages/client/src/v2-events/STYLEGUIDE.md new file mode 100644 index 0000000000..c623bedc92 --- /dev/null +++ b/packages/client/src/v2-events/STYLEGUIDE.md @@ -0,0 +1,95 @@ +# Styleguide + +## Zod Schemas and types + +good: + +``` +// This way we can import both the type and validator under same alias. +const FontFamily = z.object({ + normal: z.string(), + bold: z.string(), + italics: z.string(), + bolditalics: z.string() +}) + +type FontFamily = z.infer +``` + +- Use the same name for file and the main export +- Prefer variable names without postfix (e.g. schema, data) + +## Naming files + +good: + +``` +- ZodInterfacePascalCase.ts +- ReactComponentPascalCase.tsx +- useHookCamelCase.ts +- anything-else-in-kebab-case.ts +``` + +## Naming interfaces + +good: + +``` +interface ApplicationConfig { + certificateTemplates: CertificateTemplateConfig[] + language?: LanguageConfig +} +``` + +not-so-good: + +``` +interface IApplicationConfig { + certificateTemplates: ICertificateTemplateConfig[] + language?: ILanguageConfig +} +``` + +# Coding conventions, definition of done + +- When introducing a new `MessageDescriptor` create a new row in `client.csv` +- They should all have `v2.`-prefix + +## Naming, abbreviation + +When naming things with known abbreviations use camel case format despite of it. + +good: + +``` +export interface SvgTemplate { + definition: string +} + +export function printPdf(template: PdfTemplate, declarationId: string) { + const pdf = pdfMake.createPdf(template.definition, undefined, template.fonts) + if (isMobileDevice()) { + pdf.download(`${declarationId}`) + } else { + pdf.print() + } +} +``` + +not-so-good: + +``` +export interface ISVGTemplate { + definition: string +} + +export function printPDF(template: PDFTemplate, declarationId: string) { + // note: example uses external lib with the same convention. + const PDF = pdfMake.createPdf(template.definition, undefined, template.fonts) + if (isMobileDevice()) { + PDF.download(`${declarationId}`) + } else { + PDF.print() + } +} +``` diff --git a/packages/client/src/v2-events/features/events/actions/correct/request/Pages.tsx b/packages/client/src/v2-events/features/events/actions/correct/request/Pages.tsx index 6f775f91b3..116f193c1a 100644 --- a/packages/client/src/v2-events/features/events/actions/correct/request/Pages.tsx +++ b/packages/client/src/v2-events/features/events/actions/correct/request/Pages.tsx @@ -74,13 +74,9 @@ export function Pages() { }, [pageId, currentPageId, navigate, eventId]) return ( - + {modal} + {modal} () + const formEventId = useEventFormData((state) => state.eventId) + const intl = useIntl() + const navigate = useNavigate() + const events = useEvents() + const { modal } = useEventFormNavigation() + + const [event] = events.getEvent.useSuspenseQuery(eventId) + + const certTemplateFieldConfig = + useCertificateTemplateSelectorFieldConfig(event) + const currentState = getCurrentEventState(event) + const { setFormValues, getFormValues } = useEventFormData() + const form = getFormValues(eventId) + + useEffect(() => { + if (formEventId !== event.id) { + setFormValues(event.id, currentState.data) + } + }, [currentState.data, event.id, formEventId, setFormValues]) + + const { eventConfiguration: configuration } = useEventConfiguration( + event.type + ) + const formPages = configuration.actions + .find((action) => action.type === ActionType.PRINT_CERTIFICATE) + ?.forms.find((f) => f.active)?.pages + + if (!formPages) { + throw new Error('Form configuration not found for type: ' + event.type) + } + + const currentPageId = + formPages.find((p) => p.id === pageId)?.id || formPages[0]?.id + + if (!currentPageId) { + throw new Error('Form does not have any pages') + } + + useEffect(() => { + if (pageId !== currentPageId) { + navigate( + ROUTES.V2.EVENTS.PRINT_CERTIFICATE.PAGES.buildPath({ + eventId, + pageId: currentPageId + }), + { replace: true } + ) + } + }, [pageId, currentPageId, navigate, eventId]) + + return ( + } + route={ROUTES.V2.EVENTS.PRINT_CERTIFICATE} + > + {modal} + setFormValues(eventId, data)} + showReviewButton={searchParams.from === 'review'} + onFormPageChange={(nextPageId: string) => + navigate( + ROUTES.V2.EVENTS.PRINT_CERTIFICATE.PAGES.buildPath({ + eventId, + pageId: nextPageId + }) + ) + } + onSubmit={() => { + if (templateId) { + navigate( + ROUTES.V2.EVENTS.PRINT_CERTIFICATE.REVIEW.buildPath( + { eventId }, + { templateId } + ) + ) + } + }} + > + {(page: FormPage) => ( + // hard coded certificate template selector form field + <> + {formPages[0].id === page.id && ( + +