diff --git a/packages/client/src/v2-events/components/forms/FormFieldGenerator.tsx b/packages/client/src/v2-events/components/forms/FormFieldGenerator.tsx index a7710e49002..b1977795aae 100644 --- a/packages/client/src/v2-events/components/forms/FormFieldGenerator.tsx +++ b/packages/client/src/v2-events/components/forms/FormFieldGenerator.tsx @@ -15,6 +15,7 @@ import { DATE, PARAGRAPH, TEXT } from '@client/forms' import { DateField } from '@opencrvs/components/lib/DateField' import { Text } from '@opencrvs/components/lib/Text' import { TextInput } from '@opencrvs/components/lib/TextInput' +import { RadioGroup } from '@opencrvs/components/lib/Radio' import * as React from 'react' import styled, { keyframes } from 'styled-components' @@ -196,7 +197,19 @@ const GeneratedInputField = React.memo( ) } - return
Unsupported field type {fieldDefinition.type}
+ if (fieldDefinition.type === 'RADIO_GROUP') { + return ( + + setFieldValue(fieldDefinition.id, val)} + options={fieldDefinition.options} + value={inputProps.value as string} + /> + + ) + } + // return
Unsupported field type {fieldDefinition.type}
} ) diff --git a/packages/client/src/v2-events/features/events/actions/collect-certificate/Review.tsx b/packages/client/src/v2-events/features/events/actions/collect-certificate/Review.tsx index 682200f13b0..bf0a345014b 100644 --- a/packages/client/src/v2-events/features/events/actions/collect-certificate/Review.tsx +++ b/packages/client/src/v2-events/features/events/actions/collect-certificate/Review.tsx @@ -13,6 +13,8 @@ import React, { useEffect } from 'react' import { defineMessages } from 'react-intl' import { useNavigate } from 'react-router-dom' import { v4 as uuid } from 'uuid' +import styled from 'styled-components' +import { useIntl } from 'react-intl' import { useTypedParams } from 'react-router-typesafe-routes/dom' import { getCurrentEventState, ActionType } from '@opencrvs/commons/client' import { ROUTES } from '@client/v2-events/routes' @@ -23,7 +25,25 @@ import { useEventFormNavigation } from '@client/v2-events/features/events/useEve import { useEventConfiguration } from '@client/v2-events/features/events/useEventConfiguration' import { useEventFormData } from '@client/v2-events/features/events/useEventFormData' import { FormLayout } from '@client/v2-events/layouts/form' +import { usePrintableCertificate } from '@client/v2-events/hooks/usePrintableCertificate' +import { + Box, + Button, + Content, + Frame, + Icon, + ResponsiveModal, + Spinner, + Stack +} from '@opencrvs/components' +import { api } from '@client/v2-events/trpc' +const CertificateContainer = styled.div` + svg { + /* limits the certificate overflowing on small screens */ + max-width: 100%; + } +` const messages = defineMessages({ registerActionTitle: { id: 'registerAction.title', @@ -40,6 +60,54 @@ const messages = defineMessages({ id: 'registerAction.Declare', defaultMessage: 'Register', description: 'The label for declare button of register action' + }, + printModalTitle: { + id: 'print.certificate.review.printModalTitle', + defaultMessage: 'Print certificate?', + description: 'Print certificate modal title text' + }, + printAndIssueModalTitle: { + id: 'print.certificate.review.printAndIssueModalTitle', + defaultMessage: 'Print and issue certificate?', + description: 'Print and issue certificate modal title text' + }, + printModalBody: { + id: 'print.certificate.review.modal.body.print', + defaultMessage: + 'A PDF of the certificate will open in a new tab for you to print. This record will then be moved to your ready to issue work-queue', + description: 'Print certificate modal body text' + }, + printAndIssueModalBody: { + id: 'print.certificate.review.modal.body.printAndIssue', + defaultMessage: + 'A PDF of the certificate will open in a new tab for you to print and issue', + description: 'Print certificate modal body text' + }, + confirmAndPrint: { + defaultMessage: 'Yes, print certificate', + description: 'The text for print button', + id: 'print.certificate.button.confirmPrint' + }, + reviewTitle: { + defaultMessage: 'Ready to certify?', + description: 'Certificate review title', + id: 'print.certificate.review.title' + }, + reviewDescription: { + defaultMessage: + 'Please confirm that the informant has reviewed that the information on the certificate is correct and that it is ready to print.', + description: 'Certificate review description', + id: 'print.certificate.review.description' + }, + cancel: { + defaultMessage: 'Cancel', + description: 'Cancel button text in the modal', + id: 'buttons.cancel' + }, + print: { + defaultMessage: 'Print', + description: 'Print button text', + id: 'buttons.print' } }) @@ -49,6 +117,7 @@ const messages = defineMessages({ */ export function Review() { const { eventId } = useTypedParams(ROUTES.V2.EVENTS.COLLECT_CERTIFICATE) + const intl = useIntl() const events = useEvents() const [modal, openModal] = useModal() const navigate = useNavigate() @@ -56,7 +125,6 @@ export function Review() { const collectCertificateMutation = events.actions.collectCertificate const [event] = events.getEvent.useSuspenseQuery(eventId) - const { eventConfiguration: config } = useEventConfiguration(event.type) if (!config) { @@ -70,11 +138,20 @@ export function Review() { const setFormValues = useEventFormData((state) => state.setFormValues) const getFormValues = useEventFormData((state) => state.getFormValues) - useEffect(() => { - setFormValues(eventId, getCurrentEventState(event).data) - }, [event, eventId, setFormValues]) + // useEffect(() => { + // setFormValues(eventId, getCurrentEventState(event).data) + // }, [event, eventId, setFormValues]) const form = getFormValues(eventId) + console.trace(form) + + const { + isLoadingInProgress, + svgCode, + handleCertify, + isPrintInAdvance, + canUserEditRecord + } = usePrintableCertificate(form) async function handleEdit({ pageId, @@ -115,6 +192,53 @@ export function Review() { goToHome() } } + const confirmAndPrint = async () => { + const saveAndExitConfirm = await openModal((close) => ( + { + close(false) + }} + id="close-modal" + > + {intl.formatMessage(messages.cancel)} + , + + ]} + show={true} + handleClose={() => close(false)} + contentHeight={100} + > + {isPrintInAdvance + ? intl.formatMessage(messages.printModalBody) + : intl.formatMessage(messages.printAndIssueModalBody)} + + )) + + if (saveAndExitConfirm) { + handleCertify() + } + } + + if (!isLoadingInProgress) { + return + } return ( - + + + + + + handleEdit({ + pageId: formConfigs[0].pages[0].id, + fieldId: undefined + }) + } + size="large" + fullWidth + > + + {intl.formatMessage(messages.registerActionDeclare)} + + ) : ( + <> + ), + + + ]} + > + {intl.formatMessage(messages.reviewDescription)} + + + + {/* {modal} - + */} + {/* */} ) } diff --git a/packages/client/src/v2-events/hooks/useAppConfig.ts b/packages/client/src/v2-events/hooks/useAppConfig.ts new file mode 100644 index 00000000000..2bca172c4c4 --- /dev/null +++ b/packages/client/src/v2-events/hooks/useAppConfig.ts @@ -0,0 +1,30 @@ +/* + * 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. + */ + +import { create } from 'zustand' +import { api } from '@client/v2-events/trpc' +import { ApplicationConfigResponseSchema } from '@opencrvs/commons/events' +import { UseTRPCQueryResult } from '@trpc/react-query/shared' +import { TRPCClientErrorLike } from '@trpc/client' +import { DefaultErrorShape } from '@trpc/server/unstable-core-do-not-import' + +interface IApplicationConfig { + appConfig: ApplicationConfigResponseSchema + initiateAppConfig: () => Promise +} + +export const useAppConfig = create((set, get) => ({ + appConfig: { config: undefined, certificates: [] }, + initiateAppConfig: async () => { + const { data: appConfig } = api.appConfig.get.useQuery() + set({ appConfig }) + } +})) diff --git a/packages/client/src/v2-events/hooks/usePrintableCertificate.ts b/packages/client/src/v2-events/hooks/usePrintableCertificate.ts new file mode 100644 index 00000000000..a15100e5154 --- /dev/null +++ b/packages/client/src/v2-events/hooks/usePrintableCertificate.ts @@ -0,0 +1,76 @@ +/* + * 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. + */ + +import { formatLongDate } from '@client/utils/date-formatting' +import { EventType } from '@client/utils/gateway' +import { getLocationHierarchy } from '@client/utils/locationUtils' +import { getUserName, UserDetails } from '@client/utils/userUtils' +import { cloneDeep } from 'lodash' +import { useDispatch, useSelector } from 'react-redux' +import { useNavigate } from 'react-router-dom' +import { addFontsToSvg, compileSvg, svgToPdfTemplate } from './utils/PDFUtils' +import { + calculatePrice, + getEventDate, + getRegisteredDate, + isCertificateForPrintInAdvance +} from './utils/certificateUtils' +import { CertificateDataSchema } from '@opencrvs/commons/events' +import { ActionFormData, isMinioUrl } from '@opencrvs/commons/client' +import { fetchImageAsBase64 } from '@client/utils/imageUtils' +import { useAppConfig } from '@client/v2-events/hooks/useAppConfig' +import { useEffect } from 'react' +import { api } from '../trpc' + +async function replaceMinioUrlWithBase64(template: Record) { + async function recursiveTransform(obj: any) { + if (typeof obj !== 'object' || obj === null) { + return obj + } + + const transformedObject = Array.isArray(obj) ? [...obj] : { ...obj } + + for (const key in obj) { + const value = obj[key] + if (typeof value === 'string' && isMinioUrl(value)) { + transformedObject[key] = await fetchImageAsBase64(value) + } else if (typeof value === 'object') { + transformedObject[key] = await recursiveTransform(value) + } else { + transformedObject[key] = value + } + } + + return transformedObject + } + return recursiveTransform(template) +} + +export const usePrintableCertificate = (form: ActionFormData) => { + const handleCertify = () => {} + + const isPrintInAdvance = false + const canUserEditRecord = false + const handleEdit = () => {} + const { data: svgCode, isFetched } = + api.appConfig.getCertificateTemplateSVGById.useQuery({ + id: form['collector.certificateTemplateId'] as string + }) + + return { + isLoadingInProgress: isFetched, + svgCode, + handleCertify, + isPrintInAdvance, + canUserEditRecord, + handleEdit + } +} diff --git a/packages/client/src/v2-events/hooks/utils/PDFUtils.ts b/packages/client/src/v2-events/hooks/utils/PDFUtils.ts new file mode 100644 index 00000000000..c9c73983ac9 --- /dev/null +++ b/packages/client/src/v2-events/hooks/utils/PDFUtils.ts @@ -0,0 +1,309 @@ +/* + * 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. + */ +import { + IntlShape, + MessageDescriptor, + createIntl, + createIntlCache +} from 'react-intl' +import { AdminStructure, ILocation } from '@client/offline/reducer' +import { IPDFTemplate } from '@client/pdfRenderer' +import { certificateBaseTemplate } from '@client/templates/register' +import * as Handlebars from 'handlebars' +import { IStoreState } from '@client/store' +import { getOfflineData } from '@client/offline/selectors' +import isValid from 'date-fns/isValid' +import format from 'date-fns/format' +import { getHandlebarHelpers } from '@client/forms/handlebarHelpers' +import { + CertificateConfiguration, + FontFamilyTypes +} from '@client/utils/referenceApi' +import htmlToPdfmake from 'html-to-pdfmake' +import { Content } from 'pdfmake/interfaces' + +type TemplateDataType = string | MessageDescriptor | Array +function isMessageDescriptor( + obj: Record +): obj is MessageDescriptor & Record { + return ( + obj !== null && + obj.hasOwnProperty('id') && + obj.hasOwnProperty('defaultMessage') && + typeof (obj as MessageDescriptor).id === 'string' && + typeof (obj as MessageDescriptor).defaultMessage === 'string' + ) +} + +function formatAllNonStringValues( + templateData: Record, + intl: IntlShape +): Record { + for (const key of Object.keys(templateData)) { + if ( + typeof templateData[key] === 'object' && + isMessageDescriptor(templateData[key] as Record) + ) { + templateData[key] = intl.formatMessage( + templateData[key] as MessageDescriptor + ) + } else if (Array.isArray(templateData[key])) { + // For address field, country label is a MessageDescriptor + // but state, province is string + templateData[key] = ( + templateData[key] as Array + ) + .filter(Boolean) + .map((item) => + isMessageDescriptor(item as Record) + ? intl.formatMessage(item as MessageDescriptor) + : item + ) + .join(', ') + } else if ( + typeof templateData[key] === 'object' && + templateData[key] !== null + ) { + templateData[key] = formatAllNonStringValues( + templateData[key] as Record, + intl + ) + } + } + return templateData as Record +} + +const cache = createIntlCache() + +export function compileSvg( + templateString: string, + data: Record = {}, + state: IStoreState +): string { + const intl = createIntl( + { + locale: state.i18n.language, + messages: state.i18n.messages + }, + cache + ) + + const customHelpers = getHandlebarHelpers() + + for (const helperName of Object.keys(customHelpers)) { + /* + * Note for anyone adding new context variables to handlebar helpers: + * Everything you expose to country config's here will become API surface area, + * This means that countries will become dependant on it and it will be hard to remove or rename later on. + * If you need to expose the full record, please consider only exposing the specific values you know are needed. + * Otherwise what happens is that we lose the ability to refactor and remove things later on. + */ + const helper = customHelpers[helperName]({ intl }) + Handlebars.registerHelper(helperName, helper) + } + + Handlebars.registerHelper( + 'intl', + function (this: any, ...args: [...string[], Handlebars.HelperOptions]) { + // If even one of the parts is undefined, then return empty string + const idParts = args.slice(0, -1) + if (idParts.some((part) => part === undefined)) { + return '' + } + + const id = idParts.join('.') + + return intl.formatMessage({ + id, + defaultMessage: 'Missing translation for ' + id + }) + } as any /* This is here because Handlebars typing is insufficient and we can make the function type stricter */ + ) + + Handlebars.registerHelper( + 'ifCond', + function ( + this: any, + v1: string, + operator: string, + v2: string, + options: Handlebars.HelperOptions + ) { + switch (operator) { + case '===': + return v1 === v2 ? options.fn(this) : options.inverse(this) + case '!==': + return v1 !== v2 ? options.fn(this) : options.inverse(this) + case '<': + return v1 < v2 ? options.fn(this) : options.inverse(this) + case '<=': + return v1 <= v2 ? options.fn(this) : options.inverse(this) + case '>': + return v1 > v2 ? options.fn(this) : options.inverse(this) + case '>=': + return v1 >= v2 ? options.fn(this) : options.inverse(this) + case '&&': + return v1 && v2 ? options.fn(this) : options.inverse(this) + case '||': + return v1 || v2 ? options.fn(this) : options.inverse(this) + default: + return options.inverse(this) + } + } + ) + + Handlebars.registerHelper( + 'formatDate', + function (this: any, dateString: string, formatString: string) { + const date = new Date(dateString) + return isValid(date) ? format(date, formatString) : '' + } + ) + + Handlebars.registerHelper( + 'location', + function (this: any, locationId: string | undefined, key: keyof ILocation) { + const offlineData = getOfflineData(state) + + if (!locationId) { + return '' + } + + if (!['name', 'alias'].includes(key)) { + return `Unknown property ${key}` + } + + const location: AdminStructure | undefined = + offlineData.locations[locationId] ?? + offlineData.facilities[locationId] ?? + offlineData.offices[locationId] + + const fallback = import.meta.env.DEV + ? `Missing location for id: ${locationId}` + : locationId + + return location?.[key] ?? fallback + } + ) + + const template = Handlebars.compile(templateString) + const formattedTemplateData = formatAllNonStringValues(data, intl) + const output = template(formattedTemplateData) + return output +} + +export function addFontsToSvg( + svgString: string, + fonts: Record +) { + const parser = new DOMParser() + const doc = parser.parseFromString(svgString, 'image/svg+xml') + const svg = doc.documentElement + const style = document.createElement('style') + style.innerHTML = Object.entries(fonts) + .flatMap(([font, families]) => + Object.entries(families).map( + ([family, url]) => ` +@font-face { +font-family: "${font}"; +font-weight: ${family}; +src: url("${url}") format("truetype"); +}` + ) + ) + .join('') + svg.prepend(style) + const serializer = new XMLSerializer() + return serializer.serializeToString(svg) +} +export function svgToPdfTemplate( + svg: string, + certificateFonts: CertificateConfiguration +) { + const pdfTemplate: IPDFTemplate = { + ...certificateBaseTemplate, + definition: { + ...certificateBaseTemplate.definition, + defaultStyle: { + font: + Object.keys(certificateFonts)[0] || + certificateBaseTemplate.definition.defaultStyle.font + } + }, + fonts: { + ...certificateBaseTemplate.fonts, + ...certificateFonts + } + } + + const parser = new DOMParser() + const svgElement = parser.parseFromString( + svg, + 'image/svg+xml' + ).documentElement + + const widthValue = svgElement.getAttribute('width') + const heightValue = svgElement.getAttribute('height') + + if (widthValue && heightValue) { + const width = Number.parseInt(widthValue) + const height = Number.parseInt(heightValue) + pdfTemplate.definition.pageSize = { + width, + height + } + if (width > height) { + pdfTemplate.definition.pageOrientation = 'landscape' + } + } + + const foreignObjects = svgElement.getElementsByTagName('foreignObject') + const absolutelyPositionedHTMLs: Content[] = [] + for (const foreignObject of foreignObjects) { + const width = Number.parseInt(foreignObject.getAttribute('width')!) + const x = Number.parseInt(foreignObject.getAttribute('x')!) + const y = Number.parseInt(foreignObject.getAttribute('y')!) + const htmlContent = foreignObject.innerHTML + const pdfmakeContent = htmlToPdfmake(htmlContent, { + ignoreStyles: ['font-family'] + }) + absolutelyPositionedHTMLs.push({ + columns: [ + { + width, + stack: pdfmakeContent + } + ], + absolutePosition: { x, y } + } as Content) + } + + pdfTemplate.definition.content = [ + { + svg + }, + ...absolutelyPositionedHTMLs + ] + + return pdfTemplate +} + +export function downloadFile( + contentType: string, + data: string, + fileName: string +) { + const linkSource = `data:${contentType};base64,${window.btoa(data)}` + const downloadLink = document.createElement('a') + downloadLink.setAttribute('href', linkSource) + downloadLink.setAttribute('download', fileName) + downloadLink.click() +} diff --git a/packages/client/src/v2-events/hooks/utils/certificateUtils.ts b/packages/client/src/v2-events/hooks/utils/certificateUtils.ts new file mode 100644 index 00000000000..65f4288722b --- /dev/null +++ b/packages/client/src/v2-events/hooks/utils/certificateUtils.ts @@ -0,0 +1,295 @@ +/* + * 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. + */ +import { IFormData, IFormSectionGroup, ISelectOption } from '@client/forms' +import { Event, EventType } from '@client/utils/gateway' +import { dynamicMessages } from '@client/i18n/messages/views/certificate' +import { getAvailableLanguages } from '@client/i18n/utils' +import { ILanguageState } from '@client/i18n/reducer' +import { ICertificate, IPrintableDeclaration } from '@client/declarations' +import { IntlShape } from 'react-intl' +import { IOfflineData } from '@client/offline/reducer' +import differenceInDays from 'date-fns/differenceInDays' + +const MONTH_IN_DAYS = 30 +const YEAR_IN_DAYS = 365 + +interface IRange { + start: number + end?: number + value: number +} + +export interface ICountry { + value: string + name: string +} + +export interface IAvailableCountries { + language?: string + countries?: ICountry[] +} + +export function getCountryTranslations( + languageState: ILanguageState, + countries: ISelectOption[] +): IAvailableCountries[] { + const certificateCountries: IAvailableCountries[] = [] + getAvailableLanguages().forEach((language: string) => { + const certificateCountry: IAvailableCountries = { language } + const availableCountries: ICountry[] = [] + countries.forEach((country) => { + availableCountries.push({ + value: country.value, + name: languageState[language].messages[`countries.${country.value}`] + }) + }) + certificateCountry.countries = availableCountries + certificateCountries.push(certificateCountry) + }) + return certificateCountries +} + +function getDayRanges( + offlineData: IOfflineData, + certificate: ICertificate +): IRange[] { + const templateConfig = offlineData.templates.certificates.find( + (x) => x.id === certificate.certificateTemplateId + ) + switch (templateConfig?.event) { + case EventType.Birth: { + const BIRTH_REGISTRATION_TARGET = + offlineData.config.BIRTH.REGISTRATION_TARGET + const BIRTH_LATE_REGISTRATION_TARGET = + offlineData.config.BIRTH.LATE_REGISTRATION_TARGET + const BIRTH_ON_TIME_FEE = templateConfig?.fee.onTime + const BIRTH_LATE_FEE = templateConfig?.fee.late + const BIRTH_DELAYED_FEE = templateConfig?.fee.delayed + const birthRanges = [ + { start: 0, end: BIRTH_REGISTRATION_TARGET, value: BIRTH_ON_TIME_FEE }, + { + start: BIRTH_REGISTRATION_TARGET + 1, + end: BIRTH_LATE_REGISTRATION_TARGET, + value: BIRTH_LATE_FEE + }, + { start: BIRTH_LATE_REGISTRATION_TARGET + 1, value: BIRTH_DELAYED_FEE } + ] + return birthRanges + } + + case EventType.Death: { + const DEATH_REGISTRATION_TARGET = + offlineData.config.DEATH.REGISTRATION_TARGET + const DEATH_ON_TIME_FEE = templateConfig?.fee.onTime + const DEATH_DELAYED_FEE = templateConfig?.fee.delayed + + const deathRanges = [ + { start: 0, end: DEATH_REGISTRATION_TARGET, value: DEATH_ON_TIME_FEE }, + { start: DEATH_REGISTRATION_TARGET + 1, value: DEATH_DELAYED_FEE } + ] + return deathRanges + } + case EventType.Marriage: { + const MARRIAGE_REGISTRATION_TARGET = + offlineData.config.MARRIAGE.REGISTRATION_TARGET + const MARRIAGE_ON_TIME_FEE = templateConfig?.fee.onTime + const MARRIAGE_DELAYED_FEE = templateConfig?.fee.delayed + const marriageRanges = [ + { + start: 0, + end: MARRIAGE_REGISTRATION_TARGET, + value: MARRIAGE_ON_TIME_FEE + }, + { start: MARRIAGE_REGISTRATION_TARGET + 1, value: MARRIAGE_DELAYED_FEE } + ] + + return marriageRanges + } + default: + return [] + } +} + +function getValue( + offlineData: IOfflineData, + certificate: ICertificate, + check: number +): IRange['value'] { + const rangeByEvent = getDayRanges(offlineData, certificate) as IRange[] + const foundRange = rangeByEvent.find((range) => + range.end + ? check >= range.start && check <= range.end + : check >= range.start + ) + return foundRange ? foundRange.value : rangeByEvent[0]?.value || 0 +} + +export function calculateDaysFromToday(doE: string) { + const todaysDate = new Date(Date.now()) + const eventDate = new Date(doE) + const diffInDays = differenceInDays(todaysDate, eventDate) + return diffInDays +} + +function calculateDays(doE: string, regDate: string) { + const registeredDate = new Date(regDate) + const eventDate = new Date(doE) + const diffInDays = differenceInDays(registeredDate, eventDate) + return diffInDays +} + +export function timeElapsed(days: number) { + const output: { unit: string; value: number } = { value: 0, unit: 'Day' } + + const year = Math.floor(days / YEAR_IN_DAYS) + const month = Math.floor(days / MONTH_IN_DAYS) + + if (year > 0) { + output.value = year + output.unit = 'Year' + } else if (month > 0) { + output.value = month + output.unit = 'Month' + } else { + output.value = days + } + + return output +} + +export function calculatePrice( + event: EventType, + eventDate: string, + registeredDate: string, + offlineData: IOfflineData, + certificate: ICertificate +) { + if (!certificate) return 0 + const days = calculateDays(eventDate, registeredDate) + const result = getValue(offlineData, certificate, days) + return result +} + +export function getServiceMessage( + intl: IntlShape, + event: EventType, + eventDate: string, + registeredDate: string, + offlineData: IOfflineData +) { + const days = calculateDays(eventDate, registeredDate) + + if (event === EventType.Birth) { + if (days <= offlineData.config.BIRTH.REGISTRATION_TARGET) { + return intl.formatMessage(dynamicMessages[`${event}ServiceBefore`], { + target: offlineData.config.BIRTH.REGISTRATION_TARGET + }) + } else if ( + days > offlineData.config.BIRTH.REGISTRATION_TARGET && + days <= offlineData.config.BIRTH.LATE_REGISTRATION_TARGET + ) { + return intl.formatMessage(dynamicMessages[`${event}ServiceBetween`], { + target: offlineData.config.BIRTH.REGISTRATION_TARGET, + latetarget: offlineData.config.BIRTH.LATE_REGISTRATION_TARGET + }) + } else { + return intl.formatMessage(dynamicMessages[`${event}ServiceAfter`], { + target: offlineData.config.BIRTH.LATE_REGISTRATION_TARGET + }) + } + } else if (event === EventType.Death) { + if (days <= offlineData.config.DEATH.REGISTRATION_TARGET) { + return intl.formatMessage(dynamicMessages[`${event}ServiceBefore`], { + target: offlineData.config.DEATH.REGISTRATION_TARGET + }) + } else { + return intl.formatMessage(dynamicMessages[`${event}ServiceAfter`], { + target: offlineData.config.DEATH.REGISTRATION_TARGET + }) + } + } else if (event === EventType.Marriage) { + if (days <= offlineData.config.DEATH.REGISTRATION_TARGET) { + return intl.formatMessage(dynamicMessages[`${event}ServiceBefore`], { + target: offlineData.config.MARRIAGE.REGISTRATION_TARGET + }) + } else { + return intl.formatMessage(dynamicMessages[`${event}ServiceAfter`], { + target: offlineData.config.MARRIAGE.REGISTRATION_TARGET + }) + } + } +} + +export function isFreeOfCost( + certificate: ICertificate, + eventDate: string, + registeredDate: string, + offlineData: IOfflineData +): boolean { + const days = calculateDays(eventDate, registeredDate) + const result = getValue(offlineData, certificate, days) + return result === 0 +} + +export function getEventDate(data: IFormData, event: EventType) { + switch (event) { + case EventType.Birth: + return data.child.childBirthDate as string + case EventType.Death: + return data.deathEvent.deathDate as string + case EventType.Marriage: + return data.marriageEvent.marriageDate as string + } +} + +export function getRegisteredDate(data: IFormData) { + const historyList = data.history as unknown as { [key: string]: any }[] + const regHistory = historyList.find( + (history) => history.regStatus === 'REGISTERED' + ) + return regHistory && regHistory.date +} + +export function getEvent(eventType: string | undefined) { + switch (eventType && eventType.toLowerCase()) { + case 'birth': + default: + return EventType.Birth + case 'death': + return EventType.Death + case 'marriage': + return EventType.Marriage + } +} + +export function isCertificateForPrintInAdvance( + declaration: IPrintableDeclaration | undefined +) { + const collectorType = + declaration?.data?.registration?.certificates?.[0]?.collector?.type + if (collectorType && collectorType === 'PRINT_IN_ADVANCE') { + return true + } + return false +} + +export function filterPrintInAdvancedOption(collectionForm: IFormSectionGroup) { + const filtredCollectionForm = collectionForm.fields.map((field) => { + if (field.type !== 'RADIO_GROUP') return field + + const filteredOption = field.options.filter( + (option) => option.value !== 'PRINT_IN_ADVANCE' + ) + return { ...field, options: filteredOption } + }) + + return { ...collectionForm, fields: filtredCollectionForm } +} diff --git a/packages/commons/src/events/AppConfig.ts b/packages/commons/src/events/AppConfig.ts new file mode 100644 index 00000000000..56bf87d05b6 --- /dev/null +++ b/packages/commons/src/events/AppConfig.ts @@ -0,0 +1,123 @@ +/* + * 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. + */ + +import { z } from 'zod' + +const iCountryLogoSchema = z.object({ + fileName: z.string(), + file: z.string() +}) + +const iLoginBackgroundSchema = z.object({ + backgroundColor: z.string().optional(), + backgroundImage: z.string().optional(), + imageFit: z.string().optional() +}) + +enum EventType { + Birth = 'birth', + Death = 'death', + Marriage = 'marriage', + TENNIS_CLUB_MEMBERSHIP = 'TENNIS_CLUB_MEMBERSHIP' +} +const eventTypeSchema = z.nativeEnum(EventType) + +const fontFamilyTypesSchema = z.object({ + normal: z.string(), + bold: z.string(), + italics: z.string(), + bolditalics: z.string() +}) + +const CertificateConfigDataSchema = z.object({ + id: z.string(), + event: eventTypeSchema, + label: z.object({ + id: z.string(), + defaultMessage: z.string(), + description: z.string() + }), + isDefault: z.boolean(), + fee: z.object({ + onTime: z.number(), + late: z.number(), + delayed: z.number() + }), + svgUrl: z.string(), + fonts: z.record(fontFamilyTypesSchema).optional() +}) + +const CertificateDataSchema = CertificateConfigDataSchema.extend({ + hash: z.string().optional(), + svg: z.string() +}) + +export type CertificateDataSchema = z.infer + +const iCurrencySchema = z.object({ + isoCode: z.string(), + languagesAndCountry: z.array(z.string()) +}) + +enum SearchCriteria { + TRACKING_ID = 'TRACKING_ID', + REGISTRATION_NUMBER = 'REGISTRATION_NUMBER', + NATIONAL_ID = 'NATIONAL_ID', + NAME = 'NAME', + PHONE_NUMBER = 'PHONE_NUMBER', + EMAIL = 'EMAIL' +} +const searchCriteriaTypeSchema = z.nativeEnum(SearchCriteria) + +const ApplicationConfigSchema = z.object({ + APPLICATION_NAME: z.string(), + BIRTH: z.object({ + REGISTRATION_TARGET: z.number(), + LATE_REGISTRATION_TARGET: z.number(), + PRINT_IN_ADVANCE: z.boolean() + }), + COUNTRY_LOGO: iCountryLogoSchema, + CURRENCY: iCurrencySchema, + DEATH: z.object({ + REGISTRATION_TARGET: z.number(), + PRINT_IN_ADVANCE: z.boolean() + }), + MARRIAGE: z.object({ + REGISTRATION_TARGET: z.number(), + PRINT_IN_ADVANCE: z.boolean() + }), + FEATURES: z.object({ + DEATH_REGISTRATION: z.boolean(), + MARRIAGE_REGISTRATION: z.boolean(), + EXTERNAL_VALIDATION_WORKQUEUE: z.boolean(), + INFORMANT_SIGNATURE: z.boolean(), + PRINT_DECLARATION: z.boolean(), + DATE_OF_BIRTH_UNKNOWN: z.boolean(), + INFORMANT_SIGNATURE_REQUIRED: z.boolean() + }), + FIELD_AGENT_AUDIT_LOCATIONS: z.string(), + DECLARATION_AUDIT_LOCATIONS: z.string(), + PHONE_NUMBER_PATTERN: z.string(), + NID_NUMBER_PATTERN: z.string(), + LOGIN_BACKGROUND: iLoginBackgroundSchema, + USER_NOTIFICATION_DELIVERY_METHOD: z.string(), + INFORMANT_NOTIFICATION_DELIVERY_METHOD: z.string(), + SEARCH_DEFAULT_CRITERIA: searchCriteriaTypeSchema.optional() +}) + +export const ApplicationConfigResponseSchema = z.object({ + config: ApplicationConfigSchema.optional(), + certificates: z.array(CertificateDataSchema) +}) + +export type ApplicationConfigResponseSchema = z.infer< + typeof ApplicationConfigResponseSchema +> diff --git a/packages/commons/src/events/index.ts b/packages/commons/src/events/index.ts index ae6d21b7f26..6d0fdbddd9d 100644 --- a/packages/commons/src/events/index.ts +++ b/packages/commons/src/events/index.ts @@ -9,6 +9,7 @@ * Copyright (C) The OpenCRVS Authors located at https://github.com/opencrvs/opencrvs-core/blob/master/AUTHORS. */ export * from './ActionConfig' +export * from './AppConfig' export * from './EventConfig' export * from './FieldConfig' export * from './FormConfig' diff --git a/packages/events/src/environment.ts b/packages/events/src/environment.ts index 5d3457caef3..d2a8a4ca780 100644 --- a/packages/events/src/environment.ts +++ b/packages/events/src/environment.ts @@ -16,5 +16,6 @@ export const env = cleanEnv(process.env, { ES_HOST: url({ devDefault: 'http://localhost:9200' }), COUNTRY_CONFIG_URL: url({ devDefault: 'http://localhost:3040' }), DOCUMENTS_URL: url({ devDefault: 'http://localhost:9050' }), - USER_MANAGEMENT_URL: url({ devDefault: 'http://localhost:3030/' }) + USER_MANAGEMENT_URL: url({ devDefault: 'http://localhost:3030/' }), + CONFIG_API_URL: url({ devDefault: 'http://localhost:2021' }) }) diff --git a/packages/events/src/router/router.ts b/packages/events/src/router/router.ts index 5bf40925b88..c7cc1159a8e 100644 --- a/packages/events/src/router/router.ts +++ b/packages/events/src/router/router.ts @@ -39,8 +39,13 @@ import { NotifyActionInput, RegisterActionInput, ValidateActionInput, - CollectCertificateActionInput + CollectCertificateActionInput, + ApplicationConfigResponseSchema } from '@opencrvs/commons/events' +import { + getAppConfigurations, + getCertificateTemplateById +} from '@events/service/appConfig' const validateEventType = ({ eventTypes, @@ -72,6 +77,23 @@ const publicProcedure = t.procedure export type AppRouter = typeof appRouter export const appRouter = router({ + appConfig: router({ + get: publicProcedure + .output(ApplicationConfigResponseSchema) + .query(async (options) => { + return getAppConfigurations(options.ctx.token) + }), + getCertificateTemplateSVGById: publicProcedure + .input(z.object({ id: z.string() })) + .output(z.string()) + .query(async (options) => { + const template = await getCertificateTemplateById( + options.ctx.token, + options.input.id + ) + return template + }) + }), config: router({ get: publicProcedure.output(z.array(EventConfig)).query(async (options) => { return getEventConfigurations(options.ctx.token) diff --git a/packages/events/src/service/appConfig.ts b/packages/events/src/service/appConfig.ts new file mode 100644 index 00000000000..7e129ddfea4 --- /dev/null +++ b/packages/events/src/service/appConfig.ts @@ -0,0 +1,51 @@ +/* + * 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. + */ + +import { env } from '@events/environment' +import fetch from 'node-fetch' +import { ApplicationConfigResponseSchema } from '@opencrvs/commons' +import z from 'zod' + +export async function getAppConfigurations(token: string) { + const res = await fetch(new URL('/config', env.CONFIG_API_URL), { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + }) + + if (!res.ok) { + throw new Error('Failed to fetch events config') + } + + return ApplicationConfigResponseSchema.parse(await res.json()) +} + +export async function getCertificateTemplateById(token: string, id: string) { + console.log(new URL(`/certificates/${id}.svg`, env.COUNTRY_CONFIG_URL)) + console.log(token) + const res = await fetch( + new URL(`/certificates/${id}.svg`, env.COUNTRY_CONFIG_URL), + { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + } + ) + + if (!res.ok) { + console.log(res) + throw new Error('Failed to fetch events config') + } + + return z.string().parse(await res.text()) +}