From 08fb8167e173583415cebee2cd2b893cf0f5b86f Mon Sep 17 00:00:00 2001 From: alexmcdermid Date: Tue, 2 Jul 2024 12:21:19 -0700 Subject: [PATCH 01/26] start of search and edit students in open coll --- .../components/composable/alertComposable.js | 17 + .../AllStudents/AllStudentsComponent.vue | 82 ++ .../AllStudents/DetailComponent.vue | 254 +++++ .../AllStudents/EditStudent.vue | 1009 +++++++++++++++++ .../data-collection/AllStudents/Filters.vue | 388 +++++++ .../ViewStudentDetailsComponent.vue | 216 ++++ .../data-collection/CollectionView.vue | 14 + .../utils/sdc/collectionTableConfiguration.js | 798 +++++++++++++ .../utils/sdc/sdcValidationFieldMappings.js | 38 + frontend/src/utils/validation.js | 7 + 10 files changed, 2823 insertions(+) create mode 100644 frontend/src/components/composable/alertComposable.js create mode 100644 frontend/src/components/data-collection/AllStudents/AllStudentsComponent.vue create mode 100644 frontend/src/components/data-collection/AllStudents/DetailComponent.vue create mode 100644 frontend/src/components/data-collection/AllStudents/EditStudent.vue create mode 100644 frontend/src/components/data-collection/AllStudents/Filters.vue create mode 100644 frontend/src/components/data-collection/AllStudents/ViewStudentDetailsComponent.vue create mode 100644 frontend/src/utils/sdc/sdcValidationFieldMappings.js diff --git a/frontend/src/components/composable/alertComposable.js b/frontend/src/components/composable/alertComposable.js new file mode 100644 index 000000000..d874dbaf7 --- /dev/null +++ b/frontend/src/components/composable/alertComposable.js @@ -0,0 +1,17 @@ +import {ALERT_NOTIFICATION_TYPES} from '../../utils/constants/AlertNotificationTypes.js'; +import { appStore } from '../../store/modules/app'; + +export const setWarningAlert = (message) => { + const useAppStore = appStore(); + return useAppStore.addAlertNotification({text: message, alertType: ALERT_NOTIFICATION_TYPES.WARN}); +}; + +export const setSuccessAlert = (message) => { + const useAppStore = appStore(); + return useAppStore.addAlertNotification({text: message, alertType: ALERT_NOTIFICATION_TYPES.SUCCESS}); +}; + +export const setFailureAlert = (message) => { + const useAppStore = appStore(); + return useAppStore.addAlertNotification({text: message, alertType: ALERT_NOTIFICATION_TYPES.ERROR}); +}; diff --git a/frontend/src/components/data-collection/AllStudents/AllStudentsComponent.vue b/frontend/src/components/data-collection/AllStudents/AllStudentsComponent.vue new file mode 100644 index 000000000..4c8ccbc47 --- /dev/null +++ b/frontend/src/components/data-collection/AllStudents/AllStudentsComponent.vue @@ -0,0 +1,82 @@ + + + + + + diff --git a/frontend/src/components/data-collection/AllStudents/DetailComponent.vue b/frontend/src/components/data-collection/AllStudents/DetailComponent.vue new file mode 100644 index 000000000..890edab13 --- /dev/null +++ b/frontend/src/components/data-collection/AllStudents/DetailComponent.vue @@ -0,0 +1,254 @@ + + + + + diff --git a/frontend/src/components/data-collection/AllStudents/EditStudent.vue b/frontend/src/components/data-collection/AllStudents/EditStudent.vue new file mode 100644 index 000000000..84cd89b7a --- /dev/null +++ b/frontend/src/components/data-collection/AllStudents/EditStudent.vue @@ -0,0 +1,1009 @@ + + + + + + diff --git a/frontend/src/components/data-collection/AllStudents/Filters.vue b/frontend/src/components/data-collection/AllStudents/Filters.vue new file mode 100644 index 000000000..35ba98900 --- /dev/null +++ b/frontend/src/components/data-collection/AllStudents/Filters.vue @@ -0,0 +1,388 @@ + + + + + + + + diff --git a/frontend/src/components/data-collection/AllStudents/ViewStudentDetailsComponent.vue b/frontend/src/components/data-collection/AllStudents/ViewStudentDetailsComponent.vue new file mode 100644 index 000000000..ecd3f3dba --- /dev/null +++ b/frontend/src/components/data-collection/AllStudents/ViewStudentDetailsComponent.vue @@ -0,0 +1,216 @@ + + + + + + diff --git a/frontend/src/components/data-collection/CollectionView.vue b/frontend/src/components/data-collection/CollectionView.vue index 0477ffaf0..511994b65 100644 --- a/frontend/src/components/data-collection/CollectionView.vue +++ b/frontend/src/components/data-collection/CollectionView.vue @@ -74,6 +74,11 @@ > Resolve Provincial Duplicates + + All Students + + + + @@ -124,6 +136,7 @@ import PenMatch from '@/components/data-collection/PenMatch.vue'; import ProvincialDuplicates from '@/components/data-collection/provincialDuplicates/ProvincialDuplicates.vue'; import DistrictMonitoring from '@/components/data-collection/DistrictMonitoring.vue'; import Spinner from '@/components/common/Spinner.vue'; +import AllStudentsComponent from '@/components/data-collection/AllStudents/AllStudentsComponent.vue'; export default { name: 'CollectionView', @@ -131,6 +144,7 @@ export default { Spinner, DistrictMonitoring, ProvincialDuplicates, + AllStudentsComponent, PenMatch, IndySchoolMonitoring, }, diff --git a/frontend/src/utils/sdc/collectionTableConfiguration.js b/frontend/src/utils/sdc/collectionTableConfiguration.js index 26dda6bb2..5c39528a8 100644 --- a/frontend/src/utils/sdc/collectionTableConfiguration.js +++ b/frontend/src/utils/sdc/collectionTableConfiguration.js @@ -1,3 +1,702 @@ +/** + * Filters + */ + +export const WARNING_FILTER = Object.freeze( + { + heading: 'Warnings', + id: 'warnings', + multiple: true, + key: 'warnings', + filterOptions: [ + { + title: 'Has Funding Warnings', + id: 'hasFundingWarning', + value: 'FUNDING_WARNING' + }, + { + title: 'Has Info Warnings', + id: 'hasInfoWarning', + value: 'INFO_WARNING' + } + ] + } +); + +export const STUDENT_TYPE_FILTER = Object.freeze( + { + heading: 'Student Type', + id: 'studentType', + multiple: true, + key: 'studentType', + filterOptions: [ + { + title: 'Adult', + id: 'isAdult', + value: 'isAdult' + }, + { + title: 'School Aged', + id: 'isSchoolAged', + value: 'isSchoolAged' + }, + { + title: 'Preschool Aged', + id: 'isUnderSchoolAged', + value: 'isUnderSchoolAged' + } + ] + }, +); + +export const FTE_FILTER = Object.freeze( + { + heading: 'FTE', + id: 'fte', + multiple: true, + key: 'fte', + filterOptions: [ + { + title: 'FTE = 0', + id: 'fteEq0', + value: 'fteEq0' + }, + { + title: 'FTE < 1', + id: 'fteLt1', + value: 'fteLt1' + }, + { + title: 'FTE > 0', + id: 'fteGt0', + value: 'fteGt0' + } + ] + }, +); + +export const FUNDING_TYPE_FILTER = Object.freeze( + { + heading: 'Funding Type', + id: 'fundingtype', + multiple: true, + key: 'fundingType', + filterOptions: [ + { + title: '14 - Out of Province/International', + id: 'funding14', + value: '14' + }, + { + title: '20 - Living on Reserve', + id: 'funding20', + value: '20' + }, + { + title: '16 - Newcomer Refugee', + id: 'funding16', + value: '16' + }, + { + title: 'No Funding Code', + id: 'noFunding', + value: 'No Funding' + } + ] + }, +); + +export const GRADE_FILTER = Object.freeze( + { + heading: 'Grade', + id: 'grade', + multiple: true, + key: 'grade', + filterOptions: [ + { + title: 'Kind. Half', + id: 'gradeKH', + value: 'KH' + }, + { + title: 'Kind. Full', + id: 'gradeKF', + value: 'KF' + }, + { + title: 'Gr. 1', + id: 'grade1', + value: '01' + }, + { + title: 'Gr. 2', + id: 'grade2', + value: '02' + }, + { + title: 'Gr. 3', + id: 'grade3', + value: '03' + }, + { + title: 'Gr. 4', + id: 'grade4', + value: '04' + }, + { + title: 'Gr. 5', + id: 'grade5', + value: '05' + }, + { + title: 'Gr. 6', + id: 'grade6', + value: '06' + }, + { + title: 'Gr. 7', + id: 'grade7', + value: '07' + }, + { + title: 'Elem. Ungraded', + id: 'gradeEU', + value: 'EU' + }, + { + title: 'Gr. 8', + id: 'grade8', + value: '08' + }, + { + title: 'Gr. 9', + id: 'grade9', + value: '09' + }, + { + title: 'Gr. 10', + id: 'grade10', + value: '10' + }, + { + title: 'Gr. 11', + id: 'grade11', + value: '11' + }, + { + title: 'Gr. 12', + id: 'grade12', + value: '12' + }, + { + title: 'Sec. Ungraded', + id: 'gradeSU', + value: 'SU' + }, + { + title: 'Graduated Adult', + id: 'gradeGA', + value: 'GA' + }, + { + title: 'Home School', + id: 'gradeHS', + value: 'HS' + }, + ] + }, +); + +export const SUPPORT_BLOCKS_FILTER = Object.freeze( + { + heading: 'Support Blocks', + id: 'supportblocks', + multiple: false, + key: 'support', + filterOptions: [ + { + title: 'Has Support Blocks', + id: 'hasSupportBlocks', + value: 'hasSupportBlocks' + }, + { + title: 'No Support Blocks', + id: 'noSupportBlocks', + value: 'noSupportBlocks' + } + ] + } +); + +export const FTE_ZERO_FILTER = Object.freeze( + { + heading: 'Reasons for FTE = 0 ', + id: 'zeroFteReasons', + multiple: true, + key: 'fteZero', + filterOptions: [ + { + title: 'Out of Province International ', + id: 'outOfprov', + value: 'OUTOFPROV' + }, + { + title: 'Nominal Roll Eligible', + id: 'nomRoll', + value: 'NOMROLL' + }, + { + title: 'Student Too Young', + id: 'tooYoung', + value: 'TOOYOUNG' + }, + { + title: 'Graduated Adult', + id: 'indyAdult', + value: 'INDYADULT' + }, + { + title: 'No new active courses', + id: 'inactive', + value: 'INACTIVE' + }, + { + title: 'District already received funding', + id: 'distdup', + value: 'DISTDUP' + }, + { + title: 'Authority already received funding', + id: 'authdup', + value: 'AUTHDUP' + } + ] + } +); + +export const FRENCH_PROGRAMS_FILTER = Object.freeze( + { + heading: 'French Programs', + id: 'frenchPrograms', + multiple: true, + key: 'frenchProgram', + filterOptions: [ + { + title: '11 - Early French Immersion', + id: 'french11', + value: '11' + }, + { + title: '14 - Late French Immersion', + id: 'french14', + value: '14' + }, + { + title: '08 - Core French', + id: 'french08', + value: '08' + } + ] + } +); + +export const ENGLISH_PROGRAMS_FILTER = Object.freeze( + { + heading: 'English Language Learning', + id: 'englishPrograms', + multiple: false, + key: 'englishProgram', + filterOptions: [ + { + title: '17 - English Language Learning', + id: 'english17', + value: '17' + }, + { + title: 'No English Language Learning', + id: 'noEnglish17', + value: 'noEnglish17' + } + ] + } +); + +export const FRENCH_FUNDING_FILTER = Object.freeze( + { + heading: 'French Program Funding Eligibility', + id: 'frenchFunding', + multiple: false, + key: 'frenchFunding', + filterOptions: [ + { + title: 'Funding Eligible', + id: 'frenchFundingElig', + value: 'true' + }, + { + title: 'Not Funding Eligible', + id: 'frenchFundingNotElig', + value: 'false' + } + ] + } +); + +export const CAREER_CODE_FILTER = Object.freeze( + { + heading: 'Career Code', + id: 'careerCode', + multiple: true, + key: 'careerCode', + filterOptions: [ + { + title: 'XA - Business & Applied Business', + id: 'codeXA', + value: 'XA' + }, + { + title: 'XB - Fine Arts, Design, & Media', + id: 'codeXB', + value: 'XB' + }, + { + title: 'XC - Fitness & Recreation', + id: 'codeXC', + value: 'XC' + }, + { + title: 'XD - Health & Human Services', + id: 'codeXD', + value: 'XD' + }, + { + title: 'XE - Liberal Arts & Humanities', + id: 'codeXE', + value: 'XE' + }, + { + title: 'XF - Science & Applied Science ', + id: 'codeXF', + value: 'XF' + }, + { + title: 'XG - Tourism, Hospitality, & Foods', + id: 'codeXG', + value: 'XG' + }, + { + title: 'XH - Trades & Technology', + id: 'codeXH', + value: 'XH' + } + ] + }, +); + +export const CAREER_PROGRAM_FILTER = Object.freeze( + { + heading: 'Career Programs', + id: 'careerPrograms', + multiple: true, + key: 'careerPrograms', + filterOptions: [ + { + title: '40 - Career Preparation', + id: 'career40', + value: '40' + }, + { + title: '41 - Co-Operative Education', + id: 'career41', + value: '41' + }, + { + title: '42 - Youth Work in Trades Program', + id: 'career42', + value: '42' + }, + { + title: '43 - Career Technical or Youth Train in Trades', + id: 'career43', + value: '43' + }, + ] + } +); + +export const CAREER_FUNDING_FILTER = Object.freeze( + { + heading: 'Career Program Funding Eligibility', + id: 'careerFunding', + multiple: false, + key: 'careerProgramsFunding', + filterOptions: [ + { + title: 'Funding Eligible', + id: 'isCareerFundingEligible', + value: 'isCareerFundingEligible' + }, + { + title: 'Not Funding Eligible', + id: 'isNotCareerFundingEligible', + value: 'isNotCareerFundingEligible' + } + ] + } +); + +export const INDIGENOUS_PROGRAM_FILTER = Object.freeze( + { + heading: 'Indigenous Support Programs', + id: 'indigenousPrograms', + multiple: true, + key: 'indigenousPrograms', + filterOptions: [ + { + title: '29 - Language & Culture', + id: 'indigenous29', + value: '29' + }, + { + title: '33 - Support Services', + id: 'indigenous33', + value: '33' + }, + { + title: '36 - Other Approved Programs', + id: 'indigenous36', + value: '36' + }, + ] + } +); + +export const ANCESTRY_FILTER = Object.freeze( + { + heading: 'Indigenous Ancestry', + id: 'ancestry', + multiple: false, + key: 'ancestry', + filterOptions: [ + { + title: 'Has Indigenous Ancestry', + id: 'hasIndiAncestry', + value: 'true' + }, + { + title: 'No Indigenous Ancestry', + id: 'hasNoIndAncestry', + value: 'false' + } + ] + } +); + +export const INDIGENOUS_FUNDING_FILTER = Object.freeze( + { + heading: 'Indigenous Support Program Funding Eligibility', + id: 'indigenousFunding', + multiple: false, + key: 'indigenousProgramsFunding', + filterOptions: [ + { + title: 'Funding Eligible', + id: 'indFundingEligible', + value: 'true' + }, + { + title: 'Not Funding Eligible', + id: 'indFundingNotEligible', + value: 'false' + } + ] + } +); + +export const BAND_FILTER = Object.freeze( + { + heading: 'Band of Residence', + id: 'band', + multiple: true, + key: 'bandCode', + filterOptions: [ + { + title: 'Has Band Code', + id: 'hasBandCode', + value: 'true' + }, + { + title: 'No Band Code', + id: 'hasNoBandCode', + value: 'false' + }, + ] + } +); + +export const SPED_FILTER = Object.freeze( + { + heading: 'Special Education', + id: 'sped', + multiple: true, + key: 'sped', + filterOptions: [ + { + title: 'A - Physically Dependent', + id: 'spedA', + value: 'A' + }, + { + title: 'B - Deafblind', + id: 'spedB', + value: 'B' + }, + { + title: 'C - Moderate to Profound Intellectual Disability', + id: 'spedC', + value: 'C' + }, + { + title: 'D - Physical Disability or Chronic Health Impairment', + id: 'spedD', + value: 'D' + }, + { + title: 'E - Visual Impairment', + id: 'spedE', + value: 'E' + }, + { + title: 'F - Deaf or Hard of Hearing', + id: 'spedF', + value: 'F' + }, + { + title: 'G - Autism Spectrum Disorder', + id: 'spedG', + value: 'G' + }, + { + title: 'H - Intensive Behaviour Intervention/Serious Mental Illness', + id: 'spedH', + value: 'H' + }, + { + title: 'K - Mild Intellectual Disability', + id: 'spedK', + value: 'K' + }, + { + title: 'P - Gifted', + id: 'spedP', + value: 'P' + }, + { + title: 'Q - Learning Disability', + id: 'spedQ', + value: 'Q' + }, + { + title: 'R - Moderate Behaviour Support/Mental Illness', + id: 'spedR', + value: 'R' + }, + ] + } +); + +export const SPED_FUNDING_FILTER = Object.freeze( + { + heading: 'Special Education Funding Eligibility', + id: 'spedFunding', + multiple: false, + key: 'spedFunding', + filterOptions: [ + { + title: 'Funding Eligible', + id: 'spedFundingEligible', + value: 'true' + }, + { + title: 'Not Funding Eligible', + id: 'spedFundingNotEligible', + value: 'false' + } + ] + } +); + +export const ELL_FUNDING_FILTER = Object.freeze( + { + heading: 'English Language Learning Funding Eligibility', + id: 'ellFunding', + multiple: false, + key: 'ellFunding', + filterOptions: [ + { + title: 'Funding Eligible', + id: 'ellFundingEligible', + value: 'true' + }, + { + title: 'Not Funding Eligible', + id: 'ellFundingNotEligible', + value: 'false' + } + ] + } +); + +export const REFUGEE_FUNDING_FILTER = Object.freeze( + { + heading: 'Refugee Funding Eligibility', + id: 'refugeeFunding', + multiple: false, + key: 'refugeeFunding', + filterOptions: [ + { + title: 'Funding Eligible', + id: 'refugeeFundingEligible', + value: 'true' + }, + { + title: 'Not Funding Eligible', + id: 'refugeeFundingNotEligible', + value: 'false' + } + ] + } +); + +export const ELL_YEARS_FILTER = Object.freeze( + { + heading: 'Years in ELL', + id: 'ellYears', + multiple: true, + key: 'ellYears', + filterOptions: [ + { + title: '1-5 years in ELL', + id: 'ell1Between5', + value: 'ell1Between5' + }, + { + title: '6+ years in ELL', + id: 'ellGtEq6', + value: 'ellGtEq6' + } + ] + } +); + +export const COURSE_FILTER = Object.freeze( + { + heading: 'Number of Courses', + id: 'numCourses', + key: 'courses' + } +); + export const MONITORING = Object.freeze( { allowedFilters: { @@ -141,3 +840,102 @@ export const NEW_PEN = Object.freeze( ] } ); + +export const FTE = Object.freeze( + { + tableHeaders: [ + { title: 'select', key: 'select' }, + { key: 'sdcSchoolCollectionStudentStatusCode' }, + { title: 'School', key: 'schoolName' }, + { title: 'FTE', key: 'fte', align: 'start' }, + { title: 'Assigned PEN', key: 'assignedPen', subHeader: { title: 'Local ID', key: 'localID' } }, + { title: 'Legal Surname, Given (Middle)', key: 'legalName', subHeader: { title: 'Usual Surname, Given (Middle)', key: 'usualName' } }, + { title: 'Adult', key: 'isAdult', subHeader: { title: 'Grad', key: 'isGraduated' } }, + { title: 'Grade', key: 'enrolledGradeCode', subHeader: { title: 'Funding Code', key: 'mappedSchoolFunding' } }, + { title: 'Courses For Grad', key: 'mappedNoOfCourses', subHeader: { title: 'Support Blocks', key: 'supportBlocks' } }, + { title: 'Language Program', key: 'mappedLanguageEnrolledProgram', subHeader: { title: 'Years in ELL', key: 'yearsInELL' } }, + { title: 'Career Program', key: 'mappedCareerProgram', subHeader: { title: 'Career Code', key: 'mappedCareerProgramCode' } }, + { title: 'Indigenous Ancestry', key: 'mappedAncestryIndicator', subHeader: { title: 'Band Code', key: 'mappedBandCode' } }, + { title: 'Indigenous Support Program', key: 'mappedIndigenousEnrolledProgram', subHeader: { title: 'Special Education Category', key: 'mappedSpedCode' } }, + ], + summaryReport: [ + { title: 'Eligible Enrolment & Eligible FTE', endpoint:'enrollment'}, + { title: 'Grade Enrolment & FTE per School', endpoint:'grade-enrollment'} + ], + allowedFilters: { + studentType: STUDENT_TYPE_FILTER, + fte: FTE_FILTER, + grade: GRADE_FILTER, + fundingType: FUNDING_TYPE_FILTER, + warnings: WARNING_FILTER, + courses: COURSE_FILTER, + support: SUPPORT_BLOCKS_FILTER, + fteZero: FTE_ZERO_FILTER, + frenchProgram: { + ...FRENCH_PROGRAMS_FILTER, + filterOptions: [ + ...FRENCH_PROGRAMS_FILTER.filterOptions, + { + title: '05 - Programme Francophone', + id: 'french05', + value: '05' + }, + { + title: 'No French Programs', + id: 'noFrenchProgram', + value: 'noFrenchPrograms' + } + ] + }, + englishProgram: ENGLISH_PROGRAMS_FILTER, + ellYears: ELL_YEARS_FILTER, + careerPrograms: { + ...CAREER_PROGRAM_FILTER, + filterOptions: [ + ...CAREER_PROGRAM_FILTER.filterOptions, + { + title: 'No Career Programs', + id: 'noCareerProgram', + value: 'noCareerPrograms' + } + ] + }, + careerCode: { + ...CAREER_CODE_FILTER, + filterOptions: [ + ...CAREER_CODE_FILTER.filterOptions, + { + title: 'No Career Code', + id: 'noCareerCode', + value: 'noCareerCodes' + } + ] + }, + indigenousPrograms: { + ...INDIGENOUS_PROGRAM_FILTER, + filterOptions: [ + ...INDIGENOUS_PROGRAM_FILTER.filterOptions, + { + title: 'No Indigenous Support Programs', + id: 'noIndigenousPrograms', + value: 'noIndigenousPrograms' + } + ] + }, + bandCode: BAND_FILTER, + ancestry: ANCESTRY_FILTER, + sped: { + ...SPED_FILTER, + filterOptions: [ + ...SPED_FILTER.filterOptions, + { + title: 'No Special Education Category', + id: 'noSpedCategory', + value: 'noSpedCode' + } + ] + } + } + } +); + diff --git a/frontend/src/utils/sdc/sdcValidationFieldMappings.js b/frontend/src/utils/sdc/sdcValidationFieldMappings.js new file mode 100644 index 000000000..7e8075348 --- /dev/null +++ b/frontend/src/utils/sdc/sdcValidationFieldMappings.js @@ -0,0 +1,38 @@ +import * as formRules from '../../utils/institute/formRules'; +import {isValidPEN, checkEnrolledProgramLength} from '../../utils/validation'; + +// contains the mappings for validation field errors used in StepTwoValidateData.vue +// type: refers to the type of user input example input => , select => +// label: refers to label in input field. +// key: refers to the property in sdcSchoolCollectionStudent. +// options: various attributes to add into the input fields. further explanation below +// { +// rules: custom rules for input element +// items: only for type select and multiselect refers to the pinia store where the dropdown items reside +// itemValue: only for type select and multiselect refers to where we grab the value +// } + +export const SDC_VALIDATION_FIELD_MAPPINGS = Object.freeze({ + LOCALID: {label: 'Local ID', key: 'localID', type: 'input', options: {maxlength: '12'}}, + STUDENT_PEN: {label: 'PEN', key: 'studentPen', type: 'input', options: {maxlength: '9', rules: [v => isValidPEN(v) || 'Must be a valid PEN']}}, + LEGAL_FIRST_NAME: {label: 'Legal Given', key: 'legalFirstName', type: 'input', options: {maxlength: '255'}}, + LEGAL_MIDDLE_NAMES: {label: 'Legal Middle', key: 'legalMiddleNames', type: 'input', options: {maxlength: '255'}}, + LEGAL_LAST_NAME: {label: 'Legal Surname', key: 'legalLastName', type: 'input', options: {maxlength: '255', rules: [formRules.required()]}}, + USUAL_FIRST_NAME: {label: 'Usual Given', key: 'usualFirstName', type: 'input', options: {maxlength: '255'}}, + USUAL_MIDDLE_NAMES: {label: 'Usual Middle', key: 'usualMiddleNames', type: 'input', options: {maxlength: '255'}}, + USUAL_LAST_NAME: {label: 'Usual Surname', key: 'usualLastName', type: 'input', options: {maxlength: '255'}}, + DOB: {label: 'Birthdate', key: 'dob', type: 'datePicker', options: {rules: [formRules.required()]}}, + GENDER_CODE: {label: 'Gender', key: 'gender', type: 'select', options: {rules: [formRules.required()], items: 'genderCodes', itemValue: 'genderCode'}}, + SPECIAL_EDUCATION_CATEGORY_CODE: {label: 'Special Education Category', key: 'specialEducationCategoryCode', type: 'select', options: {items: 'specialEducationCodes', itemValue: 'specialEducationCategoryCode'}}, + SCHOOL_FUNDING_CODE: {label: 'Funding Code', key: 'schoolFundingCode', type: 'select', options: {items: 'schoolFundingCodes', itemValue: 'schoolFundingCode'}}, + NATIVE_ANCESTRY_IND: {label: 'Indigenous Ancestry', key: 'nativeAncestryInd', type: 'select', options: {rules: [formRules.required()], items: 'ancestryItems', itemValue:'code'}}, + HOME_LANGUAGE_SPOKEN_CODE: {label: 'Home Language Spoken Code', key: 'homeLanguageSpokenCode', type: 'select', options: {items: 'homeLanguageSpokenCodes', itemValue: 'homeLanguageSpokenCode'}}, + OTHER_COURSES: {label: 'Other Courses', key: 'otherCourses', type: 'select', options: {items: 'otherCoursesValidNumbers', itemValue: 'otherCourses'}}, + SUPPORT_BLOCKS: {label: 'Support Blocks', key: 'supportBlocks', type: 'select', options: {items: 'supportBlocksValidNumbers', itemValue: 'supportBlocks'}}, + ENROLLED_GRADE_CODE: {label: 'Enrolled Grade Codes', key: 'enrolledGradeCode', type: 'select', options: {items: 'enrolledGradeCodes', itemValue: 'enrolledGradeCode'}}, + ENROLLED_PROGRAM_CODE: {label: 'Program Codes', key: 'filteredEnrolledProgramCodes', type: 'multiselect', options: {rules:[v => checkEnrolledProgramLength(v) || 'Select a maximum of 8 Enrolled Programs'], items: 'enrolledProgramCodes', itemValue: 'enrolledProgramCode'}}, + CAREER_PROGRAM_CODE: {label: 'Career Code', key: 'careerProgramCode', type: 'select', options: {items: 'careerProgramCodes', itemValue: 'careerProgramCode'}}, + NUMBER_OF_COURSES: {label: 'Number of Courses', key: 'numberOfCourses', type: 'select', options: {maxlength: '4'}}, + BAND_CODE: {label: 'Band Codes', key: 'bandCode', type: 'select', options: {items: 'bandCodes', itemValue: 'bandCode'}}, + POSTAL_CODE: {label: 'Postal Code', key: 'postalCode', type: 'input', options: {maxlength: '6'}} +}); diff --git a/frontend/src/utils/validation.js b/frontend/src/utils/validation.js index 988377e79..66c2b8367 100644 --- a/frontend/src/utils/validation.js +++ b/frontend/src/utils/validation.js @@ -159,3 +159,10 @@ export function isValidNumber(evt) { export function isValidEmail(value) { return !!(value && /^(?=[A-Za-z0-9][A-Za-z0-9@._%+-]{5,253}$)[A-Za-z0-9._%+-]{1,64}@(?:(?=[A-Za-z0-9-]{1,63}\.)[A-Za-z0-9]+(?:-[A-Za-z0-9]+)*\.){1,8}[A-Za-z]{2,63}$/.test(value)); } + +export function checkEnrolledProgramLength(v) { + if(v === null || v.length === 0) { + return true; + } + return v.length > 0 && v.length <= 8; +} \ No newline at end of file From 11290da55eabf26bf7ad832026f24395f04da927 Mon Sep 17 00:00:00 2001 From: alexmcdermid Date: Tue, 2 Jul 2024 12:39:43 -0700 Subject: [PATCH 02/26] correct route to get data for collection and pipe in collection info --- .../AllStudents/AllStudentsComponent.vue | 32 +-- .../AllStudents/DetailComponent.vue | 110 ++++----- .../data-collection/AllStudents/Filters.vue | 227 +++++++++--------- .../data-collection/CollectionView.vue | 2 +- 4 files changed, 179 insertions(+), 192 deletions(-) diff --git a/frontend/src/components/data-collection/AllStudents/AllStudentsComponent.vue b/frontend/src/components/data-collection/AllStudents/AllStudentsComponent.vue index 4c8ccbc47..34867fe4e 100644 --- a/frontend/src/components/data-collection/AllStudents/AllStudentsComponent.vue +++ b/frontend/src/components/data-collection/AllStudents/AllStudentsComponent.vue @@ -1,27 +1,13 @@ @@ -127,7 +127,7 @@ export default { type: Object, default: null }, - district: { + collectionObject: { type: Object, required: true, default: null @@ -194,7 +194,7 @@ export default { }, loadStudents() { this.isLoading= true; - ApiService.apiAxios.get(`${Routes.sdc.SDC_DISTRICT_COLLECTION}/${this.$route.params.sdcDistrictCollectionID}/paginated?tableFormat=true`, { + ApiService.apiAxios.get(`${Routes.sdc.BASE_URL}/collection/${this.collectionObject.collectionID}/students-paginated?tableFormat=true`, { params: { pageNumber: this.pageNumber - 1, pageSize: this.pageSize, diff --git a/frontend/src/components/data-collection/AllStudents/Filters.vue b/frontend/src/components/data-collection/AllStudents/Filters.vue index 35ba98900..ee1c2415f 100644 --- a/frontend/src/components/data-collection/AllStudents/Filters.vue +++ b/frontend/src/components/data-collection/AllStudents/Filters.vue @@ -7,13 +7,13 @@ @@ -22,87 +22,87 @@
{{ filter?.heading }}
{{ option?.title }} @@ -110,61 +110,61 @@ @@ -209,6 +209,11 @@ export default { required: false, default: null }, + collectionObject: { + type: Object, + required: true, + default: null + }, showStudentSearch: { type: Boolean, required: false, @@ -259,21 +264,21 @@ export default { setupSchoolList(){ this.schoolSearchNames = []; ApiService.apiAxios.get(`${Routes.sdc.SDC_DISTRICT_COLLECTION}/${this.$route.params.sdcDistrictCollectionID}/sdcSchoolCollections`) - .then((res) => { - res.data.forEach(schoolCollection => { - const school = this.notClosedSchoolsMap.get(schoolCollection.schoolID); - let schoolItem = { - schoolCodeName: school.schoolName + ' - ' + school.mincode, - schoolID: school.schoolID, - districtID: school.districtID - }; - this.schoolSearchNames.push(schoolItem); - }); - this.schoolSearchNames = sortBy(this.schoolSearchNames, ['schoolCodeName']); - }) - .catch(error => { - console.error(error); + .then((res) => { + res.data.forEach(schoolCollection => { + const school = this.notClosedSchoolsMap.get(schoolCollection.schoolID); + let schoolItem = { + schoolCodeName: school.schoolName + ' - ' + school.mincode, + schoolID: school.schoolID, + districtID: school.districtID + }; + this.schoolSearchNames.push(schoolItem); }); + this.schoolSearchNames = sortBy(this.schoolSearchNames, ['schoolCodeName']); + }) + .catch(error => { + console.error(error); + }); }, close() { this.$emit('close'); diff --git a/frontend/src/components/data-collection/CollectionView.vue b/frontend/src/components/data-collection/CollectionView.vue index 511994b65..4f3b95262 100644 --- a/frontend/src/components/data-collection/CollectionView.vue +++ b/frontend/src/components/data-collection/CollectionView.vue @@ -118,7 +118,7 @@ transition="false" reverse-transition="false" > - + From 43ebfb7b7525b7a46cd6a0312809507a3c6027d2 Mon Sep 17 00:00:00 2001 From: alexmcdermid Date: Tue, 2 Jul 2024 13:02:11 -0700 Subject: [PATCH 03/26] todo for later --- .../data-collection/AllStudents/Filters.vue | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/frontend/src/components/data-collection/AllStudents/Filters.vue b/frontend/src/components/data-collection/AllStudents/Filters.vue index ee1c2415f..6680316e8 100644 --- a/frontend/src/components/data-collection/AllStudents/Filters.vue +++ b/frontend/src/components/data-collection/AllStudents/Filters.vue @@ -42,7 +42,7 @@ { this.isDistrictUser = true; - appStore().getInstitutesData().then(() => { + appStore().getInstituteCodes().then(() => { this.setupSchoolList(); this.loading = false; }); @@ -263,6 +258,8 @@ export default { methods: { setupSchoolList(){ this.schoolSearchNames = []; + // TODO school name or number filter dropdown population + /* ApiService.apiAxios.get(`${Routes.sdc.SDC_DISTRICT_COLLECTION}/${this.$route.params.sdcDistrictCollectionID}/sdcSchoolCollections`) .then((res) => { res.data.forEach(schoolCollection => { @@ -279,6 +276,7 @@ export default { .catch(error => { console.error(error); }); + */ }, close() { this.$emit('close'); From 7e178fe2809c90fb2255fa31c1efda32d04ffa89 Mon Sep 17 00:00:00 2001 From: alexmcdermid Date: Tue, 2 Jul 2024 13:19:29 -0700 Subject: [PATCH 04/26] table config to ticket specs --- frontend/src/utils/sdc/collectionTableConfiguration.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/frontend/src/utils/sdc/collectionTableConfiguration.js b/frontend/src/utils/sdc/collectionTableConfiguration.js index 5c39528a8..a61c5d4d6 100644 --- a/frontend/src/utils/sdc/collectionTableConfiguration.js +++ b/frontend/src/utils/sdc/collectionTableConfiguration.js @@ -844,12 +844,11 @@ export const NEW_PEN = Object.freeze( export const FTE = Object.freeze( { tableHeaders: [ - { title: 'select', key: 'select' }, - { key: 'sdcSchoolCollectionStudentStatusCode' }, + { title: 'District', key: 'districtName' }, { title: 'School', key: 'schoolName' }, - { title: 'FTE', key: 'fte', align: 'start' }, { title: 'Assigned PEN', key: 'assignedPen', subHeader: { title: 'Local ID', key: 'localID' } }, { title: 'Legal Surname, Given (Middle)', key: 'legalName', subHeader: { title: 'Usual Surname, Given (Middle)', key: 'usualName' } }, + { title: 'Birthdate', key: 'dob', subHeader: { title: 'Gender', key: 'gender' } }, { title: 'Adult', key: 'isAdult', subHeader: { title: 'Grad', key: 'isGraduated' } }, { title: 'Grade', key: 'enrolledGradeCode', subHeader: { title: 'Funding Code', key: 'mappedSchoolFunding' } }, { title: 'Courses For Grad', key: 'mappedNoOfCourses', subHeader: { title: 'Support Blocks', key: 'supportBlocks' } }, From 40c80bf6707a5e624b2655176710f158f2cccc33 Mon Sep 17 00:00:00 2001 From: alexmcdermid Date: Tue, 2 Jul 2024 13:21:16 -0700 Subject: [PATCH 05/26] dont show export all button --- .../data-collection/AllStudents/AllStudentsComponent.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/data-collection/AllStudents/AllStudentsComponent.vue b/frontend/src/components/data-collection/AllStudents/AllStudentsComponent.vue index 34867fe4e..3995e494f 100644 --- a/frontend/src/components/data-collection/AllStudents/AllStudentsComponent.vue +++ b/frontend/src/components/data-collection/AllStudents/AllStudentsComponent.vue @@ -7,7 +7,7 @@
From 37699b4c52b56654171bfa29a9704d3e295d853b Mon Sep 17 00:00:00 2001 From: alexmcdermid Date: Tue, 2 Jul 2024 14:36:43 -0700 Subject: [PATCH 06/26] working update with no student lock or update user --- backend/src/components/sdc/sdc.js | 63 +- backend/src/components/utils.js | 61 ++ backend/src/routes/sdc.js | 5 +- backend/src/util/redis/redis-utils.js | 48 +- .../src/components/common/CustomTable.vue | 8 +- .../AllStudents/EditStudent.vue | 772 +++++++++--------- .../ViewStudentDetailsComponent.vue | 76 +- 7 files changed, 601 insertions(+), 432 deletions(-) diff --git a/backend/src/components/sdc/sdc.js b/backend/src/components/sdc/sdc.js index cf8afa70f..b313bad11 100644 --- a/backend/src/components/sdc/sdc.js +++ b/backend/src/components/sdc/sdc.js @@ -1,10 +1,11 @@ 'use strict'; -const { logApiError, getData, errorResponse, postData} = require('../utils'); +const { logApiError, getData, errorResponse, postData, getCreateOrUpdateUserValue, stripNumberFormattingNumberOfCourses, formatNumberOfCourses, handleExceptionResponse} = require('../utils'); const HttpStatus = require('http-status-codes'); const config = require('../../config'); const utils = require('../utils'); const cacheService = require('../cache-service'); const { FILTER_OPERATION, VALUE_TYPE, CONDITION, ENROLLED_PROGRAM_TYPE_CODE_MAP, DUPLICATE_TYPE_CODES} = require('../../util/constants'); +const {lockSdcStudentBeingProcessedInRedis, unlockSdcStudentBeingProcessedInRedis} = require('../../util/redis/redis-utils'); async function getSnapshotFundingDataForSchool(req, res) { try { @@ -368,6 +369,63 @@ async function checkDuplicatesInCollection(req, res) { } } +async function updateAndValidateSdcSchoolCollectionStudent(req, res) { + try { + let studentLock; + if(req.body.sdcSchoolCollectionStudentID) { + let sdcSchoolCollectionStudentID = req.body.sdcSchoolCollectionStudentID; + let currentStudent = await getData(`${config.get('sdc:schoolCollectionStudentURL')}/${sdcSchoolCollectionStudentID}`); + if (req.body.updateDate !== currentStudent.updateDate) { + throw new Error(HttpStatus.CONFLICT.toString()); + } + //TODO fix student lock + //studentLock = await lockSdcStudentBeingProcessedInRedis(sdcSchoolCollectionStudentID); + } + + const payload = req.body; + console.log(payload); + payload.createDate = null; + payload.createUser = null; + payload.updateDate = null; + // TODO fix update user + //payload.updateUser = getCreateOrUpdateUserValue(req); + + if (payload?.enrolledProgramCodes) { + payload.enrolledProgramCodes = payload.enrolledProgramCodes.join(''); + } + + if (payload?.numberOfCourses) { + payload.numberOfCourses = stripNumberFormattingNumberOfCourses(payload.numberOfCourses); + } + + payload.sdcSchoolCollectionStudentValidationIssues = null; + payload.sdcSchoolCollectionStudentEnrolledPrograms = null; + + const data = await postData(config.get('sdc:schoolCollectionStudentURL'), payload); + + if(studentLock) { + await unlockSdcStudentBeingProcessedInRedis(studentLock); + } + if (data?.enrolledProgramCodes) { + data.enrolledProgramCodes = data?.enrolledProgramCodes.match(/.{1,2}/g); + } + + if (data?.numberOfCourses) { + data.numberOfCourses = formatNumberOfCourses(data?.numberOfCourses); + } + return res.status(HttpStatus.OK).json(data); + } catch (e) { + if (e.message === '409' || e.status === '409' || e.status === 409) { + return res.status(HttpStatus.CONFLICT).json({ + status: HttpStatus.CONFLICT, + message: 'The student you are attempting to update is already being saved by another user. Please refresh your screen and try again.' + }); + } + return handleExceptionResponse(e, res); + } + +} + module.exports = { getSnapshotFundingDataForSchool, getAllCollectionsForSchool, @@ -380,5 +438,6 @@ module.exports = { getSDCSchoolCollectionStudentDetail, getInDistrictDuplicates, updateStudentPEN, - checkDuplicatesInCollection + checkDuplicatesInCollection, + updateAndValidateSdcSchoolCollectionStudent }; diff --git a/backend/src/components/utils.js b/backend/src/components/utils.js index 57eee339a..872231d54 100644 --- a/backend/src/components/utils.js +++ b/backend/src/components/utils.js @@ -65,6 +65,63 @@ function addTokenToHeader(params, token) { return params; } +function getCreateOrUpdateUserValue(req){ + if(req.session.passport.user._json.idir_username){ + return req.session.passport.user._json.idir_username; + }else{ + return 'EDX/' + req.session.edxUserData.edxUserID; + } +} + +function stripNumberFormattingNumberOfCourses(value) { + if (!value) return '0000'; + return value.replace('.', ''); +} + +function formatNumberOfCourses(value) { + if (!value) return '00.00'; + + let formatted = ''; + switch (value.length) { + case 1: + formatted = `0${value}.00`; + break; + case 2: + formatted = `${value}.00`; + break; + case 3: + formatted = `0${value.slice(0, 1)}.${value.slice(1)}`; + break; + case 4: + formatted = `${value.slice(0, 2)}.${value.slice(2)}`; + break; + default: + formatted = '00.00'; + } + return formatted; +} + +function handleExceptionResponse(e, res) { + if (e.message === '404' || e.status === '404' || e.status === 404) { + return res.status(HttpStatus.NOT_FOUND).json(); + } else if(e.message === '403') { + return res.status(HttpStatus.FORBIDDEN).json({ + status: HttpStatus.FORBIDDEN, + message: 'You do not have permission to access this information' + }); + } else if(e.message === '401'){ + return res.status(HttpStatus.UNAUTHORIZED).json({ + status: HttpStatus.UNAUTHORIZED, + message: 'Token is not valid' + }); + } else { + return res.status(HttpStatus.INTERNAL_SERVER_ERROR).json({ + message: 'INTERNAL SERVER ERROR', + code: HttpStatus.INTERNAL_SERVER_ERROR + }); + } +} + async function deleteData(url) { try { const delConfig = { @@ -532,6 +589,10 @@ const utils = { forwardGet, isPdf, isImage, + getCreateOrUpdateUserValue, + stripNumberFormattingNumberOfCourses, + formatNumberOfCourses, + handleExceptionResponse }; module.exports = utils; diff --git a/backend/src/routes/sdc.js b/backend/src/routes/sdc.js index d58ea61f9..234a0bdbd 100644 --- a/backend/src/routes/sdc.js +++ b/backend/src/routes/sdc.js @@ -6,7 +6,7 @@ const perm = require('../util/Permission'); const extendSession = utils.extendSession(); const { getSnapshotFundingDataForSchool, getAllCollectionsForSchool, getActiveCollection, getSdcDistrictCollectionMonitoringByCollectionId, getIndySdcSchoolCollectionMonitoringByCollectionId, unsubmitSdcDistrictCollection, unsubmitSdcSchoolCollection, getInDistrictDuplicates, - getSDCSchoolCollectionStudentPaginated, getSDCSchoolCollectionStudentDetail, updateStudentPEN, checkDuplicatesInCollection + getSDCSchoolCollectionStudentPaginated, getSDCSchoolCollectionStudentDetail, updateStudentPEN, checkDuplicatesInCollection, updateAndValidateSdcSchoolCollectionStudent } = require('../components/sdc/sdc'); const {getCachedSDCData} = require('../components/sdc/sdc-cache'); const constants = require('../util/constants'); @@ -43,6 +43,9 @@ router.get('/collection/:collectionID/duplicates', passport.authenticate('jwt', router.get('/sdcSchoolCollectionStudent/:sdcSchoolCollectionStudentID', passport.authenticate('jwt', {session: false}, undefined), permUtils.checkUserHasPermission(PERMISSION.STUDENT_DATA_COLLECTION), extendSession, getSDCSchoolCollectionStudentDetail); router.post('/sdcSchoolCollectionStudent/:sdcSchoolCollectionStudentID/update-pen/:penCode', passport.authenticate('jwt', {session: false}, undefined), permUtils.checkUserHasPermission(PERMISSION.STUDENT_DATA_COLLECTION), extendSession, updateStudentPEN); +//update student +router.post('/sdcSchoolCollectionStudent', passport.authenticate('jwt', {session: false}, undefined), permUtils.checkUserHasPermission(PERMISSION.STUDENT_DATA_COLLECTION), extendSession, updateAndValidateSdcSchoolCollectionStudent); + router.post('/sdcDistrictCollection/:sdcDistrictCollectionID/unsubmit', passport.authenticate('jwt', {session: false}, undefined), permUtils.checkUserHasPermission(PERMISSION.STUDENT_DATA_COLLECTION), extendSession, unsubmitSdcDistrictCollection); router.post('/sdcSchoolCollection/:sdcSchoolCollectionID/unsubmit', passport.authenticate('jwt', {session: false}, undefined), permUtils.checkUserHasPermission(PERMISSION.STUDENT_DATA_COLLECTION), extendSession, unsubmitSdcSchoolCollection); diff --git a/backend/src/util/redis/redis-utils.js b/backend/src/util/redis/redis-utils.js index bd4c73942..d8a9ca363 100644 --- a/backend/src/util/redis/redis-utils.js +++ b/backend/src/util/redis/redis-utils.js @@ -4,7 +4,9 @@ const log = require('../../components/logger'); const safeStringify = require('fast-safe-stringify'); const RedLock = require('redlock'); const {LocalDateTime} = require('@js-joda/core'); +const HttpStatus = require('http-status-codes'); let redLock; +let redLockNoRetry; /** * @@ -79,7 +81,21 @@ const redisUtil = { createSagaRecord, removeEventRecordFromRedis, getSagaEventsByRedisKey, - + async lockSdcStudentBeingProcessedInRedis(sdcSchoolStudentID) { + try { + return await this.getRedLockNoRetry().acquire(`locks:edx-sdc-student:${sdcSchoolStudentID}`, 6000); + } catch (e) { + log.info(`This pod could not acquire lock for locks:edx-sdc-student:${sdcSchoolStudentID}, check other pods. ${e}`); + throw new Error(HttpStatus.CONFLICT.toString()); + } + }, + async unlockSdcStudentBeingProcessedInRedis(lock) { + try { + await this.getRedLockNoRetry().unlock(lock); + } catch (e) { + log.info(`This pod could not release lock for: ${lock}, check other pods. ${e}`); + } + }, getRedLock() { if (redLock) { return redLock; // reusable red lock object. @@ -111,6 +127,36 @@ const redisUtil = { }); return redLock; }, + getRedLockNoRetry() { + if (redLockNoRetry) { + return redLockNoRetry; // reusable red lock object. + } else { + redLockNoRetry = new RedLock( + [Redis.getRedisClient()], + { + // the expected clock drift; for more details + // see http://redis.io/topics/distlock + driftFactor: 0.01, // time in ms + + // the max number of times Redlock will attempt + // to lock a resource before erroring + retryCount: 0, + + // the time in ms between attempts + retryDelay: 50, // time in ms + + // the max time in ms randomly added to retries + // to improve performance under high contention + // see https://www.awsarchitectureblog.com/2015/03/backoff.html + retryJitter: 25 // time in ms + } + ); + } + redLockNoRetry.on('clientError', function (err) { + log.error('A redis connection error has occurred in getRedLock of redis-util:', err); + }); + return redLockNoRetry; + }, /** * this is central so that it is in one common place accessed by different js , files, accidental update wont cause any damage to the app, as it will be referred from all the files. * @param pen diff --git a/frontend/src/components/common/CustomTable.vue b/frontend/src/components/common/CustomTable.vue index e8263e550..4a51b67ea 100644 --- a/frontend/src/components/common/CustomTable.vue +++ b/frontend/src/components/common/CustomTable.vue @@ -197,7 +197,7 @@ export default { default: false } }, - emits: ['reload', 'openStudentDetails', 'selections'], + emits: ['reload', 'openStudentDetails', 'selections', 'editSelectedRow'], data() { return { masterCheckbox: false, @@ -249,9 +249,9 @@ export default { }); }, methods: { - // rowclicked(props) { - // this.$emit('openStudentDetails', props); - // }, + rowclicked(props) { + this.$emit('editSelectedRow', props); + }, onClick(prop) { let selectedValue = prop.item.raw; if(this.isSelected(selectedValue)) { diff --git a/frontend/src/components/data-collection/AllStudents/EditStudent.vue b/frontend/src/components/data-collection/AllStudents/EditStudent.vue index 84cd89b7a..6c15ce30e 100644 --- a/frontend/src/components/data-collection/AllStudents/EditStudent.vue +++ b/frontend/src/components/data-collection/AllStudents/EditStudent.vue @@ -1,36 +1,36 @@