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())
+}