Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions app/data/session-data-defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,15 +93,15 @@ const base = {
},
'FLU_AND_COVID': {
id: 'FLU_AND_COVID',
title: 'Flu and COVID-19 services'
title: 'Flu and COVID-19 co-admin services'
},
'RSV': {
id: 'RSV',
title: 'RSV services'
},
'RSV_AND_COVID': {
id: 'RSV_AND_COVID',
title: 'RSV and COVID-19 services'
title: 'RSV and COVID-19 co-admin services'
}
},
services: {
Expand Down
24 changes: 24 additions & 0 deletions app/filters.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
* @param {Environment} env
*/

const { DateTime } = require("luxon");

const registerDateTimeFilters = require("./filters/datetime")
const util = require("util")

Expand Down Expand Up @@ -54,6 +56,28 @@ module.exports = function (env) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}

filters.featureType = (value) => {
if (value === null || value === undefined) return "empty";

if (Array.isArray(value)) return "json";

const t = typeof value;

if (t === "boolean") return "boolean";
if (t === "number") return Number.isInteger(value) ? "int" : "number";
if (t === "object") return "json";

if (t === "string") {
// ISO date heuristic: yyyy-mm-dd
const dt = DateTime.fromISO(value, { zone: "utc" });
if (dt.isValid && value.length === 10) return "date";
return "string";
}

return "string";
}


return filters
}

Expand Down
3 changes: 2 additions & 1 deletion app/flags/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// eg. featureX: true, featureY: false
module.exports = {
allAvailability: false,
cancelDateRange: true
cancelDateRange: true,
today: null
}

35 changes: 7 additions & 28 deletions app/flags/env.js
Original file line number Diff line number Diff line change
@@ -1,35 +1,14 @@
require('dotenv').config();
require("dotenv").config();
const { interpretUserInput } = require("./parser");

const fromEnv = () => {
const flags = {}
const flags = {};
for (const [key, value] of Object.entries(process.env)) {
if (key.startsWith('FEATURE_')) {
const name = key.replace('FEATURE_', '').toLowerCase();

//parse bools, numbers, JSON or leave as a string
let parsedValue = value;

//bools
if (value === 'true') parsedValue = true;
else if (value === 'false') parsedValue = false;

//numbers
else if (!Number.isNaN(Number(value))) parsedValue = Number(value);

//JSON
else if(
(value.startsWith('{') && value.endsWith('}')) ||
(value.startsWith('[') && value.endsWith(']'))
) {
try { parsedValue = JSON.parse(value); }
catch { parsedValue = value; }
}

flags[name] = parsedValue;

}
if (!key.startsWith("FEATURE_")) continue;
const name = key.replace("FEATURE_", "").toLowerCase();
flags[name] = interpretUserInput(value);
}
return flags;
}
};

module.exports = fromEnv();
97 changes: 33 additions & 64 deletions app/flags/flags-library.js
Original file line number Diff line number Diff line change
@@ -1,93 +1,62 @@
const envFlags = require('./env')
const defaults = require('./defaults')
const envFlags = require("./env");
const defaults = require("./defaults");
const { interpretUserInput } = require("./parser");


// Parse overrides from the querystring
// E.g., `?feature=calendar:on,search:off`
const parseQueryOverrides = (req) => {
const overrides = {};
if (!req.query.features) return overrides;

if(req.query.features) {
const items = String(req.query.features).split(',');
for (const item of items) {
const [rawKey, rawVal] = item.split(':');
if (!rawKey) continue;
const key = rawKey.trim();
//set value to true if none is provided
const val = (rawVal ?? 'true').trim();
for (const item of String(req.query.features).split(",")) {
const [rawKey, rawVal] = item.split(":");
if (!rawKey) continue;

//normalise and add the key/value to the overrides object
overrides[key] = normaliseValue(val);
}
}
const key = rawKey.trim();
const raw = (rawVal ?? "true").trim();

overrides[key] = interpretUserInput(raw);
}
return overrides;
};

}

const parseFormOverrdes = (req) => {
const parseFormOverrides = (req) => {
const overrides = {};

//only process form post data
if (req.method !== 'POST') return overrides;

//only process POST with body data
if (!req.body) return overrides;
if (req.method !== "POST" || !req.body) return overrides;

const featuresFromForm = req.body.features || {};
for (const [key, value] of Object.entries(featuresFromForm)) {
if (value === '' || value === null) continue;
overrides[key] = normaliseValue(String(value));
for (const [key, raw] of Object.entries(featuresFromForm)) {
if (raw === "" || raw === null) continue;
overrides[key] = interpretUserInput(raw);
}
return overrides;
}

const normaliseValue = (val) => {
if (val === 'on' || val === 'true') return true;
if (val === 'off' || val === 'false') return false;
if (!Number.isNaN(Number(val))) return Number(val);
try { return JSON.parse(val); } catch { return val; }
}
};

const buildFlags = (req) => {
const sessionOverrides = req.session?.features || {};
const queryOverrides = parseQueryOverrides(req);
const formOverrides = parseFormOverrdes(req);
const formOverrides = parseFormOverrides(req);

const flags = {
...defaults,
...envFlags,
...sessionOverrides,
...queryOverrides,
...formOverrides
}
...formOverrides,
};

//persist anything new in the form or query string
const newValues = {
...queryOverrides,
...formOverrides
}
// persist query/form overrides into session (raw values only)
const newValues = { ...queryOverrides, ...formOverrides };
if (Object.keys(newValues).length) {
req.session.features = {
...sessionOverrides,
...newValues
}
req.session.features = { ...sessionOverrides, ...newValues };
}

return flags;
}

const flagsMiddleware = () => {
return (req, res, next) => {
const flags = buildFlags(req);

//attach flags to req to use in routes
req.features = flags;

//attach flags to locals to use in views
res.locals.features = flags;

next();
}
}
};

const flagsMiddleware = () => (req, res, next) => {
const flags = buildFlags(req);
req.features = flags;
res.locals.features = flags;
next();
};

module.exports = { flagsMiddleware };
20 changes: 20 additions & 0 deletions app/flags/parser.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@

function interpretUserInput(raw) {
const s = String(raw ?? "").trim();

if (s === "on" || s === "true") return true;
if (s === "off" || s === "false") return false;

if (!Number.isNaN(Number(s))) return Number(s);

if (
(s.startsWith("{") && s.endsWith("}")) ||
(s.startsWith("[") && s.endsWith("]"))
) {
try { return JSON.parse(s); } catch {}
}

return s;
}

module.exports = { interpretUserInput };
47 changes: 30 additions & 17 deletions app/routes/base.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,32 @@ const router = express.Router();
const { DateTime } = require('luxon');

const { availabilityGroups } = require('../helpers/availabilityGroups');
const { calendar } = require('../helpers/calendar');
const updateDailyAvailability = require('../helpers/updateDailyAvailability');
const enhanceData = require('../helpers/enhanceData');
const summarise = require('../helpers/summaries');
const compareGroups = require('../helpers/compareGroups');
const removeServicesFromDailyAvailability = require('../helpers/removeDailyAvailability');
const { every } = require('lodash');

const override_today = '2026-09-28'; //for testing purposes
// -----------------------------------------------------------------------------
// TODAY OVERRIDE
// -----------------------------------------------------------------------------
function resolveToday(req) {
const override = req.features?.today;
const realToday = DateTime.now().toFormat("yyyy-MM-dd");

if (override) {
const dt = DateTime.fromISO(String(override), { zone: "utc" });
if (dt.isValid) return dt.toFormat("yyyy-MM-dd");
}

return realToday;
}

router.use((req, res, next) => {
//make 'today' available globally
override_today = resolveToday(req);
res.locals.today = override_today;
next();
});

// -----------------------------------------------------------------------------
// PARAM HANDLER – capture site_id once for all /site/:id routes
Expand All @@ -30,9 +47,10 @@ router.param('id', (req, res, next, id) => {
router.use('/site/:id', (req, res, next) => {
const data = req.session.data;
const site_id = String(req.site_id);
const today = resolveToday(req);

// Maybe: Remove this filters stuff as we're not doing any filtering in this prototype?
//use filters everywhere
const today = override_today || DateTime.now().toFormat('yyyy-MM-dd');
const sessionFilters = data.filters?.[site_id] || {};

// --- Prefer query, then session, then default ---
Expand Down Expand Up @@ -63,8 +81,6 @@ router.use('/site/:id', (req, res, next) => {
until: resolvedUntil
};

data.today = today; //expose to session data for convenience


if (!data?.sites?.[site_id]) {
console.warn(`⚠️ Site ${site_id} not found in session data`);
Expand Down Expand Up @@ -101,7 +117,7 @@ router.use('/site/:id', (req, res, next) => {
router.post('/set-filters', (req, res) => {
const filters = { ...req.body.filters }

req.session.data.filters = filters;
req.session.filters = filters;

res.redirect(req.body.next);
});
Expand Down Expand Up @@ -191,21 +207,18 @@ router.get('/site/:id/create-availability/process-new-session', (req, res) => {
// VIEW AVAILABILITY
// -----------------------------------------------------------------------------
router.get('/site/:id/availability/day', (req, res) => {
const date = req.query.date || override_today || DateTime.now().toFormat('yyyy-MM-dd');
const startFromDate = req.query.date || resolveToday(req);

res.render('site/availability/day', {
date,
today: override_today || DateTime.now().toFormat('yyyy-MM-dd'),
tomorrow: DateTime.fromISO(date).plus({ days: 1 }).toISODate(),
yesterday: DateTime.fromISO(date).minus({ days: 1 }).toISODate()
date: startFromDate,
tomorrow: DateTime.fromISO(startFromDate).plus({ days: 1 }).toISODate(),
yesterday: DateTime.fromISO(startFromDate).minus({ days: 1 }).toISODate()
});
});

router.get('/site/:id/availability/week', (req, res) => {
const data = req.session.data;
const site_id = req.site_id;
const startFromDate = req.query.date || override_today || DateTime.now().toFormat('yyyy-MM-dd');
const today = override_today || DateTime.now().toFormat('yyyy-MM-dd');
const startFromDate = req.query.date || resolveToday(req);
const today = resolveToday(req);

//return dates for the week containing 'date'
const week = [];
Expand Down
6 changes: 5 additions & 1 deletion app/views/concepts/custom-components/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
<div class="nhsuk-form-group">
<span class="nhsuk-caption-l">Add availability</span>
<h1 class="nhsuk-heading-l">Add services to your session</h1>
<p>Co-admin appointments can only be booked by people eligible for both vaccinations.</p>
<fieldset class="nhsuk-fieldset app-checkbox-group">
<legend class="nhsuk-fieldset__legend nhsuk-fieldset__legend--m">
<span class="nhsuk-u-visually-hidden">Add </span>Flu services<span class="nhsuk-u-visually-hidden"> to your session</span>
Expand Down Expand Up @@ -65,6 +66,7 @@ <h1 class="nhsuk-heading-l">Add services to your session</h1>
<p id="services-error" class="nhsuk-error-message">
<span class="nhsuk-u-visually-hidden">Error:</span> Select a service
</p>
<p>Co-admin appointments can only be booked by people eligible for both vaccinations.</p>
<fieldset class="nhsuk-fieldset app-checkbox-group">
<legend class="nhsuk-fieldset__legend nhsuk-fieldset__legend--m">
<span class="nhsuk-u-visually-hidden">Add </span>Flu services<span class="nhsuk-u-visually-hidden"> to your session</span>
Expand Down Expand Up @@ -161,6 +163,7 @@ <h2 class="nhsuk-heading-m nhsuk-u-margin-top-9" id="grouped-checkboxs">Grouped
<div class="nhsuk-form-group">
<span class="nhsuk-caption-l">Add availability</span>
<h1 class="nhsuk-heading-l">Add services to your session</h1>
<p>Co-admin appointments can only be booked by people eligible for both vaccinations.</p>
{% for groupId, group in data.serviceGroups %}
<fieldset class="nhsuk-fieldset app-checkbox-group">
<legend class="nhsuk-fieldset__legend nhsuk-fieldset__legend--s">
Expand Down Expand Up @@ -193,7 +196,7 @@ <h1 class="nhsuk-heading-l">Add services to your session</h1>
<div class="nhsuk-grid-row">

<div class="nhsuk-grid-column-two-thirds">
<h2 class="nhsuk-heading-m nhsuk-u-margin-top-9" id="grouped-checkboxs">Grouped checkboxes error state</h2>
<h2 class="nhsuk-heading-m nhsuk-u-margin-top-9" id="grouped-checkboxs-error">Grouped checkboxes error state</h2>
<p>The error state of grouped checkboxes also needs some custom markup.</p>
<p><a href="/site/1/create-availability/services?error=true">See this in action on the add availability page</a>.</p>
</div>
Expand All @@ -207,6 +210,7 @@ <h1 class="nhsuk-heading-l">Add services to your session</h1>
<p id="services-error" class="nhsuk-error-message">
<span class="nhsuk-u-visually-hidden">Error:</span> Select a service
</p>
<p>Co-admin appointments can only be booked by people eligible for both vaccinations.</p>
{% for groupId, group in data.serviceGroups %}
<fieldset class="nhsuk-fieldset app-checkbox-group">
<legend class="nhsuk-fieldset__legend nhsuk-fieldset__legend--s">
Expand Down
Loading