This repository is for the MedCalc tool, a web application that helps healthcare providers quickly and easily convert a Parkinson’s patient’s regular medicine into an equivalent dose of another medicine.
The need for this tool is based on the fact that sometimes patients with Parkinson's Disease (PD) who have been admitted to hospital are unable to take their medications orally. Therefore the patient's usual oral medications need to be converted to a 'Levodopa equivalent dose' (LED), which is delivered either through a patch or via a naso-gastric tube.
The application is written in JavaScript and uses the Next.js framework and React.js library.
The Next.js framework is configured for static export. This means the compiled application runs entirely in the browser and has no server side execution.
Stylesheets are written using the Sass
There is no database component. The details of the drugs involved in the calculation are defined in the file src/app/data/data.js
.
The application uses the Git version control system and the GitFLow branching strategy.
The code is hosted on GitHub. The location of the repository is: https://github.com/tpximpact/pdmedcalc-v2
The production branch is called main
The development branch is called develop
The application is hosted on CloudFlare pages. CloudFlare is also used as the NameServer and provides security protection for the site including DDOS protection.
CloudFlare Pages also provides the deployment pipeline. The CloudFlare pages application is connected to the GitHub repository. A deployment is triggered when code is committed to the main or develop branch. The application is automatically tested and built as part of the deployment pipeline.
Code changes to the main branch are deployed to production. Code changes to any other branch are deployed to a test environment.
The production URL is: https://pdmedcalc.co.uk
A new test environment is created each time code is pushed to a non production branch. The test URL follows the pattern: https://[commit-hash].pdmedcalc-v2.pages.dev/ e.g. https://7047263d.pdmedcalc-v2.pages.dev/
Note: There isn't a configuration file in the repository for CloudFlare. Configuration is provided inside the CloudFlare pages interface.
- Clone the repo to your machine
- cd into the directory where you cloned it
npm install
to install the relevant JavaScript dependencies.
Run npm run dev
to start the development server and visit http://localhost:3000 to see the home page
Run npm run test
to run the jest (unit and integration) tests
Run npm run cypress:open
to run the cypress (end-to-end) tests
Run npm run build
The static assets can then be found in the generated out
folder.
The automated deployment script which runs when a deployment is triggered is:
npm run test && npm run build
Note: this is configured inside the CloudFlare pages interface, there isn't a separate build script file or hosting configuration file.
CloudFlare Analytics is integrated with the site for the purposes of counting the number of users. It can be accessed on the Web Analytics part of the CloudFlare Dashboard.
The site does not set cookies or store any activity undertaken by the users.
There are no commercially licensed software libraries in this application. The CloudFlare hosting environment is being used on the free tier. CloudFlare Analytics is being used on the free tier. The production domain registration is being provided by 123-reg.co.uk
This project is based upon an older version of the MedCalc.
This is therefore version 2 of the MedCalc tool.

- Users land on the home page, read the information presented and accept the disclaimer. This redirects the user to the calculator page.
- On the calculator page, users enter the respective medications and frequencies using the select dropdowns and the add button. Once the user is done adding medicines they then submit the form by clicking on the calculate button. This redirects the user to the results page.
- The medicines and frequencies the user entered on the calculator page are on the search params of the url. If these are missing (for example because the user went straight to /results and skipped the calculator page), the user is automatically redirected to the home page. If however they are present as expected, then the results page uses them to present to the user:
- what was entered on the calculator page
- a calculation of the total levodopa equivalent dose (in mg per day)
- a conversion of the entered medications to a dispersible madopar split across four dose times i.e. option 1
- a conversion of medications to a transdermal rotigotine patch i.e. option 2
See the 'rules of the calculator doc' for a detailed explanation written by James Fisher.
Although this project uses javascript rather than typescript, the types for the expected inputs and outputs of the various functions are nonetheless described for the sake of clarity.
No user data is stored. The only data this app stores is related to the medicines themselves.
This app therefore does not have a database because it does not really need one since there are only 56 different medicines and we only need to know three different properties of each; having a database for the sake of one table with four columns and just 56 entries is unnecessary.
The data related to the medicines is in src/app/data/data.js
.
The medications
object is of the type :
type Medications = {
string : {
led: number
isDa: boolean
isComt: boolean
hasLevodopa: boolean
}
}
Each property of the medications
object is a string representing the name of that medicine. The value of this property is an object with led
, isDa
and isComt
properties.
led
represents the levodopa equivalent dose for one unit of this medicine.
isDa
represents whether or not this medicine is a dopamine agonist.
isComt
represents whether or not this medicine is a comt inhibitor.
hasLevodopa
determines if a mediciene contains Levodopa. Levodopa mediciens are affected by the presense of a comt inhibitor.
All the functions responsible for the actual conversion of the medicines are in src/app/calculator/calculator-functions.js
.
These functions are written in a functional programming style; they are pure i.e. they take an input and return an output without mutating the original input or having any other side-effect.

The mainTransform
function takes an array of medicine objects. Each medicine object is formed by looping through the search params of the results page's url and is of the type
{
name: string
frequencyPerDay: number
}
The two options object that the mainTransform
function returns is of the type
{
option1: TimeMadoparObject
option2: number
}
where
type TimeMadoparObject = {
"0800": TimeMadparTuple
"1200": TimeMadparTuple
"1600": TimeMadparTuple
"2000": TimeMadparTuple
}
and
type TimeMadparTuple = [
{ name: 'Madopar Dispersible 125mg (25/100mg)', quantity: number, led: 100 },
{ name: 'Madopar Dispersible 62.5mg (12.5/50mg)', quantity: number, led: 50 }
]

The calculateRotigotine
function takes an array of medicine objects and returns a number representing the equivalent patchdose in mg for these medicines.
The patchdose is calculated by:
- splitting out the dopamine agonists from the non-dopamine agonists
- calculating the total led of the dopamine agonists and the total led of the non-dopamine agonists
- dividing the totalLedOfDopamineAgonists by the adjustment (30)
- multiplying the totalLedOfNonDopamineAgonists by the correction factor (0.25) and then divding the result by the adjustment (30)
- summing the results of the previous two steps
- rounding the result to a multiple of 2

The calculateTotalLed
function takes an array of medicine objects and returns a number representing the total levodopa equivalent dose for these medicines.
This total led is calculated by:
- splitting out the comt inhibtors from the non-comt inhibitors
- calculating the total led of the non-comt inhibitors by multiplying the frequencyPerDay of each medicine with its led and all the results
- picking out the comt inhibitor with the largest totalLedAdjustment* and multiplying its totalLedAdjustment against the totalLedFromNonComtInhibitors
- summing the totalLedFromNonComtInhibitors and the result of the previous step
*Note that, in practice, "patients would only ever be on one COMT inhibitor, since [...] wouldn't prescribe more than one of them as there would be no value in doing so (once the COMT enzyme is blocked, it's blocked)". The function nevertheless handles the case where a user might input multiple comt inhibitors on the calculator page

The calculateMadopar
function takes a targetLED and returns a TimeMadoparObject
representing an equivalent total led in the form of dispersible madopar split across four dose times.
This is calculated by:
- rounding the targetLED to the nearest 50 since the dispersible madopar comes in two sizes 50 (small) and 100 (big)
- finding every possible combination of big and small madopars whose totalLed equals the roundedTargetLed
- for each of the above combinations, allocating the big and small madopars to four time zones to create a TimeMadoparObject for each
- for each TimeMadoparObject above calculating the maximum spread between the total leds of the four dose times
- returning the TimeMadoparObject which has the lowest max spread (i.e. the one in which the doses are most evenly spread across the four time slots)

The allocateMadopar
function takes two arguments; noOfBigMadopars
and noOfSmallMadopars
. It returns a TimeMadoparObject
representing an equivalent total led in the form of dispersible madopar split across four dose times.
This allocation is done by:
- looping through the big madopars and placing one in each time slot (starting at 0800) until there are none left
- looping through the small madopars and placing one in each time slot (starting at the time slot which has the fewest big madopars) until there are none left

The calculateMaxSpread
function takes a TimeMadoparObject
and returns a number representing the maximum difference in total leds between the time slot with the highest total led and the time slot with the lowest total led.
This is calculated by:
- calculating the total led for each time slot
- calculating the difference in total leds between each time slot
- returning the maximum difference in total led between the time slots
Given that this is a medical tool, the need for it to be accurate and reliable is obvious.
The following testing strategy has therefore been implemented:
- manual/user acceptance testing by a clinician prior to go live
- integration tests for the
mainTransform
function - end-to-end tests covering the full user journey from the home page to the results page
- unit tests for all the other functions that build up to the
mainTransform
function
To give confidence that the tool is correctly calculating the conversions for the various medicines, a trained medical professional has created example patients and calculated by hand what the correct dose output should be for each. These have then been compared against the results that the tool produces to make sure the two align.
Since the mainTransform
function is ultimately responsible for taking the array of medicines entered on the calculator page as an argument and returning the output (conversion to dispersible madopar and transdermal rotigotine patch) which will be shown on the results page, this is the most important function to test.
Proving its robustness also has the nice side effect of simultaneously proving that all the other functions which mainTransform
relies on are themselves working correctly.
There are 61 different medicines a user could potentially enter on the calculator page. For each medicine entered there are 10 possible frequencies they could select. If we think of a medicine/frequency pair as one "thing", then there are effectively 610 different "things" that could be entered.
Combinatorics allows us to calculate the number of different possible combinaitions of these 610 "things".
The combinations formula is:
C(n,r) = n!/r!(n-r)!
where n
is the total number of things you are choosing from, r
is the number of things you are choosing and !
is factorial.
Suppose that a user enters 3 different "things". 610!/3!(610 - 3)!
is the same as 610 * 609 * 608 / 3!
which equals 37,644,320.
This illustrates the vast number of possibilities. And that's just with the user entering 3 different medicine/frequency pairs.
It's obviously not feasible to write over 37 million integration tests so we need to reduce the number of "things" while retaining confidence in our tests and app.
The frequency only affects the overall conversion via the calculateTotalLed
function and so, given that this function is itself unit tested, we don't have to consider the frequencies in the integration test cases thus reducing the number of "things" back down to a more manageable 61.
Nevertheless, 61!/3!(61 - 3)!
is still quite big (35,990) and writing thirty-five thousand integration tests would also not be practical.
We need to reduce the number of "things" further (while still maintaining confidence in our tests and app).
The overall conversion treats medcines differently based on whether they are a dopamine agonist, a comt-inhibitor or neither. Therefore we in effect have just 3 "things" to create test cases for.
- If a user enters just one medicine, there are three possibililities (
3!/1!(3 - 1)! = 3
) i.e. they entered a dopamine agonist or they entered a comt inhibitor, or they entered a medicine that is neither a dopamine agonist nor a comt inhibitor - If a user enters two medicines, there are three possibililities (
3!/2!(3 - 2)! = 3
) i.e. they entered a dopamine agonist and a comt inhibitor, or they entered a dopamine agonist and a medicine that is neither a dopamine agonist nor a comt inhibitor, or they entered a comt inhibitor and a medicine that is neither a dopamine agonist nor a comt inhibitor - If a user enters three medicines, there is just one possibility (
3!/3!(3 - 3)! = 1
) i.e. they entered a dopamine agonist, a comt inhibitor and a medicine that is neither a dopamine agonist nor a comt inhibitor
Thus we can see that we only have to write 7 integration tests rather than tens of millions!
These 7 tests are in src/app/tests/integration-tests/mainTransform.test.js
.
Cypress is used to run end-to-end tests on the actual live site. It uses the same 7 test cases as the integration tests.
These end-to-end tests are in cypress/e2e/home-calc-results-flow.cy.js
Each of the individual functions that feed into the mainTransform
function are themselves unit tested. These unit tests can be found in src/app/tests/unit-tests
.