← work

2013 — 2020 · work

Exercises from the physiotherapist to the patient’s phone

developer, MijnOefening

A platform where a physiotherapist composes an exercise list, schedules it per weekday, sends it to a patient’s phone, and gets pain, ease and discipline back as charts. Built twice: Drupal in 2013, Laravel in 2020.

Laravel 5.8 · Passport OAuth · MySQL · Blade · Drupal 7 · Python · Voxeet

A physiotherapist sees a patient for twenty minutes and then sends them home with a sheet of exercises. MijnOefening is the sheet, done properly: the therapist picks exercises from a library of filmed ones, sets how many sets and repetitions and on which days, writes a note, and presses send. The patient gets it on their phone, does the exercises, and says how much it hurt. The therapist sees that as a chart before the next appointment, and can call the patient on video from the same page.

It was built twice. The first version was a Drupal 7 site from 2013, made at Itsavirus, the agency I was freelancing for. The second is a Laravel application, written from August 2018 by the clinic’s own developer, which I took over in the spring of 2020 to get it launched. The live site today is the second one.

The Drupal version: a list you could e-mail

In 2013 an exercise was a node with a photograph, a body text, and four vocabularies: where the complaint is, how long it has lasted, its pain class, and the movement category. The front page was a search block built from those four filters, and a therapist collected exercises into an exercise list. Sending was a form on the list: type an address, and a module mailed an HTML page with every exercise’s picture and a link, signed with the practice’s name and address from the therapist’s profile, and logged a mail_log node so the practice could see what had gone out.

Who could see which exercise was a per-node field, anonymous, authenticated or chiropractor, turned into Drupal access grants, so the public got a taste and the paying therapists got the rest. Everything was in Dutch and English through the i18n stack, which needed two small modules of its own because Views would not translate taxonomy terms in exposed filters or term listings on its own.

What that version could not do is the whole point of the second one: the patient did not exist. An e-mail went out and nothing came back.

A message is the unit

The Laravel rebuild has patients as users, and the thing a therapist sends is a message. A message carries the note, an exercise list, a start and an end date, a schedule per exercise with seven booleans for the weekdays, any advice documents, and the patient’s own PSK items, the patient-specific complaints a physiotherapist scores, such as climbing stairs or getting out of a car. Saving the message to an address the system does not know creates the patient and sends a registration link; to a known patient it links them to the therapist and, if they asked for it, e-mails that something new is waiting.

The patient’s day is a query over that shape. Take every message that has started and not ended, join the schedule on today’s weekday, join the exercise list for the order, and drop what has already been reported today or given a reason. Each exercise reports back separately: pain from one to ten, how many sets and repetitions were done, capped at what was asked, and a reason if it could not be done. At the end of the day there is one more form, the ease of the list and of each PSK item, on the same scale.

One message, both ends. The therapist writes the left box once; the right box is what the patient sees every morning, computed from it. What comes back is averaged per day per message and drawn as the charts.

Everything the therapist sees afterwards is arithmetic on those reports: pain per exercise per day, ease of the list, and discipline, which is sets done divided by sets expected. The same numbers feed the patient’s side of the app, where they become a monthly leaderboard against anonymous others in the clinic and twelve achievements with experience points, from “100% discipline for a day” at 25 points to “a month of exercises” at 800. The discipline number underneath them is the one a therapist actually wants.

Composing an exercise list in the browser: a grid of exercise thumbnails with a checkbox each, a title search, and on the right the list name, description, start date 22 March 2020, end date and seven weekday checkboxes all ticked.
Composing a list: pick from the thumbnails, set the dates and the weekdays.
The exercise picker as a modal over the patient page: filters for location, intensity, category, pain and equipment, sliders for repetitions, sets and rest, and a grid of exercise thumbnails.
The library, filtered by the five attributes and three sliders.
A patient’s chart in the therapist’s browser: three lines over seven days for two PSK items, climbing stairs and getting out of a car, and the ease of the exercise list, on a scale of one to ten, with tabs for ease, pain and discipline.
What comes back: ease per PSK item and per list, over the last seven days.
The live site’s therapist demo: the exercise library with attribute filters on the left, a grid of filmed exercises on a blue mat, and an orange demo bar at the foot offering the patient and manager views.
The library on the live site, in the demo.
The live site’s patient demo: a message from a demo therapist, then an agenda with today’s and tomorrow’s exercises, and the demo bar at the foot.
The patient’s view of the same message, in the browser.
The front page of mijnoefening.nl: the MO logo, the line “De Online Oplossing voor Fysiotherapeuten”, and a dark box offering one month free for an e-mail address.
The front page, September 2026.

6 images — swipe or scroll sideways

Six hundred exercises from three film days

The library came from three days of filming, and the therapists’ notes for each shot came back as one long text: a code, a Dutch title, an English title, a YouTube link, starting position, execution, and the attributes. A Python script cuts that text into 604 exercises, maps every attribute to its English name, writes the database seeder, and then, for each YouTube id, downloads the clip and cuts two GIFs from it, a small and a large one, at a default moment or at a timestamp the therapist can set later per exercise. A GIMP script clears the white studio floor from the photographs. Adding an exercise in the admin runs the same GIF cutter when the video or the timestamp changes, if the server has it switched on.

The number in the seeder is also the honest size of the product: 604 exercises, in two languages, each with a video, is what a practice gets for its ten euros per therapist per month, or a hundred a year, which Mollie collects as a recurring payment per clinic manager.

What I did in 2020

I arrived at a readme that begins with how to install the project on a new machine, which is what a handover looks like. The clinic behind it wanted to go live earlier than planned because of COVID, free until the first of June 2020, and the code was a working application with no front door: no public page that explained it, no way to try it, and a dashboard that had grown a view per feature. Rewriting a hundred and fifty Blade views was not on the table in seven weeks. Three things were.

A landing page, with the galleries fed from a config file listing the screenshots, so that the marketing renders and the product could be updated separately. A demo: one click creates a throwaway clinic, manager, therapist and patient, logs the visitor in as the therapist, and a bar at the foot lets them switch to the patient or the manager, ask a question or register. And a dashboard shell around the existing views: the therapist’s menu is a tree in a config file, each node naming a view and whether it can load lazily. The server walks the tree, turns every path into a pattern, and renders all panels into one page; the browser matches the URL against the patterns, shows the right panel, pushes the state, and marks the breadcrumb. The old views did not change; they got a frame that behaves like one application.

The pieces. The browser and the app are two doors into one Laravel application; the phone talks OAuth with a two-factor code that expires. The videos live on YouTube and are cut into GIFs locally.

Around the edges

A practice already has an electronic patient record, Fysiomanager or Crossuite, and does not want to type a patient twice. Each therapist holds a token per system; the record system posts the patient’s name, e-mail and that token, and the request lands either on the send page for an existing patient, linked to this therapist if they were not, or on a pre-filled form for a new one. Video calls are Voxeet rooms: the therapist starts one from the patient page and the phone fetches its own token from the API.

Operations run through a Telegram bot. A scheduled job checks a list of the clinic’s domains and reports the ones that are down, a daily job pulls the abuse database’s blacklist into a table that a middleware checks on every request, and any path containing wp is looked up on the spot. Payment failures go to the same chat with the error attached; the error log in the archive is Mollie answering 429 to a scheduled run that tried a card, then Bancontact, then direct debit for the same amount in one go.

The live site still answers with a Laravel session cookie, the front page still lists the screenshots from that config file, and the demo now lives at its own set of routes with a patient called Peter Parker, which is not in my copy of the code. It kept being built after I left.

Three phone screens side by side: the overview with a message from therapist Sten Hofstad to Jan Janssen and a “Begin uw oefening” card; the exercise evaluation with three sets of fifteen repetitions, a pain slider at 1 and a field for why it did not work; and the documents page with two diet PDFs.
The patient’s day: the message, the report per exercise, and the advice documents.
Two phone screens: average discipline per day as a line over three weeks and discipline per day per exercise as bars; and pain per exercise per day as bars on a scale of zero to ten with a legend of five exercises.
Discipline and pain, per day and per exercise, on the phone.
Two phone screens: achievements at level 28 with six badges dated 12 March 2020; and a monthly leaderboard of anonymous patients ranked by discipline, with Jan Jansen fourth at 30%.
Achievements and the monthly leaderboard against anonymous others in the clinic.

3 images — swipe or scroll sideways