From ddae534d47c4108d3411c6340b60c5ffacb862f9 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Fri, 8 Aug 2025 07:52:19 +0000 Subject: [PATCH] Ver 1 --- .gitignore | 5 + app-shell/src/_schema.json | 7 +- backend/src/db/api/classes.js | 4 + backend/src/db/api/enrollment.js | 596 ++++++++++++++++++ backend/src/db/api/parents.js | 4 + backend/src/db/migrations/1754636315840.js | 72 +++ backend/src/db/migrations/1754636343963.js | 54 ++ backend/src/db/migrations/1754636367157.js | 54 ++ backend/src/db/migrations/1754636391981.js | 49 ++ backend/src/db/migrations/1754636417758.js | 47 ++ backend/src/db/migrations/1754636444343.js | 49 ++ backend/src/db/migrations/1754636479419.js | 49 ++ backend/src/db/migrations/1754636510354.js | 47 ++ backend/src/db/migrations/1754636544515.js | 49 ++ backend/src/db/migrations/1754636607371.js | 49 ++ backend/src/db/migrations/1754636637272.js | 49 ++ backend/src/db/migrations/1754636663438.js | 49 ++ backend/src/db/migrations/1754636688009.js | 49 ++ backend/src/db/migrations/1754636712387.js | 49 ++ backend/src/db/migrations/1754636735623.js | 49 ++ backend/src/db/migrations/1754636758062.js | 52 ++ backend/src/db/migrations/1754636779970.js | 49 ++ backend/src/db/migrations/1754636803744.js | 49 ++ backend/src/db/migrations/1754636828945.js | 49 ++ backend/src/db/models/classes.js | 8 + backend/src/db/models/enrollment.js | 134 ++++ backend/src/db/models/parents.js | 8 + .../db/seeders/20200430130760-user-roles.js | 26 + .../db/seeders/20231127130745-sample-data.js | 206 +++++- backend/src/db/seeders/20250808065835.js | 87 +++ backend/src/index.js | 8 + backend/src/routes/enrollment.js | 496 +++++++++++++++ backend/src/services/enrollment.js | 114 ++++ backend/src/services/search.js | 28 + .../components/Enrollment/CardEnrollment.tsx | 286 +++++++++ .../components/Enrollment/ListEnrollment.tsx | 194 ++++++ .../components/Enrollment/TableEnrollment.tsx | 487 ++++++++++++++ .../Enrollment/configureEnrollmentCols.tsx | 304 +++++++++ .../components/WebPageComponents/Footer.tsx | 4 +- .../components/WebPageComponents/Header.tsx | 2 +- frontend/src/helpers/dataFormatter.js | 19 + frontend/src/menuAside.ts | 8 + frontend/src/pages/classes/classes-view.tsx | 113 ++++ frontend/src/pages/dashboard.tsx | 35 + .../src/pages/enrollment/[enrollmentId].tsx | 260 ++++++++ .../src/pages/enrollment/enrollment-edit.tsx | 258 ++++++++ .../src/pages/enrollment/enrollment-list.tsx | 181 ++++++ .../src/pages/enrollment/enrollment-new.tsx | 221 +++++++ .../src/pages/enrollment/enrollment-table.tsx | 180 ++++++ .../src/pages/enrollment/enrollment-view.tsx | 189 ++++++ frontend/src/pages/index.tsx | 2 +- frontend/src/pages/parents/parents-view.tsx | 113 ++++ frontend/src/pages/web_pages/about.tsx | 2 +- .../src/stores/enrollment/enrollmentSlice.ts | 236 +++++++ frontend/src/stores/store.ts | 2 + 55 files changed, 5776 insertions(+), 14 deletions(-) create mode 100644 backend/src/db/api/enrollment.js create mode 100644 backend/src/db/migrations/1754636315840.js create mode 100644 backend/src/db/migrations/1754636343963.js create mode 100644 backend/src/db/migrations/1754636367157.js create mode 100644 backend/src/db/migrations/1754636391981.js create mode 100644 backend/src/db/migrations/1754636417758.js create mode 100644 backend/src/db/migrations/1754636444343.js create mode 100644 backend/src/db/migrations/1754636479419.js create mode 100644 backend/src/db/migrations/1754636510354.js create mode 100644 backend/src/db/migrations/1754636544515.js create mode 100644 backend/src/db/migrations/1754636607371.js create mode 100644 backend/src/db/migrations/1754636637272.js create mode 100644 backend/src/db/migrations/1754636663438.js create mode 100644 backend/src/db/migrations/1754636688009.js create mode 100644 backend/src/db/migrations/1754636712387.js create mode 100644 backend/src/db/migrations/1754636735623.js create mode 100644 backend/src/db/migrations/1754636758062.js create mode 100644 backend/src/db/migrations/1754636779970.js create mode 100644 backend/src/db/migrations/1754636803744.js create mode 100644 backend/src/db/migrations/1754636828945.js create mode 100644 backend/src/db/models/enrollment.js create mode 100644 backend/src/db/seeders/20250808065835.js create mode 100644 backend/src/routes/enrollment.js create mode 100644 backend/src/services/enrollment.js create mode 100644 frontend/src/components/Enrollment/CardEnrollment.tsx create mode 100644 frontend/src/components/Enrollment/ListEnrollment.tsx create mode 100644 frontend/src/components/Enrollment/TableEnrollment.tsx create mode 100644 frontend/src/components/Enrollment/configureEnrollmentCols.tsx create mode 100644 frontend/src/pages/enrollment/[enrollmentId].tsx create mode 100644 frontend/src/pages/enrollment/enrollment-edit.tsx create mode 100644 frontend/src/pages/enrollment/enrollment-list.tsx create mode 100644 frontend/src/pages/enrollment/enrollment-new.tsx create mode 100644 frontend/src/pages/enrollment/enrollment-table.tsx create mode 100644 frontend/src/pages/enrollment/enrollment-view.tsx create mode 100644 frontend/src/stores/enrollment/enrollmentSlice.ts diff --git a/.gitignore b/.gitignore index e427ff3..d0eb167 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,8 @@ node_modules/ */node_modules/ */build/ + +**/node_modules/ +**/build/ +.DS_Store +.env \ No newline at end of file diff --git a/app-shell/src/_schema.json b/app-shell/src/_schema.json index 16acfdc..15d723a 100644 --- a/app-shell/src/_schema.json +++ b/app-shell/src/_schema.json @@ -1,5 +1,4 @@ - - { - "Initial version": "{\"iv\":\"YjROqyxU7X800RYM\",\"encryptedData\":\"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\"}" -} + "Initial version": "{\"iv\":\"YjROqyxU7X800RYM\",\"encryptedData\":\"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\"}", + "Ver 1": "{\"iv\":\"HdcJ9jw97f+Hg90B\",\"encryptedData\":\"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\"}" +} \ No newline at end of file diff --git a/backend/src/db/api/classes.js b/backend/src/db/api/classes.js index 9a342fc..d072747 100644 --- a/backend/src/db/api/classes.js +++ b/backend/src/db/api/classes.js @@ -153,6 +153,10 @@ module.exports = class ClassesDBApi { const output = classes.get({ plain: true }); + output.enrollment_class = await classes.getEnrollment_class({ + transaction, + }); + output.ai_tutor = await classes.getAi_tutor({ transaction, }); diff --git a/backend/src/db/api/enrollment.js b/backend/src/db/api/enrollment.js new file mode 100644 index 0000000..310ea2d --- /dev/null +++ b/backend/src/db/api/enrollment.js @@ -0,0 +1,596 @@ +const db = require('../models'); +const FileDBApi = require('./file'); +const crypto = require('crypto'); +const Utils = require('../utils'); + +const Sequelize = db.Sequelize; +const Op = Sequelize.Op; + +module.exports = class EnrollmentDBApi { + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const enrollment = await db.enrollment.create( + { + id: data.id || undefined, + + language: data.language || null, + board: data.board || null, + enrollmentmonth: data.enrollmentmonth || null, + studentname: data.studentname || null, + age: data.age || null, + gender: data.gender || null, + nationality: data.nationality || null, + dateofbirth: data.dateofbirth || null, + marksheeturl: data.marksheeturl || null, + parentgovtidurl: data.parentgovtidurl || null, + studentgovtidurl: data.studentgovtidurl || null, + feeamount: data.feeamount || null, + freetrial: data.freetrial || false, + + paymentmethod: data.paymentmethod || null, + paymentstatus: data.paymentstatus || null, + transactionid: data.transactionid || null, + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + await enrollment.setClass(data.class || null, { + transaction, + }); + + await enrollment.setParent(data.parent || null, { + transaction, + }); + + return enrollment; + } + + static async bulkImport(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + // Prepare data - wrapping individual data transformations in a map() method + const enrollmentData = data.map((item, index) => ({ + id: item.id || undefined, + + language: item.language || null, + board: item.board || null, + enrollmentmonth: item.enrollmentmonth || null, + studentname: item.studentname || null, + age: item.age || null, + gender: item.gender || null, + nationality: item.nationality || null, + dateofbirth: item.dateofbirth || null, + marksheeturl: item.marksheeturl || null, + parentgovtidurl: item.parentgovtidurl || null, + studentgovtidurl: item.studentgovtidurl || null, + feeamount: item.feeamount || null, + freetrial: item.freetrial || false, + + paymentmethod: item.paymentmethod || null, + paymentstatus: item.paymentstatus || null, + transactionid: item.transactionid || null, + importHash: item.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + // Bulk create items + const enrollment = await db.enrollment.bulkCreate(enrollmentData, { + transaction, + }); + + // For each item created, replace relation files + + return enrollment; + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const enrollment = await db.enrollment.findByPk(id, {}, { transaction }); + + const updatePayload = {}; + + if (data.language !== undefined) updatePayload.language = data.language; + + if (data.board !== undefined) updatePayload.board = data.board; + + if (data.enrollmentmonth !== undefined) + updatePayload.enrollmentmonth = data.enrollmentmonth; + + if (data.studentname !== undefined) + updatePayload.studentname = data.studentname; + + if (data.age !== undefined) updatePayload.age = data.age; + + if (data.gender !== undefined) updatePayload.gender = data.gender; + + if (data.nationality !== undefined) + updatePayload.nationality = data.nationality; + + if (data.dateofbirth !== undefined) + updatePayload.dateofbirth = data.dateofbirth; + + if (data.marksheeturl !== undefined) + updatePayload.marksheeturl = data.marksheeturl; + + if (data.parentgovtidurl !== undefined) + updatePayload.parentgovtidurl = data.parentgovtidurl; + + if (data.studentgovtidurl !== undefined) + updatePayload.studentgovtidurl = data.studentgovtidurl; + + if (data.feeamount !== undefined) updatePayload.feeamount = data.feeamount; + + if (data.freetrial !== undefined) updatePayload.freetrial = data.freetrial; + + if (data.paymentmethod !== undefined) + updatePayload.paymentmethod = data.paymentmethod; + + if (data.paymentstatus !== undefined) + updatePayload.paymentstatus = data.paymentstatus; + + if (data.transactionid !== undefined) + updatePayload.transactionid = data.transactionid; + + updatePayload.updatedById = currentUser.id; + + await enrollment.update(updatePayload, { transaction }); + + if (data.class !== undefined) { + await enrollment.setClass( + data.class, + + { transaction }, + ); + } + + if (data.parent !== undefined) { + await enrollment.setParent( + data.parent, + + { transaction }, + ); + } + + return enrollment; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const enrollment = await db.enrollment.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of enrollment) { + await record.update({ deletedBy: currentUser.id }, { transaction }); + } + for (const record of enrollment) { + await record.destroy({ transaction }); + } + }); + + return enrollment; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const enrollment = await db.enrollment.findByPk(id, options); + + await enrollment.update( + { + deletedBy: currentUser.id, + }, + { + transaction, + }, + ); + + await enrollment.destroy({ + transaction, + }); + + return enrollment; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const enrollment = await db.enrollment.findOne({ where }, { transaction }); + + if (!enrollment) { + return enrollment; + } + + const output = enrollment.get({ plain: true }); + + output.class = await enrollment.getClass({ + transaction, + }); + + output.parent = await enrollment.getParent({ + transaction, + }); + + return output; + } + + static async findAll(filter, options) { + const limit = filter.limit || 0; + let offset = 0; + let where = {}; + const currentPage = +filter.page; + + offset = currentPage * limit; + + const orderBy = null; + + const transaction = (options && options.transaction) || undefined; + + let include = [ + { + model: db.classes, + as: 'class', + + where: filter.class + ? { + [Op.or]: [ + { + id: { + [Op.in]: filter.class + .split('|') + .map((term) => Utils.uuid(term)), + }, + }, + { + subject: { + [Op.or]: filter.class + .split('|') + .map((term) => ({ [Op.iLike]: `%${term}%` })), + }, + }, + ], + } + : {}, + }, + + { + model: db.parents, + as: 'parent', + + where: filter.parent + ? { + [Op.or]: [ + { + id: { + [Op.in]: filter.parent + .split('|') + .map((term) => Utils.uuid(term)), + }, + }, + { + first_name: { + [Op.or]: filter.parent + .split('|') + .map((term) => ({ [Op.iLike]: `%${term}%` })), + }, + }, + ], + } + : {}, + }, + ]; + + if (filter) { + if (filter.id) { + where = { + ...where, + ['id']: Utils.uuid(filter.id), + }; + } + + if (filter.language) { + where = { + ...where, + [Op.and]: Utils.ilike('enrollment', 'language', filter.language), + }; + } + + if (filter.board) { + where = { + ...where, + [Op.and]: Utils.ilike('enrollment', 'board', filter.board), + }; + } + + if (filter.enrollmentmonth) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'enrollment', + 'enrollmentmonth', + filter.enrollmentmonth, + ), + }; + } + + if (filter.studentname) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'enrollment', + 'studentname', + filter.studentname, + ), + }; + } + + if (filter.gender) { + where = { + ...where, + [Op.and]: Utils.ilike('enrollment', 'gender', filter.gender), + }; + } + + if (filter.nationality) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'enrollment', + 'nationality', + filter.nationality, + ), + }; + } + + if (filter.marksheeturl) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'enrollment', + 'marksheeturl', + filter.marksheeturl, + ), + }; + } + + if (filter.parentgovtidurl) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'enrollment', + 'parentgovtidurl', + filter.parentgovtidurl, + ), + }; + } + + if (filter.studentgovtidurl) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'enrollment', + 'studentgovtidurl', + filter.studentgovtidurl, + ), + }; + } + + if (filter.paymentmethod) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'enrollment', + 'paymentmethod', + filter.paymentmethod, + ), + }; + } + + if (filter.paymentstatus) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'enrollment', + 'paymentstatus', + filter.paymentstatus, + ), + }; + } + + if (filter.transactionid) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'enrollment', + 'transactionid', + filter.transactionid, + ), + }; + } + + if (filter.ageRange) { + const [start, end] = filter.ageRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + age: { + ...where.age, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + age: { + ...where.age, + [Op.lte]: end, + }, + }; + } + } + + if (filter.dateofbirthRange) { + const [start, end] = filter.dateofbirthRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + dateofbirth: { + ...where.dateofbirth, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + dateofbirth: { + ...where.dateofbirth, + [Op.lte]: end, + }, + }; + } + } + + if (filter.feeamountRange) { + const [start, end] = filter.feeamountRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + feeamount: { + ...where.feeamount, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + feeamount: { + ...where.feeamount, + [Op.lte]: end, + }, + }; + } + } + + if (filter.active !== undefined) { + where = { + ...where, + active: filter.active === true || filter.active === 'true', + }; + } + + if (filter.freetrial) { + where = { + ...where, + freetrial: filter.freetrial, + }; + } + + if (filter.createdAtRange) { + const [start, end] = filter.createdAtRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.lte]: end, + }, + }; + } + } + } + + const queryOptions = { + where, + include, + distinct: true, + order: + filter.field && filter.sort + ? [[filter.field, filter.sort]] + : [['createdAt', 'desc']], + transaction: options?.transaction, + logging: console.log, + }; + + if (!options?.countOnly) { + queryOptions.limit = limit ? Number(limit) : undefined; + queryOptions.offset = offset ? Number(offset) : undefined; + } + + try { + const { rows, count } = await db.enrollment.findAndCountAll(queryOptions); + + return { + rows: options?.countOnly ? [] : rows, + count: count, + }; + } catch (error) { + console.error('Error executing query:', error); + throw error; + } + } + + static async findAllAutocomplete(query, limit, offset) { + let where = {}; + + if (query) { + where = { + [Op.or]: [ + { ['id']: Utils.uuid(query) }, + Utils.ilike('enrollment', 'studentname', query), + ], + }; + } + + const records = await db.enrollment.findAll({ + attributes: ['id', 'studentname'], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + orderBy: [['studentname', 'ASC']], + }); + + return records.map((record) => ({ + id: record.id, + label: record.studentname, + })); + } +}; diff --git a/backend/src/db/api/parents.js b/backend/src/db/api/parents.js index d3d2a4a..9c24ece 100644 --- a/backend/src/db/api/parents.js +++ b/backend/src/db/api/parents.js @@ -145,6 +145,10 @@ module.exports = class ParentsDBApi { transaction, }); + output.enrollment_parent = await parents.getEnrollment_parent({ + transaction, + }); + output.children = await parents.getChildren({ transaction, }); diff --git a/backend/src/db/migrations/1754636315840.js b/backend/src/db/migrations/1754636315840.js new file mode 100644 index 0000000..38b4a58 --- /dev/null +++ b/backend/src/db/migrations/1754636315840.js @@ -0,0 +1,72 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.createTable( + 'enrollment', + { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.DataTypes.UUIDV4, + primaryKey: true, + }, + createdById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + updatedById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + createdAt: { type: Sequelize.DataTypes.DATE }, + updatedAt: { type: Sequelize.DataTypes.DATE }, + deletedAt: { type: Sequelize.DataTypes.DATE }, + importHash: { + type: Sequelize.DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { transaction }, + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async down(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.dropTable('enrollment', { transaction }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1754636343963.js b/backend/src/db/migrations/1754636343963.js new file mode 100644 index 0000000..e06b6e6 --- /dev/null +++ b/backend/src/db/migrations/1754636343963.js @@ -0,0 +1,54 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.addColumn( + 'enrollment', + 'classId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'classes', + key: 'id', + }, + }, + { transaction }, + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async down(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.removeColumn('enrollment', 'classId', { + transaction, + }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1754636367157.js b/backend/src/db/migrations/1754636367157.js new file mode 100644 index 0000000..f591c55 --- /dev/null +++ b/backend/src/db/migrations/1754636367157.js @@ -0,0 +1,54 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.addColumn( + 'enrollment', + 'parentId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'parents', + key: 'id', + }, + }, + { transaction }, + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async down(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.removeColumn('enrollment', 'parentId', { + transaction, + }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1754636391981.js b/backend/src/db/migrations/1754636391981.js new file mode 100644 index 0000000..d466c2a --- /dev/null +++ b/backend/src/db/migrations/1754636391981.js @@ -0,0 +1,49 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.addColumn( + 'enrollment', + 'language', + { + type: Sequelize.DataTypes.TEXT, + }, + { transaction }, + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async down(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.removeColumn('enrollment', 'language', { + transaction, + }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1754636417758.js b/backend/src/db/migrations/1754636417758.js new file mode 100644 index 0000000..5506941 --- /dev/null +++ b/backend/src/db/migrations/1754636417758.js @@ -0,0 +1,47 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.addColumn( + 'enrollment', + 'board', + { + type: Sequelize.DataTypes.TEXT, + }, + { transaction }, + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async down(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.removeColumn('enrollment', 'board', { transaction }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1754636444343.js b/backend/src/db/migrations/1754636444343.js new file mode 100644 index 0000000..26580cd --- /dev/null +++ b/backend/src/db/migrations/1754636444343.js @@ -0,0 +1,49 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.addColumn( + 'enrollment', + 'enrollmentmonth', + { + type: Sequelize.DataTypes.TEXT, + }, + { transaction }, + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async down(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.removeColumn('enrollment', 'enrollmentmonth', { + transaction, + }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1754636479419.js b/backend/src/db/migrations/1754636479419.js new file mode 100644 index 0000000..57e8c1a --- /dev/null +++ b/backend/src/db/migrations/1754636479419.js @@ -0,0 +1,49 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.addColumn( + 'enrollment', + 'studentname', + { + type: Sequelize.DataTypes.TEXT, + }, + { transaction }, + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async down(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.removeColumn('enrollment', 'studentname', { + transaction, + }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1754636510354.js b/backend/src/db/migrations/1754636510354.js new file mode 100644 index 0000000..f3f9f1b --- /dev/null +++ b/backend/src/db/migrations/1754636510354.js @@ -0,0 +1,47 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.addColumn( + 'enrollment', + 'age', + { + type: Sequelize.DataTypes.INTEGER, + }, + { transaction }, + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async down(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.removeColumn('enrollment', 'age', { transaction }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1754636544515.js b/backend/src/db/migrations/1754636544515.js new file mode 100644 index 0000000..c4e6dcf --- /dev/null +++ b/backend/src/db/migrations/1754636544515.js @@ -0,0 +1,49 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.addColumn( + 'enrollment', + 'gender', + { + type: Sequelize.DataTypes.TEXT, + }, + { transaction }, + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async down(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.removeColumn('enrollment', 'gender', { + transaction, + }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1754636607371.js b/backend/src/db/migrations/1754636607371.js new file mode 100644 index 0000000..c399d10 --- /dev/null +++ b/backend/src/db/migrations/1754636607371.js @@ -0,0 +1,49 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.addColumn( + 'enrollment', + 'nationality', + { + type: Sequelize.DataTypes.TEXT, + }, + { transaction }, + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async down(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.removeColumn('enrollment', 'nationality', { + transaction, + }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1754636637272.js b/backend/src/db/migrations/1754636637272.js new file mode 100644 index 0000000..9762c58 --- /dev/null +++ b/backend/src/db/migrations/1754636637272.js @@ -0,0 +1,49 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.addColumn( + 'enrollment', + 'dateofbirth', + { + type: Sequelize.DataTypes.DATEONLY, + }, + { transaction }, + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async down(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.removeColumn('enrollment', 'dateofbirth', { + transaction, + }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1754636663438.js b/backend/src/db/migrations/1754636663438.js new file mode 100644 index 0000000..053d16e --- /dev/null +++ b/backend/src/db/migrations/1754636663438.js @@ -0,0 +1,49 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.addColumn( + 'enrollment', + 'marksheeturl', + { + type: Sequelize.DataTypes.TEXT, + }, + { transaction }, + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async down(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.removeColumn('enrollment', 'marksheeturl', { + transaction, + }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1754636688009.js b/backend/src/db/migrations/1754636688009.js new file mode 100644 index 0000000..94d6a3e --- /dev/null +++ b/backend/src/db/migrations/1754636688009.js @@ -0,0 +1,49 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.addColumn( + 'enrollment', + 'parentgovtidurl', + { + type: Sequelize.DataTypes.TEXT, + }, + { transaction }, + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async down(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.removeColumn('enrollment', 'parentgovtidurl', { + transaction, + }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1754636712387.js b/backend/src/db/migrations/1754636712387.js new file mode 100644 index 0000000..ce9288c --- /dev/null +++ b/backend/src/db/migrations/1754636712387.js @@ -0,0 +1,49 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.addColumn( + 'enrollment', + 'studentgovtidurl', + { + type: Sequelize.DataTypes.TEXT, + }, + { transaction }, + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async down(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.removeColumn('enrollment', 'studentgovtidurl', { + transaction, + }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1754636735623.js b/backend/src/db/migrations/1754636735623.js new file mode 100644 index 0000000..f177526 --- /dev/null +++ b/backend/src/db/migrations/1754636735623.js @@ -0,0 +1,49 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.addColumn( + 'enrollment', + 'feeamount', + { + type: Sequelize.DataTypes.INTEGER, + }, + { transaction }, + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async down(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.removeColumn('enrollment', 'feeamount', { + transaction, + }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1754636758062.js b/backend/src/db/migrations/1754636758062.js new file mode 100644 index 0000000..007be04 --- /dev/null +++ b/backend/src/db/migrations/1754636758062.js @@ -0,0 +1,52 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.addColumn( + 'enrollment', + 'freetrial', + { + type: Sequelize.DataTypes.BOOLEAN, + + defaultValue: false, + allowNull: false, + }, + { transaction }, + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async down(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.removeColumn('enrollment', 'freetrial', { + transaction, + }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1754636779970.js b/backend/src/db/migrations/1754636779970.js new file mode 100644 index 0000000..cd9301a --- /dev/null +++ b/backend/src/db/migrations/1754636779970.js @@ -0,0 +1,49 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.addColumn( + 'enrollment', + 'paymentmethod', + { + type: Sequelize.DataTypes.TEXT, + }, + { transaction }, + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async down(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.removeColumn('enrollment', 'paymentmethod', { + transaction, + }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1754636803744.js b/backend/src/db/migrations/1754636803744.js new file mode 100644 index 0000000..25218cb --- /dev/null +++ b/backend/src/db/migrations/1754636803744.js @@ -0,0 +1,49 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.addColumn( + 'enrollment', + 'paymentstatus', + { + type: Sequelize.DataTypes.TEXT, + }, + { transaction }, + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async down(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.removeColumn('enrollment', 'paymentstatus', { + transaction, + }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1754636828945.js b/backend/src/db/migrations/1754636828945.js new file mode 100644 index 0000000..6de3d61 --- /dev/null +++ b/backend/src/db/migrations/1754636828945.js @@ -0,0 +1,49 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.addColumn( + 'enrollment', + 'transactionid', + { + type: Sequelize.DataTypes.TEXT, + }, + { transaction }, + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async down(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.removeColumn('enrollment', 'transactionid', { + transaction, + }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/models/classes.js b/backend/src/db/models/classes.js index 2b675fe..26f5fed 100644 --- a/backend/src/db/models/classes.js +++ b/backend/src/db/models/classes.js @@ -60,6 +60,14 @@ module.exports = function (sequelize, DataTypes) { /// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity + db.classes.hasMany(db.enrollment, { + as: 'enrollment_class', + foreignKey: { + name: 'classId', + }, + constraints: false, + }); + //end loop db.classes.belongsTo(db.ai_tutors, { diff --git a/backend/src/db/models/enrollment.js b/backend/src/db/models/enrollment.js new file mode 100644 index 0000000..8bad175 --- /dev/null +++ b/backend/src/db/models/enrollment.js @@ -0,0 +1,134 @@ +const config = require('../../config'); +const providers = config.providers; +const crypto = require('crypto'); +const bcrypt = require('bcrypt'); +const moment = require('moment'); + +module.exports = function (sequelize, DataTypes) { + const enrollment = sequelize.define( + 'enrollment', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + + language: { + type: DataTypes.TEXT, + }, + + board: { + type: DataTypes.TEXT, + }, + + enrollmentmonth: { + type: DataTypes.TEXT, + }, + + studentname: { + type: DataTypes.TEXT, + }, + + age: { + type: DataTypes.INTEGER, + }, + + gender: { + type: DataTypes.TEXT, + }, + + nationality: { + type: DataTypes.TEXT, + }, + + dateofbirth: { + type: DataTypes.DATEONLY, + + get: function () { + return this.getDataValue('dateofbirth') + ? moment.utc(this.getDataValue('dateofbirth')).format('YYYY-MM-DD') + : null; + }, + }, + + marksheeturl: { + type: DataTypes.TEXT, + }, + + parentgovtidurl: { + type: DataTypes.TEXT, + }, + + studentgovtidurl: { + type: DataTypes.TEXT, + }, + + feeamount: { + type: DataTypes.INTEGER, + }, + + freetrial: { + type: DataTypes.BOOLEAN, + + allowNull: false, + defaultValue: false, + }, + + paymentmethod: { + type: DataTypes.TEXT, + }, + + paymentstatus: { + type: DataTypes.TEXT, + }, + + transactionid: { + type: DataTypes.TEXT, + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + enrollment.associate = (db) => { + /// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity + + //end loop + + db.enrollment.belongsTo(db.classes, { + as: 'class', + foreignKey: { + name: 'classId', + }, + constraints: false, + }); + + db.enrollment.belongsTo(db.parents, { + as: 'parent', + foreignKey: { + name: 'parentId', + }, + constraints: false, + }); + + db.enrollment.belongsTo(db.users, { + as: 'createdBy', + }); + + db.enrollment.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return enrollment; +}; diff --git a/backend/src/db/models/parents.js b/backend/src/db/models/parents.js index cf41ee6..81f6716 100644 --- a/backend/src/db/models/parents.js +++ b/backend/src/db/models/parents.js @@ -68,6 +68,14 @@ module.exports = function (sequelize, DataTypes) { constraints: false, }); + db.parents.hasMany(db.enrollment, { + as: 'enrollment_parent', + foreignKey: { + name: 'parentId', + }, + constraints: false, + }); + //end loop db.parents.belongsTo(db.users, { diff --git a/backend/src/db/seeders/20200430130760-user-roles.js b/backend/src/db/seeders/20200430130760-user-roles.js index 390b3b0..7d86111 100644 --- a/backend/src/db/seeders/20200430130760-user-roles.js +++ b/backend/src/db/seeders/20200430130760-user-roles.js @@ -92,6 +92,7 @@ module.exports = { 'students', 'roles', 'permissions', + 'enrollment', , ]; await queryInterface.bulkInsert( @@ -725,6 +726,31 @@ primary key ("roles_permissionsId", "permissionId") permissionId: getId('DELETE_PERMISSIONS'), }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('CREATE_ENROLLMENT'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('READ_ENROLLMENT'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('UPDATE_ENROLLMENT'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('DELETE_ENROLLMENT'), + }, + { createdAt, updatedAt, diff --git a/backend/src/db/seeders/20231127130745-sample-data.js b/backend/src/db/seeders/20231127130745-sample-data.js index 1887840..b5763f1 100644 --- a/backend/src/db/seeders/20231127130745-sample-data.js +++ b/backend/src/db/seeders/20231127130745-sample-data.js @@ -11,6 +11,8 @@ const Parents = db.parents; const Students = db.students; +const Enrollment = db.enrollment; + const AiTutorsData = [ { subject: 'Mathematics', @@ -153,9 +155,9 @@ const StudentsData = [ grade: '3', - preferred_language: 'Mandarin', + preferred_language: 'Spanish', - educational_board: 'CBSE', + educational_board: 'ICSE', // type code here for "relation_one" field }, @@ -169,7 +171,7 @@ const StudentsData = [ preferred_language: 'Spanish', - educational_board: 'StateBoard', + educational_board: 'ICSE', // type code here for "relation_one" field }, @@ -181,14 +183,130 @@ const StudentsData = [ grade: '7', - preferred_language: 'Mandarin', + preferred_language: 'German', - educational_board: 'StateBoard', + educational_board: 'ICSE', // type code here for "relation_one" field }, ]; +const EnrollmentData = [ + { + // type code here for "relation_one" field + + // type code here for "relation_one" field + + language: 'Murray Gell-Mann', + + board: 'Jonas Salk', + + enrollmentmonth: 'John Bardeen', + + studentname: 'Anton van Leeuwenhoek', + + age: 7, + + gender: 'Nicolaus Copernicus', + + nationality: 'Hans Bethe', + + dateofbirth: new Date(Date.now()), + + marksheeturl: 'Anton van Leeuwenhoek', + + parentgovtidurl: 'Louis Pasteur', + + studentgovtidurl: 'Sheldon Glashow', + + feeamount: 5, + + freetrial: true, + + paymentmethod: 'Isaac Newton', + + paymentstatus: 'Konrad Lorenz', + + transactionid: 'Jean Baptiste Lamarck', + }, + + { + // type code here for "relation_one" field + + // type code here for "relation_one" field + + language: 'Heike Kamerlingh Onnes', + + board: 'Francis Crick', + + enrollmentmonth: 'Leonard Euler', + + studentname: 'Hans Bethe', + + age: 4, + + gender: 'Leonard Euler', + + nationality: 'Karl Landsteiner', + + dateofbirth: new Date(Date.now()), + + marksheeturl: 'Sheldon Glashow', + + parentgovtidurl: 'John von Neumann', + + studentgovtidurl: 'John Dalton', + + feeamount: 3, + + freetrial: false, + + paymentmethod: 'Jonas Salk', + + paymentstatus: 'Max Planck', + + transactionid: 'Frederick Sanger', + }, + + { + // type code here for "relation_one" field + + // type code here for "relation_one" field + + language: 'Nicolaus Copernicus', + + board: 'Charles Darwin', + + enrollmentmonth: 'Marie Curie', + + studentname: 'Hans Bethe', + + age: 8, + + gender: 'Hans Selye', + + nationality: 'Nicolaus Copernicus', + + dateofbirth: new Date(Date.now()), + + marksheeturl: 'Claude Bernard', + + parentgovtidurl: 'Johannes Kepler', + + studentgovtidurl: 'Edwin Hubble', + + feeamount: 8, + + freetrial: false, + + paymentmethod: 'Joseph J. Thomson', + + paymentstatus: 'Gregor Mendel', + + transactionid: 'William Harvey', + }, +]; + // Similar logic for "relation_many" // Similar logic for "relation_many" @@ -302,6 +420,76 @@ async function associateStudentWithParent() { } } +async function associateEnrollmentWithClass() { + const relatedClass0 = await Classes.findOne({ + offset: Math.floor(Math.random() * (await Classes.count())), + }); + const Enrollment0 = await Enrollment.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (Enrollment0?.setClass) { + await Enrollment0.setClass(relatedClass0); + } + + const relatedClass1 = await Classes.findOne({ + offset: Math.floor(Math.random() * (await Classes.count())), + }); + const Enrollment1 = await Enrollment.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (Enrollment1?.setClass) { + await Enrollment1.setClass(relatedClass1); + } + + const relatedClass2 = await Classes.findOne({ + offset: Math.floor(Math.random() * (await Classes.count())), + }); + const Enrollment2 = await Enrollment.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (Enrollment2?.setClass) { + await Enrollment2.setClass(relatedClass2); + } +} + +async function associateEnrollmentWithParent() { + const relatedParent0 = await Parents.findOne({ + offset: Math.floor(Math.random() * (await Parents.count())), + }); + const Enrollment0 = await Enrollment.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (Enrollment0?.setParent) { + await Enrollment0.setParent(relatedParent0); + } + + const relatedParent1 = await Parents.findOne({ + offset: Math.floor(Math.random() * (await Parents.count())), + }); + const Enrollment1 = await Enrollment.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (Enrollment1?.setParent) { + await Enrollment1.setParent(relatedParent1); + } + + const relatedParent2 = await Parents.findOne({ + offset: Math.floor(Math.random() * (await Parents.count())), + }); + const Enrollment2 = await Enrollment.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (Enrollment2?.setParent) { + await Enrollment2.setParent(relatedParent2); + } +} + module.exports = { up: async (queryInterface, Sequelize) => { await AiTutors.bulkCreate(AiTutorsData); @@ -314,6 +502,8 @@ module.exports = { await Students.bulkCreate(StudentsData); + await Enrollment.bulkCreate(EnrollmentData); + await Promise.all([ // Similar logic for "relation_many" @@ -328,6 +518,10 @@ module.exports = { // Similar logic for "relation_many" await associateStudentWithParent(), + + await associateEnrollmentWithClass(), + + await associateEnrollmentWithParent(), ]); }, @@ -341,5 +535,7 @@ module.exports = { await queryInterface.bulkDelete('parents', null, {}); await queryInterface.bulkDelete('students', null, {}); + + await queryInterface.bulkDelete('enrollment', null, {}); }, }; diff --git a/backend/src/db/seeders/20250808065835.js b/backend/src/db/seeders/20250808065835.js new file mode 100644 index 0000000..1c8aaad --- /dev/null +++ b/backend/src/db/seeders/20250808065835.js @@ -0,0 +1,87 @@ +const { v4: uuid } = require('uuid'); +const db = require('../models'); +const Sequelize = require('sequelize'); +const config = require('../../config'); + +module.exports = { + /** + * @param{import("sequelize").QueryInterface} queryInterface + * @return {Promise} + */ + async up(queryInterface) { + const createdAt = new Date(); + const updatedAt = new Date(); + + /** @type {Map} */ + const idMap = new Map(); + + /** + * @param {string} key + * @return {string} + */ + function getId(key) { + if (idMap.has(key)) { + return idMap.get(key); + } + const id = uuid(); + idMap.set(key, id); + return id; + } + + /** + * @param {string} name + */ + function createPermissions(name) { + return [ + { + id: getId(`CREATE_${name.toUpperCase()}`), + createdAt, + updatedAt, + name: `CREATE_${name.toUpperCase()}`, + }, + { + id: getId(`READ_${name.toUpperCase()}`), + createdAt, + updatedAt, + name: `READ_${name.toUpperCase()}`, + }, + { + id: getId(`UPDATE_${name.toUpperCase()}`), + createdAt, + updatedAt, + name: `UPDATE_${name.toUpperCase()}`, + }, + { + id: getId(`DELETE_${name.toUpperCase()}`), + createdAt, + updatedAt, + name: `DELETE_${name.toUpperCase()}`, + }, + ]; + } + + const entities = ['enrollment']; + + const createdPermissions = entities.flatMap(createPermissions); + + // Add permissions to database + await queryInterface.bulkInsert('permissions', createdPermissions); + // Get permissions ids + const permissionsIds = createdPermissions.map((p) => p.id); + // Get admin role + const adminRole = await db.roles.findOne({ + where: { name: config.roles.admin }, + }); + + if (adminRole) { + // Add permissions to admin role if it exists + await adminRole.addPermissions(permissionsIds); + } + }, + down: async (queryInterface, Sequelize) => { + await queryInterface.bulkDelete( + 'permissions', + entities.flatMap(createPermissions), + ); + }, +}; diff --git a/backend/src/index.js b/backend/src/index.js index 8ae4f53..706532f 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -35,6 +35,8 @@ const rolesRoutes = require('./routes/roles'); const permissionsRoutes = require('./routes/permissions'); +const enrollmentRoutes = require('./routes/enrollment'); + const getBaseUrl = (url) => { if (!url) return ''; return url.endsWith('/api') ? url.slice(0, -4) : url; @@ -148,6 +150,12 @@ app.use( permissionsRoutes, ); +app.use( + '/api/enrollment', + passport.authenticate('jwt', { session: false }), + enrollmentRoutes, +); + app.use( '/api/openai', passport.authenticate('jwt', { session: false }), diff --git a/backend/src/routes/enrollment.js b/backend/src/routes/enrollment.js new file mode 100644 index 0000000..c486499 --- /dev/null +++ b/backend/src/routes/enrollment.js @@ -0,0 +1,496 @@ +const express = require('express'); + +const EnrollmentService = require('../services/enrollment'); +const EnrollmentDBApi = require('../db/api/enrollment'); +const wrapAsync = require('../helpers').wrapAsync; + +const router = express.Router(); + +const { parse } = require('json2csv'); + +const { checkCrudPermissions } = require('../middlewares/check-permissions'); + +router.use(checkCrudPermissions('enrollment')); + +/** + * @swagger + * components: + * schemas: + * Enrollment: + * type: object + * properties: + + * language: + * type: string + * default: language + * board: + * type: string + * default: board + * enrollmentmonth: + * type: string + * default: enrollmentmonth + * studentname: + * type: string + * default: studentname + * gender: + * type: string + * default: gender + * nationality: + * type: string + * default: nationality + * marksheeturl: + * type: string + * default: marksheeturl + * parentgovtidurl: + * type: string + * default: parentgovtidurl + * studentgovtidurl: + * type: string + * default: studentgovtidurl + * paymentmethod: + * type: string + * default: paymentmethod + * paymentstatus: + * type: string + * default: paymentstatus + * transactionid: + * type: string + * default: transactionid + + * age: + * type: integer + * format: int64 + * feeamount: + * type: integer + * format: int64 + + */ + +/** + * @swagger + * tags: + * name: Enrollment + * description: The Enrollment managing API + */ + +/** + * @swagger + * /api/enrollment: + * post: + * security: + * - bearerAuth: [] + * tags: [Enrollment] + * summary: Add new item + * description: Add new item + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Enrollment" + * responses: + * 200: + * description: The item was successfully added + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Enrollment" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + */ +router.post( + '/', + wrapAsync(async (req, res) => { + const referer = + req.headers.referer || + `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await EnrollmentService.create( + req.body.data, + req.currentUser, + true, + link.host, + ); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/budgets/bulk-import: + * post: + * security: + * - bearerAuth: [] + * tags: [Enrollment] + * summary: Bulk import items + * description: Bulk import items + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated items + * type: array + * items: + * $ref: "#/components/schemas/Enrollment" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Enrollment" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + * + */ +router.post( + '/bulk-import', + wrapAsync(async (req, res) => { + const referer = + req.headers.referer || + `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await EnrollmentService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/enrollment/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Enrollment] + * summary: Update the data of the selected item + * description: Update the data of the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to update + * required: true + * schema: + * type: string + * requestBody: + * description: Set new item data + * required: true + * content: + * application/json: + * schema: + * properties: + * id: + * description: ID of the updated item + * type: string + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Enrollment" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Enrollment" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.put( + '/:id', + wrapAsync(async (req, res) => { + await EnrollmentService.update(req.body.data, req.body.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/enrollment/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Enrollment] + * summary: Delete the selected item + * description: Delete the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to delete + * required: true + * schema: + * type: string + * responses: + * 200: + * description: The item was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Enrollment" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.delete( + '/:id', + wrapAsync(async (req, res) => { + await EnrollmentService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/enrollment/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Enrollment] + * summary: Delete the selected item list + * description: Delete the selected item list + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * ids: + * description: IDs of the updated items + * type: array + * responses: + * 200: + * description: The items was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Enrollment" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post( + '/deleteByIds', + wrapAsync(async (req, res) => { + await EnrollmentService.deleteByIds(req.body.data, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/enrollment: + * get: + * security: + * - bearerAuth: [] + * tags: [Enrollment] + * summary: Get all enrollment + * description: Get all enrollment + * responses: + * 200: + * description: Enrollment list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Enrollment" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get( + '/', + wrapAsync(async (req, res) => { + const filetype = req.query.filetype; + + const currentUser = req.currentUser; + const payload = await EnrollmentDBApi.findAll(req.query, { currentUser }); + if (filetype && filetype === 'csv') { + const fields = [ + 'id', + 'language', + 'board', + 'enrollmentmonth', + 'studentname', + 'gender', + 'nationality', + 'marksheeturl', + 'parentgovtidurl', + 'studentgovtidurl', + 'paymentmethod', + 'paymentstatus', + 'transactionid', + 'age', + 'feeamount', + + 'dateofbirth', + ]; + const opts = { fields }; + try { + const csv = parse(payload.rows, opts); + res.status(200).attachment(csv); + res.send(csv); + } catch (err) { + console.error(err); + } + } else { + res.status(200).send(payload); + } + }), +); + +/** + * @swagger + * /api/enrollment/count: + * get: + * security: + * - bearerAuth: [] + * tags: [Enrollment] + * summary: Count all enrollment + * description: Count all enrollment + * responses: + * 200: + * description: Enrollment count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Enrollment" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get( + '/count', + wrapAsync(async (req, res) => { + const currentUser = req.currentUser; + const payload = await EnrollmentDBApi.findAll(req.query, null, { + countOnly: true, + currentUser, + }); + + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/enrollment/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [Enrollment] + * summary: Find all enrollment that match search criteria + * description: Find all enrollment that match search criteria + * responses: + * 200: + * description: Enrollment list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Enrollment" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/autocomplete', async (req, res) => { + const payload = await EnrollmentDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/enrollment/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Enrollment] + * summary: Get selected item + * description: Get selected item + * parameters: + * - in: path + * name: id + * description: ID of item to get + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Selected item successfully received + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Enrollment" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.get( + '/:id', + wrapAsync(async (req, res) => { + const payload = await EnrollmentDBApi.findBy({ id: req.params.id }); + + res.status(200).send(payload); + }), +); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/services/enrollment.js b/backend/src/services/enrollment.js new file mode 100644 index 0000000..469bbce --- /dev/null +++ b/backend/src/services/enrollment.js @@ -0,0 +1,114 @@ +const db = require('../db/models'); +const EnrollmentDBApi = require('../db/api/enrollment'); +const processFile = require('../middlewares/upload'); +const ValidationError = require('./notifications/errors/validation'); +const csv = require('csv-parser'); +const axios = require('axios'); +const config = require('../config'); +const stream = require('stream'); + +module.exports = class EnrollmentService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await EnrollmentDBApi.create(data, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async bulkImport(req, res, sendInvitationEmails = true, host) { + const transaction = await db.sequelize.transaction(); + + try { + await processFile(req, res); + const bufferStream = new stream.PassThrough(); + const results = []; + + await bufferStream.end(Buffer.from(req.file.buffer, 'utf-8')); // convert Buffer to Stream + + await new Promise((resolve, reject) => { + bufferStream + .pipe(csv()) + .on('data', (data) => results.push(data)) + .on('end', async () => { + console.log('CSV results', results); + resolve(); + }) + .on('error', (error) => reject(error)); + }); + + await EnrollmentDBApi.bulkImport(results, { + transaction, + ignoreDuplicates: true, + validate: true, + currentUser: req.currentUser, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async update(data, id, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + let enrollment = await EnrollmentDBApi.findBy({ id }, { transaction }); + + if (!enrollment) { + throw new ValidationError('enrollmentNotFound'); + } + + const updatedEnrollment = await EnrollmentDBApi.update(id, data, { + currentUser, + transaction, + }); + + await transaction.commit(); + return updatedEnrollment; + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await EnrollmentDBApi.deleteByIds(ids, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async remove(id, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await EnrollmentDBApi.remove(id, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; diff --git a/backend/src/services/search.js b/backend/src/services/search.js index 75cbac3..0180dab 100644 --- a/backend/src/services/search.js +++ b/backend/src/services/search.js @@ -52,9 +52,37 @@ module.exports = class SearchService { parents: ['first_name', 'last_name', 'email'], students: ['first_name', 'last_name', 'grade'], + + enrollment: [ + 'language', + + 'board', + + 'enrollmentmonth', + + 'studentname', + + 'gender', + + 'nationality', + + 'marksheeturl', + + 'parentgovtidurl', + + 'studentgovtidurl', + + 'paymentmethod', + + 'paymentstatus', + + 'transactionid', + ], }; const columnsInt = { assessments: ['score'], + + enrollment: ['age', 'feeamount'], }; let allFoundRecords = []; diff --git a/frontend/src/components/Enrollment/CardEnrollment.tsx b/frontend/src/components/Enrollment/CardEnrollment.tsx new file mode 100644 index 0000000..688703b --- /dev/null +++ b/frontend/src/components/Enrollment/CardEnrollment.tsx @@ -0,0 +1,286 @@ +import React from 'react'; +import ImageField from '../ImageField'; +import ListActionsPopover from '../ListActionsPopover'; +import { useAppSelector } from '../../stores/hooks'; +import dataFormatter from '../../helpers/dataFormatter'; +import { Pagination } from '../Pagination'; +import { saveFile } from '../../helpers/fileSaver'; +import LoadingSpinner from '../LoadingSpinner'; +import Link from 'next/link'; + +import { hasPermission } from '../../helpers/userPermissions'; + +type Props = { + enrollment: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const CardEnrollment = ({ + enrollment, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const asideScrollbarsStyle = useAppSelector( + (state) => state.style.asideScrollbarsStyle, + ); + const bgColor = useAppSelector((state) => state.style.cardsColor); + const darkMode = useAppSelector((state) => state.style.darkMode); + const corners = useAppSelector((state) => state.style.corners); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + + const currentUser = useAppSelector((state) => state.auth.currentUser); + const hasUpdatePermission = hasPermission(currentUser, 'UPDATE_ENROLLMENT'); + + return ( +
+ {loading && } +
    + {!loading && + enrollment.map((item, index) => ( +
  • +
    + + {item.studentname} + + +
    + +
    +
    +
    +
    +
    Class
    +
    +
    + {dataFormatter.classesOneListFormatter(item.class)} +
    +
    +
    + +
    +
    + Parent +
    +
    +
    + {dataFormatter.parentsOneListFormatter(item.parent)} +
    +
    +
    + +
    +
    + Language +
    +
    +
    + {item.language} +
    +
    +
    + +
    +
    Board
    +
    +
    {item.board}
    +
    +
    + +
    +
    + Enrollmentmonth +
    +
    +
    + {item.enrollmentmonth} +
    +
    +
    + +
    +
    + Studentname +
    +
    +
    + {item.studentname} +
    +
    +
    + +
    +
    Age
    +
    +
    {item.age}
    +
    +
    + +
    +
    + Gender +
    +
    +
    + {item.gender} +
    +
    +
    + +
    +
    + Nationality +
    +
    +
    + {item.nationality} +
    +
    +
    + +
    +
    + Dateofbirth +
    +
    +
    + {dataFormatter.dateFormatter(item.dateofbirth)} +
    +
    +
    + +
    +
    + Marksheeturl +
    +
    +
    + {item.marksheeturl} +
    +
    +
    + +
    +
    + Parentgovtidurl +
    +
    +
    + {item.parentgovtidurl} +
    +
    +
    + +
    +
    + Studentgovtidurl +
    +
    +
    + {item.studentgovtidurl} +
    +
    +
    + +
    +
    + Feeamount +
    +
    +
    + {item.feeamount} +
    +
    +
    + +
    +
    + Freetrial +
    +
    +
    + {dataFormatter.booleanFormatter(item.freetrial)} +
    +
    +
    + +
    +
    + Paymentmethod +
    +
    +
    + {item.paymentmethod} +
    +
    +
    + +
    +
    + Paymentstatus +
    +
    +
    + {item.paymentstatus} +
    +
    +
    + +
    +
    + Transactionid +
    +
    +
    + {item.transactionid} +
    +
    +
    +
    +
  • + ))} + {!loading && enrollment.length === 0 && ( +
    +

    No data to display

    +
    + )} +
+
+ +
+
+ ); +}; + +export default CardEnrollment; diff --git a/frontend/src/components/Enrollment/ListEnrollment.tsx b/frontend/src/components/Enrollment/ListEnrollment.tsx new file mode 100644 index 0000000..a42a33b --- /dev/null +++ b/frontend/src/components/Enrollment/ListEnrollment.tsx @@ -0,0 +1,194 @@ +import React from 'react'; +import CardBox from '../CardBox'; +import ImageField from '../ImageField'; +import dataFormatter from '../../helpers/dataFormatter'; +import { saveFile } from '../../helpers/fileSaver'; +import ListActionsPopover from '../ListActionsPopover'; +import { useAppSelector } from '../../stores/hooks'; +import { Pagination } from '../Pagination'; +import LoadingSpinner from '../LoadingSpinner'; +import Link from 'next/link'; + +import { hasPermission } from '../../helpers/userPermissions'; + +type Props = { + enrollment: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const ListEnrollment = ({ + enrollment, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const currentUser = useAppSelector((state) => state.auth.currentUser); + const hasUpdatePermission = hasPermission(currentUser, 'UPDATE_ENROLLMENT'); + + const corners = useAppSelector((state) => state.style.corners); + const bgColor = useAppSelector((state) => state.style.cardsColor); + + return ( + <> +
+ {loading && } + {!loading && + enrollment.map((item) => ( +
+ +
+ dark:divide-dark-700 overflow-x-auto' + } + > +
+

Class

+

+ {dataFormatter.classesOneListFormatter(item.class)} +

+
+ +
+

Parent

+

+ {dataFormatter.parentsOneListFormatter(item.parent)} +

+
+ +
+

Language

+

{item.language}

+
+ +
+

Board

+

{item.board}

+
+ +
+

+ Enrollmentmonth +

+

{item.enrollmentmonth}

+
+ +
+

Studentname

+

{item.studentname}

+
+ +
+

Age

+

{item.age}

+
+ +
+

Gender

+

{item.gender}

+
+ +
+

Nationality

+

{item.nationality}

+
+ +
+

Dateofbirth

+

+ {dataFormatter.dateFormatter(item.dateofbirth)} +

+
+ +
+

Marksheeturl

+

{item.marksheeturl}

+
+ +
+

+ Parentgovtidurl +

+

{item.parentgovtidurl}

+
+ +
+

+ Studentgovtidurl +

+

{item.studentgovtidurl}

+
+ +
+

Feeamount

+

{item.feeamount}

+
+ +
+

Freetrial

+

+ {dataFormatter.booleanFormatter(item.freetrial)} +

+
+ +
+

+ Paymentmethod +

+

{item.paymentmethod}

+
+ +
+

+ Paymentstatus +

+

{item.paymentstatus}

+
+ +
+

+ Transactionid +

+

{item.transactionid}

+
+ + +
+
+
+ ))} + {!loading && enrollment.length === 0 && ( +
+

No data to display

+
+ )} +
+
+ +
+ + ); +}; + +export default ListEnrollment; diff --git a/frontend/src/components/Enrollment/TableEnrollment.tsx b/frontend/src/components/Enrollment/TableEnrollment.tsx new file mode 100644 index 0000000..dfcdf11 --- /dev/null +++ b/frontend/src/components/Enrollment/TableEnrollment.tsx @@ -0,0 +1,487 @@ +import React, { useEffect, useState, useMemo } from 'react'; +import { createPortal } from 'react-dom'; +import { ToastContainer, toast } from 'react-toastify'; +import BaseButton from '../BaseButton'; +import CardBoxModal from '../CardBoxModal'; +import CardBox from '../CardBox'; +import { + fetch, + update, + deleteItem, + setRefetch, + deleteItemsByIds, +} from '../../stores/enrollment/enrollmentSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { Field, Form, Formik } from 'formik'; +import { DataGrid, GridColDef } from '@mui/x-data-grid'; +import { loadColumns } from './configureEnrollmentCols'; +import _ from 'lodash'; +import dataFormatter from '../../helpers/dataFormatter'; +import { dataGridStyles } from '../../styles'; + +const perPage = 10; + +const TableSampleEnrollment = ({ + filterItems, + setFilterItems, + filters, + showGrid, +}) => { + const notify = (type, msg) => toast(msg, { type, position: 'bottom-center' }); + + const dispatch = useAppDispatch(); + const router = useRouter(); + + const pagesList = []; + const [id, setId] = useState(null); + const [currentPage, setCurrentPage] = useState(0); + const [filterRequest, setFilterRequest] = React.useState(''); + const [columns, setColumns] = useState([]); + const [selectedRows, setSelectedRows] = useState([]); + const [sortModel, setSortModel] = useState([ + { + field: '', + sort: 'desc', + }, + ]); + + const { + enrollment, + loading, + count, + notify: enrollmentNotify, + refetch, + } = useAppSelector((state) => state.enrollment); + const { currentUser } = useAppSelector((state) => state.auth); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + const bgColor = useAppSelector((state) => state.style.bgLayoutColor); + const corners = useAppSelector((state) => state.style.corners); + const numPages = + Math.floor(count / perPage) === 0 ? 1 : Math.ceil(count / perPage); + for (let i = 0; i < numPages; i++) { + pagesList.push(i); + } + + const loadData = async (page = currentPage, request = filterRequest) => { + if (page !== currentPage) setCurrentPage(page); + if (request !== filterRequest) setFilterRequest(request); + const { sort, field } = sortModel[0]; + + const query = `?page=${page}&limit=${perPage}${request}&sort=${sort}&field=${field}`; + dispatch(fetch({ limit: perPage, page, query })); + }; + + useEffect(() => { + if (enrollmentNotify.showNotification) { + notify( + enrollmentNotify.typeNotification, + enrollmentNotify.textNotification, + ); + } + }, [enrollmentNotify.showNotification]); + + useEffect(() => { + if (!currentUser) return; + loadData(); + }, [sortModel, currentUser]); + + useEffect(() => { + if (refetch) { + loadData(0); + dispatch(setRefetch(false)); + } + }, [refetch, dispatch]); + + const [isModalInfoActive, setIsModalInfoActive] = useState(false); + const [isModalTrashActive, setIsModalTrashActive] = useState(false); + + const handleModalAction = () => { + setIsModalInfoActive(false); + setIsModalTrashActive(false); + }; + + const handleDeleteModalAction = (id: string) => { + setId(id); + setIsModalTrashActive(true); + }; + const handleDeleteAction = async () => { + if (id) { + await dispatch(deleteItem(id)); + await loadData(0); + setIsModalTrashActive(false); + } + }; + + const generateFilterRequests = useMemo(() => { + let request = '&'; + filterItems.forEach((item) => { + const isRangeFilter = filters.find( + (filter) => + filter.title === item.fields.selectedField && + (filter.number || filter.date), + ); + + if (isRangeFilter) { + const from = item.fields.filterValueFrom; + const to = item.fields.filterValueTo; + if (from) { + request += `${item.fields.selectedField}Range=${from}&`; + } + if (to) { + request += `${item.fields.selectedField}Range=${to}&`; + } + } else { + const value = item.fields.filterValue; + if (value) { + request += `${item.fields.selectedField}=${value}&`; + } + } + }); + return request; + }, [filterItems, filters]); + + const deleteFilter = (value) => { + const newItems = filterItems.filter((item) => item.id !== value); + + if (newItems.length) { + setFilterItems(newItems); + } else { + loadData(0, ''); + + setFilterItems(newItems); + } + }; + + const handleSubmit = () => { + loadData(0, generateFilterRequests); + }; + + const handleChange = (id) => (e) => { + const value = e.target.value; + const name = e.target.name; + + setFilterItems( + filterItems.map((item) => { + if (item.id !== id) return item; + if (name === 'selectedField') return { id, fields: { [name]: value } }; + + return { id, fields: { ...item.fields, [name]: value } }; + }), + ); + }; + + const handleReset = () => { + setFilterItems([]); + loadData(0, ''); + }; + + const onPageChange = (page: number) => { + loadData(page); + setCurrentPage(page); + }; + + useEffect(() => { + if (!currentUser) return; + + loadColumns(handleDeleteModalAction, `enrollment`, currentUser).then( + (newCols) => setColumns(newCols), + ); + }, [currentUser]); + + const handleTableSubmit = async (id: string, data) => { + if (!_.isEmpty(data)) { + await dispatch(update({ id, data })) + .unwrap() + .then((res) => res) + .catch((err) => { + throw new Error(err); + }); + } + }; + + const onDeleteRows = async (selectedRows) => { + await dispatch(deleteItemsByIds(selectedRows)); + await loadData(0); + }; + + const controlClasses = + 'w-full py-2 px-2 my-2 rounded dark:placeholder-gray-400 ' + + ` ${bgColor} ${focusRing} ${corners} ` + + 'dark:bg-slate-800 border'; + + const dataGrid = ( +
+ `datagrid--row`} + rows={enrollment ?? []} + columns={columns} + initialState={{ + pagination: { + paginationModel: { + pageSize: 10, + }, + }, + }} + disableRowSelectionOnClick + onProcessRowUpdateError={(params) => { + console.log('Error', params); + }} + processRowUpdate={async (newRow, oldRow) => { + const data = dataFormatter.dataGridEditFormatter(newRow); + + try { + await handleTableSubmit(newRow.id, data); + return newRow; + } catch { + return oldRow; + } + }} + sortingMode={'server'} + checkboxSelection + onRowSelectionModelChange={(ids) => { + setSelectedRows(ids); + }} + onSortModelChange={(params) => { + params.length + ? setSortModel(params) + : setSortModel([{ field: '', sort: 'desc' }]); + }} + rowCount={count} + pageSizeOptions={[10]} + paginationMode={'server'} + loading={loading} + onPaginationModelChange={(params) => { + onPageChange(params.page); + }} + /> +
+ ); + + return ( + <> + {filterItems && Array.isArray(filterItems) && filterItems.length ? ( + + null} + > +
+ <> + {filterItems && + filterItems.map((filterItem) => { + return ( +
+
+
+ Filter +
+ + {filters.map((selectOption) => ( + + ))} + +
+ {filters.find( + (filter) => + filter.title === filterItem?.fields?.selectedField, + )?.type === 'enum' ? ( +
+
Value
+ + + {filters + .find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + ) + ?.options?.map((option) => ( + + ))} + +
+ ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + )?.number ? ( +
+
+
+ From +
+ +
+
+
+ To +
+ +
+
+ ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + )?.date ? ( +
+
+
+ From +
+ +
+
+
+ To +
+ +
+
+ ) : ( +
+
+ Contains +
+ +
+ )} +
+
+ Action +
+ { + deleteFilter(filterItem.id); + }} + /> +
+
+ ); + })} +
+ + +
+ +
+
+
+ ) : null} + +

Are you sure you want to delete this item?

+
+ + {dataGrid} + + {selectedRows.length > 0 && + createPortal( + onDeleteRows(selectedRows)} + />, + document.getElementById('delete-rows-button'), + )} + + + ); +}; + +export default TableSampleEnrollment; diff --git a/frontend/src/components/Enrollment/configureEnrollmentCols.tsx b/frontend/src/components/Enrollment/configureEnrollmentCols.tsx new file mode 100644 index 0000000..e408721 --- /dev/null +++ b/frontend/src/components/Enrollment/configureEnrollmentCols.tsx @@ -0,0 +1,304 @@ +import React from 'react'; +import BaseIcon from '../BaseIcon'; +import { mdiEye, mdiTrashCan, mdiPencilOutline } from '@mdi/js'; +import axios from 'axios'; +import { + GridActionsCellItem, + GridRowParams, + GridValueGetterParams, +} from '@mui/x-data-grid'; +import ImageField from '../ImageField'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import DataGridMultiSelect from '../DataGridMultiSelect'; +import ListActionsPopover from '../ListActionsPopover'; + +import { hasPermission } from '../../helpers/userPermissions'; + +type Params = (id: string) => void; + +export const loadColumns = async ( + onDelete: Params, + entityName: string, + + user, +) => { + async function callOptionsApi(entityName: string) { + if (!hasPermission(user, 'READ_' + entityName.toUpperCase())) return []; + + try { + const data = await axios(`/${entityName}/autocomplete?limit=100`); + return data.data; + } catch (error) { + console.log(error); + return []; + } + } + + const hasUpdatePermission = hasPermission(user, 'UPDATE_ENROLLMENT'); + + return [ + { + field: 'class', + headerName: 'Class', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + sortable: false, + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('classes'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + }, + + { + field: 'parent', + headerName: 'Parent', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + sortable: false, + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('parents'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + }, + + { + field: 'language', + headerName: 'Language', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + + { + field: 'board', + headerName: 'Board', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + + { + field: 'enrollmentmonth', + headerName: 'Enrollmentmonth', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + + { + field: 'studentname', + headerName: 'Studentname', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + + { + field: 'age', + headerName: 'Age', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + type: 'number', + }, + + { + field: 'gender', + headerName: 'Gender', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + + { + field: 'nationality', + headerName: 'Nationality', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + + { + field: 'dateofbirth', + headerName: 'Dateofbirth', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + type: 'date', + valueGetter: (params: GridValueGetterParams) => + new Date(params.row.dateofbirth), + }, + + { + field: 'marksheeturl', + headerName: 'Marksheeturl', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + + { + field: 'parentgovtidurl', + headerName: 'Parentgovtidurl', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + + { + field: 'studentgovtidurl', + headerName: 'Studentgovtidurl', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + + { + field: 'feeamount', + headerName: 'Feeamount', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + type: 'number', + }, + + { + field: 'freetrial', + headerName: 'Freetrial', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + type: 'boolean', + }, + + { + field: 'paymentmethod', + headerName: 'Paymentmethod', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + + { + field: 'paymentstatus', + headerName: 'Paymentstatus', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + + { + field: 'transactionid', + headerName: 'Transactionid', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + + { + field: 'actions', + type: 'actions', + minWidth: 30, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + getActions: (params: GridRowParams) => { + return [ +
+ +
, + ]; + }, + }, + ]; +}; diff --git a/frontend/src/components/WebPageComponents/Footer.tsx b/frontend/src/components/WebPageComponents/Footer.tsx index 827aaf1..7899651 100644 --- a/frontend/src/components/WebPageComponents/Footer.tsx +++ b/frontend/src/components/WebPageComponents/Footer.tsx @@ -17,9 +17,9 @@ export default function WebSiteFooter({ projectName }: WebSiteFooterProps) { const borders = useAppSelector((state) => state.style.borders); const websiteHeder = useAppSelector((state) => state.style.websiteHeder); - const style = FooterStyle.WITH_PAGES; + const style = FooterStyle.WITH_PROJECT_NAME; - const design = FooterDesigns.DEFAULT_DESIGN; + const design = FooterDesigns.DESIGN_DIVERSITY; return (
item.subject); + }, + classesOneListFormatter(val) { + if (!val) return ''; + return val.subject; + }, + classesManyListFormatterEdit(val) { + if (!val || !val.length) return []; + return val.map((item) => { + return { id: item.id, label: item.subject }; + }); + }, + classesOneListFormatterEdit(val) { + if (!val) return ''; + return { label: val.subject, id: val.id }; + }, + parentsManyListFormatter(val) { if (!val || !val.length) return []; return val.map((item) => item.first_name); diff --git a/frontend/src/menuAside.ts b/frontend/src/menuAside.ts index 38c5870..32e2578 100644 --- a/frontend/src/menuAside.ts +++ b/frontend/src/menuAside.ts @@ -87,6 +87,14 @@ const menuAside: MenuAsideItem[] = [ icon: icon.mdiShieldAccountOutline ?? icon.mdiTable, permissions: 'READ_PERMISSIONS', }, + { + href: '/enrollment/enrollment-list', + label: 'Enrollment', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: icon.mdiTable ?? icon.mdiTable, + permissions: 'READ_ENROLLMENT', + }, { href: '/profile', label: 'Profile', diff --git a/frontend/src/pages/classes/classes-view.tsx b/frontend/src/pages/classes/classes-view.tsx index 0075b48..8cc93c1 100644 --- a/frontend/src/pages/classes/classes-view.tsx +++ b/frontend/src/pages/classes/classes-view.tsx @@ -160,6 +160,119 @@ const ClassesView = () => { + <> +

Enrollment Class

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {classes.enrollment_class && + Array.isArray(classes.enrollment_class) && + classes.enrollment_class.map((item: any) => ( + + router.push( + `/enrollment/enrollment-view/?id=${item.id}`, + ) + } + > + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ))} + +
LanguageBoardEnrollmentmonthStudentnameAgeGenderNationalityDateofbirthMarksheeturlParentgovtidurlStudentgovtidurlFeeamountFreetrialPaymentmethodPaymentstatusTransactionid
{item.language}{item.board} + {item.enrollmentmonth} + {item.studentname}{item.age}{item.gender}{item.nationality} + {dataFormatter.dateFormatter(item.dateofbirth)} + {item.marksheeturl} + {item.parentgovtidurl} + + {item.studentgovtidurl} + {item.feeamount} + {dataFormatter.booleanFormatter(item.freetrial)} + + {item.paymentmethod} + + {item.paymentstatus} + + {item.transactionid} +
+
+ {!classes?.enrollment_class?.length && ( +
No data
+ )} +
+ + { const [students, setStudents] = React.useState(loadingMessage); const [roles, setRoles] = React.useState(loadingMessage); const [permissions, setPermissions] = React.useState(loadingMessage); + const [enrollment, setEnrollment] = React.useState(loadingMessage); const [widgetsRole, setWidgetsRole] = React.useState({ role: { value: '', label: '' }, @@ -55,6 +56,7 @@ const Dashboard = () => { 'students', 'roles', 'permissions', + 'enrollment', ]; const fns = [ setUsers, @@ -65,6 +67,7 @@ const Dashboard = () => { setStudents, setRoles, setPermissions, + setEnrollment, ]; const requests = entities.map((entity, index) => { @@ -458,6 +461,38 @@ const Dashboard = () => {
)} + + {hasPermission(currentUser, 'READ_ENROLLMENT') && ( + +
+
+
+
+ Enrollment +
+
+ {enrollment} +
+
+
+ +
+
+
+ + )}
diff --git a/frontend/src/pages/enrollment/[enrollmentId].tsx b/frontend/src/pages/enrollment/[enrollmentId].tsx new file mode 100644 index 0000000..eb1ceec --- /dev/null +++ b/frontend/src/pages/enrollment/[enrollmentId].tsx @@ -0,0 +1,260 @@ +import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement, useEffect, useState } from 'react'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; + +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { SwitchField } from '../../components/SwitchField'; +import { RichTextField } from '../../components/RichTextField'; + +import { update, fetch } from '../../stores/enrollment/enrollmentSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; + +const EditEnrollment = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + class: null, + + parent: null, + + language: '', + + board: '', + + enrollmentmonth: '', + + studentname: '', + + age: '', + + gender: '', + + nationality: '', + + dateofbirth: new Date(), + + marksheeturl: '', + + parentgovtidurl: '', + + studentgovtidurl: '', + + feeamount: '', + + freetrial: false, + + paymentmethod: '', + + paymentstatus: '', + + transactionid: '', + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { enrollment } = useAppSelector((state) => state.enrollment); + + const { enrollmentId } = router.query; + + useEffect(() => { + dispatch(fetch({ id: enrollmentId })); + }, [enrollmentId]); + + useEffect(() => { + if (typeof enrollment === 'object') { + setInitialValues(enrollment); + } + }, [enrollment]); + + useEffect(() => { + if (typeof enrollment === 'object') { + const newInitialVal = { ...initVals }; + + Object.keys(initVals).forEach( + (el) => (newInitialVal[el] = enrollment[el]), + ); + + setInitialValues(newInitialVal); + } + }, [enrollment]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: enrollmentId, data })); + await router.push('/enrollment/enrollment-list'); + }; + + return ( + <> + + {getPageTitle('Edit enrollment')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + setInitialValues({ ...initialValues, dateofbirth: date }) + } + /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + router.push('/enrollment/enrollment-list')} + /> + + +
+
+
+ + ); +}; + +EditEnrollment.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditEnrollment; diff --git a/frontend/src/pages/enrollment/enrollment-edit.tsx b/frontend/src/pages/enrollment/enrollment-edit.tsx new file mode 100644 index 0000000..ec295d7 --- /dev/null +++ b/frontend/src/pages/enrollment/enrollment-edit.tsx @@ -0,0 +1,258 @@ +import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement, useEffect, useState } from 'react'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; + +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { SwitchField } from '../../components/SwitchField'; +import { RichTextField } from '../../components/RichTextField'; + +import { update, fetch } from '../../stores/enrollment/enrollmentSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; + +const EditEnrollmentPage = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + class: null, + + parent: null, + + language: '', + + board: '', + + enrollmentmonth: '', + + studentname: '', + + age: '', + + gender: '', + + nationality: '', + + dateofbirth: new Date(), + + marksheeturl: '', + + parentgovtidurl: '', + + studentgovtidurl: '', + + feeamount: '', + + freetrial: false, + + paymentmethod: '', + + paymentstatus: '', + + transactionid: '', + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { enrollment } = useAppSelector((state) => state.enrollment); + + const { id } = router.query; + + useEffect(() => { + dispatch(fetch({ id: id })); + }, [id]); + + useEffect(() => { + if (typeof enrollment === 'object') { + setInitialValues(enrollment); + } + }, [enrollment]); + + useEffect(() => { + if (typeof enrollment === 'object') { + const newInitialVal = { ...initVals }; + Object.keys(initVals).forEach( + (el) => (newInitialVal[el] = enrollment[el]), + ); + setInitialValues(newInitialVal); + } + }, [enrollment]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: id, data })); + await router.push('/enrollment/enrollment-list'); + }; + + return ( + <> + + {getPageTitle('Edit enrollment')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + setInitialValues({ ...initialValues, dateofbirth: date }) + } + /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + router.push('/enrollment/enrollment-list')} + /> + + +
+
+
+ + ); +}; + +EditEnrollmentPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditEnrollmentPage; diff --git a/frontend/src/pages/enrollment/enrollment-list.tsx b/frontend/src/pages/enrollment/enrollment-list.tsx new file mode 100644 index 0000000..77f154a --- /dev/null +++ b/frontend/src/pages/enrollment/enrollment-list.tsx @@ -0,0 +1,181 @@ +import { mdiChartTimelineVariant } from '@mdi/js'; +import Head from 'next/head'; +import { uniqueId } from 'lodash'; +import React, { ReactElement, useState } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; +import TableEnrollment from '../../components/Enrollment/TableEnrollment'; +import BaseButton from '../../components/BaseButton'; +import axios from 'axios'; +import Link from 'next/link'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import CardBoxModal from '../../components/CardBoxModal'; +import DragDropFilePicker from '../../components/DragDropFilePicker'; +import { setRefetch, uploadCsv } from '../../stores/enrollment/enrollmentSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const EnrollmentTablesPage = () => { + const [filterItems, setFilterItems] = useState([]); + const [csvFile, setCsvFile] = useState(null); + const [isModalActive, setIsModalActive] = useState(false); + const [showTableView, setShowTableView] = useState(false); + + const { currentUser } = useAppSelector((state) => state.auth); + + const dispatch = useAppDispatch(); + + const [filters] = useState([ + { label: 'Language', title: 'language' }, + { label: 'Board', title: 'board' }, + { label: 'Enrollmentmonth', title: 'enrollmentmonth' }, + { label: 'Studentname', title: 'studentname' }, + { label: 'Gender', title: 'gender' }, + { label: 'Nationality', title: 'nationality' }, + { label: 'Marksheeturl', title: 'marksheeturl' }, + { label: 'Parentgovtidurl', title: 'parentgovtidurl' }, + { label: 'Studentgovtidurl', title: 'studentgovtidurl' }, + { label: 'Paymentmethod', title: 'paymentmethod' }, + { label: 'Paymentstatus', title: 'paymentstatus' }, + { label: 'Transactionid', title: 'transactionid' }, + { label: 'Age', title: 'age', number: 'true' }, + { label: 'Feeamount', title: 'feeamount', number: 'true' }, + + { label: 'Class', title: 'class' }, + + { label: 'Parent', title: 'parent' }, + ]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_ENROLLMENT'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getEnrollmentCSV = async () => { + const response = await axios({ + url: '/enrollment?filetype=csv', + method: 'GET', + responseType: 'blob', + }); + const type = response.headers['content-type']; + const blob = new Blob([response.data], { type: type }); + const link = document.createElement('a'); + link.href = window.URL.createObjectURL(blob); + link.download = 'enrollmentCSV.csv'; + link.click(); + }; + + const onModalConfirm = async () => { + if (!csvFile) return; + await dispatch(uploadCsv(csvFile)); + dispatch(setRefetch(true)); + setCsvFile(null); + setIsModalActive(false); + }; + + const onModalCancel = () => { + setCsvFile(null); + setIsModalActive(false); + }; + + return ( + <> + + {getPageTitle('Enrollment')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
+
+
+
+ + + + +
+ + + + + ); +}; + +EnrollmentTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EnrollmentTablesPage; diff --git a/frontend/src/pages/enrollment/enrollment-new.tsx b/frontend/src/pages/enrollment/enrollment-new.tsx new file mode 100644 index 0000000..5e705f4 --- /dev/null +++ b/frontend/src/pages/enrollment/enrollment-new.tsx @@ -0,0 +1,221 @@ +import { + mdiAccount, + mdiChartTimelineVariant, + mdiMail, + mdiUpload, +} from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SwitchField } from '../../components/SwitchField'; + +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { RichTextField } from '../../components/RichTextField'; + +import { create } from '../../stores/enrollment/enrollmentSlice'; +import { useAppDispatch } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import moment from 'moment'; + +const initialValues = { + class: '', + + parent: '', + + language: '', + + board: '', + + enrollmentmonth: '', + + studentname: '', + + age: '', + + gender: '', + + nationality: '', + + dateofbirth: '', + dateDateofbirth: '', + + marksheeturl: '', + + parentgovtidurl: '', + + studentgovtidurl: '', + + feeamount: '', + + freetrial: false, + + paymentmethod: '', + + paymentstatus: '', + + transactionid: '', +}; + +const EnrollmentNew = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + + const handleSubmit = async (data) => { + await dispatch(create(data)); + await router.push('/enrollment/enrollment-list'); + }; + return ( + <> + + {getPageTitle('New Item')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + router.push('/enrollment/enrollment-list')} + /> + + +
+
+
+ + ); +}; + +EnrollmentNew.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EnrollmentNew; diff --git a/frontend/src/pages/enrollment/enrollment-table.tsx b/frontend/src/pages/enrollment/enrollment-table.tsx new file mode 100644 index 0000000..135af46 --- /dev/null +++ b/frontend/src/pages/enrollment/enrollment-table.tsx @@ -0,0 +1,180 @@ +import { mdiChartTimelineVariant } from '@mdi/js'; +import Head from 'next/head'; +import { uniqueId } from 'lodash'; +import React, { ReactElement, useState } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; +import TableEnrollment from '../../components/Enrollment/TableEnrollment'; +import BaseButton from '../../components/BaseButton'; +import axios from 'axios'; +import Link from 'next/link'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import CardBoxModal from '../../components/CardBoxModal'; +import DragDropFilePicker from '../../components/DragDropFilePicker'; +import { setRefetch, uploadCsv } from '../../stores/enrollment/enrollmentSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const EnrollmentTablesPage = () => { + const [filterItems, setFilterItems] = useState([]); + const [csvFile, setCsvFile] = useState(null); + const [isModalActive, setIsModalActive] = useState(false); + const [showTableView, setShowTableView] = useState(false); + + const { currentUser } = useAppSelector((state) => state.auth); + + const dispatch = useAppDispatch(); + + const [filters] = useState([ + { label: 'Language', title: 'language' }, + { label: 'Board', title: 'board' }, + { label: 'Enrollmentmonth', title: 'enrollmentmonth' }, + { label: 'Studentname', title: 'studentname' }, + { label: 'Gender', title: 'gender' }, + { label: 'Nationality', title: 'nationality' }, + { label: 'Marksheeturl', title: 'marksheeturl' }, + { label: 'Parentgovtidurl', title: 'parentgovtidurl' }, + { label: 'Studentgovtidurl', title: 'studentgovtidurl' }, + { label: 'Paymentmethod', title: 'paymentmethod' }, + { label: 'Paymentstatus', title: 'paymentstatus' }, + { label: 'Transactionid', title: 'transactionid' }, + { label: 'Age', title: 'age', number: 'true' }, + { label: 'Feeamount', title: 'feeamount', number: 'true' }, + + { label: 'Class', title: 'class' }, + + { label: 'Parent', title: 'parent' }, + ]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_ENROLLMENT'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getEnrollmentCSV = async () => { + const response = await axios({ + url: '/enrollment?filetype=csv', + method: 'GET', + responseType: 'blob', + }); + const type = response.headers['content-type']; + const blob = new Blob([response.data], { type: type }); + const link = document.createElement('a'); + link.href = window.URL.createObjectURL(blob); + link.download = 'enrollmentCSV.csv'; + link.click(); + }; + + const onModalConfirm = async () => { + if (!csvFile) return; + await dispatch(uploadCsv(csvFile)); + dispatch(setRefetch(true)); + setCsvFile(null); + setIsModalActive(false); + }; + + const onModalCancel = () => { + setCsvFile(null); + setIsModalActive(false); + }; + + return ( + <> + + {getPageTitle('Enrollment')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
+
+
+
+ + + +
+ + + + + ); +}; + +EnrollmentTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EnrollmentTablesPage; diff --git a/frontend/src/pages/enrollment/enrollment-view.tsx b/frontend/src/pages/enrollment/enrollment-view.tsx new file mode 100644 index 0000000..dcd8917 --- /dev/null +++ b/frontend/src/pages/enrollment/enrollment-view.tsx @@ -0,0 +1,189 @@ +import React, { ReactElement, useEffect } from 'react'; +import Head from 'next/head'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { fetch } from '../../stores/enrollment/enrollmentSlice'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import { getPageTitle } from '../../config'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import SectionMain from '../../components/SectionMain'; +import CardBox from '../../components/CardBox'; +import BaseButton from '../../components/BaseButton'; +import BaseDivider from '../../components/BaseDivider'; +import { mdiChartTimelineVariant } from '@mdi/js'; +import { SwitchField } from '../../components/SwitchField'; +import FormField from '../../components/FormField'; + +const EnrollmentView = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const { enrollment } = useAppSelector((state) => state.enrollment); + + const { id } = router.query; + + function removeLastCharacter(str) { + console.log(str, `str`); + return str.slice(0, -1); + } + + useEffect(() => { + dispatch(fetch({ id })); + }, [dispatch, id]); + + return ( + <> + + {getPageTitle('View enrollment')} + + + + + + +
+

Class

+ +

{enrollment?.class?.subject ?? 'No data'}

+
+ +
+

Parent

+ +

{enrollment?.parent?.first_name ?? 'No data'}

+
+ +
+

Language

+

{enrollment?.language}

+
+ +
+

Board

+

{enrollment?.board}

+
+ +
+

Enrollmentmonth

+

{enrollment?.enrollmentmonth}

+
+ +
+

Studentname

+

{enrollment?.studentname}

+
+ +
+

Age

+

{enrollment?.age || 'No data'}

+
+ +
+

Gender

+

{enrollment?.gender}

+
+ +
+

Nationality

+

{enrollment?.nationality}

+
+ + + {enrollment.dateofbirth ? ( + + ) : ( +

No Dateofbirth

+ )} +
+ +
+

Marksheeturl

+

{enrollment?.marksheeturl}

+
+ +
+

Parentgovtidurl

+

{enrollment?.parentgovtidurl}

+
+ +
+

Studentgovtidurl

+

{enrollment?.studentgovtidurl}

+
+ +
+

Feeamount

+

{enrollment?.feeamount || 'No data'}

+
+ + + null }} + disabled + /> + + +
+

Paymentmethod

+

{enrollment?.paymentmethod}

+
+ +
+

Paymentstatus

+

{enrollment?.paymentstatus}

+
+ +
+

Transactionid

+

{enrollment?.transactionid}

+
+ + + + router.push('/enrollment/enrollment-list')} + /> +
+
+ + ); +}; + +EnrollmentView.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EnrollmentView; diff --git a/frontend/src/pages/index.tsx b/frontend/src/pages/index.tsx index 8ae4379..0223da5 100644 --- a/frontend/src/pages/index.tsx +++ b/frontend/src/pages/index.tsx @@ -129,7 +129,7 @@ export default function WebSite() { { + <> +

Enrollment Parent

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {parents.enrollment_parent && + Array.isArray(parents.enrollment_parent) && + parents.enrollment_parent.map((item: any) => ( + + router.push( + `/enrollment/enrollment-view/?id=${item.id}`, + ) + } + > + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ))} + +
LanguageBoardEnrollmentmonthStudentnameAgeGenderNationalityDateofbirthMarksheeturlParentgovtidurlStudentgovtidurlFeeamountFreetrialPaymentmethodPaymentstatusTransactionid
{item.language}{item.board} + {item.enrollmentmonth} + {item.studentname}{item.age}{item.gender}{item.nationality} + {dataFormatter.dateFormatter(item.dateofbirth)} + {item.marksheeturl} + {item.parentgovtidurl} + + {item.studentgovtidurl} + {item.feeamount} + {dataFormatter.booleanFormatter(item.freetrial)} + + {item.paymentmethod} + + {item.paymentstatus} + + {item.transactionid} +
+
+ {!parents?.enrollment_parent?.length && ( +
No data
+ )} +
+ + { + const { id, query } = data; + const result = await axios.get(`enrollment${query || (id ? `/${id}` : '')}`); + return id + ? result.data + : { rows: result.data.rows, count: result.data.count }; +}); + +export const deleteItemsByIds = createAsyncThunk( + 'enrollment/deleteByIds', + async (data: any, { rejectWithValue }) => { + try { + await axios.post('enrollment/deleteByIds', { data }); + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const deleteItem = createAsyncThunk( + 'enrollment/deleteEnrollment', + async (id: string, { rejectWithValue }) => { + try { + await axios.delete(`enrollment/${id}`); + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const create = createAsyncThunk( + 'enrollment/createEnrollment', + async (data: any, { rejectWithValue }) => { + try { + const result = await axios.post('enrollment', { data }); + return result.data; + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const uploadCsv = createAsyncThunk( + 'enrollment/uploadCsv', + async (file: File, { rejectWithValue }) => { + try { + const data = new FormData(); + data.append('file', file); + data.append('filename', file.name); + + const result = await axios.post('enrollment/bulk-import', data, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }); + + return result.data; + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const update = createAsyncThunk( + 'enrollment/updateEnrollment', + async (payload: any, { rejectWithValue }) => { + try { + const result = await axios.put(`enrollment/${payload.id}`, { + id: payload.id, + data: payload.data, + }); + return result.data; + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const enrollmentSlice = createSlice({ + name: 'enrollment', + initialState, + reducers: { + setRefetch: (state, action: PayloadAction) => { + state.refetch = action.payload; + }, + }, + extraReducers: (builder) => { + builder.addCase(fetch.pending, (state) => { + state.loading = true; + resetNotify(state); + }); + builder.addCase(fetch.rejected, (state, action) => { + state.loading = false; + rejectNotify(state, action); + }); + + builder.addCase(fetch.fulfilled, (state, action) => { + if (action.payload.rows && action.payload.count >= 0) { + state.enrollment = action.payload.rows; + state.count = action.payload.count; + } else { + state.enrollment = action.payload; + } + state.loading = false; + }); + + builder.addCase(deleteItemsByIds.pending, (state) => { + state.loading = true; + resetNotify(state); + }); + + builder.addCase(deleteItemsByIds.fulfilled, (state) => { + state.loading = false; + fulfilledNotify(state, 'Enrollment has been deleted'); + }); + + builder.addCase(deleteItemsByIds.rejected, (state, action) => { + state.loading = false; + rejectNotify(state, action); + }); + + builder.addCase(deleteItem.pending, (state) => { + state.loading = true; + resetNotify(state); + }); + + builder.addCase(deleteItem.fulfilled, (state) => { + state.loading = false; + fulfilledNotify(state, `${'Enrollment'.slice(0, -1)} has been deleted`); + }); + + builder.addCase(deleteItem.rejected, (state, action) => { + state.loading = false; + rejectNotify(state, action); + }); + + builder.addCase(create.pending, (state) => { + state.loading = true; + resetNotify(state); + }); + builder.addCase(create.rejected, (state, action) => { + state.loading = false; + rejectNotify(state, action); + }); + + builder.addCase(create.fulfilled, (state) => { + state.loading = false; + fulfilledNotify(state, `${'Enrollment'.slice(0, -1)} has been created`); + }); + + builder.addCase(update.pending, (state) => { + state.loading = true; + resetNotify(state); + }); + builder.addCase(update.fulfilled, (state) => { + state.loading = false; + fulfilledNotify(state, `${'Enrollment'.slice(0, -1)} has been updated`); + }); + builder.addCase(update.rejected, (state, action) => { + state.loading = false; + rejectNotify(state, action); + }); + + builder.addCase(uploadCsv.pending, (state) => { + state.loading = true; + resetNotify(state); + }); + builder.addCase(uploadCsv.fulfilled, (state) => { + state.loading = false; + fulfilledNotify(state, 'Enrollment has been uploaded'); + }); + builder.addCase(uploadCsv.rejected, (state, action) => { + state.loading = false; + rejectNotify(state, action); + }); + }, +}); + +// Action creators are generated for each case reducer function +export const { setRefetch } = enrollmentSlice.actions; + +export default enrollmentSlice.reducer; diff --git a/frontend/src/stores/store.ts b/frontend/src/stores/store.ts index b295b75..6023a81 100644 --- a/frontend/src/stores/store.ts +++ b/frontend/src/stores/store.ts @@ -12,6 +12,7 @@ import parentsSlice from './parents/parentsSlice'; import studentsSlice from './students/studentsSlice'; import rolesSlice from './roles/rolesSlice'; import permissionsSlice from './permissions/permissionsSlice'; +import enrollmentSlice from './enrollment/enrollmentSlice'; export const store = configureStore({ reducer: { @@ -28,6 +29,7 @@ export const store = configureStore({ students: studentsSlice, roles: rolesSlice, permissions: permissionsSlice, + enrollment: enrollmentSlice, }, });