Skip to content
This repository was archived by the owner on May 7, 2025. It is now read-only.
Merged
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
5 changes: 5 additions & 0 deletions app/controllers/claims.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
const { DateTime } = require('luxon')

const claimModel = require('../models/claims')
const mentorModel = require('../models/mentors')
const organisationModel = require('../models/organisations')
Expand Down Expand Up @@ -29,6 +31,8 @@ exports.claim_list = (req, res) => {

const currentClaimWindow = claimWindowHelper.getCurrentClaimWindow()

const now = new Date()

let academicYears = academicYearHelper.getAcademicYears()

// sort academic years newest to oldest
Expand Down Expand Up @@ -79,6 +83,7 @@ exports.claim_list = (req, res) => {
mentors,
// pagination,
currentClaimWindow,
now,
actions: {
new: `/organisations/${req.params.organisationId}/claims/new`,
view: `/organisations/${req.params.organisationId}/claims`,
Expand Down
2 changes: 1 addition & 1 deletion app/data/seed/settings/academic-years.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
[{"id":"4727b7c1-26ba-4621-b05a-73cd17d5248f","code":"2023_2024","name":"2023 to 2024","active":true},{"id":"a646ba82-abf6-4702-932e-92a7f6b8a34b","code":"2024_2025","name":"2024 to 2025","active":true},{"id":"bb9ab142-e28b-40f4-a4e1-c003686d845f","code":"2025_2026","name":"2025 to 2026","active":false},{"id":"ed4c4b94-e523-4699-bf81-9d7de7d099b0","code":"2026_2027","name":"2026 to 2027","active":false},{"id":"f1bab5bb-8f7e-4386-a09f-221865a57997","code":"2027_2028","name":"2027 to 2028","active":false}]
[{"id":"4727b7c1-26ba-4621-b05a-73cd17d5248f","code":"2023_2024","name":"2023 to 2024","active":true},{"id":"a646ba82-abf6-4702-932e-92a7f6b8a34b","code":"2024_2025","name":"2024 to 2025","active":true},{"id":"bb9ab142-e28b-40f4-a4e1-c003686d845f","code":"2025_2026","name":"2025 to 2026","active":true},{"id":"ed4c4b94-e523-4699-bf81-9d7de7d099b0","code":"2026_2027","name":"2026 to 2027","active":false},{"id":"f1bab5bb-8f7e-4386-a09f-221865a57997","code":"2027_2028","name":"2027 to 2028","active":false}]
153 changes: 94 additions & 59 deletions app/helpers/academic-years.js
Original file line number Diff line number Diff line change
@@ -1,85 +1,120 @@
const { DateTime } = require('luxon')

const claimWindows = require('../data/dist/settings/claim-windows')
const academicYears = require('../data/dist/settings/academic-years')

exports.getAcademicYear = (date) => {
// find the academic year for a given date
const item = claimWindows.find((item) =>
new Date(date) >= new Date(item.opensAt)
&& new Date(date) <= new Date(item.closesAt)
)

// if the item is found, return its academic year
if (item) {
return item.academicYear
} else {
console.log("No item found for the specified date")
/**
* Convert a date input (string or Date) into a Luxon DateTime instance.
* If parsing fails, returns null.
*/
const toDateTime = (dateInput) => {
if (!dateInput) {
return null
}
}

exports.getAcademicYears = () => {
return academicYears.filter((academicYear) => academicYear.active === true)
}

exports.getCurrentAcademicYear = () => {
const academicYears = this.getAcademicYears()
// If it's already a Date object, convert from that; otherwise assume ISO string
const dt = (dateInput instanceof Date)
? DateTime.fromJSDate(dateInput)
: DateTime.fromISO(dateInput)

// sort academic years newest to oldest
academicYears.sort((a, b) => {
return b.code.localeCompare(a.code)
})

return academicYears[0]
return dt.isValid ? dt : null
}

exports.getAcademicYearOptions = (selectedItem) => {
const items = []
/**
* Returns the academic year (e.g., "2023/24") that contains the given date
*/
exports.getAcademicYear = (date) => {
const targetDate = toDateTime(date)

academicYears.forEach((academicYear, i) => {
const item = {}
// If date is invalid, return null early
if (!targetDate) {
console.warn('Invalid date passed to getAcademicYear')
return null
}

item.text = academicYear.name
item.value = academicYear.code
item.id = academicYear.id
item.checked = (selectedItem && selectedItem.includes(academicYear.code)) ? 'checked' : ''
// Look up the first claim window that includes the targetDate
const windowItem = claimWindows.find(item => {
const opensAt = toDateTime(item.opensAt)
const closesAt = toDateTime(item.closesAt)
if (!opensAt || !closesAt) return false // skip invalid dates

if (academicYear.active) {
items.push(item)
}
return targetDate >= opensAt && targetDate <= closesAt
})

items.sort((a, b) => {
return a.text.localeCompare(b.text)
})
if (!windowItem) {
console.log('No item found for the specified date')
return null
}

return items
return windowItem.academicYear
}

exports.getAcademicYearLabel = (code) => {
let label
/**
* Returns all active academic years
*/
exports.getAcademicYears = () => {
return academicYears.filter((ay) => ay.active === true)
}

const academicYear = academicYears.find((academicYear) => academicYear.code === code)
/**
* Returns the newest academic year
* (assumes codes are something like "2023/24" and that sorting by descending code is valid)
*/
exports.getCurrentAcademicYear = () => {
// We can directly invoke getAcademicYears without `this`
const activeYears = exports.getAcademicYears()

if (academicYear) {
label = academicYear.name
}
// Sort descending by code
activeYears.sort((a, b) => b.code.localeCompare(a.code))

return label
return activeYears[0]
}

exports.isCurrentAcademicYear = (code) => {
const currentDate = new Date()
/**
* Builds a list of academic year options for use in a form, marking the selected items
*/
exports.getAcademicYearOptions = (selectedItems = []) => {
const items = academicYears
.filter(ay => ay.active)
.map(ay => {
return {
text: ay.name,
value: ay.code,
id: ay.id,
checked: selectedItems.includes(ay.code) ? 'checked' : ''
}
})

// Sort by text ascending
items.sort((a, b) => a.text.localeCompare(b.text))

for (let window of claimWindows) {
const opensAt = new Date(window.opensAt)
const closesAt = new Date(window.closesAt)
return items
}

// Check if current date is between opensAt and closesAt and
// if academicYear matches target academic year
if (currentDate >= opensAt && currentDate <= closesAt && window.academicYear === code) {
return true
}
}
/**
* Returns the display name ("2023/24") given an academic year code
*/
exports.getAcademicYearLabel = (code) => {
const ay = academicYears.find((ay) => ay.code === code)
return ay ? ay.name : undefined
}

return false
/**
* Checks whether the provided code matches the "current" academic year
* based on the current system date
*/
exports.isCurrentAcademicYear = (code) => {
const now = DateTime.now()

return claimWindows.some(window => {
const opensAt = toDateTime(window.opensAt)
const closesAt = toDateTime(window.closesAt)
if (!opensAt || !closesAt) return false

return (
now >= opensAt &&
now <= closesAt &&
window.academicYear === code
)
})
}
64 changes: 41 additions & 23 deletions app/helpers/claim-windows.js
Original file line number Diff line number Diff line change
@@ -1,33 +1,51 @@
const claimWindows = require('../data/dist/settings/claim-windows')
const { DateTime } = require('luxon')

exports.getClaimWindows = () => {
return claimWindows
}
const claimWindows = require('../data/dist/settings/claim-windows')

/**
* Sorts the claim windows by the most recent open date (descending),
* falling back to most recent close date (descending) in case of ties,
* and returns the first (most recently opened) claim window.
*
* @returns {Object|undefined} The "current" claim window object if available,
* otherwise `undefined` if `claimWindows` is empty.
*/
exports.getCurrentClaimWindow = () => {
const claimWindows = this.getClaimWindows()

claimWindows.sort((a, b) => {
return new Date(b.opensAt) - new Date(a.opensAt)
|| new Date(b.closesAt) - new Date(a.closesAt)
const aOpens = DateTime.fromISO(a.opensAt)
const bOpens = DateTime.fromISO(b.opensAt)
const aCloses = DateTime.fromISO(a.closesAt)
const bCloses = DateTime.fromISO(b.closesAt)

// Compare opensAt descending
const opensDiff = bOpens.toMillis() - aOpens.toMillis()
if (opensDiff !== 0) {
return opensDiff
}

// If there's a tie on opensAt, compare closesAt descending
return bCloses.toMillis() - aCloses.toMillis()
})

// After sorting, return the top item (the most recently opened window)
return claimWindows[0]
}

exports.isClaimWindowOpen = () => {
const currentDate = new Date()

// iterate through each item in the claim windows array
for (let window of claimWindows) {
const opensAt = new Date(window.opensAt)
const closesAt = new Date(window.closesAt)

// check if current date is between opensAt and closesAt
if (currentDate >= opensAt && currentDate <= closesAt) {
return true
}
}

return false
/**
* Checks whether the provided date (JS Date) falls within any claim window.
* Using Luxon for consistent and predictable date parsing & comparison.
*/
exports.isClaimWindowOpen = (currentDate) => {
// Convert currentDate (a JS Date) into a Luxon DateTime
const current = DateTime.fromJSDate(currentDate)

// Return true if any window is "open" for the given date.
return claimWindows.some(window => {
// Parse the window’s opensAt and closesAt as Luxon DateTime
const opensAt = DateTime.fromISO(window.opensAt)
const closesAt = DateTime.fromISO(window.closesAt)

// Compare using Luxon DateTime objects
return current >= opensAt && current <= closesAt
})
}
Loading