addv-questionnaire
v0.1.10
Published
Reusable questionnaire filler, question types, validation, auto-calculation and Redux slice for ADDV projects.
Downloads
1,363
Readme
addv-questionnaire
Centralized, reusable questionnaire engine for ADDV projects. Ships the full patient questionnaire flow — every question type, validation, auto-calculation (date-range → days, BMI, BP classification), a Redux slice, and journey orchestration — as a single source of truth so changes flow to every consumer app from one package.
- Framework: React 18+/19, Next.js 14+ (App Router friendly)
- Source-only package: ships
src/**as-is. Consumers transpile the package via their bundler (see Consumer setup). - Styling: Tailwind + SCSS Modules. Host app must support both.
Contents
- Install
- Consumer setup
- What the package exports
- Peer dependencies
- End-to-end example
- Versioning & publishing
Install
npm install addv-questionnaire
# or
pnpm add addv-questionnaire
# or
yarn add addv-questionnaireThen install the peer dependencies your app is missing.
Consumer setup
The package is source-only and has three wiring requirements. Do all four steps once at app startup.
1. Next.js transpile config
The package ships JS/JSX/SCSS source — your bundler must compile it. In Next.js:
// next.config.ts
import type { NextConfig } from "next";
const config: NextConfig = {
transpilePackages: ["addv-questionnaire"],
};
export default config;For Vite / webpack consumers, ensure node_modules/addv-questionnaire/** is included by your JSX + SCSS loaders.
2. Register the Redux store
Call setStoreRef(store) immediately after configureStore — before any component that uses the slice mounts. Include questionnaireReducer and localStore in your root reducer.
// store/index.ts
import { configureStore, combineReducers } from "@reduxjs/toolkit";
import { persistReducer, persistStore } from "redux-persist";
import storage from "redux-persist/lib/storage";
import {
questionnaireReducer,
localStore,
setStoreRef,
} from "addv-questionnaire";
const rootReducer = combineReducers({
questionnaire: questionnaireReducer,
localStore,
// ...your other reducers
});
const persistedReducer = persistReducer(
{ key: "root", storage, whitelist: ["questionnaire"] },
rootReducer,
);
export const store = configureStore({
reducer: persistedReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({ serializableCheck: false }),
});
setStoreRef(store); // ← register BEFORE persistStore / any mount
export const persistor = persistStore(store);⚠️ If you forget this step you'll see:
[addv-questionnaire] Tried to access store.dispatch before setStoreRef(store) was called.
3. Inject external dependencies (setGlobalDeps)
The heavy journey components (QuestionnaireFillUpTokenNew, QuestionnaireFromPINVerification) were extracted with thousands of call-sites to app-specific APIs, toasts and constants. Rather than rewriting all of them, the package proxies these through setGlobalDeps. Call it once from a top-level client component.
// app/providers.tsx
"use client";
import { useRef } from "react";
import { Provider as ReduxProvider } from "react-redux";
import { setGlobalDeps } from "addv-questionnaire";
import { store } from "@/store";
import { addItemToCart } from "@/store/slices/cartSlice";
import { GET_DEV_CORPORATE_ID, GET_PROD_CORPORATE_ID } from "@/lib/constants";
import { sendMailAfterAssessmentNextAPI } from "@/lib/services/NextServices/NextJSApis";
import {
errorToast,
successToast,
getErrorObject,
} from "@/lib/utils/utilities/utils";
import {
submitQuestionAnswersAPI,
submitQuestionAnswersAPIProduct,
submitHypertensionAnswers,
createPreconsultation,
createPreconsultationForMedication,
shareConsentAfterPreConsult,
checkPatientQuestionnaireAuthorization,
checkTokenValidity,
fetchQuestionnaireTypes,
} from "@/lib/services/patientQuestionnaire";
export function Providers({ children }: { children: React.ReactNode }) {
const inited = useRef(false);
if (!inited.current) {
setGlobalDeps({
store,
addItemToCart,
GET_DEV_CORPORATE_ID,
GET_PROD_CORPORATE_ID,
sendMailAfterAssessmentNextAPI,
errorToast,
successToast,
getErrorObject,
submitQuestionAnswersAPI,
submitQuestionAnswersAPIProduct,
submitHypertensionAnswers,
createPreconsultation,
createPreconsultationForMedication,
shareConsentAfterPreConsult,
checkPatientQuestionnaireAuthorization,
checkTokenValidity,
fetchQuestionnaireTypes,
});
inited.current = true;
}
return <ReduxProvider store={store}>{children}</ReduxProvider>;
}Full list of injectable deps
| Name | Signature | Purpose |
| --- | --- | --- |
| store | Redux store | Internal store.dispatch(...) / store.getState() (also handled by setStoreRef) |
| addItemToCart | (item) => void | Adds a medication to the host cart after a product outcome |
| GET_DEV_CORPORATE_ID | () => string | Corporate id for non-prod env |
| GET_PROD_CORPORATE_ID | () => string | Corporate id for prod env |
| errorToast | (msg) => void | Error notification |
| successToast | (msg) => void | Success notification |
| getErrorObject | (err) => { message, code? } | Normalises backend error payloads |
| sendMailAfterAssessmentNextAPI | (payload) => Promise<void> | Emails the outcome summary |
| submitQuestionAnswersAPI | (payload) => Promise<Response> | Submit answers (condition flow) |
| submitQuestionAnswersAPIProduct | (payload) => Promise<Response> | Submit answers (product flow) |
| submitHypertensionAnswers | (payload) => Promise<Response> | Hypertension-specific submit |
| createPreconsultation | (payload) => Promise<Response> | Create pre-consult for condition |
| createPreconsultationForMedication | (payload) => Promise<Response> | Create pre-consult for medication |
| shareConsentAfterPreConsult | (payload) => Promise<Response> | Share consent form |
| checkPatientQuestionnaireAuthorization | (token) => Promise<Response> | Token-gated authorization check |
| checkTokenValidity | (token) => Promise<Response> | Generic JWT validity check |
| fetchQuestionnaireTypes | (params) => Promise<Response> | Loads the list of questionnaire templates |
4. Wrap questionnaire routes with QuestionnaireProvider
QuestionnaireProvider carries React-native props the package needs — primarily fetchDropdownOptions (for drug / pharmacy / snomed search dropdowns) — plus config and callbacks.
"use client";
import { QuestionnaireProvider } from "addv-questionnaire";
import { getDrugsApiForPatient } from "@/lib/services/...";
import { generateTokenFromJWT } from "addv-questionnaire";
export function QuestionnairePageWrapper({ children }: { children: React.ReactNode }) {
const services = {
fetchDropdownOptions: async ({ apiEndpoint, search, page, extraParams }) => {
const token = generateTokenFromJWT({ /* ... */ }, process.env.NEXT_PUBLIC_JWT_SECRET);
const res = await getDrugsApiForPatient({
endpoint: apiEndpoint,
search,
page,
token,
...extraParams,
});
return { data: res.data?.items ?? [], pagination: res.data?.pagination };
},
// Optional:
// sendMailAfterAssessment: async (payload) => { ... },
// addItemToCart: (item) => dispatch(addItemToCart(item)),
};
const config = {
apiBaseUrl: process.env.NEXT_PUBLIC_API_BASE_URL,
jwtSecret: process.env.NEXT_PUBLIC_JWT_SECRET,
corporateIds: { dev: "...", prod: "..." },
branding: { primaryColor: "#0c4f4a", logoUrl: "/logo.svg", corporateName: "Paydens" },
routes: { onSuccess: "/booking/success", onCancel: "/", signIn: "/sign-in" },
featureFlags: { enableNHS: true, enableReviewScreen: true, isPrivateBooking: false },
};
return (
<QuestionnaireProvider
services={services}
config={config}
callbacks={{
onJourneyChange: (step) => {},
onSubmitSuccess: (result) => {},
onError: (err) => {},
}}
patientContext={{ token: undefined, dob: undefined, gender: undefined }}
>
{children}
</QuestionnaireProvider>
);
}What the package exports
All exports come from the root (import { X } from "addv-questionnaire").
Constants
DATE_FORMAT, DATE_TIME_FORMAT, HH_HOUR_MIN_FORMAT, H_HOUR_MIN_FORMAT,
SPLIT_SYMBOL, BLOOD_PRESSURE, MASKED_BP, DROP_DOWN_APIS,
HealthlabelMap, healthUnits, healthUnitsMinMax, desiredOrderBiometrics,
BmiQuestion, bpQuestions, biometricPressureClassificationsUtilities
calculateDaysDifference, applyDateCalculation, // date math
classifyBloodPressure, // BP → category
normalizeLabel, labelsEqual, // string compare
buildHealthUnitsMinMax, getRangeForGender, // biometrics ranges
capitalizeFirstLetter, verifyObject,
generateTokenFromJWT, // JWT helper
resetInlineStyleFromEditerContainer,
cn, handleErrors, getColorTint, hexToRgbValidation
ValidateFillUpAnswer(data, handleErrors, gender)
// Validates a questionnaire payload. `handleErrors` is invoked as
// (message, question, child?, grandchild?) for every error.Context / Provider
QuestionnaireProvider,
useQuestionnaireContext,
useQuestionnaireServices,
useQuestionnaireConfigGlobal DI
setGlobalDeps(deps), // register external functions / store
setStoreRef(store), // register Redux store
getStore() // read the registered storeRedux slice + actions
questionnaireReducer, // default export — mount as `questionnaire` in rootReducer
localStore, // second reducer — mount as `localStore`
actionTypes, // plain action-type constants
setHeaderConditionData,
setFooterHeight,
// plus every action creator from the questionnaire slice (21 actions)Hooks
useMediaQuery,
useWindowWidth,
useLeavePageConfirmQuestion type components
Every renderable question variant:
CheckAgreeQuestion, CheckAgreeQuestionAnswer,
CheckboxChoiceQuestionAnswer, CheckboxGroupQuestion,
CustomDropDown, CustomDropDownItem,
DatePickerItem, DatePickerQuestion, DateRangeQuestion,
FileInputQuestion, FileInputTypeItem,
HealthDataPointInputItem, HealthDataPointQuestion,
InformationTextItem, InformationTextQuestion,
NumericalItem, NumericalQuestion,
QuestionTitle, RangePickerItem,
SingleChoiceQuestion, SingleChoiceQuestionAnswer,
TextAreaQuestion, TextAreaQuestionInput,
TextBoxInputItem, TextBoxQuestionWrapper / sibling components
QuestionsChildren, QuestionsChildOfChild, QuestionsAdditionalInformation,
InformationTextItemSibling, InformationTextQuestionSibling,
AnswerItem, AnswerReview, MaskDateInputPicker,
PatientDataForQuestionnaire, QuestionnaireFillUpItem,
QuestionnaireWizardItem, WizardFooter, WizardStepJourney components
Higher-level orchestrators you'll typically mount directly:
QuestionnaireFillUpTokenNew // token-based deep-link entry (main filler)
QuestionnaireFromPINVerification // PIN / OTP entry variant
QuestionnaireWizard // multi-step wizard wrapper
OutComeScreen, PatientSpecificOutComeScreen, SuccessScreen,
PageExpired, ConditionNotFound,
PrivateConditionDisabledMessageScreen,
ProgressContent, DummySectionPeer dependencies
The package declares these as peers — install them in your app:
react≥ 18,react-dom≥ 18,next≥ 14@reduxjs/toolkit≥ 1.9,react-redux≥ 8antd≥ 5,@ant-design/icons≥ 5@mui/x-date-pickers≥ 5react-use-wizard≥ 2,react-albus≥ 2,framer-motion≥ 10axios≥ 1,dayjs≥ 1.11,moment≥ 2.29,lodash≥ 4jsonwebtoken≥ 8,nanoid≥ 3,uuid≥ 9clsx≥ 2,tailwind-merge≥ 2values.js≥ 2,sass≥ 1
@mona-health/react-input-mask and react-imask used to be optional peers.
They are now bundled as regular dependencies — npm installs them
automatically when you add addv-questionnaire, because the package's source
unconditionally imports them from the health-data-point inputs.
End-to-end example
// app/(appointmentFlow)/questionnaire/page.tsx
import { QuestionnaireFillUpTokenNew } from "addv-questionnaire";
import { QuestionnairePageWrapper } from "@/components/QuestionnairePageWrapper";
export default function Page() {
return (
<QuestionnairePageWrapper>
<QuestionnaireFillUpTokenNew />
</QuestionnairePageWrapper>
);
}Prerequisites satisfied:
next.config.tshastranspilePackages: ["addv-questionnaire"]store/index.tscallssetStoreRef(store)right afterconfigureStoreapp/providers.tsxcallssetGlobalDeps({ ... })onceQuestionnairePageWrapperwraps with<QuestionnaireProvider services={{ fetchDropdownOptions }} ... />
Versioning & publishing
Semver:
- patch — bug fix, no API change
- minor — new export / additive feature
- major — removed export, renamed prop, changed action payload
Publish flow:
cd packages/questionnaire
# bump version in package.json
npm publishLicense
UNLICENSED — internal ADDV use only.
