diff --git a/app/assets/sass/components/_session-slots.scss b/app/assets/sass/components/_session-slots.scss new file mode 100644 index 00000000..175fe706 --- /dev/null +++ b/app/assets/sass/components/_session-slots.scss @@ -0,0 +1,58 @@ +@use "nhsuk-frontend/dist/nhsuk/core" as *; + +.slot { + background-color: nhsuk-colour("white"); + border: 1px solid $nhsuk-input-border-colour; + padding-left: nhsuk-spacing(2); + user-select: none; + @include nhsuk-responsive-margin(2, "bottom"); + min-height: nhsuk-px-to-rem(46px); +} + +.slot-end { + @include nhsuk-font-size(16); + margin-right: 1rem; +} +.slot-tag { margin-left: 1rem; } + +.slot.is-hold {} + +.slot.is-staff-break { + background: repeating-linear-gradient(130deg, nhsuk-colour("grey-4"), nhsuk-colour("grey-4") 1px, transparent 1px, transparent 12px); + border-color: $nhsuk-border-colour; +} + +.slot.is-special-appointment {} + +.slot.is-merged { + border-bottom: none; + margin-bottom: 0; +} +.slot.is-merged-child { + border-top: 1px dashed #212b32; + height: 42px; +} +.slot.is-merged-child > .nhsuk-checkboxes__item, +.slot.is-merged-child > div { + display: none; +} +.slot.is-merged-last-child { + border-bottom: 1px solid #212b32; + margin-bottom: 12px; +} + +.slot.is-staff-break.is-merged-last-child { + border-bottom-style: dashed; +} + +.slot.is-merged-child > .slot-time, +.slot.is-merged-child > .slot-end { + display: none; +} + +.slot .nhsuk-checkboxes__label { + width: 100%; +} + +.panel-item { display: block; cursor: pointer; } +.panel-item.disabled { opacity: 0.38; cursor: default; pointer-events: none; } diff --git a/app/assets/sass/main.scss b/app/assets/sass/main.scss index cdf5f741..0360003d 100755 --- a/app/assets/sass/main.scss +++ b/app/assets/sass/main.scss @@ -20,8 +20,6 @@ $nhsuk-gutter: 24px, // Reduce layout spacing points (5–9). Points 0–4 stay at NHS defaults. - // Checkbox and radio geometry is fixed in px rather than driven by the spacing scale, - // and _compact.scss applies the --small variant to all checkboxes/radios. $nhsuk-spacing-points: ( 0: 0, 1: 4px, @@ -94,6 +92,7 @@ @forward "components/participant-table"; //@forward "components/participant-search"; @forward "components/progress-bar"; +@forward "components/session-slots"; @forward "components/template-card"; @forward "components/template-preview-dialog"; @forward "components/workflow"; diff --git a/app/data/session-data-defaults.js b/app/data/session-data-defaults.js index 8003d2d0..45ef5a4b 100644 --- a/app/data/session-data-defaults.js +++ b/app/data/session-data-defaults.js @@ -14,6 +14,7 @@ module.exports = { clinicCapacityPercentBooked: "0", clinicCapacityAvailable: "10", clinicCapacityTotal: "10", + missionOne: {}, createCapacityFromZero: { schedules: [], selectedDays: [], diff --git a/app/routes/mission-1.js b/app/routes/mission-1.js index c0ea97cd..8ab3387a 100644 --- a/app/routes/mission-1.js +++ b/app/routes/mission-1.js @@ -8,6 +8,31 @@ const router = express.Router() */ +const isWholeNumber = str => /^\d+$/.test(str) + +// Checks a { day, month, year } object has numeric values for each field, regardless of whether they form a real date +const hasDateFields = (dateObj) => + Boolean(dateObj) && isWholeNumber(dateObj.day) && isWholeNumber(dateObj.month) && isWholeNumber(dateObj.year) + +// Parses a { day, month, year } object into a Date, or null if invalid +const parseDateFields = (dateObj) => { + if (!hasDateFields(dateObj)) { + return null + } + + const day = parseInt(dateObj.day, 10) + const month = parseInt(dateObj.month, 10) + // Treat 2-digit years as shorthand for the 2000s, eg "26" becomes 2026 + const year = dateObj.year.length === 2 ? parseInt(`20${dateObj.year}`, 10) : parseInt(dateObj.year, 10) + const date = new Date(year, month - 1, day) + + if (date.getFullYear() !== year || date.getMonth() !== month - 1 || date.getDate() !== day) { + return null + } + + return date +} + // simple search router.get('/september-iteration-2/clickthru/04a-example-search-result', function (req, res) { const query = (req.query['search-params'] || '').trim() @@ -61,6 +86,227 @@ router.get('/action/unstage/:participantId', function (req, res) { res.redirect(req.get('referer') || '/september-iteration-2/clickthru/04-choose-participants') }); +// validating clinic name for 1 day clinic creation +router.post('/september-iteration-2/create-clinic-rev-1-name', function (req, res) { + const clinicName = (req.body.clinicName || '').trim() + + if (!req.session.data.missionOne || typeof req.session.data.missionOne !== 'object') { + req.session.data.missionOne = {} + } + req.session.data.missionOne.clinicName = clinicName + + const errors = {} + + if (!clinicName) { + errors.clinicName = 'Clinic must be given a name' + } + + if (Object.keys(errors).length > 0) { + return res.render('september-iteration-2/create-clinic-rev-1-name', { + errors + }) + } + + res.redirect('/september-iteration-2/create-clinic-rev-1-schedule') +}) + +// validating the scheduled date for 1 day clinic creation +router.post('/september-iteration-2/create-clinic-rev-1-schedule', function (req, res) { + const clinicDate = req.body.clinicDate || {} + + if (typeof clinicDate.year === 'string' && /^\d{2}$/.test(clinicDate.year)) { + clinicDate.year = `20${clinicDate.year}` + } + + if (!req.session.data.missionOne || typeof req.session.data.missionOne !== 'object') { + req.session.data.missionOne = {} + } + req.session.data.missionOne.clinicDate = clinicDate + + const errors = {} + + if (!hasDateFields(clinicDate)) { + errors.clinicDate = 'Date of clinic must be given a day, month, and year' + } else { + const parsedDate = parseDateFields(clinicDate) + const today = new Date() + today.setHours(0, 0, 0, 0) + + if (!parsedDate || parsedDate <= today) { + errors.clinicDate = 'Date of clinic must be in the future' + } + } + + if (Object.keys(errors).length > 0) { + return res.render('september-iteration-2/create-clinic-rev-1-schedule', { + errors + }) + } + + res.redirect('/september-iteration-2/create-clinic-rev-1-set-timings') +}) + +// validating clinic session times for 1 day clinic creation +router.post('/september-iteration-2/create-clinic-rev-1-set-timings', function (req, res) { + const newSession = req.body.newSession || {} + const startTime = newSession.startTime || {} + const endTime = newSession.endTime || {} + + const startHourStr = (startTime.hour || '').trim() + const startMinuteStr = (startTime.minute || '').trim() + const endHourStr = (endTime.hour || '').trim() + const endMinuteStr = (endTime.minute || '').trim() + const durationStr = (newSession.duration || '').trim() + + const startHour = parseInt(startHourStr, 10) + const startMinute = parseInt(startMinuteStr, 10) + const endHour = parseInt(endHourStr, 10) + const endMinute = parseInt(endMinuteStr, 10) + const duration = parseInt(durationStr, 10) + + const validStartHour = isWholeNumber(startHourStr) && startHour >= 0 && startHour <= 23 + const validStartMinute = isWholeNumber(startMinuteStr) && startMinute >= 0 && startMinute <= 59 + const validEndHour = isWholeNumber(endHourStr) && endHour >= 0 && endHour <= 23 + const validEndMinute = isWholeNumber(endMinuteStr) && endMinute >= 0 && endMinute <= 59 + + const errors = {} + + if (!validStartHour || !validStartMinute) { + errors.startTime = 'Start time must be entered, in 24 hour format' + } + + if (!validEndHour || !validEndMinute) { + errors.endTime = 'End time must be entered, in 24 hour format' + } + + if (!isWholeNumber(durationStr) || duration <= 0) { + errors.duration = 'Slot length must be entered, in minutes' + } + + if (!errors.startTime && !errors.endTime) { + const startTotalMinutes = startHour * 60 + startMinute + const endTotalMinutes = endHour * 60 + endMinute + if (endTotalMinutes <= startTotalMinutes) { + errors.startTime = 'Start time must be earlier than end time' + errors.endTime = 'End time must be later than start time' + } + } + + if (typeof req.session.data.missionOne !== 'object') { + req.session.data.missionOne = {} + } + req.session.data.missionOne.startTime = startTime + req.session.data.missionOne.endTime = endTime + req.session.data.missionOne.duration = durationStr + + if (Object.keys(errors).length > 0) { + return res.render('september-iteration-2/create-clinic-rev-1-set-timings', { + errors + }) + } + + res.redirect('/september-iteration-2/create-clinic-rev-1-slot-structure') +}) + +// Formats a total minutes-from-midnight value as e.g. "10:08" +const formatClock = (totalMinutes) => { + const hour = Math.floor(totalMinutes / 60) % 24 + const minute = totalMinutes % 60 + return `${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}` +} + +// Builds the slot checkbox data for a clinic session, flagging any slots marked as staff breaks +const buildSessionSlots = (missionOne) => { + const startTime = missionOne.startTime || {} + const endTime = missionOne.endTime || {} + + const startMinutes = (parseInt(startTime.hour, 10) || 0) * 60 + (parseInt(startTime.minute, 10) || 0) + const endMinutes = (parseInt(endTime.hour, 10) || 0) * 60 + (parseInt(endTime.minute, 10) || 0) + const duration = parseInt(missionOne.duration, 10) || 0 + const staffBreakSlots = (missionOne.staffBreakSlots || []).map(Number).filter(Number.isInteger) + + const slots = [] + + if (duration > 0 && endMinutes > startMinutes) { + const slotCount = Math.floor((endMinutes - startMinutes) / duration) + + for (let i = 0; i < slotCount; i++) { + const slotStartMinutes = startMinutes + (i * duration) + slots.push({ + index: i, + startLabel: formatClock(slotStartMinutes), + endLabel: formatClock(slotStartMinutes + duration), + isStaffBreak: staffBreakSlots.includes(i) + }) + } + } + + const bookableSlotCount = slots.filter((slot) => !slot.isStaffBreak).length + const staffBreakSlotCount = slots.filter((slot) => slot.isStaffBreak).length + missionOne.bookableSlotCount = bookableSlotCount + missionOne.staffBreakSlotCount = staffBreakSlotCount + + return { + slots, + bookableSlotCount, + staffBreakSlotCount, + sessionStartLabel: formatClock(startMinutes), + sessionEndLabel: formatClock(endMinutes) + } +} + +// computing the real slot structure from the submitted session times for 1 day clinic creation +router.get('/september-iteration-2/create-clinic-rev-1-slot-structure', function (req, res) { + const missionOne = req.session.data.missionOne || {} + + res.render('september-iteration-2/create-clinic-rev-1-slot-structure', buildSessionSlots(missionOne)) +}) + +// marking the selected slots as staff breaks for 1 day clinic creation +router.post('/september-iteration-2/create-clinic-rev-1-slot-structure/mark-staff-break', function (req, res) { + const missionOne = req.session.data.missionOne || {} + // nhsuk-frontend's checkboxes component submits an "_unchecked" sentinel for every untoggled box, so filter down to real indices + const selectedSlots = [].concat(req.body.computedSlots || []).map(Number).filter(Number.isInteger) + + const errors = {} + if (selectedSlots.length === 0) { + errors.slots = 'you need to select some slots' + } + + if (Object.keys(errors).length > 0) { + return res.render('september-iteration-2/create-clinic-rev-1-slot-structure', Object.assign({ errors }, buildSessionSlots(missionOne))) + } + + const staffBreakSlots = new Set((missionOne.staffBreakSlots || []).map(Number).filter(Number.isInteger)) + selectedSlots.forEach((index) => staffBreakSlots.add(index)) + missionOne.staffBreakSlots = Array.from(staffBreakSlots) + req.session.data.missionOne = missionOne + + res.redirect('/september-iteration-2/create-clinic-rev-1-slot-structure') +}) + +// clearing staff break status from the selected slots for 1 day clinic creation +router.post('/september-iteration-2/create-clinic-rev-1-slot-structure/clear-slot-type', function (req, res) { + const missionOne = req.session.data.missionOne || {} + // nhsuk-frontend's checkboxes component submits an "_unchecked" sentinel for every untoggled box, so filter down to real indices + const selectedSlots = [].concat(req.body.computedSlots || []).map(Number).filter(Number.isInteger) + + const errors = {} + if (selectedSlots.length === 0) { + errors.slots = 'you need to select some slots' + } + + if (Object.keys(errors).length > 0) { + return res.render('september-iteration-2/create-clinic-rev-1-slot-structure', Object.assign({ errors }, buildSessionSlots(missionOne))) + } + + const selectedSet = new Set(selectedSlots) + missionOne.staffBreakSlots = (missionOne.staffBreakSlots || []).map(Number).filter(Number.isInteger).filter((index) => !selectedSet.has(index)) + req.session.data.missionOne = missionOne + + res.redirect('/september-iteration-2/create-clinic-rev-1-slot-structure') +}) + // creating and passing Total Slots through for 1 day clinic creation router.post('/september-iteration-2/create-clinic-rev-1-publish-check', function (req, res) { const newSession = req.session.data.newSession || {} diff --git a/app/views/september-iteration-2/create-clinic-rev-1-name.html b/app/views/september-iteration-2/create-clinic-rev-1-name.html new file mode 100644 index 00000000..77b89b54 --- /dev/null +++ b/app/views/september-iteration-2/create-clinic-rev-1-name.html @@ -0,0 +1,103 @@ +{% extends 'layout.html' %} + +{% set errorState = "true" if errors else "false" %} + +{% set pageName = "Create day clinic" %} + +{% block content %} + +{% set tickSvg %} + +{% endset %} + +
+
+ +
+ +
+ +
+
+
+ + {% if errorState === "true" %} + {% set errorList = [] %} + {% if errors.clinicName %} + {% set errorList = errorList.concat([{ text: errors.clinicName, href: "#clinic-name" }]) %} + {% endif %} + {% if errors.clinicDate %} + {% set errorList = errorList.concat([{ text: errors.clinicDate, href: "#clinic-date-day" }]) %} + {% endif %} + {{ errorSummary({ + titleText: "There is a problem", + errorList: errorList + }) }} + {% endif %} + +
+ +
+

+ +

+ {% if errors and errors.clinicName %} + + Error: {{ errors.clinicName }} + + {% endif %} + +
+ + + +
+
+
+ + + +{% endblock %} \ No newline at end of file diff --git a/app/views/september-iteration-2/create-clinic-rev-1-publish-check.html b/app/views/september-iteration-2/create-clinic-rev-1-publish-check.html index fb7bed5c..f9662c54 100644 --- a/app/views/september-iteration-2/create-clinic-rev-1-publish-check.html +++ b/app/views/september-iteration-2/create-clinic-rev-1-publish-check.html @@ -19,27 +19,37 @@

Workflow steps - Create day clinic + Create clinic session

  1. - - - {{ tickSvg | safe }} - - Clinic details - + + 1 + Clinic name +
  2. - + 2 - Set date and time - + Scheduled date + +
  3. +
  4. + + 2 + Session times + +
  5. +
  6. + + 3 + Slot structure +
  7. - 3 - Save clinic + 4 + Check and confirm
@@ -50,45 +60,59 @@

-

Save clinic

+

Check and confirm

- Clinic details + Clinic name
- {{ data.clinicName }} + {{ data.missionOne.clinicName }}
- Change clinic details + Change clinic name
- Date and time + Scheduled date
- {{ data.clinicDate | formatDate }}
- {{ data.newSession.startTime.hour | zeroPad }}:{{ data.newSession.startTime.minute | zeroPad }} to {{ data.newSession.endTime.hour | zeroPad }}:{{ data.newSession.endTime.minute | zeroPad }}
- {{ data.newSession.duration }} minute slot length + {{ data.missionOne.clinicDate | formatDate }}
- Change date and time + Change scheduled date
- Total capacity + Session times
- {{ data.newSession.totalSlots }} slots + {{ data.missionOne.startTime.hour | zeroPad }}:{{ data.newSession.startTime.minute | zeroPad }} to {{ data.newSession.endTime.hour | zeroPad }}:{{ data.newSession.endTime.minute | zeroPad }}
+ {{ data.missionOne.duration }} minute slot length +
+
+ Change session times +
+
+
+
+ Slot structure +
+
+ {{ data.missionOne.bookableSlotCount }} bookable slots
+ {{ data.missionOne.staffBreakSlotCount }} slots set as staff break +
+
+ Change slot structure

- Save clinic + Save clinic session

diff --git a/app/views/september-iteration-2/create-clinic-rev-1-schedule.html b/app/views/september-iteration-2/create-clinic-rev-1-schedule.html new file mode 100644 index 00000000..7b0381a5 --- /dev/null +++ b/app/views/september-iteration-2/create-clinic-rev-1-schedule.html @@ -0,0 +1,130 @@ +{% extends 'layout.html' %} + +{% set errorState = "true" if errors else "false" %} + +{% set pageName = "Create day clinic" %} + +{% block content %} + +{% set tickSvg %} + +{% endset %} + +
+
+ +
+ +
+ +
+
+
+ + {% if errorState === "true" %} + {% set errorList = [] %} + {% if errors.clinicDate %} + {% set errorList = errorList.concat([{ text: errors.clinicDate, href: "#clinic-date-day" }]) %} + {% endif %} + {{ errorSummary({ + titleText: "There is a problem", + errorList: errorList + }) }} + {% endif %} + +
+ +
+
+ +

+ Scheduled date +

+
+
+ For example, 18 6 2026 +
+ {% if errors and errors.clinicDate %} + + Error: {{ errors.clinicDate }} + + {% endif %} +
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+
+ + + +
+
+
+ + + +{% endblock %} \ No newline at end of file diff --git a/app/views/september-iteration-2/create-clinic-rev-1-set-timings.html b/app/views/september-iteration-2/create-clinic-rev-1-set-timings.html index 7949e0d4..7d6b6421 100644 --- a/app/views/september-iteration-2/create-clinic-rev-1-set-timings.html +++ b/app/views/september-iteration-2/create-clinic-rev-1-set-timings.html @@ -1,6 +1,6 @@ {% extends 'layout.html' %} -{% set errorState = "false" %} +{% set errorState = "true" if errors else "false" %} {% set pageName = "Set date and time" %} @@ -21,27 +21,37 @@

Workflow steps - Create day clinic + Create clinic session

  1. - - - 1 - - Clinic details - + + 1 + Clinic name + +
  2. +
  3. + + 2 + Scheduled date +
  4. 2 - Set date and time + Session times
  5. 3 - Save clinic + Slot structure + +
  6. +
  7. + + 4 + Check and confirm
@@ -53,88 +63,27 @@

{% if errorState === "true" %} + {% set errorList = [] %} + {% if errors.startTime %} + {% set errorList = errorList.concat([{ text: errors.startTime, href: "#start-time-hour" }]) %} + {% endif %} + {% if errors.endTime %} + {% set errorList = errorList.concat([{ text: errors.endTime, href: "#end-time-hour" }]) %} + {% endif %} + {% if errors.duration %} + {% set errorList = errorList.concat([{ text: errors.duration, href: "#duration" }]) %} + {% endif %} {{ errorSummary({ titleText: "There is a problem", - errorList: [ - { - text: "Date of clinic must be given a day, month, and year", - href: "#" - }, - { - text: "Date of clinic must be in the future", - href: "#" - }, - { - text: "Start time must be entered, in 24 hour format", - href: "#" - }, - { - text: "End time must be entered, in 24 hour format", - href: "#" - }, - { - text: "End time must be later than start time", - href: "#" - }, - { - text: "Start time must be earlier than end time", - href: "#" - }, - { - text: "Slot length must be entered, in minutes", - href: "#" - } - ] + errorList: errorList }) }} {% endif %} -

Set date and time

+

Clinic session times

-
- -
-
- - Date of clinic - -
- For example, 18 6 2026 -
- {% if errorState === "true" %} - - Error: Date of clinic must be given a day, month, and year - - {% endif %} -
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
-
+ -
+
Session start time @@ -142,9 +91,9 @@

Set date and time

Use 24 hour format, for example, 10:00
- {% if errorState === "true" %} + {% if errors and errors.startTime %} - Error: Start time must be entered, in 24 hour format + Error: {{ errors.startTime }} {% endif %} @@ -154,8 +103,8 @@

Set date and time

- +
@@ -164,15 +113,15 @@

Set date and time

- +

-
+
Session end time @@ -180,9 +129,9 @@

Set date and time

Use 24 hour format, for example, 17:00
- {% if errorState === "true" %} + {% if errors and errors.endTime %} - Error: End time must be entered, in 24 hour format + Error: {{ errors.endTime }} {% endif %} @@ -192,8 +141,8 @@

Set date and time

- +
@@ -201,26 +150,26 @@

Set date and time

- +
-
+
- {% if errorState === "true" %} + {% if errors and errors.duration %} - Error: Slot length must be entered, in minutes + Error: {{ errors.duration }} {% endif %}
- +
diff --git a/app/views/september-iteration-2/create-clinic-rev-1-slot-structure.html b/app/views/september-iteration-2/create-clinic-rev-1-slot-structure.html new file mode 100644 index 00000000..7752e2d6 --- /dev/null +++ b/app/views/september-iteration-2/create-clinic-rev-1-slot-structure.html @@ -0,0 +1,128 @@ +{% extends 'layout.html' %} + +{% set errorState = "true" if errors else "false" %} + +{% set pageName = "Set date and time" %} + +{% block content %} + +{% set tickSvg %} + +{% endset %} + +
+
+ +
+ +
+ +
+ +
+ + +
+ {% if errorState === "true" %} + {% set errorList = [] %} + {% if errors.slots %} + {% set errorList = errorList.concat([{ text: errors.slots, href: "#computed-slot-0" }]) %} + {% endif %} + {{ errorSummary({ + titleText: "There is a problem", + errorList: errorList + }) }} + {% endif %} +

Slot structure

+

+ Session time + {{ sessionStartLabel }} to {{ sessionEndLabel }} +

+
+ +
+
+
+ + {% for slot in slots %} +
+
+ + +
+
+ {% endfor %} + +
+
+
+
+

Set selected slots as:

+ +
+ +
+
+

+ {{ bookableSlotCount }} bookable slots +

+

+ +

+
+
+ + +
+
+ +{% endblock %} \ No newline at end of file diff --git a/app/views/september-iteration-2/create-clinic-rev-1.html b/app/views/september-iteration-2/create-clinic-rev-1.html deleted file mode 100644 index b246513c..00000000 --- a/app/views/september-iteration-2/create-clinic-rev-1.html +++ /dev/null @@ -1,91 +0,0 @@ -{% extends 'layout.html' %} - -{% set errorState = "false" %} - -{% set pageName = "Create day clinic" %} - -{% block content %} - -{% set tickSvg %} - -{% endset %} - -
-
- -
- -
- -
-
-
- - {% if errorState === "true" %} - {{ errorSummary({ - titleText: "There is a problem", - errorList: [ - { - text: "Clinic must be given a name", - href: "#" - } - ] - }) }} - {% endif %} - -

Clinic details

- -
- -
- - {% if errorState === "true" %} - - Error: Clinic must be given a name - - {% endif %} - -
- - - -
-
-
- - - -{% endblock %} \ No newline at end of file diff --git a/app/views/september-iteration-2/create-clinic.html b/app/views/september-iteration-2/create-clinic.html index b96f9c29..e5e3af82 100644 --- a/app/views/september-iteration-2/create-clinic.html +++ b/app/views/september-iteration-2/create-clinic.html @@ -11,7 +11,7 @@

Clinics

{{ button({ text: "Create a new clinic", - href: "create-clinic-rev-1", + href: "create-clinic-rev-1-name", classes: "nhsuk-button--secondary" }) }}
@@ -47,6 +47,41 @@

Clinics

+ {% if data.missionOne and data.missionOne.clinicName %} + + + {{ data.missionOne.clinicName }} + + + no data + + + no data + + + {% if data.missionOne.clinicDate %} + {{ data.missionOne.clinicDate | formatDate }}
+ 1 day + {% else %} + no data + {% endif %} + + + {% if data.missionOne.bookableSlotCount is not undefined %} +
+ {{ data.missionOne.bookableSlotCount }} slots available of {{ data.missionOne.bookableSlotCount }} + {% else %} + no data + {% endif %} + + + + Select this clinic + + + + {% endif %} +