diff --git a/backend/src/components/eas/eas.js b/backend/src/components/eas/eas.js index afff6dfb..f48e260d 100644 --- a/backend/src/components/eas/eas.js +++ b/backend/src/components/eas/eas.js @@ -1,19 +1,26 @@ 'use strict'; -const { logApiError, getAccessToken, getData, putData, deleteData, getCreateOrUpdateUserValue, getDataWithParams, handleExceptionResponse } = require('../utils'); +const { logApiError, getAccessToken, getData, putData, postData, deleteData, getCreateOrUpdateUserValue, getDataWithParams, handleExceptionResponse} = require('../utils'); const HttpStatus = require('http-status-codes'); const config = require('../../config'); const cacheService = require('../cache-service'); const { createMoreFiltersSearchCriteria } = require('./studentFilters'); const moment = require('moment'); +const {DateTimeFormatter, LocalDate, LocalDateTime} = require("@js-joda/core"); async function getAssessmentSessions(req, res) { try { const url = `${config.get('eas:assessmentSessionsURL')}`; const token = getAccessToken(req); const data = await getData(token, url); + const today = LocalDate.now(); + const formatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME; + data.forEach(session => { - session.isOpen = new Date(session.activeFromDate) <= new Date() && new Date(session.activeUntilDate) >= new Date(); + const activeFromDate = LocalDate.parse(session.activeFromDate, formatter); + const activeUntilDate = LocalDate.parse(session.activeUntilDate, formatter); + session.isOpen = activeFromDate.isBefore(today) && activeUntilDate.isAfter(today); }); + return res.status(200).json(data); } catch (e) { logApiError(e, 'getAssessmentSessions', 'Error occurred while attempting to GET assessment sessions.'); @@ -38,15 +45,21 @@ async function getAssessmentSessionsBySchoolYear(req, res) { const url = `${config.get('eas:assessmentSessionsURL')}/school-year/${req.params.schoolYear}`; const token = getAccessToken(req); let data = await getData(token, url); + + const now = LocalDateTime.now(); + data.forEach(session => { - session.isOpen = new Date(session.activeFromDate) <= new Date() && new Date(session.activeUntilDate) >= new Date(); + const activeFrom = LocalDateTime.parse(session.activeFromDate); + const activeUntil = LocalDateTime.parse(session.activeUntilDate); + session.isOpen = activeFrom.isBefore(now) && activeUntil.isAfter(now); + session.assessments.forEach(assessment => { let assessmentType = cacheService.getAssessmentTypeByCode(assessment.assessmentTypeCode); - assessment.assessmentTypeName = assessmentType.label+' ('+assessment.assessmentTypeCode+')'; + assessment.assessmentTypeName = assessmentType.label + ' (' + assessment.assessmentTypeCode + ')'; assessment.displayOrder = assessmentType.displayOrder; - }); }); + return res.status(200).json(data); } catch (e) { logApiError(e, 'getSessions', 'Error occurred while attempting to GET sessions by school year.'); @@ -95,6 +108,24 @@ async function getAssessmentStudentsPaginated(req, res) { } } +async function postAssessmentStudent(req, res){ + try { + req.body.districtID = cacheService.getSchoolBySchoolID(req.body.schoolID).districtID; + const payload = { + ...req.body, + updateUser: getCreateOrUpdateUserValue(req), + updateDate: null, + createDate: null + }; + const token = getAccessToken(req); + const result = await postData(token, payload, `${config.get('eas:assessmentStudentsURL')}`, req.session?.correlationID); + return res.status(HttpStatus.OK).json(result); + } catch (e) { + await logApiError(e, 'postAssessmentStudent', 'Error occurred while attempting to create the assessment student registration.'); + return handleExceptionResponse(e, res); + } +} + async function getAssessmentStudentByID(req, res) { try { const token = getAccessToken(req); @@ -190,5 +221,6 @@ module.exports = { getAssessmentStudentByID, updateAssessmentStudentByID, deleteAssessmentStudentByID, - getAssessmentSpecialCases + getAssessmentSpecialCases, + postAssessmentStudent }; diff --git a/backend/src/routes/eas.js b/backend/src/routes/eas.js index 4abf8c3a..6ba3e641 100644 --- a/backend/src/routes/eas.js +++ b/backend/src/routes/eas.js @@ -1,18 +1,19 @@ const passport = require('passport'); const express = require('express'); const router = express.Router(); -const { getAssessmentSessions, getActiveAssessmentSessions, getAssessmentSessionsBySchoolYear, getAssessmentStudentsPaginated, getAssessmentStudentByID, updateAssessmentStudentByID, getAssessmentSpecialCases, deleteAssessmentStudentByID } = require('../components/eas/eas'); +const { getAssessmentSessions, getActiveAssessmentSessions, getAssessmentSessionsBySchoolYear, getAssessmentStudentsPaginated, getAssessmentStudentByID, updateAssessmentStudentByID, getAssessmentSpecialCases, deleteAssessmentStudentByID, postAssessmentStudent } = require('../components/eas/eas'); const auth = require('../components/auth'); const isValidBackendToken = auth.isValidBackendToken(); const { validateAccessToken, findInstituteType_params, checkPermissionForRequestedInstitute } = require('../components/permissionUtils'); const { PERMISSION } = require('../util/Permission'); const validate = require('../components/validator'); -const {putStudentAssessmentSchema} = require('../validations/eas'); +const {putStudentAssessmentSchema, postAssessmentStudentSchema} = require('../validations/eas'); router.get('/assessment-sessions/:instituteType', passport.authenticate('jwt', {session: false}, undefined), isValidBackendToken, validateAccessToken, findInstituteType_params, checkPermissionForRequestedInstitute(PERMISSION.EAS_DIS_EDIT, PERMISSION.EAS_SCH_EDIT), getAssessmentSessions); router.get('/assessment-sessions/active/:instituteType', passport.authenticate('jwt', {session: false}, undefined), isValidBackendToken, validateAccessToken, findInstituteType_params, checkPermissionForRequestedInstitute(PERMISSION.EAS_DIS_EDIT, PERMISSION.EAS_SCH_EDIT), getActiveAssessmentSessions); router.get('/assessment-sessions/school-year/:schoolYear/:instituteType', passport.authenticate('jwt', {session: false}, undefined), isValidBackendToken, validateAccessToken, findInstituteType_params, checkPermissionForRequestedInstitute(PERMISSION.EAS_DIS_EDIT, PERMISSION.EAS_SCH_EDIT), getAssessmentSessionsBySchoolYear); +router.post('/assessment-registrations/student/:instituteType', passport.authenticate('jwt', {session: false}, undefined), isValidBackendToken, validateAccessToken, findInstituteType_params, checkPermissionForRequestedInstitute(PERMISSION.EAS_DIS_EDIT, PERMISSION.EAS_SCH_EDIT), validate(postAssessmentStudentSchema), postAssessmentStudent); router.get('/assessment-registrations/student/:instituteType/:assessmentStudentID', passport.authenticate('jwt', {session: false}, undefined), isValidBackendToken, validateAccessToken, findInstituteType_params, checkPermissionForRequestedInstitute(PERMISSION.EAS_DIS_EDIT, PERMISSION.EAS_SCH_EDIT), getAssessmentStudentByID); router.put('/assessment-registrations/student/:instituteType/:assessmentStudentID', passport.authenticate('jwt', {session: false}, undefined), isValidBackendToken, validateAccessToken, findInstituteType_params, checkPermissionForRequestedInstitute(PERMISSION.EAS_DIS_EDIT, PERMISSION.EAS_SCH_EDIT), validate(putStudentAssessmentSchema), updateAssessmentStudentByID); router.get('/assessment-registrations/paginated/:instituteType', passport.authenticate('jwt', {session: false}, undefined), isValidBackendToken, validateAccessToken, findInstituteType_params, checkPermissionForRequestedInstitute(PERMISSION.EAS_DIS_EDIT, PERMISSION.EAS_SCH_EDIT), getAssessmentStudentsPaginated); diff --git a/backend/src/validations/eas.js b/backend/src/validations/eas.js index 9d894963..429226c3 100644 --- a/backend/src/validations/eas.js +++ b/backend/src/validations/eas.js @@ -3,7 +3,7 @@ const { baseRequestSchema } = require('./base'); const putStudentAssessmentSchema = object({ body: object({ - assessmentStudentID: string().nonNullable(), + assessmentStudentID: string().nullable().optional, sessionID:string().nonNullable(), districtID: string().nonNullable(), schoolID: string().nonNullable(), @@ -23,11 +23,11 @@ const putStudentAssessmentSchema = object({ courseMonth: number().optional(), courseYear: number().optional(), assessmentStudentValidationIssues: array().of(object({ - assessmentStudentValidationIssueID:string().nullable().optional(), assessmentStudentID:string().nullable().optional(), validationIssueSeverityCode:string().nullable().optional(), validationIssueCode:string().nullable().optional(), - validationIssueFieldCode:string().nullable().optional() + validationLabel:string().nullable().optional(), + validationMessage:string().nullable().optional(), }).concat(baseRequestSchema)).nullable().optional() }).concat(baseRequestSchema).noUnknown(), params: object({ @@ -36,6 +36,41 @@ const putStudentAssessmentSchema = object({ query: object().noUnknown(), }).noUnknown(); +const postAssessmentStudentSchema = object({ + body: object({ + sessionID:string().nonNullable(), + districtID: string().nonNullable(), + schoolID: string().nonNullable(), + assessmentCenterID: string().nonNullable(), + assessmentID:string().nonNullable(), + assessmentTypeCode: string().nonNullable(), + studentID: string().nullable().optional(), + assessmentStudentID: string().nullable().optional(), + courseStatusCode: string().nullable().optional(), + numberOfAttempts: string().nullable().optional(), + pen: string().max(9).nonNullable(), + localID: string().max(12).nonNullable(), + givenName: string().max(25).nonNullable(), + surName: string().max(25).nonNullable(), + isElectronicExam: boolean().nullable().optional(), + proficiencyScore: number().nullable().optional(), + provincialSpecialCaseCode: string().max(1).nullable().optional(), + assessmentStudentValidationIssues: array().of(object({ + assessmentStudentID:string().nullable().optional(), + validationIssueSeverityCode:string().nullable().optional(), + validationIssueCode:string().nullable().optional(), + validationIssueFieldCode: string().nullable().optional(), + validationLabel:string().nullable().optional(), + validationMessage:string().nullable().optional(), + }).concat(baseRequestSchema)).nullable().optional() + }).concat(baseRequestSchema).noUnknown(), + query: object().noUnknown(), + params: object({ + instituteType: string().nonNullable(), + }).noUnknown(), +}).noUnknown(); + module.exports = { putStudentAssessmentSchema, + postAssessmentStudentSchema }; diff --git a/frontend/src/components/assessments/AssessmentSessions.vue b/frontend/src/components/assessments/AssessmentSessions.vue index 0a9a0903..6f0d89c3 100644 --- a/frontend/src/components/assessments/AssessmentSessions.vue +++ b/frontend/src/components/assessments/AssessmentSessions.vue @@ -112,6 +112,7 @@ import { ApiRoutes } from '../../utils/constants'; import { authStore } from '../../store/modules/auth'; import { mapState } from 'pinia'; import moment from 'moment'; +import { LocalDateTime } from "@js-joda/core"; export default { name: 'AssessmentSessions', @@ -147,18 +148,19 @@ export default { }; }, computed: { - ...mapState(authStore, ['userInfo']), + ...mapState(authStore, ['userInfo']), activeSessions() { const orderedSessions = []; const allSessions = this.allSessions - .filter(session => session.schoolYear === this.schoolYear) - .map((session) => { - return { + .filter(session => session.schoolYear === this.schoolYear) + .map(session => ({ ...session, courseMonth: this.formatMonth(session.courseMonth) - }; - }); - allSessions.sort((a, b) => new Date(a.activeUntilDate) - new Date(b.activeUntilDate)); + })) + .sort((a, b) => + LocalDateTime.parse(a.activeUntilDate).compareTo(LocalDateTime.parse(b.activeUntilDate)) + ); + for (let i = 0; i < allSessions.length; i += 2) { orderedSessions.push(allSessions.slice(i, i + 2)); } @@ -185,20 +187,22 @@ export default { getAllAssessmentSessions() { this.loading = true; ApiService.apiAxios - .get(`${ApiRoutes.eas.GET_ASSESSMENT_SESSIONS}` + '/' + this.userInfo.activeInstituteType, {}) - .then((response) => { - this.allSessions = response.data.sort((a, b) => new Date(b.activeUntilDate) - new Date(a.activeUntilDate)); - if(this.allSessions.length >0) { - this.schoolYear = this.allSessions[0].schoolYear; - } - }) - .catch((error) => { - console.error(error); - }) - .finally(() => { - this.loading = false; - }); - }, + .get(`${ApiRoutes.eas.GET_ASSESSMENT_SESSIONS}` + '/' + this.userInfo.activeInstituteType, {}) + .then((response) => { + this.allSessions = response.data.sort((a, b) => + LocalDateTime.parse(b.activeUntilDate).compareTo(LocalDateTime.parse(a.activeUntilDate)) + ); + if (this.allSessions.length > 0) { + this.schoolYear = this.allSessions[0].schoolYear; + } + }) + .catch((error) => { + console.error(error); + }) + .finally(() => { + this.loading = false; + }); + }, formattoDate(date) { return moment(JSON.stringify(date), 'YYYY-MM-DDTHH:mm:ss').format('YYYY/MM/DD'); }, diff --git a/frontend/src/components/assessments/registrations/StudentRegistrations.vue b/frontend/src/components/assessments/registrations/StudentRegistrations.vue index 15b5f78b..3a4e2ac2 100644 --- a/frontend/src/components/assessments/registrations/StudentRegistrations.vue +++ b/frontend/src/components/assessments/registrations/StudentRegistrations.vue @@ -9,6 +9,14 @@ + + + + + + + + Add Assessment Registration + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +

+ {{ issue.validationLabel }} +

+
+
+ + + {{issue.validationMessage}} + + +
+
+
+
+
+
+
+
+ + + + +
+
+ + + + diff --git a/frontend/src/components/common/forms/SchoolContactsForm.vue b/frontend/src/components/common/forms/SchoolContactsForm.vue index ebb10a35..88455d3d 100644 --- a/frontend/src/components/common/forms/SchoolContactsForm.vue +++ b/frontend/src/components/common/forms/SchoolContactsForm.vue @@ -173,10 +173,13 @@ import {instituteStore} from '../../../store/modules/institute'; import {SCHOOL_CONTACT_TYPES} from '../../../utils/constants/SchoolContactTypes'; import {SCHOOL_CATEGORY_CODES} from '../../../utils/constants/SchoolCategoryCodeTypes'; import {MINISTRY_CONTACTS} from '../../../utils/constants/MinistryContactsInfo'; +import {LocalDateTime} from "@js-joda/core"; // checks the expiry of a contact function isExpired(contact) { - return (contact.expiryDate) ? new Date(contact.expiryDate) < new Date() : false; + return contact.expiryDate + ? LocalDateTime.parse(contact.expiryDate).isBefore(LocalDateTime.now()) + : false; } export default { diff --git a/frontend/src/components/school/SchoolList.vue b/frontend/src/components/school/SchoolList.vue index deb99549..8d4ac6e8 100644 --- a/frontend/src/components/school/SchoolList.vue +++ b/frontend/src/components/school/SchoolList.vue @@ -272,6 +272,7 @@ import alertMixin from '../../mixins/alertMixin'; import {formatPhoneNumber, formatContactName} from '../../utils/format'; import {getStatusColorAuthorityOrSchool, getStatusAuthorityOrSchool, isContactCurrent} from '../../utils/institute/status'; import {edxStore} from '../../store/modules/edx'; +import {LocalDateTime} from "@js-joda/core"; export default { name: 'SchoolListPage', @@ -449,21 +450,19 @@ export default { getPrincipalsName(contacts) { let oldestPrincipal = null; for (const contact of contacts) { - if (contact.schoolContactTypeCode !== 'PRINCIPAL') { + if (contact.schoolContactTypeCode !== 'PRINCIPAL' || !isContactCurrent(contact)) { continue; } - if (!isContactCurrent(contact)) { - continue; - } - if ((oldestPrincipal !== null) && (new Date(oldestPrincipal.effectiveDate) < new Date(contact.effectiveDate))) { + + if (oldestPrincipal !== null && + LocalDateTime.parse(oldestPrincipal.effectiveDate) + .isBefore(LocalDateTime.parse(contact.effectiveDate))) { continue; } oldestPrincipal = contact; } - if (oldestPrincipal == null) { - return ''; - } - return formatContactName(oldestPrincipal); + + return oldestPrincipal ? formatContactName(oldestPrincipal) : ''; }, getStatusColorAuthorityOrSchool, openSchool(schoolId){ diff --git a/frontend/src/components/sdcCollection/sdcDistrictCollection/StepOneUploadData.vue b/frontend/src/components/sdcCollection/sdcDistrictCollection/StepOneUploadData.vue index 4e3b89e7..72cee236 100644 --- a/frontend/src/components/sdcCollection/sdcDistrictCollection/StepOneUploadData.vue +++ b/frontend/src/components/sdcCollection/sdcDistrictCollection/StepOneUploadData.vue @@ -480,15 +480,16 @@ export default { this.initialLoad = false; } }, - sortSchoolsInProgress(){ + sortSchoolsInProgress() { this.schoolCollectionsInProgress.sort((a, b) => { - const dateA = new Date(a.uploadDate); - const dateB = new Date(b.uploadDate); + const dateA = a.uploadDate ? LocalDate.parse(a.uploadDate) : null; + const dateB = b.uploadDate ? LocalDate.parse(b.uploadDate) : null; + if (!dateA && !dateB) return 0; if (!dateA) return 1; if (!dateB) return -1; - return dateB - dateA; + return dateB.compareTo(dateA); }); } } diff --git a/frontend/src/store/modules/sdcCollection.js b/frontend/src/store/modules/sdcCollection.js index aedbf9db..3f171f1d 100644 --- a/frontend/src/store/modules/sdcCollection.js +++ b/frontend/src/store/modules/sdcCollection.js @@ -2,7 +2,7 @@ import { defineStore } from 'pinia'; import ApiService from '../../common/apiService'; import { ApiRoutes } from '../../utils/constants'; import {capitalize} from 'lodash'; -import {LocalDate} from '@js-joda/core'; +import {LocalDate, LocalDateTime} from '@js-joda/core'; export const sdcCollectionStore = defineStore('sdcCollection', { id: 'sdcCollection', @@ -182,9 +182,17 @@ export const sdcCollectionStore = defineStore('sdcCollection', { }); }, async setCollectionTypeCodes(collectionTypes) { - collectionTypes.sort(function(a, b) { - return new Date(Date.parse(a.label + " 1, 2000")) - new Date(Date.parse(b.label + " 1, 2000")); - }); + try { + collectionTypes.sort((a, b) => { + const dateA = LocalDateTime.parse(a.effectiveDate); + const dateB = LocalDateTime.parse(b.effectiveDate); + + return dateA.compareTo(dateB); + }); + } catch (error) { + console.error("Error parsing date in setCollectionTypeCodes:", error); + } + this.collectionTypeCodes = collectionTypes; this.collectionTypeCodesMap = new Map(); collectionTypes.forEach(element => { diff --git a/frontend/src/utils/institute/formRules.js b/frontend/src/utils/institute/formRules.js index 8648aa47..e7d84202 100644 --- a/frontend/src/utils/institute/formRules.js +++ b/frontend/src/utils/institute/formRules.js @@ -106,6 +106,46 @@ const website = (message = 'Website must be valid and secure (i.e., https)') => return v => !v || /^https:\/\/(?:www\.)?[-a-zA-Z0-9@:%._\\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_+.~#?&/=]*)$/.test(v) || message; }; +const penIsValid = (message = 'PEN is invalid') => { + return v => { + if (!v) return true; // Allow empty values + + if (v.length !== 9 || !/^-?\d+(?:\.\d+)?$/.test(v)) { + return message; + } + + const odds = []; + const evens = []; + for (let i = 0; i < v.length - 1; i++) { + const number = parseInt(v[i], 10); + if (i % 2 === 0) { + odds.push(number); + } else { + evens.push(number); + } + } + + const sumOdds = odds.reduce((acc, val) => acc + val, 0); + + let fullEvenString = ""; + evens.forEach(num => fullEvenString += num); + + const doubledEvens = []; + const doubledEvenString = (parseInt(fullEvenString, 10) * 2).toString(); + for (const digit of doubledEvenString) { + doubledEvens.push(parseInt(digit, 10)); + } + + const sumEvens = doubledEvens.reduce((acc, val) => acc + val, 0); + + const finalSum = sumEvens + sumOdds; + const checkDigit = v[8]; + + return ((finalSum % 10 === 0 && checkDigit === "0") || + (10 - finalSum % 10 === parseInt(checkDigit, 10))) || message; + }; +}; + export { email, endDateRule, @@ -117,5 +157,6 @@ export { postalCode, required, website, - validPEN + validPEN, + penIsValid };