Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
8f40e2a
Initial work on pharmacy chain mode
frankieroberto Apr 24, 2026
41a628b
More work in progress
frankieroberto Apr 24, 2026
d319388
More work in progress
frankieroberto Apr 24, 2026
2c13df8
Add lead administrators page
frankieroberto Apr 24, 2026
93e50b2
Some more progress
frankieroberto Apr 24, 2026
e4a8c67
Update add another so that none are shown by default
frankieroberto Apr 24, 2026
e08d333
Add a user to a pharmacy
frankieroberto Apr 24, 2026
7df70a8
Merge branch 'main' into pharmacy-chain-mode
frankieroberto Apr 28, 2026
3126c2c
Merge branch 'main' into pharmacy-chain-mode
frankieroberto Apr 29, 2026
339a3a1
Make more of the pages work
frankieroberto Apr 30, 2026
6916ff9
Add ability to add pharmacies
frankieroberto Apr 30, 2026
0ff4830
Fix address
frankieroberto Apr 30, 2026
388e6eb
Fix adding users to pharmacies
frankieroberto Apr 30, 2026
9ece659
Extract checkbox count javascript
frankieroberto Apr 30, 2026
2b1809d
Use I18n module
frankieroberto Apr 30, 2026
4239ab9
Use javascript extracted
frankieroberto Apr 30, 2026
bdb4744
Update javascript
frankieroberto Apr 30, 2026
b814b5c
Update behaviour of checkbox search
frankieroberto May 1, 2026
6ae5269
More tweaks
frankieroberto May 1, 2026
0d60db7
Extract checkbox filter javascript
frankieroberto May 1, 2026
cbc76d3
Extract Select all checkbox javascript
frankieroberto May 1, 2026
04b0398
Add radios filter
frankieroberto May 1, 2026
e9e59fd
Add success notification banner
frankieroberto May 1, 2026
5666658
Add email addresses and make region fixed height
frankieroberto May 1, 2026
d3cd668
Merge branch 'main' into pharmacy-chain-mode
frankieroberto May 8, 2026
499ae2c
Rename tab to "By pharmacy"
frankieroberto May 8, 2026
31d8343
Content changes
frankieroberto May 11, 2026
683888e
Merge branch 'main' into pharmacy-chain-mode
frankieroberto May 11, 2026
9ffb77e
Fix code style issues
frankieroberto May 11, 2026
f9557a3
Merge branch 'main' into pharmacy-chain-mode
frankieroberto May 11, 2026
11ea5c7
Refactor "By organisation" as "By pharmacy"
frankieroberto May 11, 2026
e892dbf
Remove 'currentMode'
frankieroberto May 11, 2026
80aba37
Remove 'Select mode' screen
frankieroberto May 11, 2026
fa558ee
Separate site and pharmacies in reports
frankieroberto May 11, 2026
02beecd
Fix choose data page
frankieroberto May 11, 2026
21c795a
Update data model
frankieroberto May 11, 2026
b4d36a1
Fix
frankieroberto May 11, 2026
c4e9661
Fix header
frankieroberto May 11, 2026
febab30
Tidy up the code
frankieroberto May 11, 2026
4e96fac
Simplify code
frankieroberto May 11, 2026
13e57a0
Revert default user
frankieroberto May 11, 2026
1da9808
Add check page
frankieroberto May 11, 2026
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
37 changes: 30 additions & 7 deletions app/assets/javascript/add-another.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { Component } from 'nhsuk-frontend'
* - Add `data-add-another-item="N"` to each item section (where N is the item index: 1, 2, 3, etc.)
* - Add `data-add-another-add` to the "Add another" button (hidden by default)
* - Add `data-add-another-remove="N"` to the "Remove" button within each section (hidden by default)
* - Optionally add `data-add-another-min="0"` to allow starting with no items visible (default is 1)
*
* @augments Component<HTMLElement>
*/
Expand All @@ -29,11 +30,13 @@ export class AddAnother extends Component {
this.$items = Array.from(this.$root.querySelectorAll('[data-add-another-item]'))
this.$addButton = this.$root.querySelector('[data-add-another-add]')
this.$addButtonWrapper = this.$addButton?.closest('.nhsuk-button-group')
this.minItems = parseInt(this.$root.dataset.addAnotherMin ?? '1', 10)

this.initializeItemVisibility()
this.setupAddButton()
this.setupRemoveButtons()
this.updateAddButtonVisibility()
this.updateAddButtonText()
this.updateRemoveButtonVisibility()
}

Expand Down Expand Up @@ -71,17 +74,18 @@ export class AddAnother extends Component {
*/
initializeItemVisibility() {
// Find the last item with values
let lastFilledIndex = 0
let lastFilledIndex = -1
this.$items.forEach(($item, index) => {
if (this.hasInputValues($item)) {
lastFilledIndex = index
}
})

// Show items up to and including the last filled one (minimum 1)
// Show items up to and including the last filled one (respecting minItems)
// Hide all items after that
const minVisibleIndex = this.minItems - 1
this.$items.forEach(($item, index) => {
if (index <= lastFilledIndex) {
if (index <= lastFilledIndex || index <= minVisibleIndex) {
$item.hidden = false
} else {
$item.hidden = true
Expand Down Expand Up @@ -152,6 +156,7 @@ export class AddAnother extends Component {
}

this.updateAddButtonVisibility()
this.updateAddButtonText()
this.updateRemoveButtonVisibility()
}

Expand All @@ -163,8 +168,8 @@ export class AddAnother extends Component {
removeItem(index) {
const visibleItems = this.getVisibleItems()

// Don't remove if only one item is visible
if (visibleItems.length <= 1) {
// Don't remove if at minimum items
if (visibleItems.length <= this.minItems) {
return
}

Expand All @@ -187,16 +192,34 @@ export class AddAnother extends Component {
}

this.updateAddButtonVisibility()
this.updateAddButtonText()
this.updateRemoveButtonVisibility()
}

/**
* Update the add button text based on number of visible items
* Uses data-add-another-text-first for the first item,
* data-add-another-text-another for subsequent items
*/
updateAddButtonText() {
if (!this.$addButton) return

const firstText = this.$addButton.dataset.addAnotherTextFirst
const anotherText = this.$addButton.dataset.addAnotherTextAnother

if (!firstText || !anotherText) return

const visibleItems = this.getVisibleItems()
this.$addButton.textContent = visibleItems.length === 0 ? firstText : anotherText
}

/**
* Update visibility of remove buttons based on number of visible items
* Remove buttons should only be visible when there are 2+ items
* Remove buttons should only be visible when there are more than minItems
*/
updateRemoveButtonVisibility() {
const visibleItems = this.getVisibleItems()
const showRemoveButtons = visibleItems.length >= 2
const showRemoveButtons = visibleItems.length > this.minItems

this.$items.forEach($item => {
const $removeButton = $item.querySelector('[data-add-another-remove]')
Expand Down
4 changes: 0 additions & 4 deletions app/assets/javascript/autocomplete.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,6 @@ export class Autocomplete extends Component {

/**
* Selected option
*
* @param {*} value - Current value
* @param {Array} options - Available options
* @returns {HTMLOptionElement} Selected option
*/
selectedOption(value, options) {
return [].filter.call(
Expand Down
65 changes: 65 additions & 0 deletions app/assets/javascript/checkbox-filter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { Component } from 'nhsuk-frontend'

/**
* Checkbox Filter component
*
* Filters a list of checkboxes based on search input.
*
* Usage:
* - Add `data-module="app-checkbox-filter"` to a search input element
* - The component will filter `.nhsuk-checkboxes__item` elements within the same form
* - Checkboxes with `data-select-all` attribute are not filtered
*
* @augments Component<HTMLInputElement>
*/
export class CheckboxFilter extends Component {
static elementType = HTMLInputElement

/**
* Name for the component used when initialising using data-module attributes
*/
static moduleName = 'app-checkbox-filter'

/**
* @param {Element | null} $root - HTML input element to use for component
*/
constructor($root) {
super($root)

this.$form = this.$root.closest('form') || this.$root.closest('fieldset') || document.body
this.$checkboxItems = this.$form.querySelectorAll('.nhsuk-checkboxes__item')

this.$root.addEventListener('input', () => this.filter())
}

/**
* Filter checkbox items based on the search input value
*/
filter() {
const searchTerm = this.$root.value.toLowerCase().trim().replace(/[.()]/g, '')
const searchWords = searchTerm.split(/\s+/).filter(word => word.length > 0)

this.$checkboxItems.forEach(($item) => {
const $label = $item.querySelector('.nhsuk-checkboxes__label')
const $checkbox = $item.querySelector('.nhsuk-checkboxes__input')

if (!$label || !$checkbox) return

// Skip if it's the select all checkbox
if ($checkbox.hasAttribute('data-select-all')) return

const labelText = $label.textContent.toLowerCase().replace(/[.()]/g, '')
const labelWords = labelText.split(/\s+/)
const matches = searchWords.length === 0 || searchWords.every(searchWord =>
labelWords.some(labelWord => labelWord.startsWith(searchWord))
)

// Show only if matches search term
if (matches) {
$item.removeAttribute('hidden')
} else {
$item.setAttribute('hidden', '')
}
})
}
}
67 changes: 67 additions & 0 deletions app/assets/javascript/checkbox-select-all.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { Component } from 'nhsuk-frontend'

/**
* Checkbox Select All component
*
* Provides select all functionality for a group of checkboxes.
*
* Usage:
* - Add `data-module="app-checkbox-select-all"` to a checkbox input element
* - The component will control all other checkboxes in the same form/fieldset
* - The select-all checkbox shows indeterminate state when some (but not all) are checked
*
* @augments Component<HTMLInputElement>
*/
export class CheckboxSelectAll extends Component {
static elementType = HTMLInputElement

/**
* Name for the component used when initialising using data-module attributes
*/
static moduleName = 'app-checkbox-select-all'

/**
* @param {Element | null} $root - HTML input element to use for component
*/
constructor($root) {
super($root)

this.$form = this.$root.closest('form') || this.$root.closest('fieldset') || document.body

// All checkboxes except this one
this.$checkboxes = Array.from(
this.$form.querySelectorAll('input[type="checkbox"]')
).filter(($checkbox) => $checkbox !== this.$root)

// Click on select-all toggles all checkboxes
this.$root.addEventListener('change', () => this.toggleAll())

// Click on individual checkboxes updates select-all state
this.$checkboxes.forEach(($checkbox) => {
$checkbox.addEventListener('change', () => this.updateState())
})

// Set initial state
this.updateState()
}

/**
* Toggle all checkboxes to match the select-all checkbox state
*/
toggleAll() {
this.$checkboxes.forEach(($checkbox) => {
$checkbox.checked = this.$root.checked
})
}

/**
* Update the select-all checkbox state based on individual checkbox states
*/
updateState() {
const allChecked = this.$checkboxes.every(($checkbox) => $checkbox.checked)
const noneChecked = this.$checkboxes.every(($checkbox) => !$checkbox.checked)

this.$root.checked = allChecked
this.$root.indeterminate = !allChecked && !noneChecked
}
}
109 changes: 109 additions & 0 deletions app/assets/javascript/checkbox-selected-count.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { ConfigurableComponent, I18n } from 'nhsuk-frontend'

/**
* Checkbox Selected Count component
*
* Displays a count of selected checkboxes within a container with pluralization support.
*
* Usage:
* - Add `data-module="app-checkboxes-count-select"` to a wrapper element
* - Add an element with `data-selected-count-display` attribute to show the count
* - Configure i18n via data attributes on the module element:
* `data-i18n.selected-count.one="%{count} pharmacy selected"`
* `data-i18n.selected-count.other="%{count} pharmacies selected"`
* - Checkboxes with `data-select-all` attribute are excluded from the count
*
* @augments {ConfigurableComponent<CheckboxSelectedCountConfig>}
*/
export class CheckboxSelectedCount extends ConfigurableComponent {
static elementType = HTMLElement

/**
* Name for the component used when initialising using data-module attributes
*/
static moduleName = 'app-checkboxes-count-select'

/**
* @param {Element | null} $root - HTML element to use for component
* @param {Partial<CheckboxSelectedCountConfig>} [config] - Component config
*/
constructor($root, config = {}) {
super($root, config)

this.$countDisplay = this.$root.querySelector('[data-selected-count-display]')
this.$checkboxes = this.$root.querySelectorAll('input[type="checkbox"]')

this.i18n = new I18n(this.config.i18n)

if (this.$countDisplay) {
this.setupEventListeners()
this.updateCount()
}
}

/**
* Set up change event listeners on all checkboxes
*/
setupEventListeners() {
this.$checkboxes.forEach(($checkbox) => {
$checkbox.addEventListener('change', () => this.updateCount())
})
}

/**
* Update the count display with the number of selected checkboxes
*/
updateCount() {
const checkedCount = this.$root.querySelectorAll(
'input[type="checkbox"]:checked:not([data-select-all])'
).length

this.$countDisplay.textContent = this.i18n.t('selectedCount', { count: checkedCount })
}

/**
* Checkbox Selected Count default config
*
* @constant
* @type {CheckboxSelectedCountConfig}
*/
static defaults = Object.freeze({
i18n: {
selectedCount: {
one: '%{count} selected',
other: '%{count} selected'
}
}
})

/**
* Checkbox Selected Count config schema
*
* @constant
* @satisfies {Schema<CheckboxSelectedCountConfig>}
*/
static schema = Object.freeze({
properties: {
i18n: { type: 'object' }
}
})
}

/**
* Checkbox Selected Count config
*
* @typedef {object} CheckboxSelectedCountConfig
* @property {CheckboxSelectedCountTranslations} [i18n] - Checkbox Selected Count translations
*/

/**
* Checkbox Selected Count translations
*
* @typedef {object} CheckboxSelectedCountTranslations
* @property {TranslationPluralForms} [selectedCount] - Count of selected checkboxes
*/

/**
* @import { TranslationPluralForms } from 'nhsuk-frontend'
* @import { Schema } from 'nhsuk-frontend/dist/nhsuk/common/configuration/index.mjs'
*/
8 changes: 8 additions & 0 deletions app/assets/javascript/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,19 @@ import {

import { AddAnother } from './add-another.js'
import { Autocomplete } from './autocomplete.js'
import { CheckboxFilter } from './checkbox-filter.js'
import { CheckboxSelectAll } from './checkbox-select-all.js'
import { CheckboxSelectedCount } from './checkbox-selected-count.js'
import { RadiosFilter } from './radios-filter.js'

// Initiate NHS.UK frontend components on page load
document.addEventListener('DOMContentLoaded', () => {
createAll(AddAnother)
createAll(Autocomplete)
createAll(CheckboxFilter)
createAll(CheckboxSelectAll)
createAll(CheckboxSelectedCount)
createAll(RadiosFilter)
})


Loading