diff --git a/backend/config/index.js b/backend/config/index.js index aeb8ec9f..12e35928 100644 --- a/backend/config/index.js +++ b/backend/config/index.js @@ -6,7 +6,7 @@ if (isProduction) const config = { graphdb: { - addr: isProduction ? 'http://localhost:7200' : `http://${isDocker ? 'host.docker.internal' : 'localhost'}:7200`, + addr: isProduction ? 'http://127.0.0.1:7200' : `http://${isDocker ? 'host.docker.internal' : '127.0.0.1'}:7200`, repositoryName: process.env.GRAPHDB_REPO || (process.env.test ? "snmiTest" : "snmi") }, mongodb: { diff --git a/backend/loaders/express.js b/backend/loaders/express.js index bd5b45de..e902f811 100644 --- a/backend/loaders/express.js +++ b/backend/loaders/express.js @@ -88,6 +88,9 @@ app.use('/public', partnerNetworkPublicRoute); await initOptions('Appointment Statuses', ["Requested", "Confirmed", "Cancelled", "Fulfilled", "Client No Show", "Postponed"], 'AppointmentStatus', 'appointmentStatus'); + await initOptions('Service and Program Registration Statuses', + ["Registered", "Not Registered", "Waitlisted"], + 'RegistrationStatus', 'registrationStatus'); })() diff --git a/backend/models/program/programOccurrence.js b/backend/models/program/programOccurrence.js index 855bbd94..75238903 100644 --- a/backend/models/program/programOccurrence.js +++ b/backend/models/program/programOccurrence.js @@ -15,6 +15,8 @@ const GDBProgramOccurrenceModel = createGraphDBModel({ needSatisfiers: {type: [GDBNeedSatisfierModel], internalKey: ':hasNeedSatisfier'}, // needSatisfierOccurrence: {type: [GDBNeedSatisfierOccurrenceModel], internalKey: ':hasNeedSatisfierOccurrence', onDelete: DeleteType.CASCADE}, description: {type: String, internalKey: 'cids:hasDescription'}, + capacity: {type: Number, internalKey: ':hasCapacity'}, + occupancy: {type: Number, internalKey: ':hasOccupancy'}, characteristicOccurrences: {type: [GDBCOModel], internalKey: ':hasCharacteristicOccurrence'} }, { rdfTypes: [':ProgramOccurrence'], name: 'programOccurrence' diff --git a/backend/models/program/programWaitlist.js b/backend/models/program/programWaitlist.js new file mode 100644 index 00000000..42f108a1 --- /dev/null +++ b/backend/models/program/programWaitlist.js @@ -0,0 +1,18 @@ +const {createGraphDBModel, DeleteType, Types} = require("graphdb-utils"); + +//These might need tweaking, need to figure out what things we need for this model +const {GDBProgramOccurrenceModel} = require("./programOccurrence"); +const {GDBProgramWaitlistEntryModel} = require("./programWaitlistEntry"); + +const GDBProgramWaitlistModel = createGraphDBModel({ + waitlist: {type: [GDBProgramWaitlistEntryModel], internalKey: ':hasWaitlist'}, + programOccurrence: {type: GDBProgramOccurrenceModel, internalKey: ':hasProgramOccurrence'}, +}, +{ + rdfTypes: [':ProgramWaitlist'], name: 'programWaitlist' +} + +); +module.exports = { + GDBProgramWaitlistModel +} \ No newline at end of file diff --git a/backend/models/program/programWaitlistEntry.js b/backend/models/program/programWaitlistEntry.js new file mode 100644 index 00000000..a5ff21a8 --- /dev/null +++ b/backend/models/program/programWaitlistEntry.js @@ -0,0 +1,16 @@ +const {createGraphDBModel, DeleteType, Types} = require("graphdb-utils"); +const {GDBProgramRegistrationModel} = require("../programRegistration"); + +const GDBProgramWaitlistEntryModel = createGraphDBModel({ + programRegistration: {type: GDBProgramRegistrationModel, internalKey: ':hasProgramRegistration'}, + priority: {type: Number, internalKey: ':hasPriority'}, + date: {type: Date, internalKey: ':hasDate'}, +}, +{ + rdfTypes: [':ProgramWaitlistEntry'], name: 'programWaitlistEntry' +} + +); +module.exports = { + GDBProgramWaitlistEntryModel +} \ No newline at end of file diff --git a/backend/models/programRegistration.js b/backend/models/programRegistration.js index 40eec512..91c3bc04 100644 --- a/backend/models/programRegistration.js +++ b/backend/models/programRegistration.js @@ -1,4 +1,4 @@ -const {createGraphDBModel, DeleteType, Types, getGraphDBModel} = require("graphdb-utils"); +const {createGraphDBModel, DeleteType, Types} = require("graphdb-utils"); const {GDBProgramOccurrenceModel} = require("./program/programOccurrence"); const {GDBClientModel} = require("./ClientFunctionalities/client"); const {GDBReferralModel} = require("./referral"); @@ -13,6 +13,7 @@ const GDBProgramRegistrationModel = createGraphDBModel({ client: {type: GDBClientModel, internalKey: ':hasClient'}, referral: {type: GDBReferralModel, internalKey: ':hasReferral'}, appointment: {type: GDBAppointmentModel, internalKey: ':hasAppointment'}, + status: {type: String, internalKey: ':hasRegistrationStatus'}, characteristicOccurrences: {type: [GDBCOModel], internalKey: ':hasCharacteristicOccurrence'}, address: {type: GDBAddressModel, internalKey: 'ic:hasAddress', onDelete: DeleteType.CASCADE}, }, { diff --git a/backend/models/service/serviceOccurrence.js b/backend/models/service/serviceOccurrence.js index 978e1ad0..296e995e 100644 --- a/backend/models/service/serviceOccurrence.js +++ b/backend/models/service/serviceOccurrence.js @@ -15,6 +15,8 @@ const GDBServiceOccurrenceModel = createGraphDBModel({ needSatisfiers: {type: [GDBNeedSatisfierModel], internalKey: ':hasNeedSatisfier'}, // needSatisfierOccurrence: {type: [GDBNeedSatisfierOccurrenceModel], internalKey: ':hasNeedSatisfierOccurrence', onDelete: DeleteType.CASCADE}, description: {type: String, internalKey: 'cids:hasDescription'}, + capacity: {type: Number, internalKey: ':hasCapacity'}, + occupancy: {type: Number, internalKey: ':hasOccupancy'}, characteristicOccurrences: {type: [GDBCOModel], internalKey: ':hasCharacteristicOccurrence'} }, { rdfTypes: [':ServiceOccurrence'], name: 'serviceOccurrence' diff --git a/backend/models/service/serviceWaitlist.js b/backend/models/service/serviceWaitlist.js new file mode 100644 index 00000000..479adc50 --- /dev/null +++ b/backend/models/service/serviceWaitlist.js @@ -0,0 +1,18 @@ +const {createGraphDBModel, DeleteType, Types} = require("graphdb-utils"); + +//These might need tweaking, need to figure out what things we need for this model +const {GDBServiceOccurrenceModel} = require("./serviceOccurrence"); +const {GDBServiceWaitlistEntryModel} = require("./serviceWaitlistEntry"); + +const GDBServiceWaitlistModel = createGraphDBModel({ + waitlist: {type: [GDBServiceWaitlistEntryModel], internalKey: ':hasWaitlist'}, + serviceOccurrence: {type: GDBServiceOccurrenceModel, internalKey: ':hasServiceOccurrence'}, +}, +{ + rdfTypes: [':ServiceWaitlist'], name: 'serviceWaitlist' +} + +); +module.exports = { + GDBServiceWaitlistModel +} \ No newline at end of file diff --git a/backend/models/service/serviceWaitlistEntry.js b/backend/models/service/serviceWaitlistEntry.js new file mode 100644 index 00000000..f969146a --- /dev/null +++ b/backend/models/service/serviceWaitlistEntry.js @@ -0,0 +1,16 @@ +const {createGraphDBModel, DeleteType, Types} = require("graphdb-utils"); +const { GDBServiceRegistrationModel } = require("../serviceRegistration"); + +const GDBServiceWaitlistEntryModel = createGraphDBModel({ + serviceRegistration: {type: GDBServiceRegistrationModel, internalKey: ':hasServiceRegistration'}, + priority: {type: Number, internalKey: ':hasPriority'}, + date: {type: Date, internalKey: ':hasDate'}, +}, +{ + rdfTypes: [':ServiceWaitlistEntry'], name: 'serviceWaitlistEntry' +} +); + +module.exports = { + GDBServiceWaitlistEntryModel +} \ No newline at end of file diff --git a/backend/models/serviceRegistration.js b/backend/models/serviceRegistration.js index cbe35ff9..0a34cc40 100644 --- a/backend/models/serviceRegistration.js +++ b/backend/models/serviceRegistration.js @@ -13,6 +13,7 @@ const GDBServiceRegistrationModel = createGraphDBModel({ client: {type: GDBClientModel, internalKey: ':hasClient'}, referral: {type: GDBReferralModel, internalKey: ':hasReferral'}, appointment: {type: GDBAppointmentModel, internalKey: ':hasAppointment'}, + status: {type: String, internalKey: ':hasRegistrationStatus'}, characteristicOccurrences: {type: [GDBCOModel], internalKey: ':hasCharacteristicOccurrence'}, address: {type: GDBAddressModel, internalKey: 'ic:hasAddress', onDelete: DeleteType.CASCADE}, }, { diff --git a/backend/services/characteristics/predefined/general.js b/backend/services/characteristics/predefined/general.js index a2d6ed15..03db6353 100644 --- a/backend/services/characteristics/predefined/general.js +++ b/backend/services/characteristics/predefined/general.js @@ -82,4 +82,35 @@ module.exports = [{ fieldType: FieldTypes.NumberField, }, }, + { + name: 'Capacity', + description: 'The capacity of a program or service occurrence.', + predefinedProperty: 'http://snmi#hasCapacity', + implementation: { + label: 'Capacity', + valueDataType: 'xsd:number', + fieldType: FieldTypes.NumberField, + }, + }, + { + name: 'Occupancy', + description: 'The current occupancy of a program or service occurrence.', + predefinedProperty: 'http://snmi#hasOccupancy', + implementation: { + label: 'Occupancy', + valueDataType: 'xsd:number', + fieldType: FieldTypes.NumberField, + }, + }, + { + name: 'Registration Status', + description: 'The status of a program or service registration.', + predefinedProperty: 'http://snmi#hasRegistrationStatus', + implementation: { + label: 'Registration Status', + valueDataType: 'xsd:string', + fieldType: FieldTypes.SingleSelectField, + optionsFromClass: ':RegistrationStatus' + } + }, ] diff --git a/backend/services/characteristics/predefined/internalTypes.js b/backend/services/characteristics/predefined/internalTypes.js index 4db0e5ac..afcc54f2 100644 --- a/backend/services/characteristics/predefined/internalTypes.js +++ b/backend/services/characteristics/predefined/internalTypes.js @@ -207,6 +207,80 @@ module.exports = [ } }, + // below are for service waitlist + { + name: 'waitlistForServiceWaitlist', + predefinedProperty: 'http://snmi#hasWaitlist', + formType: 'serviceWaitlist', + implementation: { + label: 'Waitlist', + valueDataType: 'owl:NamedIndividual', + fieldType: FieldTypes.MultiSelectField, + optionsFromClass: 'http://snmi#ServiceWaitlistEntry' + } + }, + { + name: 'serviceOccurrenceForServiceWaitlist', + predefinedProperty: 'http://snmi#hasServiceOccurrence', + formType: 'serviceWaitlist', + implementation: { + label: 'Service Occurrence', + valueDataType: 'owl:NamedIndividual', + fieldType: FieldTypes.SingleSelectField, + optionsFromClass: 'http://snmi#ServiceOccurrence' + } + }, + + // below are for service waitlist entry + { + name: 'serviceRegistrationForServiceWaitlistEntry', + predefinedProperty: 'http://snmi#hasServiceRegistration', + formType: 'serviceWaitlistEntry', + implementation: { + label: 'Service Registration', + valueDataType: 'owl:NamedIndividual', + fieldType: FieldTypes.SingleSelectField, + optionsFromClass: 'http://snmi#ServiceRegistration' + } + }, + + // below are for program waitlist + { + name: 'waitlistForProgramWaitlist', + predefinedProperty: 'http://snmi#hasWaitlist', + formType: 'programWaitlist', + implementation: { + label: 'Waitlist', + valueDataType: 'owl:NamedIndividual', + fieldType: FieldTypes.MultiSelectField, + optionsFromClass: 'http://snmi#ProgramWaitlistEntry' + } + }, + { + name: 'programOccurrenceForProgramWaitlist', + predefinedProperty: 'http://snmi#hasProgramOccurrence', + formType: 'programWaitlist', + implementation: { + label: 'Program Occurrence', + valueDataType: 'owl:NamedIndividual', + fieldType: FieldTypes.SingleSelectField, + optionsFromClass: 'http://snmi#ProgramOccurrence' + } + }, + + // below are for program waitlist entry + { + name: 'programRegistrationForProgramWaitlistEntry', + predefinedProperty: 'http://snmi#hasProgramRegistration', + formType: 'programWaitlistEntry', + implementation: { + label: 'Program Registration', + valueDataType: 'owl:NamedIndividual', + fieldType: FieldTypes.SingleSelectField, + optionsFromClass: 'http://snmi#ProgramRegistration' + } + }, + // below are for program Provision { name: 'needOccurrenceForProgramProvision', diff --git a/backend/services/genericData/checkers.js b/backend/services/genericData/checkers.js index fbb1867e..42066310 100644 --- a/backend/services/genericData/checkers.js +++ b/backend/services/genericData/checkers.js @@ -1,8 +1,34 @@ +const {GDBCharacteristicModel} = require("../../models"); const {Server400Error} = require("../../utils"); +const {PredefinedCharacteristics} = require("../characteristics"); function noQuestion(characteristics, questions) { if (Object.keys(questions).length > 0) throw new Server400Error('Service should not contain question.'); } -module.exports = {noQuestion} \ No newline at end of file +function checkCapacityNonNegative(characteristics, questions, fields) { + const capacityId = Object.keys(characteristics).find(id => characteristics[id].name === 'Capacity'); + if (!!capacityId) { + if (fields['characteristic_' + capacityId] < 0) { + throw new Server400Error('Capacity must be zero or greater.'); + } + } +} + +// Prevent overwriting the read-only occupancy characteristic +function unsetOccupancy(characteristics, questions, fields) { + const occupancyId = PredefinedCharacteristics['Occupancy']._id; + delete characteristics[occupancyId]; + delete fields[`characteristic_${occupancyId}`]; +} + +async function setOccupancy(characteristics, questions, fields) { + const occupancyId = PredefinedCharacteristics['Occupancy']._id; + const occupancyCharacteristic = await GDBCharacteristicModel.findOne({_id: occupancyId}, + {populates: ['implementation']}); + characteristics[occupancyId] = occupancyCharacteristic; + fields[`characteristic_${occupancyId}`] = '0'; +} + +module.exports = {noQuestion, checkCapacityNonNegative, unsetOccupancy, setOccupancy} \ No newline at end of file diff --git a/backend/services/genericData/index.js b/backend/services/genericData/index.js index 11f2ca7a..6a99d644 100644 --- a/backend/services/genericData/index.js +++ b/backend/services/genericData/index.js @@ -21,15 +21,23 @@ const {GDBAppointmentModel} = require("../../models/appointment"); const {GDBPersonModel} = require("../../models/person"); const {GDBServiceOccurrenceModel} = require("../../models/service/serviceOccurrence"); const {GDBProgramOccurrenceModel} = require("../../models/program/programOccurrence"); +const {GDBServiceWaitlistModel} = require("../../models/service/serviceWaitlist"); +const {GDBServiceWaitlistEntryModel} = require("../../models/service/serviceWaitlistEntry"); +const {GDBProgramWaitlistModel} = require("../../models/program/programWaitlist"); +const {GDBProgramWaitlistEntryModel} = require("../../models/program/programWaitlistEntry"); const {GDBInternalTypeModel} = require("../../models/internalType"); -const {noQuestion} = require('./checkers') +const {noQuestion, checkCapacityNonNegative, setOccupancy, unsetOccupancy} = require('./checkers') const { serviceOccurrenceInternalTypeCreateTreater, serviceOccurrenceInternalTypeFetchTreater, - serviceOccurrenceInternalTypeUpdateTreater + serviceOccurrenceInternalTypeUpdateTreater, + checkCapacityOnServiceOccurrenceUpdate, + afterCreateServiceOccurrence } = require("./serviceOccurrenceInternalTypeTreater"); const { programOccurrenceInternalTypeCreateTreater, programOccurrenceInternalTypeFetchTreater, - programOccurrenceInternalTypeUpdateTreater + programOccurrenceInternalTypeUpdateTreater, + afterCreateProgramOccurrence, + checkCapacityOnProgramOccurrenceUpdate } = require("./programOccurrenceInternalTypeTreater"); const { fetchCharacteristicQuestionsInternalTypesBasedOnForms, @@ -62,11 +70,15 @@ const {GDBServiceRegistrationModel} = require("../../models/serviceRegistration" const {GDBProgramRegistrationModel} = require("../../models/programRegistration"); const { serviceRegistrationInternalTypeCreateTreater, serviceRegistrationInternalTypeFetchTreater, - serviceRegistrationInternalTypeUpdateTreater + serviceRegistrationInternalTypeUpdateTreater, + updateOccurrenceOccupancyOnServiceRegistrationCreate, updateOccurrenceOccupancyOnServiceRegistrationUpdate, + updateOccurrenceOccupancyOnServiceRegistrationDelete, checkServiceOccurrenceUnchanged, afterCreateServiceRegistration, } = require("./serviceRegistration"); const { programRegistrationInternalTypeCreateTreater, programRegistrationInternalTypeFetchTreater, - programRegistrationInternalTypeUpdateTreater + programRegistrationInternalTypeUpdateTreater, + updateOccurrenceOccupancyOnProgramRegistrationCreate, updateOccurrenceOccupancyOnProgramRegistrationUpdate, + updateOccurrenceOccupancyOnProgramRegistrationDelete, checkProgramOccurrenceUnchanged, afterCreateProgramRegistration, } = require("./programRegistration"); const { appointmentInternalTypeCreateTreater, @@ -123,6 +135,11 @@ const { afterDeleteVolunteer } = require("./volunteerInternalTypeTreater"); const {GDBEligibilityModel} = require("../../models/eligibility"); +const { serviceWaitlistInternalTypeCreateTreater, serviceWaitlistInternalTypeFetchTreater, serviceWaitlistInternalTypeUpdateTreater } = require("./serviceWaitlist"); +const { serviceWaitlistEntryInternalTypeCreateTreater, serviceWaitlistEntryInternalTypeFetchTreater, serviceWaitlistEntryInternalTypeUpdateTreater } = require("./serviceWaitlistEntry"); +const { programWaitlistInternalTypeCreateTreater, programWaitlistInternalTypeFetchTreater, programWaitlistInternalTypeUpdateTreater } = require("./programWaitlist"); +const { programWaitlistEntryInternalTypeCreateTreater, programWaitlistEntryInternalTypeFetchTreater, programWaitlistEntryInternalTypeUpdateTreater } = require("./programWaitlistEntry"); + const genericType2Model = { 'client': GDBClientModel, 'organization': GDBOrganizationModel, @@ -131,6 +148,10 @@ const genericType2Model = { 'program': GDBProgramModel, 'appointment': GDBAppointmentModel, 'serviceOccurrence': GDBServiceOccurrenceModel, + 'serviceWaitlist': GDBServiceWaitlistModel, + 'serviceWaitlistEntry' : GDBServiceWaitlistEntryModel, + 'programWaitlist': GDBProgramWaitlistModel, + 'programWaitlistEntry' : GDBProgramWaitlistEntryModel, 'programOccurrence': GDBProgramOccurrenceModel, 'referral': GDBReferralModel, 'serviceRegistration': GDBServiceRegistrationModel, @@ -158,19 +179,46 @@ const genericType2Populates = { 'program': ['serviceProvider.organization.address', 'serviceProvider.volunteer.address', 'serviceProvider.organization', 'serviceProvider.volunteer', 'manager'], 'serviceOccurrence': ['address', 'occurrenceOf'], 'programOccurrence': ['address', 'occurrenceOf'], + 'serviceWaitlist': ['waitlist', 'serviceOccurrence'], + 'serviceWaitlistEntry': ['serviceRegistration', 'priority', 'date'], + 'programWaitlist': ['waitlist', 'programOccurrence'], + 'programWaitlistEntry': ['programRegistration', 'priority', 'date'], 'client': ['address', 'needs'], 'appointment': ['address', 'referral'], 'person': ['address'], 'volunteer': ['partnerOrganizations', 'organization', 'address'], }; -const genericType2Checker = { - 'service': noQuestion, - 'serviceOccurrence': noQuestion, - 'programOccurrence': noQuestion, - 'program': noQuestion +const genericType2BeforeCreateChecker = { + 'service': [noQuestion], + 'serviceOccurrence': [noQuestion, checkCapacityNonNegative, setOccupancy], + 'serviceRegistration': [updateOccurrenceOccupancyOnServiceRegistrationCreate], + 'program': [noQuestion], + 'programOccurrence': [noQuestion, checkCapacityNonNegative, setOccupancy], + 'programRegistration': [updateOccurrenceOccupancyOnProgramRegistrationCreate], + 'serviceWaitlist': [noQuestion], + 'serviceWaitlistEntry': [noQuestion], + 'programWaitlist': [noQuestion], + 'programWaitlistEntry': [noQuestion], +}; + +const genericType2BeforeUpdateChecker = { + 'service': [noQuestion], + 'serviceOccurrence': [noQuestion, checkCapacityNonNegative, checkCapacityOnServiceOccurrenceUpdate, unsetOccupancy], + 'serviceRegistration': [checkServiceOccurrenceUnchanged, updateOccurrenceOccupancyOnServiceRegistrationUpdate], + 'program': [noQuestion], + 'programOccurrence': [noQuestion, checkCapacityNonNegative, checkCapacityOnProgramOccurrenceUpdate, unsetOccupancy], + 'programRegistration': [checkProgramOccurrenceUnchanged, updateOccurrenceOccupancyOnProgramRegistrationUpdate], + 'serviceWaitlist': [noQuestion], + 'serviceWaitlistEntry' : [noQuestion], + 'programWaitlist': [noQuestion], + 'programWaitlistEntry' : [noQuestion], }; +const genericType2BeforeDeleteChecker = { + 'serviceRegistration': [updateOccurrenceOccupancyOnServiceRegistrationDelete], + 'programRegistration': [updateOccurrenceOccupancyOnProgramRegistrationDelete], +} const genericType2BeforeCreateTreater = { 'clientAssessment': beforeCreateClientAssessment @@ -203,7 +251,11 @@ const genericType2InternalTypeCreateTreater = { 'outcomeOccurrence': outcomeOccurrenceInternalTypeCreateTreater, 'clientAssessment': clientAssessmentInternalTypeCreateTreater, 'person': personInternalTypeCreateTreater, - 'volunteer': volunteerInternalTypeCreateTreater + 'volunteer': volunteerInternalTypeCreateTreater, + 'serviceWaitlist': serviceWaitlistInternalTypeCreateTreater, + 'serviceWaitlistEntry': serviceWaitlistEntryInternalTypeCreateTreater, + 'programWaitlist': programWaitlistInternalTypeCreateTreater, + 'programWaitlistEntry': programWaitlistEntryInternalTypeCreateTreater }; const genericType2InternalTypeFetchTreater = { @@ -222,7 +274,11 @@ const genericType2InternalTypeFetchTreater = { 'outcomeOccurrence': outcomeOccurrenceInternalTypeFetchTreater, 'clientAssessment': clientAssessmentInternalTypeFetchTreater, 'person': personInternalTypeFetchTreater, - 'volunteer': volunteerInternalTypeFetchTreater + 'volunteer': volunteerInternalTypeFetchTreater, + 'serviceWaitlist': serviceWaitlistInternalTypeFetchTreater, + 'serviceWaitlistEntry': serviceWaitlistEntryInternalTypeFetchTreater, + 'programWaitlist': programWaitlistInternalTypeFetchTreater, + 'programWaitlistEntry': programWaitlistEntryInternalTypeFetchTreater }; const genericType2InternalTypeUpdateTreater = { @@ -241,13 +297,21 @@ const genericType2InternalTypeUpdateTreater = { 'outcomeOccurrence': outcomeOccurrenceInternalTypeUpdateTreater, 'clientAssessment': clientAssessmentInternalTypeUpdateTreater, 'person': personInternalTypeUpdateTreater, - 'volunteer': volunteerInternalTypeUpdateTreater + 'volunteer': volunteerInternalTypeUpdateTreater, + 'serviceWaitlist': serviceWaitlistInternalTypeUpdateTreater, + 'serviceWaitlistEntry' : serviceWaitlistEntryInternalTypeUpdateTreater, + 'programWaitlist': programWaitlistInternalTypeUpdateTreater, + 'programWaitlistEntry' : programWaitlistEntryInternalTypeUpdateTreater }; const genericType2AfterCreateTreater = { 'program': afterCreateProgram, 'service': afterCreateService, - 'volunteer': afterCreateVolunteer + 'volunteer': afterCreateVolunteer, + 'serviceRegistration': afterCreateServiceRegistration, + 'programRegistration': afterCreateProgramRegistration, + 'serviceOccurrence': afterCreateServiceOccurrence, + 'programOccurrence': afterCreateProgramOccurrence, } const genericType2AfterUpdateTreater = { @@ -416,8 +480,9 @@ const createSingleGenericHelper = async (data, genericType) => { // extract questions and characteristics based on fields from the database await fetchCharacteristicQuestionsInternalTypesBasedOnForms(characteristics, questions, internalTypes, data.fields); - if (genericType2Checker[genericType]) - genericType2Checker[genericType](characteristics, questions); + if (genericType2BeforeCreateChecker[genericType]) + for (const checker of genericType2BeforeCreateChecker[genericType]) + await checker(characteristics, questions, data.fields); const instanceData = {characteristicOccurrences: [], questionOccurrences: []}; // iterating over all fields and create occurrences and store them into instanceData @@ -493,7 +558,7 @@ const createSingleGeneric = async (req, res, next) => { await newGeneric.save(); if (genericType2AfterCreateTreater[genericType]) - await genericType2AfterCreateTreater[genericType](data, req); + await genericType2AfterCreateTreater[genericType](data, req, newGeneric); return res.status(202).json({success: true, message: `Successfully created a/an ${genericType}`, createdId: newGeneric._id}); } @@ -535,6 +600,10 @@ async function updateSingleGenericHelper(genericId, data, genericType) { const internalTypes = {}; await fetchCharacteristicQuestionsInternalTypesBasedOnForms(characteristics, questions, internalTypes, data.fields); + if (genericType2BeforeUpdateChecker[genericType]) + for (const checker of genericType2BeforeUpdateChecker[genericType]) + await checker(characteristics, questions, data.fields, generic); + // check should we update or create a characteristicOccurrence or questionOccurrence // in other words, is there a characteristicOccurrence/questionOccurrence belong to this user, // and related to the characteristic/question @@ -593,6 +662,11 @@ async function updateSingleGenericHelper(genericId, data, genericType) { await specialField2Model[fieldType]?.findByIdAndDelete(id); } await GDBCOModel.findByIdAndDelete(existedCO._id); // remove the occurrence + + // Remove the CO + generic.characteristicOccurrences.splice(generic.characteristicOccurrences.indexOf(existedCO), 1) + generic.markModified('characteristicOccurrences'); + // also have to remove from usage if necessary await deleteIdFromUsageAfterChecking('characteristic', genericType, existedCO.occurrenceOf._id); } @@ -687,6 +761,10 @@ async function deleteSingleGenericHelper(genericType, id) { if (!generic) throw new Server400Error('Invalid genericType or id'); + if (genericType2BeforeDeleteChecker[genericType]) + for (const checker of genericType2BeforeDeleteChecker[genericType]) + await checker(generic); + if (genericType2BeforeDeleteTreater[genericType]) genericType2BeforeDeleteTreater[genericType](generic); diff --git a/backend/services/genericData/programOccurrenceInternalTypeTreater.js b/backend/services/genericData/programOccurrenceInternalTypeTreater.js index 9295699b..034a47f2 100644 --- a/backend/services/genericData/programOccurrenceInternalTypeTreater.js +++ b/backend/services/genericData/programOccurrenceInternalTypeTreater.js @@ -3,6 +3,10 @@ const {GDBNeedSatisfierOccurrenceModel} = require("../../models/needSatisfierOcc const {getPredefinedProperty, getInternalTypeValues} = require('./helperFunctions') const {GDBInternalTypeModel} = require("../../models/internalType"); const {SPARQL} = require('graphdb-utils'); +const {GDBProgramWaitlistModel} = require("../../models/program/programWaitlist"); +const {getIndividualsInClass} = require("../dynamicForm"); +const {PredefinedCharacteristics} = require("../characteristics"); +const {popFromWaitlist} = require("../programWaitlist/programWaitlist"); const programOccurrenceInternalTypeCreateTreater = async (internalType, instanceData, value) => { const property = getPredefinedProperty('programOccurrence', internalType); @@ -21,4 +25,51 @@ const programOccurrenceInternalTypeUpdateTreater = async (internalType, value, r await programOccurrenceInternalTypeCreateTreater(internalType, result, value); } -module.exports = {programOccurrenceInternalTypeCreateTreater, programOccurrenceInternalTypeFetchTreater, programOccurrenceInternalTypeUpdateTreater, } +const checkCapacityOnProgramOccurrenceUpdate = async function (characteristics, questions, fields, oldGeneric) { + const capacityId = Object.keys(characteristics).find(id => characteristics[id].name === 'Capacity'); + if (!capacityId) + return; + const capacity = fields['characteristic_' + capacityId]; + if (capacity < oldGeneric.occupancy) { + throw new Error('The new capacity of this program occurrence is less than its current occupancy. Please unregister ' + + 'program registrations until the occupancy is below the desired capacity, and then try editing this program ' + + 'occurrence again.'); + } else { + const statuses = await getIndividualsInClass(':RegistrationStatus'); + const registeredStatus = Object.keys(statuses).find(status => statuses[status] === 'Registered'); + const statusC = PredefinedCharacteristics['Registration Status']; + const occupancyC = PredefinedCharacteristics['Occupancy']; + const occupancyCO = oldGeneric.characteristicOccurrences.find(co => co.occurrenceOf === occupancyC._uri); + while (capacity > oldGeneric.occupancy) { + const registration = await popFromWaitlist(oldGeneric._id); + if (!!registration) { + // Make the registration's status be 'Registered' and save it + const statusCO = registration.characteristicOccurrences.find(co => co.occurrenceOf === statusC._uri); + registration.status = registeredStatus; + statusCO.dataStringValue = registeredStatus; + await registration.save(); + + oldGeneric.occupancy += 1; + occupancyCO.dataNumberValue += 1; + } else { + break; + } + } + await oldGeneric.save(); + } +} + +const afterCreateProgramOccurrence = async function (data, req, newGeneric) { + const occurrenceWaitlist = GDBProgramWaitlistModel({'programOccurrence': newGeneric, 'waitlist': []}); + // pass in the programOccurrence that was just created ("newGeneric") + // and an empty list for the queue. + await occurrenceWaitlist.save(); +} + +module.exports = { + programOccurrenceInternalTypeCreateTreater, + programOccurrenceInternalTypeFetchTreater, + programOccurrenceInternalTypeUpdateTreater, + checkCapacityOnProgramOccurrenceUpdate, + afterCreateProgramOccurrence, +} diff --git a/backend/services/genericData/programRegistration.js b/backend/services/genericData/programRegistration.js index 027371d0..69b7b58b 100644 --- a/backend/services/genericData/programRegistration.js +++ b/backend/services/genericData/programRegistration.js @@ -1,10 +1,10 @@ const {getPredefinedProperty, getInternalTypeValues} = require("./helperFunctions"); +const {GDBProgramOccurrenceModel} = require("../../models/program/programOccurrence"); +const {getIndividualsInClass} = require("../dynamicForm"); +const {PredefinedCharacteristics, PredefinedInternalTypes} = require("../characteristics"); const {GDBInternalTypeModel} = require("../../models/internalType"); const {SPARQL} = require("graphdb-utils"); -const {GDBClientModel} = require("../../models/ClientFunctionalities/client"); -const {GDBProgramModel} = require("../../models/program/program"); -const {GDBProgramOccurrenceModel} = require("../../models/program/programOccurrence"); -const {GDBNeedSatisfierOccurrenceModel} = require("../../models/needSatisfierOccurrence"); +const {pushToWaitlist, popFromWaitlist, removeFromWaitlist} = require("../programWaitlist/programWaitlist"); const FORMTYPE = 'programRegistration' @@ -17,15 +17,158 @@ const programRegistrationInternalTypeCreateTreater = async (internalType, instan }; const programRegistrationInternalTypeFetchTreater = async (data) => { - return getInternalTypeValues(['client', 'referral', 'appointment', 'programOccurrence', 'needOccurrence'], data, FORMTYPE) + const result = {}; + const schema = data.schema; + for (const property in data) { + if (property === 'client' || property === 'referral' || property === 'appointment' || + property === 'programOccurrence' || property === 'needOccurrence') { + const internalType = await GDBInternalTypeModel.findOne({ + predefinedProperty: schema[property].internalKey, + formType: FORMTYPE + }); + result['internalType_' + internalType._id] = SPARQL.ensureFullURI(data[property]); + } + } + return result; }; const programRegistrationInternalTypeUpdateTreater = async (internalType, value, result) => { await programRegistrationInternalTypeCreateTreater(internalType, result, value); } +const updateOccurrenceOccupancyOnProgramRegistrationCreate = async function (characteristics, questions, fields) { + const statuses = await getIndividualsInClass(':RegistrationStatus'); + const status = statuses[fields[`characteristic_${PredefinedCharacteristics['Registration Status']._id}`]]; + const occUri = fields['internalType_' + PredefinedInternalTypes['programOccurrenceForProgramRegistration']._id]; + if (!occUri) + return; + const occ = await GDBProgramOccurrenceModel.findOne({_uri: occUri}, {populates: ['characteristicOccurrences']}); + if (!occ) + return; + if (status === 'Registered') { + if (!occ.capacity || (occ.occupancy < occ.capacity)) { + occ.occupancy += 1; + const occupancyC = PredefinedCharacteristics['Occupancy']; + const occupancyCO = occ.characteristicOccurrences.find(co => co.occurrenceOf === occupancyC._uri); + occupancyCO.dataNumberValue += 1; + await occ.save(); + } else { + throw new Error('The requested program occurrence is now at capacity. Please refresh the form to review your current options.'); + } + } else if (status === 'Waitlisted') { + // Registration will be pushed to waitlist after creation + } else if (status !== 'Not Registered') { + throw new Error('Invalid registration status chosen.'); + } +} + +const afterCreateProgramRegistration = async function (data, req, newGeneric) { + // If the new program registration is waitlisted, push it to the appropriate waitlist + const statuses = await getIndividualsInClass(':RegistrationStatus'); + if (statuses[newGeneric.status] === 'Waitlisted') { + const date = new Date(); + await pushToWaitlist(newGeneric.programOccurrence.split('_')[1], newGeneric._id, date, date); + } +} + +const checkProgramOccurrenceUnchanged = async function (characteristics, questions, fields, oldGeneric) { + const newOccUri = fields['internalType_' + PredefinedInternalTypes['programOccurrenceForProgramRegistration']._id]; + const oldOccUri = oldGeneric.programOccurrence; + if (newOccUri !== oldOccUri) + throw new Error('A program registration\'s program occurrence cannot change.'); +} + +const updateOccurrenceOccupancyOnProgramRegistrationUpdate = async function (characteristics, questions, fields, oldGeneric) { + // checkProgramOccurrenceUnchanged must be called before this + const statuses = await getIndividualsInClass(':RegistrationStatus'); + const oldStatus = statuses[oldGeneric.status]; + const newStatus = statuses[fields[`characteristic_${PredefinedCharacteristics['Registration Status']._id}`]]; + const occUri = fields['internalType_' + PredefinedInternalTypes['programOccurrenceForProgramRegistration']._id]; + if (!occUri) + return; + const occ = await GDBProgramOccurrenceModel.findOne({_uri: occUri}, {populates: ['characteristicOccurrences']}); + if (!occ) + return; + const occupancyC = PredefinedCharacteristics['Occupancy']; + const occupancyCO = occ.characteristicOccurrences.find(co => co.occurrenceOf === occupancyC._uri); + if (oldStatus === 'Not Registered' && newStatus === 'Registered') { + if (!occ.capacity || (occ.occupancy < occ.capacity)) { + occ.occupancy += 1; + occupancyCO.dataNumberValue += 1; + await occ.save(); + } else { + throw new Error('The requested program occurrence is now at capacity. Please refresh the form to review your current options.'); + } + } else if (oldStatus === 'Registered' && newStatus === 'Not Registered') { + // Pop one registration from waitlist, if any, and change its status to registered + const registration = await popFromWaitlist(occ._id); + if (!!registration) { + const statuses = await getIndividualsInClass(':RegistrationStatus'); + const registeredStatus = Object.keys(statuses).find(status => statuses[status] === 'Registered'); + const statusC = PredefinedCharacteristics['Registration Status']; + const statusCO = registration.characteristicOccurrences.find(co => co.occurrenceOf === statusC._uri); + registration.status = registeredStatus; + statusCO.dataStringValue = registeredStatus; + await registration.save(); + // occ.occupancy doesn't change + } else { + occ.occupancy -= 1; + occupancyCO.dataNumberValue -= 1; + await occ.save(); + } + } else if (oldStatus === 'Waitlisted' && newStatus === 'Not Registered') { + // Remove this registration from waitlist + await removeFromWaitlist(oldGeneric.programOccurrence.split('_')[1], oldGeneric._id); + } else if (oldStatus === 'Not Registered' && newStatus === 'Waitlisted') { + // Push this registration to waitlist + const date = new Date(); + await pushToWaitlist(oldGeneric.programOccurrence.split('_')[1], oldGeneric._id, date, date); + } else if (oldStatus !== newStatus) { + throw new Error('Invalid registration status chosen.'); + } +} + +const updateOccurrenceOccupancyOnProgramRegistrationDelete = async function (oldGeneric) { + const statuses = await getIndividualsInClass(':RegistrationStatus'); + const status = statuses[oldGeneric.status]; + const occUri = oldGeneric.programOccurrence; + if (!occUri) + return; + const occ = await GDBProgramOccurrenceModel.findOne({_uri: occUri}, {populates: ['characteristicOccurrences']}); + if (!occ) + return; + if (status === 'Registered') { + // Pop one registration from waitlist, if any, and change its status to registered + const registration = await popFromWaitlist(occ._id); + if (!!registration) { + const statuses = await getIndividualsInClass(':RegistrationStatus'); + const registeredStatus = Object.keys(statuses).find(status => statuses[status] === 'Registered'); + const statusC = PredefinedCharacteristics['Registration Status']; + const statusCO = registration.characteristicOccurrences.find(co => co.occurrenceOf === statusC._uri); + registration.status = registeredStatus; + statusCO.dataStringValue = registeredStatus; + await registration.save(); + // occ.occupancy doesn't change + } else { + const occupancyC = PredefinedCharacteristics['Occupancy']; + const occupancyCO = occ.characteristicOccurrences.find(co => co.occurrenceOf === occupancyC._uri); + occ.occupancy -= 1; + occupancyCO.dataNumberValue -= 1; + await occ.save(); + } + } else if (status === 'Waitlisted') { + // Remove this registration from waitlist + await removeFromWaitlist(oldGeneric.programOccurrence.split('_')[1], oldGeneric._id); + } +} + module.exports = { programRegistrationInternalTypeCreateTreater, programRegistrationInternalTypeFetchTreater, - programRegistrationInternalTypeUpdateTreater + programRegistrationInternalTypeUpdateTreater, + checkProgramOccurrenceUnchanged, + updateOccurrenceOccupancyOnProgramRegistrationCreate, + afterCreateProgramRegistration, + updateOccurrenceOccupancyOnProgramRegistrationUpdate, + updateOccurrenceOccupancyOnProgramRegistrationDelete, } diff --git a/backend/services/genericData/programWaitlist.js b/backend/services/genericData/programWaitlist.js new file mode 100644 index 00000000..7b6833d9 --- /dev/null +++ b/backend/services/genericData/programWaitlist.js @@ -0,0 +1,33 @@ +const {getPredefinedProperty} = require("./helperFunctions"); +const {GDBInternalTypeModel} = require("../../models/internalType"); +const {SPARQL} = require("graphdb-utils"); +const FORMTYPE = 'programWaitlist' + +const programWaitlistInternalTypeCreateTreater = async (internalType, instanceData, value) => { + const property = getPredefinedProperty(FORMTYPE, internalType); + if (property === 'waitlist' || property === 'programOccurrence'){ + instanceData[property] = value; + } +}; + +const programWaitlistInternalTypeFetchTreater = async (data) => { + const result = {}; + const schema = data.schema; + for (const property in data) { + if (property === 'waitlist' || property === 'programOccurrence') { + const internalType = await GDBInternalTypeModel.findOne({predefinedProperty: schema[property].internalKey, formType: FORMTYPE}); + result[ 'internalType_'+ internalType._id] = SPARQL.ensureFullURI(data[property]); + } + } + return result; +}; + +const programWaitlistInternalTypeUpdateTreater = async (internalType, value, result) => { + await programWaitlistInternalTypeCreateTreater(internalType, result, value); +} + +module.exports = { + programWaitlistInternalTypeCreateTreater, + programWaitlistInternalTypeFetchTreater, + programWaitlistInternalTypeUpdateTreater +} \ No newline at end of file diff --git a/backend/services/genericData/programWaitlistEntry.js b/backend/services/genericData/programWaitlistEntry.js new file mode 100644 index 00000000..50e61e67 --- /dev/null +++ b/backend/services/genericData/programWaitlistEntry.js @@ -0,0 +1,33 @@ +const {getPredefinedProperty} = require("./helperFunctions"); +const {GDBInternalTypeModel} = require("../../models/internalType"); +const {SPARQL} = require("graphdb-utils"); +const FORMTYPE = 'waitlistEntry' + +const programWaitlistEntryInternalTypeCreateTreater = async (internalType, instanceData, value) => { + const property = getPredefinedProperty(FORMTYPE, internalType); + if (property === 'programRegistration'){ + instanceData[property] = value; + } +}; + +const programWaitlistEntryInternalTypeFetchTreater = async (data) => { + const result = {}; + const schema = data.schema; + for (const property in data) { + if (property === 'programRegistration') { + const internalType = await GDBInternalTypeModel.findOne({predefinedProperty: schema[property].internalKey, formType: FORMTYPE}); + result[ 'internalType_'+ internalType._id] = SPARQL.ensureFullURI(data[property]); + } + } + return result; +}; + +const programWaitlistEntryInternalTypeUpdateTreater = async (internalType, value, result) => { + await programWaitlistEntryInternalTypeCreateTreater(internalType, result, value); +} + +module.exports = { + programWaitlistEntryInternalTypeCreateTreater, + programWaitlistEntryInternalTypeFetchTreater, + programWaitlistEntryInternalTypeUpdateTreater +} \ No newline at end of file diff --git a/backend/services/genericData/serviceOccurrenceInternalTypeTreater.js b/backend/services/genericData/serviceOccurrenceInternalTypeTreater.js index 461e00bd..30875f6b 100644 --- a/backend/services/genericData/serviceOccurrenceInternalTypeTreater.js +++ b/backend/services/genericData/serviceOccurrenceInternalTypeTreater.js @@ -3,6 +3,10 @@ const {GDBNeedSatisfierOccurrenceModel} = require("../../models/needSatisfierOcc const {getPredefinedProperty, getInternalTypeValues} = require('./helperFunctions') const {GDBInternalTypeModel} = require("../../models/internalType"); const {SPARQL} = require('graphdb-utils'); +const {popFromWaitlist} = require("../serviceWaitlist/serviceWaitlist"); +const {getIndividualsInClass} = require("../dynamicForm"); +const {PredefinedCharacteristics} = require("../characteristics"); +const {GDBServiceWaitlistModel} = require("../../models/service/serviceWaitlist"); const serviceOccurrenceInternalTypeCreateTreater = async (internalType, instanceData, value) => { const property = getPredefinedProperty('serviceOccurrence', internalType); @@ -21,4 +25,52 @@ const serviceOccurrenceInternalTypeUpdateTreater = async (internalType, value, r await serviceOccurrenceInternalTypeCreateTreater(internalType, result, value); } -module.exports = {serviceOccurrenceInternalTypeCreateTreater, serviceOccurrenceInternalTypeFetchTreater, serviceOccurrenceInternalTypeUpdateTreater, } +const checkCapacityOnServiceOccurrenceUpdate = async function (characteristics, questions, fields, oldGeneric) { + const capacityId = Object.keys(characteristics).find(id => characteristics[id].name === 'Capacity'); + if (!capacityId) + return; + const capacity = fields['characteristic_' + capacityId]; + if (capacity < oldGeneric.occupancy) { + throw new Error('The new capacity of this service occurrence is less than its current occupancy. Please unregister ' + + 'service registrations until the occupancy is below the desired capacity, and then try editing this service ' + + 'occurrence again.'); + } else { + const statuses = await getIndividualsInClass(':RegistrationStatus'); + const registeredStatus = Object.keys(statuses).find(status => statuses[status] === 'Registered'); + const statusC = PredefinedCharacteristics['Registration Status']; + const occupancyC = PredefinedCharacteristics['Occupancy']; + const occupancyCO = oldGeneric.characteristicOccurrences.find(co => co.occurrenceOf === occupancyC._uri); + while (capacity > oldGeneric.occupancy) { + const registration = await popFromWaitlist(oldGeneric._id); + if (!!registration) { + // Make the registration's status be 'Registered' and save it + const statusCO = registration.characteristicOccurrences.find(co => co.occurrenceOf === statusC._uri); + registration.status = registeredStatus; + statusCO.dataStringValue = registeredStatus; + await registration.save(); + + oldGeneric.occupancy += 1; + occupancyCO.dataNumberValue += 1; + } else { + break; + } + } + await oldGeneric.save(); + } +} + +// Create a new waitlist here that corresponds to the serviceOccurrence newGeneric +const afterCreateServiceOccurrence = async function (data, req, newGeneric) { + const occurrenceWaitlist = GDBServiceWaitlistModel({'serviceOccurrence': newGeneric, 'waitlist': []}); + // pass in the serviceOccurrence that was just created ("newGeneric") + // and an empty list for the queue. + await occurrenceWaitlist.save(); +} + +module.exports = { + serviceOccurrenceInternalTypeCreateTreater, + serviceOccurrenceInternalTypeFetchTreater, + serviceOccurrenceInternalTypeUpdateTreater, + checkCapacityOnServiceOccurrenceUpdate, + afterCreateServiceOccurrence, +} diff --git a/backend/services/genericData/serviceRegistration.js b/backend/services/genericData/serviceRegistration.js index 003b2b52..986085de 100644 --- a/backend/services/genericData/serviceRegistration.js +++ b/backend/services/genericData/serviceRegistration.js @@ -1,10 +1,10 @@ const {getPredefinedProperty} = require("./helperFunctions"); const {GDBInternalTypeModel} = require("../../models/internalType"); const {SPARQL} = require("graphdb-utils"); -const {GDBClientModel} = require("../../models/ClientFunctionalities/client"); -const {GDBServiceModel} = require("../../models/service/service"); const {GDBServiceOccurrenceModel} = require("../../models/service/serviceOccurrence"); -const {GDBNeedSatisfierOccurrenceModel} = require("../../models/needSatisfierOccurrence"); +const {getIndividualsInClass} = require("../dynamicForm"); +const {PredefinedCharacteristics, PredefinedInternalTypes} = require("../characteristics"); +const {pushToWaitlist, popFromWaitlist, removeFromWaitlist} = require("../serviceWaitlist/serviceWaitlist"); const FORMTYPE = 'serviceRegistration' @@ -36,8 +36,139 @@ const serviceRegistrationInternalTypeUpdateTreater = async (internalType, value, await serviceRegistrationInternalTypeCreateTreater(internalType, result, value); } +const updateOccurrenceOccupancyOnServiceRegistrationCreate = async function (characteristics, questions, fields) { + const statuses = await getIndividualsInClass(':RegistrationStatus'); + const status = statuses[fields[`characteristic_${PredefinedCharacteristics['Registration Status']._id}`]]; + const occUri = fields['internalType_' + PredefinedInternalTypes['serviceOccurrenceForServiceRegistration']._id]; + if (!occUri) + return; + const occ = await GDBServiceOccurrenceModel.findOne({_uri: occUri}, {populates: ['characteristicOccurrences']}); + if (!occ) + return; + if (status === 'Registered') { + if (!occ.capacity || (occ.occupancy < occ.capacity)) { + occ.occupancy += 1; + const occupancyC = PredefinedCharacteristics['Occupancy']; + const occupancyCO = occ.characteristicOccurrences.find(co => co.occurrenceOf === occupancyC._uri); + occupancyCO.dataNumberValue += 1; + await occ.save(); + } else { + throw new Error('The requested service occurrence is now at capacity. Please refresh the form to review your current options.'); + } + } else if (status === 'Waitlisted') { + // Registration will be pushed to waitlist after creation + } else if (status !== 'Not Registered') { + throw new Error('Invalid registration status chosen.'); + } +} + +const afterCreateServiceRegistration = async function (data, req, newGeneric) { + // If the new service registration is waitlisted, push it to the appropriate waitlist + const statuses = await getIndividualsInClass(':RegistrationStatus'); + if (statuses[newGeneric.status] === 'Waitlisted') { + const date = new Date(); + await pushToWaitlist(newGeneric.serviceOccurrence.split('_')[1], newGeneric._id, date, date); + } +} + +const checkServiceOccurrenceUnchanged = async function (characteristics, questions, fields, oldGeneric) { + const newOccUri = fields['internalType_' + PredefinedInternalTypes['serviceOccurrenceForServiceRegistration']._id]; + const oldOccUri = oldGeneric.serviceOccurrence; + if (newOccUri !== oldOccUri) + throw new Error('A service registration\'s service occurrence cannot change.'); +} + +const updateOccurrenceOccupancyOnServiceRegistrationUpdate = async function (characteristics, questions, fields, oldGeneric) { + // checkServiceOccurrenceUnchanged must be called before this + const statuses = await getIndividualsInClass(':RegistrationStatus'); + const oldStatus = statuses[oldGeneric.status]; + const newStatus = statuses[fields[`characteristic_${PredefinedCharacteristics['Registration Status']._id}`]]; + const occUri = fields['internalType_' + PredefinedInternalTypes['serviceOccurrenceForServiceRegistration']._id]; + if (!occUri) + return; + const occ = await GDBServiceOccurrenceModel.findOne({_uri: occUri}, {populates: ['characteristicOccurrences']}); + if (!occ) + return; + const occupancyC = PredefinedCharacteristics['Occupancy']; + const occupancyCO = occ.characteristicOccurrences.find(co => co.occurrenceOf === occupancyC._uri); + if (oldStatus === 'Not Registered' && newStatus === 'Registered') { + if (!occ.capacity || (occ.occupancy < occ.capacity)) { + occ.occupancy += 1; + occupancyCO.dataNumberValue += 1; + await occ.save(); + } else { + throw new Error('The requested service occurrence is now at capacity. Please refresh the form to review your current options.'); + } + } else if (oldStatus === 'Registered' && newStatus === 'Not Registered') { + // Pop one registration from waitlist, if any, and change its status to registered + const registration = await popFromWaitlist(occ._id); + if (!!registration) { + const statuses = await getIndividualsInClass(':RegistrationStatus'); + const registeredStatus = Object.keys(statuses).find(status => statuses[status] === 'Registered'); + const statusC = PredefinedCharacteristics['Registration Status']; + const statusCO = registration.characteristicOccurrences.find(co => co.occurrenceOf === statusC._uri); + registration.status = registeredStatus; + statusCO.dataStringValue = registeredStatus; + await registration.save(); + // occ.occupancy doesn't change + } else { + occ.occupancy -= 1; + occupancyCO.dataNumberValue -= 1; + await occ.save(); + } + } else if (oldStatus === 'Waitlisted' && newStatus === 'Not Registered') { + // Remove this registration from waitlist + await removeFromWaitlist(oldGeneric.serviceOccurrence.split('_')[1], oldGeneric._id); + } else if (oldStatus === 'Not Registered' && newStatus === 'Waitlisted') { + // Push this registration to waitlist + const date = new Date(); + await pushToWaitlist(oldGeneric.serviceOccurrence.split('_')[1], oldGeneric._id, date, date); + } else if (oldStatus !== newStatus) { + throw new Error('Invalid registration status chosen.'); + } +} + +const updateOccurrenceOccupancyOnServiceRegistrationDelete = async function (oldGeneric) { + const statuses = await getIndividualsInClass(':RegistrationStatus'); + const status = statuses[oldGeneric.status]; + const occUri = oldGeneric.serviceOccurrence; + if (!occUri) + return; + const occ = await GDBServiceOccurrenceModel.findOne({_uri: occUri}, {populates: ['characteristicOccurrences']}); + if (!occ) + return; + if (status === 'Registered') { + // Pop one registration from waitlist, if any, and change its status to registered + const registration = await popFromWaitlist(occ._id); + if (!!registration) { + const statuses = await getIndividualsInClass(':RegistrationStatus'); + const registeredStatus = Object.keys(statuses).find(status => statuses[status] === 'Registered'); + const statusC = PredefinedCharacteristics['Registration Status']; + const statusCO = registration.characteristicOccurrences.find(co => co.occurrenceOf === statusC._uri); + registration.status = registeredStatus; + statusCO.dataStringValue = registeredStatus; + await registration.save(); + // occ.occupancy doesn't change + } else { + const occupancyC = PredefinedCharacteristics['Occupancy']; + const occupancyCO = occ.characteristicOccurrences.find(co => co.occurrenceOf === occupancyC._uri); + occ.occupancy -= 1; + occupancyCO.dataNumberValue -= 1; + await occ.save(); + } + } else if (status === 'Waitlisted') { + // Remove this registration from waitlist + await removeFromWaitlist(oldGeneric.serviceOccurrence.split('_')[1], oldGeneric._id); + } +} + module.exports = { serviceRegistrationInternalTypeCreateTreater, serviceRegistrationInternalTypeFetchTreater, - serviceRegistrationInternalTypeUpdateTreater + serviceRegistrationInternalTypeUpdateTreater, + checkServiceOccurrenceUnchanged, + updateOccurrenceOccupancyOnServiceRegistrationCreate, + afterCreateServiceRegistration, + updateOccurrenceOccupancyOnServiceRegistrationUpdate, + updateOccurrenceOccupancyOnServiceRegistrationDelete, } diff --git a/backend/services/genericData/serviceWaitlist.js b/backend/services/genericData/serviceWaitlist.js new file mode 100644 index 00000000..bb4e20ca --- /dev/null +++ b/backend/services/genericData/serviceWaitlist.js @@ -0,0 +1,36 @@ +const {getPredefinedProperty} = require("./helperFunctions"); +const {GDBInternalTypeModel} = require("../../models/internalType"); +const {SPARQL} = require("graphdb-utils"); +const FORMTYPE = 'serviceWaitlist' + +const serviceWaitlistInternalTypeCreateTreater = async (internalType, instanceData, value) => { + //get the property name from the internalType + const property = getPredefinedProperty(FORMTYPE, internalType); + //if the property is client, person or user, then set the value to the instanceData + if (property === 'waitlist' || property === 'serviceOccurrence'){ + instanceData[property] = value; + } +}; + +const serviceWaitlistInternalTypeFetchTreater = async (data) => { + const result = {}; + const schema = data.schema; + // for each property in data, if the property is client, person or user, then set the value to the result + for (const property in data) { + if (property === 'waitlist' || property === 'serviceOccurrence') { + const internalType = await GDBInternalTypeModel.findOne({predefinedProperty: schema[property].internalKey, formType: FORMTYPE}); + result[ 'internalType_'+ internalType._id] = SPARQL.ensureFullURI(data[property]); + } + } + return result; +}; + +const serviceWaitlistInternalTypeUpdateTreater = async (internalType, value, result) => { + await serviceWaitlistInternalTypeCreateTreater(internalType, result, value); +} + +module.exports = { + serviceWaitlistInternalTypeCreateTreater, + serviceWaitlistInternalTypeFetchTreater, + serviceWaitlistInternalTypeUpdateTreater +} \ No newline at end of file diff --git a/backend/services/genericData/serviceWaitlistEntry.js b/backend/services/genericData/serviceWaitlistEntry.js new file mode 100644 index 00000000..5adc3e85 --- /dev/null +++ b/backend/services/genericData/serviceWaitlistEntry.js @@ -0,0 +1,34 @@ +const {getPredefinedProperty} = require("./helperFunctions"); +const {GDBInternalTypeModel} = require("../../models/internalType"); +const {SPARQL} = require("graphdb-utils"); +const FORMTYPE = 'waitlistEntry' + +const serviceWaitlistEntryInternalTypeCreateTreater = async (internalType, instanceData, value) => { + //get the property name from the internalType + const property = getPredefinedProperty(FORMTYPE, internalType); + if (property === 'serviceRegistration'){ + instanceData[property] = value; + } +}; + +const serviceWaitlistEntryInternalTypeFetchTreater = async (data) => { + const result = {}; + const schema = data.schema; + for (const property in data) { + if (property === 'serviceRegistration') { + const internalType = await GDBInternalTypeModel.findOne({predefinedProperty: schema[property].internalKey, formType: FORMTYPE}); + result[ 'internalType_'+ internalType._id] = SPARQL.ensureFullURI(data[property]); + } + } + return result; +}; + +const serviceWaitlistEntryInternalTypeUpdateTreater = async (internalType, value, result) => { + await serviceWaitlistEntryInternalTypeCreateTreater(internalType, result, value); +} + +module.exports = { + serviceWaitlistEntryInternalTypeCreateTreater, + serviceWaitlistEntryInternalTypeFetchTreater, + serviceWaitlistEntryInternalTypeUpdateTreater +} \ No newline at end of file diff --git a/backend/services/programWaitlist/programWaitlist.js b/backend/services/programWaitlist/programWaitlist.js new file mode 100644 index 00000000..90c8a5ab --- /dev/null +++ b/backend/services/programWaitlist/programWaitlist.js @@ -0,0 +1,161 @@ +const {GDBProgramRegistrationModel} = require("../../models/programRegistration"); +const {GDBProgramWaitlistModel} = require("../../models/program/programWaitlist"); +const {GDBProgramWaitlistEntryModel} = require("../../models/program/programWaitlistEntry"); + + +// **Below are functions related to the programWaitlistModel, primarily for updating the waitlist ** + + + +/* + +given a programOccurrence ID and some programRegistration ID in "req.params", as well as priority (Number), and date (Date) +in "req.body" (see /backend/models/program/waitlistEntry.js), +add this new registration on the waitlist accordingly. +That is, we should have three attributes: + +1. the id of the programOccurrence: req.params.id +2. the id of the programRegistration to be inserted: req.body.programRegistrationId +3. the priority this registration will have in the waitlist: req.body.priority +4. the date the registration was made: req.body.date + +Waitlist insertion is based on specified priority. +Note that for now I am treating priority as a number (see the waitlistEntry model) +where a smaller number has more priority (like ranking/tiers, e.g. tier 1, tier 2, ..., tier n etc. +where tier 1 is the highest priority). Date could theoretically be used to determine the priority +however this can/will be added in a future update. + +*/ + + +const pushToWaitlist = async (id, programRegistrationId, priority, date) => { + try { + //grab the waitlist, and the corresponding client to be registered + const waitlist = await GDBProgramWaitlistModel.findOne({'programOccurrence': {_id: id}}); + const programRegistration = await GDBProgramRegistrationModel.findById(programRegistrationId); + //create a new entry, this will be added to our waitlist + const newEntry = GDBProgramWaitlistEntryModel({'programRegistration': programRegistration, 'priority': priority, 'date': date}); + //save this new entry to the db + await newEntry.save(); + //now begin insertion: + + //try to find the index of the first element in the "waitlist" attribute + //whose priority is greater than the priority of our new entry into the waitlist, as highest + //priority = lower number + let insertionIndex; + if (!waitlist.waitlist) { + waitlist.waitlist = []; + insertionIndex = -1; + } else { + insertionIndex = waitlist.waitlist.findIndex(entry => entry.priority > priority); + } + //in the event that no index is found, add our value to the end of the waitlist: + if (insertionIndex === -1) { + waitlist.waitlist.push(newEntry); + } else { + //now we need to find where the last index is with the same priority as the new registration + //this is because if someone was already there with the same priority as the new registration, + //they should go first still + while (insertionIndex < waitlist.waitlist.length && waitlist.waitlist[insertionIndex].priority === newEntry.priority) { + insertionIndex++; //just incrementing until we find where we need to insert + } + + //insert the new registration to where it belongs + waitlist.waitlist.splice(insertionIndex, 0, newEntry); + } + + //save the changes made to this waitlist after insertion + await waitlist.save(); + } catch (e) { + throw e; + } +} + + +/*given a programOccurrence, remove and return the registration (as a waitlistEntry) of the client that is next in line +from the programOccurrence's correspondingwaitlist. See the notes above on top of pushToWaitlist(). + +assume that we have: +1. the programOccurrence id: req.params.id + */ +const popFromWaitlist = async (id) => { + try { + //get the waitlist we need to modify + const waitlist = await GDBProgramWaitlistModel.findOne({'programOccurrence': {_id: id}}, + { + populates: ['programOccurrence.characteristicOccurrences', 'waitlist', 'waitlist.programRegistration', + 'waitlist.programRegistration.characteristicOccurrences'] + }); + //go into our waitlist, pop out the item in the front of our list/queue (since we are sorting) + //from ascending priority where the lowest priority value = the client that should get off the waitlist first + const dequeuedEntry = waitlist.waitlist?.shift(); + await waitlist.save(); + if (!dequeuedEntry) { + return null; + } + const programRegistration = dequeuedEntry.programRegistration; + + //now delete the item + await GDBProgramWaitlistEntryModel.findByIdAndDelete(dequeuedEntry._id); + //now return success and the thing that just got deleted: + return programRegistration; + } catch (e) { + throw e; + } +} + + + + +/*given a programOccurrence ID and the id +of a programRegistration that must be dequeued from it, delete the registration (waitlistEntry) +from the queue. See the notes above on top of pushToWaitlist(). + +assume that we will get the following from req: + +1. the programOccurrence id: req.params.id +2.the id of the programRegistration that needs to be removed from the waitlist: req.body.programRegistrationId + + +*/ +const removeFromWaitlist = async (id, programRegistrationId) => { + try { + + //get the waitlist we need to modify + const waitlist = await GDBProgramWaitlistModel.findOne({'programOccurrence': {_id: id}}, {populates: ['waitlist']}); + + //now we go through the waitlist and find where the client we need to delete is based + //on the given id. + + let removedEntry; + const newWaitlist = waitlist.waitlist?.filter(entry => { + if (entry.programRegistration?.split('_')[1] === programRegistrationId){ + removedEntry = entry; //save this so we can delete the registration from the db as well + return false; //returning false means that the item we are deleting is no long in the waitlist. + } + //if the programRegistration's id is not equal to the one we need to delete, we keep it in the new waitlist + //(it is not filtered out) + return true; + }); + if (!removedEntry) { + return false; + } + + //our new waitlist is the same as the old one except with the target client removed from the "queue" + waitlist.waitlist = newWaitlist; + //delete the entry from our database + await GDBProgramWaitlistEntryModel.findByIdAndDelete(removedEntry._id); + //save the changes + await waitlist.save(); + + return true; + } catch (e) { + throw e; + } +} + +module.exports = { + pushToWaitlist, + popFromWaitlist, + removeFromWaitlist, +} \ No newline at end of file diff --git a/backend/services/serviceWaitlist/serviceWaitlist.js b/backend/services/serviceWaitlist/serviceWaitlist.js new file mode 100644 index 00000000..2f20c512 --- /dev/null +++ b/backend/services/serviceWaitlist/serviceWaitlist.js @@ -0,0 +1,160 @@ +const {GDBServiceRegistrationModel} = require("../../models/serviceRegistration"); +const {GDBServiceWaitlistModel} = require("../../models/service/serviceWaitlist"); +const {GDBServiceWaitlistEntryModel} = require("../../models/service/serviceWaitlistEntry"); + + +// **Below are functions related to the serviceWaitlistModel, primarily for updating the waitlist ** + + + +/* +given a serviceOccurrence ID and some serviceRegistration ID in "req.params", as well as priority (Number), and date (Date) +in "req.body" (see /backend/models/service/waitlistEntry.js), +add this new registration on the waitlist accordingly. +That is, we should have three attributes: + +1. the id of the serviceOccurrence: req.params.id +2. the id of the serviceRegistration to be inserted: req.body.serviceRegistrationId +3. the priority this registration will have in the waitlist: req.body.priority +4. the date the registration was made: req.body.date + +Waitlist insertion is based on specified priority. +Note that for now I am treating priority as a number (see the waitlistEntry model) +where a smaller number has more priority (like ranking/tiers, e.g. tier 1, tier 2, ..., tier n etc. +where tier 1 is the highest priority). Date could theoretically be used to determine the priority +however this can/will be added in a future update. + +*/ + + +const pushToWaitlist = async (id, serviceRegistrationId, priority, date) => { + try { + //grab the waitlist, and the corresponding client to be registered + const waitlist = await GDBServiceWaitlistModel.findOne({'serviceOccurrence': {_id: id}}); + const serviceRegistration = await GDBServiceRegistrationModel.findById(serviceRegistrationId); + //create a new entry, this will be added to our waitlist + const newEntry = GDBServiceWaitlistEntryModel({'serviceRegistration': serviceRegistration, 'priority': priority, 'date': date}); + //save this new entry to the db + await newEntry.save(); + //now begin insertion: + + //try to find the index of the first element in the "waitlist" attribute + //whose priority is greater than the priority of our new entry into the waitlist, as highest + //priority = lower number + let insertionIndex; + if (!waitlist.waitlist) { + waitlist.waitlist = []; + insertionIndex = -1; + } else { + insertionIndex = waitlist.waitlist.findIndex(entry => entry.priority > priority); + } + //in the event that no index is found, add our value to the end of the waitlist: + if (insertionIndex === -1) { + waitlist.waitlist.push(newEntry); + } else { + //now we need to find where the last index is with the same priority as the new registration + //this is because if someone was already there with the same priority as the new registration, + //they should go first still + while (insertionIndex < waitlist.waitlist.length && waitlist.waitlist[insertionIndex].priority === newEntry.priority) { + insertionIndex++; //just incrementing until we find where we need to insert + } + + //insert the new registration to where it belongs + waitlist.waitlist.splice(insertionIndex, 0, newEntry); + } + + //save the changes made to this waitlist after insertion + await waitlist.save(); + } catch (e) { + throw e; + } +} + + +/*given a serviceOccurrence, remove and return the registration (as a waitlistEntry) of the client that is next in line +from the serviceOccurrence's correspondingwaitlist. See the notes above on top of pushToWaitlist(). + +assume that we have: +1. the serviceOccurrence id: req.params.id + */ +const popFromWaitlist = async (id) => { + try { + //get the waitlist we need to modify + const waitlist = await GDBServiceWaitlistModel.findOne({'serviceOccurrence': {_id: id}}, + { + populates: ['serviceOccurrence.characteristicOccurrences', 'waitlist', 'waitlist.serviceRegistration', + 'waitlist.serviceRegistration.characteristicOccurrences'] + }); + //go into our waitlist, pop out the item in the front of our list/queue (since we are sorting) + //from ascending priority where the lowest priority value = the client that should get off the waitlist first + const dequeuedEntry = waitlist.waitlist?.shift(); + await waitlist.save(); + if (!dequeuedEntry) { + return null; + } + const serviceRegistration = dequeuedEntry.serviceRegistration; + + //now delete the item + await GDBServiceWaitlistEntryModel.findByIdAndDelete(dequeuedEntry._id); + //now return success and the thing that just got deleted: + return serviceRegistration; + } catch (e) { + throw e; + } +} + + + + +/*given a serviceOccurrence ID and the id +of a serviceRegistration that must be dequeued from it, delete the registration (waitlistEntry) +from the queue. See the notes above on top of pushToWaitlist(). + +assume that we will get the following from req: + +1. the serviceOccurrence id: req.params.id +2.the id of the serviceRegistration that needs to be removed from the waitlist: req.body.serviceRegistrationId + + +*/ +const removeFromWaitlist = async (id, serviceRegistrationId) => { + try { + + //get the waitlist we need to modify + const waitlist = await GDBServiceWaitlistModel.findOne({'serviceOccurrence': {_id: id}}, {populates: ['waitlist']}); + + //now we go through the waitlist and find where the client we need to delete is based + //on the given id. + + let removedEntry; + const newWaitlist = waitlist.waitlist?.filter(entry => { + if (entry.serviceRegistration?.split('_')[1] === serviceRegistrationId){ + removedEntry = entry; //save this so we can delete the registration from the db as well + return false; //returning false means that the item we are deleting is no long in the waitlist. + } + //if the serviceRegistration's id is not equal to the one we need to delete, we keep it in the new waitlist + //(it is not filtered out) + return true; + }); + if (!removedEntry) { + return false; + } + + //our new waitlist is the same as the old one except with the target client removed from the "queue" + waitlist.waitlist = newWaitlist; + //delete the entry from our database + await GDBServiceWaitlistEntryModel.findByIdAndDelete(removedEntry._id); + //save the changes + await waitlist.save(); + + return true; + } catch (e) { + throw e; + } +} + +module.exports = { + pushToWaitlist, + popFromWaitlist, + removeFromWaitlist, +} \ No newline at end of file diff --git a/doc/src/content/docs/guides/data-model.md b/doc/src/content/docs/guides/data-model.md index aa5a3470..9d8c4f45 100644 --- a/doc/src/content/docs/guides/data-model.md +++ b/doc/src/content/docs/guides/data-model.md @@ -273,3 +273,57 @@ const GDBClientModel = createGraphDBModel({ rdfTypes: [':Client'], name: 'client' }); ``` + + +### Waitlists For Service Occurrences +```js +const GDBServiceWaitlistModel = createGraphDBModel({ + waitlist: {type: [GDBServiceWaitlistEntryModel], internalKey: ':hasWaitlist'}, + serviceOccurrence: {type: GDBServiceOccurrenceModel, internalKey: ':hasServiceOccurrence'}, +}, +{ + rdfTypes: [':ServiceWaitlist'], name: 'serviceWaitlist' +}); +``` + +### Waitlists for Program Occurrences +```js +const GDBProgramWaitlistModel = createGraphDBModel({ + waitlist: {type: [GDBProgramWaitlistEntryModel], internalKey: ':hasWaitlist'}, + programOccurrence: {type: GDBProgramOccurrenceModel, internalKey: ':hasProgramOccurrence'}, +}, +{ + rdfTypes: [':ProgramWaitlist'], name: 'programWaitlist' +}); +``` + +### Entry object for Service Waitlists +```js +const GDBServiceWaitlistEntryModel = createGraphDBModel({ + serviceRegistration: {type: GDBServiceRegistrationModel, internalKey: ':hasServiceRegistration'}, + priority: {type: Number, internalKey: ':hasPriority'}, + date: {type: Date, internalKey: ':hasDate'}, +}, +{ + rdfTypes: [':ServiceWaitlistEntry'], name: 'serviceWaitlistEntry' +}); +``` + + + + +### Entry object for Program Waitlists +```js +const GDBProgramWaitlistEntryModel = createGraphDBModel({ + programRegistration: {type: GDBProgramRegistrationModel, internalKey: ':hasProgramRegistration'}, + priority: {type: Number, internalKey: ':hasPriority'}, + date: {type: Date, internalKey: ':hasDate'}, +}, +{ + rdfTypes: [':ProgramWaitlistEntry'], name: 'programWaitlistEntry' +}); +``` + + + + diff --git a/doc/src/content/docs/guides/general-dev-faq.md b/doc/src/content/docs/guides/general-dev-faq.md new file mode 100644 index 00000000..7d0f5169 --- /dev/null +++ b/doc/src/content/docs/guides/general-dev-faq.md @@ -0,0 +1,90 @@ +--- +title: General FAQ For New Developers +--- + +This guide has been written for those who are new to the codebase and are unsure of where to start or where to look. This was last updated on March 22, 2024. + + +## I need to create a new model for something, where do I do this and how should I start? +All models are kept in `/backend/models`. Let's take a look at an example: + +```js +const GDBServiceWaitlistEntryModel = createGraphDBModel({ + serviceRegistration: {type: GDBServiceRegistrationModel, internalKey: ':hasServiceRegistration'}, + priority: {type: Number, internalKey: ':hasPriority'}, + date: {type: Date, internalKey: ':hasDate'}, +}, +{ + rdfTypes: [':ServiceWaitlistEntry'], name: 'serviceWaitlistEntry' +}); +``` +(Hint: you can see more examples of models in the "Data Model" guide) + +Here we are setting our model-creation function as a particular function call of `createGraphDBModel()`. This model is used as an entry for a service waitlist model that is used by a service occurrence (although this detail is not entirely necessary). note that first, we are passing in a JavaScript object containing all of the attributes we wish to have in the model. First, we have the "serviceRegistration", which is another model defined in `/backend/ServiceRegistration.js`. We can also use other predefined types like "Number" or "Date". In the second object, we are essentially declaring the name of this type of model. + +You may also have to modify the following files. Observe how other models are specified for them, as it is quite long (and relatively straightforward) to explain for a short FAQ question: + +1. `/backend/services/characteristics/predefined/internalTypes.js` +2. `/backend/services/genericData/index.js` +3. `/frontend/src/constants/provider_fields.js` +4. `/frontend/src/routes.js` + +## I have a model that I need to be able to create, modify, and display a page from the drop-down menu, what should I do? +There are a couple of things you would need to do: + +1. Setup routes for your model at `/frontend/src/Routes.js` +2. Add your model as an option for the overview drop-down menu at `/frontend/src/components/layouts/TopNavbar.js` +3. Create a form, overview page, and visualizer. As examples, check out: + + i. `/frontend/src/components/serviceOccurrence/ServiceOccurrence.js` + ii.`/frontend/src/components/ServiceOccurrences.js` + iii. `/frontend/src/components/serviceOccurrence/visualizeServiceOccurrence.js` +respectively. + + + + +## What is a "snackbar"? +This is the notification centre of the web application. These notifications appear temporarily and disappear after a set number of time, this is used to display confirmations, errors, etc. to the user when they use the site. + + +## I need to make a form for a new model, but I'm not sure where to start. +Check out `/frontend/src/components/serviceOccurrence/ServiceOccurrence.js` as an example. We basically create a function to specify the specific features of the form, then return a modified React component ("GenericForm" from `/frontend/src/components/shared/GenericForm.js`) + + +## How do I set up an overview page for a new model that I created? +You can find many examples of this, check out some examples at `/frontend/src/components/programWaitlist/programWaitlists.js` or `/frontend/src/components/Clients.js` to name a few. + + +## The frontend overview from one of the models in the drop-down menu is displaying a weird link or code instead of what I want. Why? +If you are referencing a value from another model that is stored in your own model, you need to make an API call to reference this value. Example: +```js +const columnsWithoutOptions = [ + { + label: 'ID', + body: ({_id}) => { + console.log(_id); + console.log({_id}); + + return {_id} + } + }, + ]; +``` + + +## How do I make sure that the API features I set up are correct? +You can use apps made to test web APIs like [Postman](https://www.postman.com/). This application allows you to make API calls and view the HTTP Response that was sent from the backend server. + + +## What is a route? How do I set one up? +A route is an endpoint (defined by some sort of URL that you specify) that is mapped to a particular function in the backend server. This is done via HTTP (Hypertext Transfer Protocol). This forms our API (Application Programming Interface), and can be used to request, update, create, and delete information (CRUD). You will find how to create routes on the frontend in `/frontend/src/routes.js`. + + +## I'm testing some things locally using the frontend application. When I try to create a new instance of an object/model it just says "No form available", why? + +When you created a new docker image for your databases, no forms are pre-loaded in. Click the profile picture icon on the top right of the window and click "dashboard". There you can click "Manage Forms", select the model you want to make a form for and customize it as necessary. + + + + diff --git a/frontend/src/components/ProgramOccurrences.js b/frontend/src/components/ProgramOccurrences.js index b4d99d49..d95154c0 100644 --- a/frontend/src/components/ProgramOccurrences.js +++ b/frontend/src/components/ProgramOccurrences.js @@ -20,6 +20,21 @@ const columnsWithoutOptions = [ }, sortBy: ({programName}) => programName, }, + { + label: 'Occupancy', + body: ({occupancy, capacity}) => { + return `${occupancy}/${capacity ?? '∞'}`; + }, + sortBy: ({occupancy, capacity}) => { + if (!!capacity) { + return occupancy / capacity; + } else { + // If capacity is 0, occupancy must also be 0 (letting 0 / 0 = 0) + // If capacity is unlimited, % occupancy is 0 + return 0; + } + }, + }, // { // label: 'Category', // body: ({category}) => category @@ -46,6 +61,8 @@ export default function ProgramOccurrences() { if (programOccurrence.description) { programOccurrenceData.description = programOccurrence.description; } + programOccurrenceData.capacity = programOccurrence.capacity; + programOccurrenceData.occupancy = programOccurrence.occupancy; if (programOccurrence.occurrenceOf) { programOccurrenceData.programID = programOccurrence.occurrenceOf._id; programOccurrenceData.programName = programOccurrence.occurrenceOf.name; diff --git a/frontend/src/components/ProgramWaitlists.js b/frontend/src/components/ProgramWaitlists.js new file mode 100644 index 00000000..ff1be379 --- /dev/null +++ b/frontend/src/components/ProgramWaitlists.js @@ -0,0 +1,69 @@ +import React from 'react'; +import { Link } from './shared'; +import { GenericPage } from "./shared"; +import { deleteSingleGeneric, fetchMultipleGeneric, fetchSingleGeneric } from "../api/genericDataApi"; +import {getAddressCharacteristicId} from "./shared/CharacteristicIds"; + +const TYPE = 'programWaitlist'; + +const columnsWithoutOptions = [ + { + label: 'Program Occurrence ID', + body: ({programOccurrence}) => {programOccurrence?._id} + + }, + { + label: 'Clients Waitlisted', + body: ({waitlist}) => waitlist ? waitlist.length : 0 + }, + // { + // label: 'Category', + // body: ({category}) => category + // } +]; + +export default function ProgramWaitlists() { + + const nameFormatter = programWaitlist => { + if (programWaitlist.description) { + return programWaitlist.description; + } else { + return 'Program Waitlist ' + programWaitlist._id; + } + } + + const linkFormatter = programWaitlist => `/${TYPE}/${programWaitlist._id}`; + + const fetchData = async () => { + const addressCharacteristicId = await getAddressCharacteristicId(); + const programWaitlists = (await fetchMultipleGeneric(TYPE)).data; + console.log(programWaitlists) + const data = []; + for (const programWaitlist of programWaitlists) { + const programWaitlistData = {_id: programWaitlist._id}; + if (programWaitlist.waitlist) + programWaitlistData.waitlist = programWaitlist.waitlist; + if (programWaitlist.programOccurrence) + programWaitlistData.programOccurrence = programWaitlist.programOccurrence; + data.push(programWaitlistData); + } + return data; + }; + + const deleteService = (id) => deleteSingleGeneric('programWaitlist', id); + + return ( + + ); +} diff --git a/frontend/src/components/ServiceOccurrences.js b/frontend/src/components/ServiceOccurrences.js index 2621550e..9f8bc511 100644 --- a/frontend/src/components/ServiceOccurrences.js +++ b/frontend/src/components/ServiceOccurrences.js @@ -20,6 +20,21 @@ const columnsWithoutOptions = [ }, sortBy: ({serviceName}) => serviceName, }, + { + label: 'Occupancy', + body: ({occupancy, capacity}) => { + return `${occupancy}/${capacity ?? '∞'}`; + }, + sortBy: ({occupancy, capacity}) => { + if (!!capacity) { + return occupancy / capacity; + } else { + // If capacity is 0, occupancy must also be 0 (letting 0 / 0 = 0) + // If capacity is unlimited, % occupancy is 0 + return 0; + } + }, + }, // { // label: 'Category', // body: ({category}) => category @@ -46,6 +61,8 @@ export default function ServiceOccurrences() { if (serviceOccurrence.description) { serviceOccurrenceData.description = serviceOccurrence.description; } + serviceOccurrenceData.capacity = serviceOccurrence.capacity; + serviceOccurrenceData.occupancy = serviceOccurrence.occupancy; if (serviceOccurrence.occurrenceOf) { serviceOccurrenceData.serviceID = serviceOccurrence.occurrenceOf._id; serviceOccurrenceData.serviceName = serviceOccurrence.occurrenceOf.name; diff --git a/frontend/src/components/ServiceWaitlists.js b/frontend/src/components/ServiceWaitlists.js new file mode 100644 index 00000000..bf48485e --- /dev/null +++ b/frontend/src/components/ServiceWaitlists.js @@ -0,0 +1,69 @@ +import React from 'react'; +import { Link } from './shared'; +import { GenericPage } from "./shared"; +import { deleteSingleGeneric, fetchMultipleGeneric, fetchSingleGeneric } from "../api/genericDataApi"; +import {getAddressCharacteristicId} from "./shared/CharacteristicIds"; + +const TYPE = 'serviceWaitlist'; + +const columnsWithoutOptions = [ + { + label: 'Service Occurrence ID', + body: ({serviceOccurrence}) => {serviceOccurrence?._id} + + }, + { + label: 'Clients Waitlisted', + body: ({waitlist}) => waitlist ? waitlist.length : 0 + }, + // { + // label: 'Category', + // body: ({category}) => category + // } +]; + +export default function ServiceWaitlists() { + + const nameFormatter = serviceWaitlist => { + if (serviceWaitlist.description) { + return serviceWaitlist.description; + } else { + return 'Service Waitlist ' + serviceWaitlist._id; + } + } + + const linkFormatter = serviceWaitlist => `/${TYPE}/${serviceWaitlist._id}`; + + const fetchData = async () => { + const addressCharacteristicId = await getAddressCharacteristicId(); + const serviceWaitlists = (await fetchMultipleGeneric(TYPE)).data; + console.log(serviceWaitlists) + const data = []; + for (const serviceWaitlist of serviceWaitlists) { + const serviceWaitlistData = {_id: serviceWaitlist._id}; + if (serviceWaitlist.waitlist) + serviceWaitlistData.waitlist = serviceWaitlist.waitlist; + if (serviceWaitlist.serviceOccurrence) + serviceWaitlistData.serviceOccurrence = serviceWaitlist.serviceOccurrence; + data.push(serviceWaitlistData); + } + return data; + }; + + const deleteService = (id) => deleteSingleGeneric('serviceWaitlist', id); + + return ( + + ); +} diff --git a/frontend/src/components/layouts/TopNavbar.js b/frontend/src/components/layouts/TopNavbar.js index 2f2c826b..7aa01944 100644 --- a/frontend/src/components/layouts/TopNavbar.js +++ b/frontend/src/components/layouts/TopNavbar.js @@ -131,6 +131,13 @@ function TopNavBar() { Program Occurrences + + + + + + Program Waitlists + @@ -146,6 +153,13 @@ function TopNavBar() { Service Occurrences + + + + + Service Waitlists + + diff --git a/frontend/src/components/programOccurrence/ProgramOccurrence.js b/frontend/src/components/programOccurrence/ProgramOccurrence.js index 63c344a2..8e6624e7 100644 --- a/frontend/src/components/programOccurrence/ProgramOccurrence.js +++ b/frontend/src/components/programOccurrence/ProgramOccurrence.js @@ -2,10 +2,23 @@ import React, {useEffect, useState} from 'react'; import GenericForm from "../shared/GenericForm"; import {fetchInternalTypeByFormType} from "../../api/internalTypeApi"; import {ProgramAndNeedSatisfierField} from "./ProgramAndNeedSatisfierField"; +import {fetchCharacteristics} from '../../api/characteristicApi'; +import {CapacityField} from '../shared/CapacityField'; export default function ProgramOccurrenceForm() { const formType = 'programOccurrence'; + const [characteristics, setCharacteristics] = useState({}); + useEffect(() => { + fetchCharacteristics().then(characteristics => { + const data = {}; + for (const {implementation, name, _id} of characteristics.data) { + data[name] = {implementation, _id} + } + setCharacteristics(data); + }); + }, []); + const [internalTypes, setInternalTypes] = useState({}); useEffect(() => { fetchInternalTypeByFormType(formType).then(({internalTypes}) => { @@ -16,6 +29,7 @@ export default function ProgramOccurrenceForm() { setInternalTypes(data); }); }, []); + const handleRenderField = ({required, id, type, implementation, content, _id}, index, fields, handleChange) => { if (implementation.optionsFromClass?.endsWith("#Program") ) { // Render Program & Program Occurrence & Need Satisfier @@ -26,6 +40,11 @@ export default function ProgramOccurrenceForm() { } else if (implementation.optionsFromClass?.endsWith('#NeedSatisfier')) { return ""; + } else if (implementation.label === 'Capacity') { + return ; + } else if (implementation.label === 'Occupancy') { + return ''; // Not editable by the user } } diff --git a/frontend/src/components/programProvision/ProgramAndOccurrenceAndNeedSatisfierField.js b/frontend/src/components/programProvision/ProgramAndOccurrenceAndNeedSatisfierField.js index c0ed2790..09a5d995 100644 --- a/frontend/src/components/programProvision/ProgramAndOccurrenceAndNeedSatisfierField.js +++ b/frontend/src/components/programProvision/ProgramAndOccurrenceAndNeedSatisfierField.js @@ -16,7 +16,9 @@ export function ProgramAndOccurrenceAndNeedSatisfierField({ programOccurrenceFieldId, needSatisfierFieldId, handleChange, - fixedProgramId // full URI of the program which all shown occurrences must be of, if given + changeProgramOcc, + fixedProgramId, // full URI of the program which all shown occurrences must be of, if given + ...others }) { const programKey = programFieldId ? `internalType_${programFieldId}` : null; const programOccKey = `internalType_${programOccurrenceFieldId}`; @@ -52,6 +54,8 @@ export function ProgramAndOccurrenceAndNeedSatisfierField({ const value = e.target.value; setSelectedProgramOcc(value); handleChange(key)(e); + if (changeProgramOcc) + changeProgramOcc(value); } useEffect(() => { @@ -64,6 +68,8 @@ export function ProgramAndOccurrenceAndNeedSatisfierField({ // unset program occurrence after another program is selected if (!firstProgram.current) { setSelectedProgramOcc(null); + if (changeProgramOcc) + changeProgramOcc(null); handleChange(programOccKey)(null); } setLoadingProgramOcc(false); @@ -103,7 +109,7 @@ export function ProgramAndOccurrenceAndNeedSatisfierField({ {showProgram ? + controlled {...others}/> : null } {showProgramOcc ? @@ -111,7 +117,7 @@ export function ProgramAndOccurrenceAndNeedSatisfierField({
+ onChange={handleChangeProgramOcc(programOccKey)} controlled {...others}/>
: null @@ -121,7 +127,7 @@ export function ProgramAndOccurrenceAndNeedSatisfierField({
+ onChange={handleChange(needSatisfierKey)} controlled {...others}/>
: null diff --git a/frontend/src/components/programRegistration/ProgramOccurrenceAndStatusField.js b/frontend/src/components/programRegistration/ProgramOccurrenceAndStatusField.js new file mode 100644 index 00000000..ea2907d2 --- /dev/null +++ b/frontend/src/components/programRegistration/ProgramOccurrenceAndStatusField.js @@ -0,0 +1,130 @@ +import React, {useEffect, useState} from 'react'; +import {useParams} from 'react-router-dom'; +import {fetchInternalTypeByFormType} from "../../api/internalTypeApi"; +import {ProgramAndOccurrenceAndNeedSatisfierField} from "../programProvision/ProgramAndOccurrenceAndNeedSatisfierField"; +import {fetchCharacteristics} from '../../api/characteristicApi'; +import {getInstancesInClass} from '../../api/dynamicFormApi'; +import {fetchSingleGeneric} from '../../api/genericDataApi'; +import {Box, Fade} from '@mui/material'; +import {Loading} from '../shared'; +import SelectField from '../shared/fields/SelectField'; + +export default function ProgramOccurrenceAndStatusField({handleChange, fields, serviceOrProgramId, formType}) { + const {id} = useParams(); + const mode = id ? 'edit' : 'new'; + const [characteristics, setCharacteristics] = useState(null); + const [internalTypes, setInternalTypes] = useState(null); + const [allStatusOptions, setAllStatusOptions] = useState(null); + const [programOccurrenceFieldId, setProgramOccurrenceFieldId] = useState(null); + const [programOccKey, setProgramOccKey] = useState(null); + const statusFieldKey = `characteristic_${characteristics?.['Registration Status']._id}`; + const [selectedProgramOcc, setSelectedProgramOcc] = useState(null); + const [statusOptions, setstatusOptions] = useState(null); + + useEffect(() => { + fetchCharacteristics().then(characteristics => { + const data = {}; + for (const {implementation, name, _id} of characteristics.data) { + data[name] = {implementation, _id} + } + setCharacteristics(data); + }); + }, []); + + useEffect(() => { + fetchInternalTypeByFormType(formType).then(({internalTypes}) => { + const data = {} + for (const {implementation, name, _id} of internalTypes) { + data[name] = {implementation, _id} + } + setInternalTypes(data); + }); + }, []); + + useEffect(() => { + getInstancesInClass(':RegistrationStatus') + .then(options => setAllStatusOptions(options)); + }, []); + + useEffect(() => { + setProgramOccurrenceFieldId(internalTypes?.programOccurrenceForProgramRegistration._id); + }, [internalTypes]); + + useEffect(() => { + setProgramOccKey(`internalType_${programOccurrenceFieldId}`); + }, [programOccurrenceFieldId]); + + useEffect(() => { + setSelectedProgramOcc(fields[programOccKey]); + }, [programOccKey]); + + useEffect(() => { + if (!selectedProgramOcc || !allStatusOptions || !characteristics) return; + fetchSingleGeneric('programOccurrence', selectedProgramOcc.split('_')[1]) + .then(occ => { + const occupancy = occ.data[`characteristic_${characteristics['Occupancy']._id}`]; + const capacity = occ.data[`characteristic_${characteristics['Capacity']._id}`]; + const hasCapacity = (capacity == null) || (occupancy < capacity); + + const registeredUri = Object.keys(allStatusOptions).find(uri => allStatusOptions[uri] === 'Registered'); + const notRegisteredUri = Object.keys(allStatusOptions).find(uri => allStatusOptions[uri] === 'Not Registered'); + const waitlistedUri = Object.keys(allStatusOptions).find(uri => allStatusOptions[uri] === 'Waitlisted'); + + let options = {}; + if (mode === 'new') { + if (hasCapacity) { + options[registeredUri] = 'Register'; + options[notRegisteredUri] = 'Save without registering'; + } else { + options[waitlistedUri] = 'Waitlist'; + options[notRegisteredUri] = 'Save without waitlisting'; + } + setstatusOptions(options); + } else { + fetchSingleGeneric('programRegistration', id) + .then(reg => { + const status = allStatusOptions[reg.data[`characteristic_${characteristics['Registration Status']._id}`]]; + if (status === 'Registered') { + options[registeredUri] = 'Remain registered'; + options[notRegisteredUri] = 'Unregister'; + } else if (status === 'Waitlisted') { + options[waitlistedUri] = 'Stay on waitlist'; + options[notRegisteredUri] = 'Withdraw from waitlist'; + } else { + // If updating a not-registered registration, options are the same as when creating a new registration + if (hasCapacity) { + options[registeredUri] = 'Register'; + options[notRegisteredUri] = 'Save without registering'; + } else { + options[waitlistedUri] = 'Waitlist'; + options[notRegisteredUri] = 'Save without waitlisting'; + } + } + setstatusOptions(options); + }); + } + }); + }, [selectedProgramOcc, allStatusOptions, characteristics]); + + if (!characteristics || !internalTypes) { + return ; + } + + return <> + {/* Render Program & Program Occurrence & Need Satisfier */} + setSelectedProgramOcc(value)} disabled={mode === 'edit'}/> + {!!selectedProgramOcc && !!statusOptions ? + +
+ +
+
+ : null + } + +} \ No newline at end of file diff --git a/frontend/src/components/programRegistration/ProgramRegistrationForm.js b/frontend/src/components/programRegistration/ProgramRegistrationForm.js index d320f2e3..68b3e256 100644 --- a/frontend/src/components/programRegistration/ProgramRegistrationForm.js +++ b/frontend/src/components/programRegistration/ProgramRegistrationForm.js @@ -1,12 +1,10 @@ import React, {useEffect, useState} from 'react'; import GenericForm from "../shared/GenericForm"; -import { useParams } from "react-router-dom"; import {fetchInternalTypeByFormType} from "../../api/internalTypeApi"; import {ClientAndNeedOccurrenceField} from "../serviceProvision/ClientAndNeedOccurrenceField"; -import {ProgramAndOccurrenceAndNeedSatisfierField} from "../programProvision/ProgramAndOccurrenceAndNeedSatisfierField"; +import ProgramOccurrenceAndStatusField from './ProgramOccurrenceAndStatusField'; export default function ProgramRegistrationForm() { - const formType = 'programRegistration'; const [internalTypes, setInternalTypes] = useState({}); @@ -19,6 +17,7 @@ export default function ProgramRegistrationForm() { setInternalTypes(data); }); }, []); + const handleRenderField = ({required, id, type, implementation, content, serviceOrProgramId}, index, fields, handleChange) => { console.log(implementation) if (implementation.optionsFromClass?.endsWith("#Client")) { @@ -27,19 +26,12 @@ export default function ProgramRegistrationForm() { clientFieldId={internalTypes.clientForProgramRegistration._id} needOccFieldId={internalTypes.needOccurrenceForProgramRegistration._id}/> } else if (implementation.optionsFromClass?.endsWith("#ProgramOccurrence")) { - const programOccurrenceFieldId = internalTypes.programOccurrenceForProgramRegistration._id; - - if (!programOccurrenceFieldId) { - return ; - } - - // Render Program & Program Occurrence & Need Satisfier - return + return ; } else if (implementation.optionsFromClass?.endsWith("#NeedOccurrence")) { return ""; + } else if (implementation.label === "Registration Status") { + return ''; } } return ( diff --git a/frontend/src/components/programRegistration/ProgramRegistrations.js b/frontend/src/components/programRegistration/ProgramRegistrations.js index 86869606..a0b0b988 100644 --- a/frontend/src/components/programRegistration/ProgramRegistrations.js +++ b/frontend/src/components/programRegistration/ProgramRegistrations.js @@ -1,8 +1,9 @@ import React from 'react'; import {Link} from "../shared" -import { GenericPage } from "../shared"; -import { deleteSingleGeneric, fetchMultipleGeneric, fetchSingleGeneric } from "../../api/genericDataApi"; +import {GenericPage} from "../shared"; +import {deleteSingleGeneric, fetchMultipleGeneric} from "../../api/genericDataApi"; import {getAddressCharacteristicId} from "../shared/CharacteristicIds"; +import {getInstancesInClass} from '../../api/dynamicFormApi'; const TYPE = 'programRegistrations'; @@ -16,8 +17,8 @@ const columnsWithoutOptions = [ }, { label: 'Status', - body: ({referralStatus}) =>{ - return referralStatus + body: ({status}) =>{ + return status; } }, // { @@ -39,15 +40,14 @@ export default function ProgramRegistrations() { const fetchData = async () => { const addressCharacteristicId = await getAddressCharacteristicId(); const programs = (await fetchMultipleGeneric('programRegistration')).data; + const statuses = await getInstancesInClass(':RegistrationStatus'); const data = []; for (const program of programs) { const programData = {_id: program._id}; if (program.characteristicOccurrences) for (const occ of program.characteristicOccurrences) { - if (occ.occurrenceOf?.name === 'Referral Type') { - programData.referralType = occ.dataStringValue; - } else if (occ.occurrenceOf?.name === 'Referral Status') { - programData.referralStatus = occ.objectValue; + if (occ.occurrenceOf?.name === 'Registration Status') { + programData.status = statuses[occ.dataStringValue]; } } if (program.address) diff --git a/frontend/src/components/programWaitlist/ProgramWaitlist.js b/frontend/src/components/programWaitlist/ProgramWaitlist.js new file mode 100644 index 00000000..2878303f --- /dev/null +++ b/frontend/src/components/programWaitlist/ProgramWaitlist.js @@ -0,0 +1,23 @@ +import React, {useEffect, useState} from 'react'; +import GenericForm from "../shared/GenericForm"; +import {fetchInternalTypeByFormType} from "../../api/internalTypeApi"; + +export default function ProgramWaitlistForm() { + const formType = 'programWaitlist'; + + const [internalTypes, setInternalTypes] = useState({}); + useEffect(() => { + fetchInternalTypeByFormType(formType).then(({internalTypes}) => { + const data = {} + for (const {implementation, name, _id} of internalTypes) { + data[name] = {implementation, _id} + } + setInternalTypes(data); + console.log('programWaitlist internalTypes', data); + }); + }, []); + + return ( + + ); +}; \ No newline at end of file diff --git a/frontend/src/components/programWaitlist/programWaitlists.js b/frontend/src/components/programWaitlist/programWaitlists.js new file mode 100644 index 00000000..9a62c906 --- /dev/null +++ b/frontend/src/components/programWaitlist/programWaitlists.js @@ -0,0 +1,86 @@ +import React from 'react' +import { Link } from '../shared'; +import { GenericPage } from "../shared"; +import { deleteSingleGeneric, fetchMultipleGeneric, fetchSingleGeneric } from "../../api/genericDataApi"; +import { getInstancesInClass } from "../../api/dynamicFormApi"; +import {getAddressCharacteristicId} from "../shared/CharacteristicIds"; + +const TYPE = 'programWaitlists'; + +const columnsWithoutOptions = [ + { + label: 'ID', + body: ({_id}) => { + console.log(_id); + console.log({_id}); + + return {_id} + } + }, + { + label: 'Program Occurrence', + body: ({programOccurrence}) => { + console.log(programOccurrence); + console.log(programOccurrence._id); + return programOccurrence._id; + } + }, + { + label: 'Waitlist Size', + body: ({waitlist}) => { + if (waitlist) { + console.log(waitlist); + console.log(waitlist.length); + return waitlist.length; + } else { + return ""; + } + } + }, + +]; + +export default function ProgramWaitlists() { + + const nameFormatter = programWaitlist => 'Program Waitlist' + programWaitlist._id; + + const linkFormatter = needOccurrence => `/${TYPE}/${programWaitlist._id}`; + + const fetchData = async () => { + const addressCharacteristicId = await getAddressCharacteristicId(); + const programWaitlists = (await fetchMultipleGeneric('programWatilist')).data; + const data = []; + for (const programWaitlist of programWaitlists) { + console.log(programWaitlist._id); + console.log(programWaitlist.programOccurrence); + console.log(programWaitlist.waitlist); + + + const programWaitlistData = { + _id: programWaitlist._id, + programOccurrence: programWaitlist.programOccurrence, + waitlist: programWaitlist.waitlist + }; + data.push(programWaitlistData); + console.log(data) + } + return data; + } + + const deleteProgramWaitlist = (id) => deleteSingleGeneric('programWaitlist', id); + + return ( + + ) +} diff --git a/frontend/src/components/programWaitlist/visualizeProgramWaitlist.js b/frontend/src/components/programWaitlist/visualizeProgramWaitlist.js new file mode 100644 index 00000000..c00ee8ea --- /dev/null +++ b/frontend/src/components/programWaitlist/visualizeProgramWaitlist.js @@ -0,0 +1,11 @@ +import React from "react"; + +import VisualizeGeneric from "../shared/visualizeGeneric"; + +/** + * This function is the frontend for visualizing single service + * @returns {JSX.Element} + */ +export default function VisualizeProgramWaitlist() { + return +} \ No newline at end of file diff --git a/frontend/src/components/serviceOccurrence/ServiceOccurrence.js b/frontend/src/components/serviceOccurrence/ServiceOccurrence.js index a0da645d..97f27875 100644 --- a/frontend/src/components/serviceOccurrence/ServiceOccurrence.js +++ b/frontend/src/components/serviceOccurrence/ServiceOccurrence.js @@ -2,10 +2,23 @@ import React, {useEffect, useState} from 'react'; import GenericForm from "../shared/GenericForm"; import {fetchInternalTypeByFormType} from "../../api/internalTypeApi"; import {ServiceAndNeedSatisfierField} from "./ServiceAndNeedSatisfierField"; +import {fetchCharacteristics} from '../../api/characteristicApi'; +import {CapacityField} from '../shared/CapacityField'; export default function ServiceOccurrenceForm() { const formType = 'serviceOccurrence'; + const [characteristics, setCharacteristics] = useState({}); + useEffect(() => { + fetchCharacteristics().then(characteristics => { + const data = {}; + for (const {implementation, name, _id} of characteristics.data) { + data[name] = {implementation, _id} + } + setCharacteristics(data); + }); + }, []); + const [internalTypes, setInternalTypes] = useState({}); useEffect(() => { fetchInternalTypeByFormType(formType).then(({internalTypes}) => { @@ -17,6 +30,7 @@ export default function ServiceOccurrenceForm() { console.log('serviceOccurrence internalTypes', data); }); }, []); + const handleRenderField = ({required, id, type, implementation, content, _id}, index, fields, handleChange,step) => { if (implementation.optionsFromClass?.endsWith("#Service")) { // Render Service & Service Occurrence & Need Satisfier @@ -27,6 +41,11 @@ export default function ServiceOccurrenceForm() { } else if (implementation.optionsFromClass?.endsWith('#NeedSatisfier')) { return ""; + } else if (implementation.label === 'Capacity') { + return ; + } else if (implementation.label === 'Occupancy') { + return ''; // Not editable by the user } } diff --git a/frontend/src/components/serviceProvision/ServiceAndOccurrenceAndNeedSatisfierField.js b/frontend/src/components/serviceProvision/ServiceAndOccurrenceAndNeedSatisfierField.js index 08795715..cb6a705b 100644 --- a/frontend/src/components/serviceProvision/ServiceAndOccurrenceAndNeedSatisfierField.js +++ b/frontend/src/components/serviceProvision/ServiceAndOccurrenceAndNeedSatisfierField.js @@ -16,7 +16,9 @@ export function ServiceAndOccurrenceAndNeedSatisfierField({ serviceOccurrenceFieldId, needSatisfierFieldId, handleChange, - fixedServiceId // full URI of the service which all shown occurrences must be of, if given + changeServiceOcc, + fixedServiceId, // full URI of the service which all shown occurrences must be of, if given + ...others }) { const serviceKey = serviceFieldId ? `internalType_${serviceFieldId}` : null; const serviceOccKey = `internalType_${serviceOccurrenceFieldId}`; @@ -52,6 +54,8 @@ export function ServiceAndOccurrenceAndNeedSatisfierField({ const value = e.target.value; setSelectedServiceOcc(value); handleChange(key)(e); + if (changeServiceOcc) + changeServiceOcc(value); } useEffect(() => { @@ -64,6 +68,8 @@ export function ServiceAndOccurrenceAndNeedSatisfierField({ // unset service occurrence after another service is selected if (!firstService.current) { setSelectedServiceOcc(null); + if (changeServiceOcc) + changeServiceOcc(null); handleChange(serviceOccKey)(null); } setLoadingServiceOcc(false); @@ -103,7 +109,7 @@ export function ServiceAndOccurrenceAndNeedSatisfierField({ {showService ? + controlled {...others}/> : null } {showServiceOcc ? @@ -111,7 +117,7 @@ export function ServiceAndOccurrenceAndNeedSatisfierField({
+ onChange={handleChangeServiceOcc(serviceOccKey)} controlled {...others}/>
: null @@ -121,7 +127,7 @@ export function ServiceAndOccurrenceAndNeedSatisfierField({
+ onChange={handleChange(needSatisfierKey)} controlled {...others}/>
: null diff --git a/frontend/src/components/serviceRegistration/ServiceOccurrenceAndStatusField.js b/frontend/src/components/serviceRegistration/ServiceOccurrenceAndStatusField.js new file mode 100644 index 00000000..8a6bf3cf --- /dev/null +++ b/frontend/src/components/serviceRegistration/ServiceOccurrenceAndStatusField.js @@ -0,0 +1,130 @@ +import React, {useEffect, useState} from 'react'; +import {useParams} from 'react-router-dom'; +import {fetchInternalTypeByFormType} from "../../api/internalTypeApi"; +import {ServiceAndOccurrenceAndNeedSatisfierField} from "../serviceProvision/ServiceAndOccurrenceAndNeedSatisfierField"; +import {fetchCharacteristics} from '../../api/characteristicApi'; +import {getInstancesInClass} from '../../api/dynamicFormApi'; +import {fetchSingleGeneric} from '../../api/genericDataApi'; +import {Box, Fade} from '@mui/material'; +import {Loading} from '../shared'; +import SelectField from '../shared/fields/SelectField'; + +export default function ServiceOccurrenceAndStatusField({handleChange, fields, serviceOrProgramId, formType}) { + const {id} = useParams(); + const mode = id ? 'edit' : 'new'; + const [characteristics, setCharacteristics] = useState(null); + const [internalTypes, setInternalTypes] = useState(null); + const [allStatusOptions, setAllStatusOptions] = useState(null); + const [serviceOccurrenceFieldId, setServiceOccurrenceFieldId] = useState(null); + const [serviceOccKey, setServiceOccKey] = useState(null); + const statusFieldKey = `characteristic_${characteristics?.['Registration Status']._id}`; + const [selectedServiceOcc, setSelectedServiceOcc] = useState(null); + const [statusOptions, setstatusOptions] = useState(null); + + useEffect(() => { + fetchCharacteristics().then(characteristics => { + const data = {}; + for (const {implementation, name, _id} of characteristics.data) { + data[name] = {implementation, _id} + } + setCharacteristics(data); + }); + }, []); + + useEffect(() => { + fetchInternalTypeByFormType(formType).then(({internalTypes}) => { + const data = {} + for (const {implementation, name, _id} of internalTypes) { + data[name] = {implementation, _id} + } + setInternalTypes(data); + }); + }, []); + + useEffect(() => { + getInstancesInClass(':RegistrationStatus') + .then(options => setAllStatusOptions(options)); + }, []); + + useEffect(() => { + setServiceOccurrenceFieldId(internalTypes?.serviceOccurrenceForServiceRegistration._id); + }, [internalTypes]); + + useEffect(() => { + setServiceOccKey(`internalType_${serviceOccurrenceFieldId}`); + }, [serviceOccurrenceFieldId]); + + useEffect(() => { + setSelectedServiceOcc(fields[serviceOccKey]); + }, [serviceOccKey]); + + useEffect(() => { + if (!selectedServiceOcc || !allStatusOptions || !characteristics) return; + fetchSingleGeneric('serviceOccurrence', selectedServiceOcc.split('_')[1]) + .then(occ => { + const occupancy = occ.data[`characteristic_${characteristics['Occupancy']._id}`]; + const capacity = occ.data[`characteristic_${characteristics['Capacity']._id}`]; + const hasCapacity = (capacity == null) || (occupancy < capacity); + + const registeredUri = Object.keys(allStatusOptions).find(uri => allStatusOptions[uri] === 'Registered'); + const notRegisteredUri = Object.keys(allStatusOptions).find(uri => allStatusOptions[uri] === 'Not Registered'); + const waitlistedUri = Object.keys(allStatusOptions).find(uri => allStatusOptions[uri] === 'Waitlisted'); + + let options = {}; + if (mode === 'new') { + if (hasCapacity) { + options[registeredUri] = 'Register'; + options[notRegisteredUri] = 'Save without registering'; + } else { + options[waitlistedUri] = 'Waitlist'; + options[notRegisteredUri] = 'Save without waitlisting'; + } + setstatusOptions(options); + } else { + fetchSingleGeneric('serviceRegistration', id) + .then(reg => { + const status = allStatusOptions[reg.data[`characteristic_${characteristics['Registration Status']._id}`]]; + if (status === 'Registered') { + options[registeredUri] = 'Remain registered'; + options[notRegisteredUri] = 'Unregister'; + } else if (status === 'Waitlisted') { + options[waitlistedUri] = 'Stay on waitlist'; + options[notRegisteredUri] = 'Withdraw from waitlist'; + } else { + // If updating a not-registered registration, options are the same as when creating a new registration + if (hasCapacity) { + options[registeredUri] = 'Register'; + options[notRegisteredUri] = 'Save without registering'; + } else { + options[waitlistedUri] = 'Waitlist'; + options[notRegisteredUri] = 'Save without waitlisting'; + } + } + setstatusOptions(options); + }); + } + }); + }, [selectedServiceOcc, allStatusOptions, characteristics]); + + if (!characteristics || !internalTypes) { + return ; + } + + return <> + {/* Render Service & Service Occurrence & Need Satisfier */} + setSelectedServiceOcc(value)} disabled={mode === 'edit'}/> + {!!selectedServiceOcc && !!statusOptions ? + +
+ +
+
+ : null + } + +} \ No newline at end of file diff --git a/frontend/src/components/serviceRegistration/ServiceRegistrationForm.js b/frontend/src/components/serviceRegistration/ServiceRegistrationForm.js index 29867028..ee0cf34a 100644 --- a/frontend/src/components/serviceRegistration/ServiceRegistrationForm.js +++ b/frontend/src/components/serviceRegistration/ServiceRegistrationForm.js @@ -1,12 +1,10 @@ import React, {useEffect, useState} from 'react'; import GenericForm from "../shared/GenericForm"; -import { useParams } from "react-router-dom"; import {fetchInternalTypeByFormType} from "../../api/internalTypeApi"; import {ClientAndNeedOccurrenceField} from "../serviceProvision/ClientAndNeedOccurrenceField"; -import {ServiceAndOccurrenceAndNeedSatisfierField} from "../serviceProvision/ServiceAndOccurrenceAndNeedSatisfierField"; +import ServiceOccurrenceAndStatusField from './ServiceOccurrenceAndStatusField'; export default function ServiceRegistrationForm() { - const formType = 'serviceRegistration'; const [internalTypes, setInternalTypes] = useState({}); @@ -19,6 +17,7 @@ export default function ServiceRegistrationForm() { setInternalTypes(data); }); }, []); + const handleRenderField = ({required, id, type, implementation, content, serviceOrProgramId}, index, fields, handleChange) => { console.log(implementation) if (implementation.optionsFromClass?.endsWith("#Client")) { @@ -27,19 +26,12 @@ export default function ServiceRegistrationForm() { clientFieldId={internalTypes.clientForServiceRegistration._id} needOccFieldId={internalTypes.needOccurrenceForServiceRegistration._id}/> } else if (implementation.optionsFromClass?.endsWith("#ServiceOccurrence")) { - const serviceOccurrenceFieldId = internalTypes.serviceOccurrenceForServiceRegistration._id; - - if (!serviceOccurrenceFieldId) { - return ; - } - - // Render Service & Service Occurrence & Need Satisfier - return + return ; } else if (implementation.optionsFromClass?.endsWith("#NeedOccurrence")) { return ""; + } else if (implementation.label === "Registration Status") { + return ''; } } return ( diff --git a/frontend/src/components/serviceRegistration/ServiceRegistrations.js b/frontend/src/components/serviceRegistration/ServiceRegistrations.js index 5e1a4ea8..2bbef1d1 100644 --- a/frontend/src/components/serviceRegistration/ServiceRegistrations.js +++ b/frontend/src/components/serviceRegistration/ServiceRegistrations.js @@ -1,8 +1,9 @@ import React from 'react'; import {Link} from "../shared" -import { GenericPage } from "../shared"; -import { deleteSingleGeneric, fetchMultipleGeneric, fetchSingleGeneric } from "../../api/genericDataApi"; +import {GenericPage} from "../shared"; +import {deleteSingleGeneric, fetchMultipleGeneric} from "../../api/genericDataApi"; import {getAddressCharacteristicId} from "../shared/CharacteristicIds"; +import {getInstancesInClass} from '../../api/dynamicFormApi'; const TYPE = 'serviceRegistrations'; @@ -16,8 +17,8 @@ const columnsWithoutOptions = [ }, { label: 'Status', - body: ({referralStatus}) =>{ - return referralStatus + body: ({status}) =>{ + return status; } }, // { @@ -39,15 +40,14 @@ export default function ServiceRegistrations() { const fetchData = async () => { const addressCharacteristicId = await getAddressCharacteristicId(); const services = (await fetchMultipleGeneric('serviceRegistration')).data; + const statuses = await getInstancesInClass(':RegistrationStatus'); const data = []; for (const service of services) { const serviceData = {_id: service._id}; if (service.characteristicOccurrences) for (const occ of service.characteristicOccurrences) { - if (occ.occurrenceOf?.name === 'Referral Type') { - serviceData.referralType = occ.dataStringValue; - } else if (occ.occurrenceOf?.name === 'Referral Status') { - serviceData.referralStatus = occ.objectValue; + if (occ.occurrenceOf?.name === 'Registration Status') { + serviceData.status = statuses[occ.dataStringValue]; } } if (service.address) diff --git a/frontend/src/components/serviceWaitlist/ServiceWaitlist.js b/frontend/src/components/serviceWaitlist/ServiceWaitlist.js new file mode 100644 index 00000000..17aa6af5 --- /dev/null +++ b/frontend/src/components/serviceWaitlist/ServiceWaitlist.js @@ -0,0 +1,23 @@ +import React, {useEffect, useState} from 'react'; +import GenericForm from "../shared/GenericForm"; +import {fetchInternalTypeByFormType} from "../../api/internalTypeApi"; + +export default function ServiceWaitlistForm() { + const formType = 'serviceWaitlist'; + + const [internalTypes, setInternalTypes] = useState({}); + useEffect(() => { + fetchInternalTypeByFormType(formType).then(({internalTypes}) => { + const data = {} + for (const {implementation, name, _id} of internalTypes) { + data[name] = {implementation, _id} + } + setInternalTypes(data); + console.log('serviceWaitlist internalTypes', data); + }); + }, []); + + return ( + + ); +}; \ No newline at end of file diff --git a/frontend/src/components/serviceWaitlist/serviceWaitlists.js b/frontend/src/components/serviceWaitlist/serviceWaitlists.js new file mode 100644 index 00000000..6a8bcf2a --- /dev/null +++ b/frontend/src/components/serviceWaitlist/serviceWaitlists.js @@ -0,0 +1,93 @@ +import React from 'react' +import { Link } from '../shared'; +import { GenericPage } from "../shared"; +import { deleteSingleGeneric, fetchMultipleGeneric, fetchSingleGeneric } from "../../api/genericDataApi"; +import { getInstancesInClass } from "../../api/dynamicFormApi"; +import {getAddressCharacteristicId} from "../shared/CharacteristicIds"; + +const TYPE = 'serviceWaitlists'; + +const columnsWithoutOptions = [ + { + label: 'ID', + body: ({_id}) => { + console.log(_id); + console.log({_id}); + + return {_id} + } + }, + { + label: 'Service Occurrence', + body: ({serviceOccurrence}) => { + console.log(serviceOccurrence); + console.log(serviceOccurrence._id); + return serviceOccurrence._id; + } + }, + { + label: 'Waitlist Size', + body: ({waitlist}) => { + if (waitlist) { + console.log(waitlist); + console.log(waitlist.length); + return waitlist.length; + } else { + return ""; + } + } + }, + // { + // label: 'Description', + // body: ({desc}) => desc + // }, + // { + // label: 'Category', + // body: ({category}) => category + // } +]; + +export default function ServiceWaitlists() { + + const nameFormatter = serviceWaitlist => 'Service Waitlist' + serviceWaitlist._id; + + const linkFormatter = needOccurrence => `/${TYPE}/${serviceWaitlist._id}`; + + const fetchData = async () => { + const addressCharacteristicId = await getAddressCharacteristicId(); + const serviceWaitlists = (await fetchMultipleGeneric('serviceWatilist')).data; + const data = []; + for (const serviceWaitlist of serviceWaitlists) { + console.log(serviceWaitlist._id); + console.log(serviceWaitlist.serviceOccurrence); + console.log(serviceWaitlist.waitlist); + + + const serviceWaitlistData = { + _id: serviceWaitlist._id, + serviceOccurrence: serviceWaitlist.serviceOccurrence, + waitlist: serviceWaitlist.waitlist + }; + data.push(serviceWaitlistData); + console.log(data) + } + return data; + } + + const deleteServiceWaitlist = (id) => deleteSingleGeneric('serviceWaitlist', id); + + return ( + + ) +} diff --git a/frontend/src/components/serviceWaitlist/visualizeServiceWaitlist.js b/frontend/src/components/serviceWaitlist/visualizeServiceWaitlist.js new file mode 100644 index 00000000..6f1070dd --- /dev/null +++ b/frontend/src/components/serviceWaitlist/visualizeServiceWaitlist.js @@ -0,0 +1,11 @@ +import React from "react"; + +import VisualizeGeneric from "../shared/visualizeGeneric"; + +/** + * This function is the frontend for visualizing single service + * @returns {JSX.Element} + */ +export default function VisualizeServiceWaitlist() { + return +} \ No newline at end of file diff --git a/frontend/src/components/settings/ManageFormFields.js b/frontend/src/components/settings/ManageFormFields.js index 3d606240..a5f66e73 100644 --- a/frontend/src/components/settings/ManageFormFields.js +++ b/frontend/src/components/settings/ManageFormFields.js @@ -164,6 +164,15 @@ export default function ManageFormFields() { return; } + if ((formType) === 'client'){ + //the "48" is the internal type ID for "Needs" and is hardcoded in, there + //must be some sort of dynamic way to acquire this, which I will need to look into + const hasNeeds = usedInternalTypeIds.some(item => item = fetchInternalTypeByFormType('client')); + if (!hasNeeds){ + setErrors(errors => ({...errors, formName: 'Client Forms must have a needs category'})); + return; + } + } try { if (method === 'new') { diff --git a/frontend/src/components/shared/CapacityField.js b/frontend/src/components/shared/CapacityField.js new file mode 100644 index 00000000..d9b7966e --- /dev/null +++ b/frontend/src/components/shared/CapacityField.js @@ -0,0 +1,36 @@ +import React from "react"; +import {useState} from "react"; +import SelectField from "../shared/fields/SelectField"; +import {Fade} from "@mui/material"; +import FieldGroup from "./FieldGroup"; + +export function CapacityField({fields, handleChange, capacityFieldId}) { + const capacityFieldKey = `characteristic_${capacityFieldId}`; + const [isCapacityLimited, setIsCapacityLimited] = useState(typeof fields[capacityFieldKey] !== 'undefined' + ? 'Yes' : 'No'); + + const handleChangeCapacity = e => { + const value = e.target.value; + setIsCapacityLimited(value); + + // if the new isCapacityLimited value is No, unset the inner field + if (value === 'No') { + handleChange(capacityFieldKey)(null); + } + } + + return <> + handleChangeCapacity(e)}/> + {isCapacityLimited === "Yes" + ? +
+ +
+
+ : null + } + +} diff --git a/frontend/src/components/shared/GenericForm.js b/frontend/src/components/shared/GenericForm.js index c889ce41..e4450b95 100644 --- a/frontend/src/components/shared/GenericForm.js +++ b/frontend/src/components/shared/GenericForm.js @@ -209,7 +209,7 @@ export default function GenericForm({name, mainPage, isProvider, onRenderField}) }; const handleChange = typeAndId => (e) => { - form.fields[typeAndId] = e?.target ? e?.target?.value ?? undefined : e; + form.fields[typeAndId] = e?.target ? e?.target?.value ?? null : e; }; const getStepContent = stepIdx => { diff --git a/frontend/src/components/shared/fields/GeneralField.js b/frontend/src/components/shared/fields/GeneralField.js index 7a18d83f..e81e9112 100644 --- a/frontend/src/components/shared/fields/GeneralField.js +++ b/frontend/src/components/shared/fields/GeneralField.js @@ -35,12 +35,15 @@ export default function GeneralField({type, onChange, value: defaultValue, ...pr else return null; } - return defaultValue || ''; + return defaultValue ?? ''; }); const handleChange = useCallback(e => { - const val = e.target.value; + let val = e.target.value; setValue(val); + if (type === 'number' && val === '') { + val = null; + } onChange({target: {value: val}}); }, [onChange, type]); diff --git a/frontend/src/constants/provider_fields.js b/frontend/src/constants/provider_fields.js index 14f50d3e..f0f4bb48 100644 --- a/frontend/src/constants/provider_fields.js +++ b/frontend/src/constants/provider_fields.js @@ -20,6 +20,8 @@ export const allForms = { serviceOccurrence: 'Service Occurrence', serviceRegistration: 'Service Registration', serviceProvision: 'Service Provision', + serviceWaitlist: 'Service Waitlist', + programWaitlist: 'Program Waitlist', programOccurrence: 'Program Occurrence', programRegistration: 'Program Registration', programProvision: 'Program Provision', diff --git a/frontend/src/routes.js b/frontend/src/routes.js index f2035445..549da5ba 100644 --- a/frontend/src/routes.js +++ b/frontend/src/routes.js @@ -25,6 +25,10 @@ import AdminRoute from './components/routes/AdminRoute'; import Notifications from './components/Notifications'; import Providers from './components/Providers'; import ProviderForm from './components/providers/ProviderForm2'; +import ServiceWaitlists from './components/ServiceWaitlists'; +import ServiceWaitlistForm from './components/serviceWaitlist/ServiceWaitlist'; +import ProgramWaitlists from './components/ProgramWaitlists'; +import ProgramWaitlistForm from './components/programWaitlist/ProgramWaitlist'; import Services from './components/Services'; import Programs from './components/Programs'; // import ServiceForm from './components/services/ServiceForm' @@ -52,6 +56,8 @@ import VisualizeService from "./components/services/visualizeService"; import VisualizeProgram from "./components/programs/visualizeProgram"; import VisualizeServiceOccurrence from "./components/serviceOccurrence/visualizeServiceOccurrence"; import VisualizeProgramOccurrence from "./components/programOccurrence/visualizeProgramOccurrence"; +import VisualizeServiceWaitlist from "./components/serviceWaitlist/visualizeServiceWaitlist"; +import VisualizeProgramWaitlist from "./components/programWaitlist/visualizeProgramWaitlist"; import VisualizeServiceRegistration from "./components/serviceRegistration/visualizeServiceRegistration"; import VisualizeProgramRegistration from "./components/programRegistration/visualizeProgramRegistration"; import VisualizeServiceProvision from "./components/serviceProvision/visualizeServiceProvision"; @@ -177,6 +183,16 @@ const routes = ( }/> }/> }/> + + }/> + }/> + }/> + }/> + + }/> + }/> + }/> + }/> }/> }/>