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 8c1d08d..59c34cc 100644 --- a/app-shell/src/_schema.json +++ b/app-shell/src/_schema.json @@ -1,5 +1,4 @@ - - { - "Initial version": "{\"iv\":\"ShlhcPEJvZXxV5Kb\",\"encryptedData\":\"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\"}" -} + "Initial version": "{\"iv\":\"ShlhcPEJvZXxV5Kb\",\"encryptedData\":\"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\"}", + "Updated via schema editor on 2025-09-03 01:50": "{\"iv\":\"bM29YVgxfoRY9nCi\",\"encryptedData\":\"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\"}" +} \ No newline at end of file diff --git a/backend/src/db/api/organizations.js b/backend/src/db/api/organizations.js new file mode 100644 index 0000000..79e6050 --- /dev/null +++ b/backend/src/db/api/organizations.js @@ -0,0 +1,281 @@ +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 OrganizationsDBApi { + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const organizations = await db.organizations.create( + { + id: data.id || undefined, + + name: data.name || null, + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + return organizations; + } + + 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 organizationsData = data.map((item, index) => ({ + id: item.id || undefined, + + name: item.name || null, + importHash: item.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + // Bulk create items + const organizations = await db.organizations.bulkCreate(organizationsData, { + transaction, + }); + + // For each item created, replace relation files + + return organizations; + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + const globalAccess = currentUser.app_role?.globalAccess; + + const organizations = await db.organizations.findByPk( + id, + {}, + { transaction }, + ); + + const updatePayload = {}; + + if (data.name !== undefined) updatePayload.name = data.name; + + updatePayload.updatedById = currentUser.id; + + await organizations.update(updatePayload, { transaction }); + + return organizations; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const organizations = await db.organizations.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of organizations) { + await record.update({ deletedBy: currentUser.id }, { transaction }); + } + for (const record of organizations) { + await record.destroy({ transaction }); + } + }); + + return organizations; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const organizations = await db.organizations.findByPk(id, options); + + await organizations.update( + { + deletedBy: currentUser.id, + }, + { + transaction, + }, + ); + + await organizations.destroy({ + transaction, + }); + + return organizations; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const organizations = await db.organizations.findOne( + { where }, + { transaction }, + ); + + if (!organizations) { + return organizations; + } + + const output = organizations.get({ plain: true }); + + return output; + } + + static async findAll(filter, globalAccess, options) { + const limit = filter.limit || 0; + let offset = 0; + let where = {}; + const currentPage = +filter.page; + + const user = (options && options.currentUser) || null; + const userSchools = (user && user.schools?.id) || null; + + if (userSchools) { + if (options?.currentUser?.schoolsId) { + where.schoolsId = options.currentUser.schoolsId; + } + } + + offset = currentPage * limit; + + const orderBy = null; + + const transaction = (options && options.transaction) || undefined; + + let include = []; + + if (filter) { + if (filter.id) { + where = { + ...where, + ['id']: Utils.uuid(filter.id), + }; + } + + if (filter.name) { + where = { + ...where, + [Op.and]: Utils.ilike('organizations', 'name', filter.name), + }; + } + + if (filter.active !== undefined) { + where = { + ...where, + active: filter.active === true || filter.active === 'true', + }; + } + + 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, + }, + }; + } + } + } + + if (globalAccess) { + delete where.schoolsId; + } + + 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.organizations.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, + globalAccess, + organizationId, + ) { + let where = {}; + + if (!globalAccess && organizationId) { + where.organizationId = organizationId; + } + + if (query) { + where = { + [Op.or]: [ + { ['id']: Utils.uuid(query) }, + Utils.ilike('organizations', 'id', query), + ], + }; + } + + const records = await db.organizations.findAll({ + attributes: ['id', 'id'], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + orderBy: [['id', 'ASC']], + }); + + return records.map((record) => ({ + id: record.id, + label: record.id, + })); + } +}; diff --git a/backend/src/db/migrations/1756864225393.js b/backend/src/db/migrations/1756864225393.js new file mode 100644 index 0000000..6a6c09f --- /dev/null +++ b/backend/src/db/migrations/1756864225393.js @@ -0,0 +1,85 @@ +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( + 'organizations', + { + 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 queryInterface.addColumn( + 'organizations', + 'name', + { + 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('organizations', 'name', { + transaction, + }); + + await queryInterface.dropTable('organizations', { transaction }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/models/organizations.js b/backend/src/db/models/organizations.js new file mode 100644 index 0000000..deab601 --- /dev/null +++ b/backend/src/db/models/organizations.js @@ -0,0 +1,49 @@ +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 organizations = sequelize.define( + 'organizations', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + + name: { + type: DataTypes.TEXT, + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + organizations.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.organizations.belongsTo(db.users, { + as: 'createdBy', + }); + + db.organizations.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return organizations; +}; diff --git a/backend/src/db/seeders/20200430130760-user-roles.js b/backend/src/db/seeders/20200430130760-user-roles.js index 4e1ae37..5f1522b 100644 --- a/backend/src/db/seeders/20200430130760-user-roles.js +++ b/backend/src/db/seeders/20200430130760-user-roles.js @@ -114,6 +114,7 @@ module.exports = { 'roles', 'permissions', 'schools', + 'organizations', , ]; await queryInterface.bulkInsert( @@ -713,6 +714,31 @@ primary key ("roles_permissionsId", "permissionId") permissionId: getId('DELETE_TEACHERS'), }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('CREATE_ORGANIZATIONS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('READ_ORGANIZATIONS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('UPDATE_ORGANIZATIONS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('DELETE_ORGANIZATIONS'), + }, + { createdAt, updatedAt, @@ -913,6 +939,31 @@ primary key ("roles_permissionsId", "permissionId") permissionId: getId('DELETE_SCHOOLS'), }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('CREATE_ORGANIZATIONS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('READ_ORGANIZATIONS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('UPDATE_ORGANIZATIONS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('DELETE_ORGANIZATIONS'), + }, + { createdAt, updatedAt, diff --git a/backend/src/db/seeders/20231127130745-sample-data.js b/backend/src/db/seeders/20231127130745-sample-data.js index b92bda9..f10326f 100644 --- a/backend/src/db/seeders/20231127130745-sample-data.js +++ b/backend/src/db/seeders/20231127130745-sample-data.js @@ -11,6 +11,8 @@ const Teachers = db.teachers; const Schools = db.schools; +const Organizations = db.organizations; + const CoursesData = [ { title: 'Mathematics 101', @@ -47,30 +49,6 @@ const CoursesData = [ // type code here for "relation_one" field }, - - { - title: 'Chemistry Essentials', - - description: 'Basic principles of chemistry and laboratory techniques.', - - // type code here for "relation_one" field - - // type code here for "relation_many" field - - // type code here for "relation_one" field - }, - - { - title: 'Physics Principles', - - description: 'Introduction to the principles of physics.', - - // type code here for "relation_one" field - - // type code here for "relation_many" field - - // type code here for "relation_one" field - }, ]; const GradesData = [ @@ -103,26 +81,6 @@ const GradesData = [ // type code here for "relation_one" field }, - - { - // type code here for "relation_one" field - - // type code here for "relation_one" field - - grade_value: 79.5, - - // type code here for "relation_one" field - }, - - { - // type code here for "relation_one" field - - // type code here for "relation_one" field - - grade_value: 91, - - // type code here for "relation_one" field - }, ]; const StudentsData = [ @@ -167,34 +125,6 @@ const StudentsData = [ // type code here for "relation_one" field }, - - { - first_name: 'Michael', - - last_name: 'Brown', - - enrollment_date: new Date('2023-09-04T08:00:00Z'), - - // type code here for "relation_one" field - - // type code here for "relation_many" field - - // type code here for "relation_one" field - }, - - { - first_name: 'Sarah', - - last_name: 'Davis', - - enrollment_date: new Date('2023-09-05T08:00:00Z'), - - // type code here for "relation_one" field - - // type code here for "relation_many" field - - // type code here for "relation_one" field - }, ]; const TeachersData = [ @@ -233,30 +163,6 @@ const TeachersData = [ // type code here for "relation_one" field }, - - { - first_name: 'David', - - last_name: 'Wilson', - - // type code here for "relation_one" field - - // type code here for "relation_many" field - - // type code here for "relation_one" field - }, - - { - first_name: 'Sophia', - - last_name: 'Moore', - - // type code here for "relation_one" field - - // type code here for "relation_many" field - - // type code here for "relation_one" field - }, ]; const SchoolsData = [ @@ -271,13 +177,19 @@ const SchoolsData = [ { name: 'Sunnydale School', }, +]; +const OrganizationsData = [ { - name: 'Hilltop Institute', + name: 'Konrad Lorenz', }, { - name: 'Lakeside College', + name: 'Andreas Vesalius', + }, + + { + name: 'Edward Teller', }, ]; @@ -316,28 +228,6 @@ async function associateUserWithSchool() { if (User2?.setSchool) { await User2.setSchool(relatedSchool2); } - - const relatedSchool3 = await Schools.findOne({ - offset: Math.floor(Math.random() * (await Schools.count())), - }); - const User3 = await Users.findOne({ - order: [['id', 'ASC']], - offset: 3, - }); - if (User3?.setSchool) { - await User3.setSchool(relatedSchool3); - } - - const relatedSchool4 = await Schools.findOne({ - offset: Math.floor(Math.random() * (await Schools.count())), - }); - const User4 = await Users.findOne({ - order: [['id', 'ASC']], - offset: 4, - }); - if (User4?.setSchool) { - await User4.setSchool(relatedSchool4); - } } async function associateCourseWithTeacher() { @@ -373,28 +263,6 @@ async function associateCourseWithTeacher() { if (Course2?.setTeacher) { await Course2.setTeacher(relatedTeacher2); } - - const relatedTeacher3 = await Teachers.findOne({ - offset: Math.floor(Math.random() * (await Teachers.count())), - }); - const Course3 = await Courses.findOne({ - order: [['id', 'ASC']], - offset: 3, - }); - if (Course3?.setTeacher) { - await Course3.setTeacher(relatedTeacher3); - } - - const relatedTeacher4 = await Teachers.findOne({ - offset: Math.floor(Math.random() * (await Teachers.count())), - }); - const Course4 = await Courses.findOne({ - order: [['id', 'ASC']], - offset: 4, - }); - if (Course4?.setTeacher) { - await Course4.setTeacher(relatedTeacher4); - } } // Similar logic for "relation_many" @@ -432,28 +300,6 @@ async function associateCourseWithSchool() { if (Course2?.setSchool) { await Course2.setSchool(relatedSchool2); } - - const relatedSchool3 = await Schools.findOne({ - offset: Math.floor(Math.random() * (await Schools.count())), - }); - const Course3 = await Courses.findOne({ - order: [['id', 'ASC']], - offset: 3, - }); - if (Course3?.setSchool) { - await Course3.setSchool(relatedSchool3); - } - - const relatedSchool4 = await Schools.findOne({ - offset: Math.floor(Math.random() * (await Schools.count())), - }); - const Course4 = await Courses.findOne({ - order: [['id', 'ASC']], - offset: 4, - }); - if (Course4?.setSchool) { - await Course4.setSchool(relatedSchool4); - } } async function associateGradeWithStudent() { @@ -489,28 +335,6 @@ async function associateGradeWithStudent() { if (Grade2?.setStudent) { await Grade2.setStudent(relatedStudent2); } - - const relatedStudent3 = await Students.findOne({ - offset: Math.floor(Math.random() * (await Students.count())), - }); - const Grade3 = await Grades.findOne({ - order: [['id', 'ASC']], - offset: 3, - }); - if (Grade3?.setStudent) { - await Grade3.setStudent(relatedStudent3); - } - - const relatedStudent4 = await Students.findOne({ - offset: Math.floor(Math.random() * (await Students.count())), - }); - const Grade4 = await Grades.findOne({ - order: [['id', 'ASC']], - offset: 4, - }); - if (Grade4?.setStudent) { - await Grade4.setStudent(relatedStudent4); - } } async function associateGradeWithCourse() { @@ -546,28 +370,6 @@ async function associateGradeWithCourse() { if (Grade2?.setCourse) { await Grade2.setCourse(relatedCourse2); } - - const relatedCourse3 = await Courses.findOne({ - offset: Math.floor(Math.random() * (await Courses.count())), - }); - const Grade3 = await Grades.findOne({ - order: [['id', 'ASC']], - offset: 3, - }); - if (Grade3?.setCourse) { - await Grade3.setCourse(relatedCourse3); - } - - const relatedCourse4 = await Courses.findOne({ - offset: Math.floor(Math.random() * (await Courses.count())), - }); - const Grade4 = await Grades.findOne({ - order: [['id', 'ASC']], - offset: 4, - }); - if (Grade4?.setCourse) { - await Grade4.setCourse(relatedCourse4); - } } async function associateGradeWithSchool() { @@ -603,28 +405,6 @@ async function associateGradeWithSchool() { if (Grade2?.setSchool) { await Grade2.setSchool(relatedSchool2); } - - const relatedSchool3 = await Schools.findOne({ - offset: Math.floor(Math.random() * (await Schools.count())), - }); - const Grade3 = await Grades.findOne({ - order: [['id', 'ASC']], - offset: 3, - }); - if (Grade3?.setSchool) { - await Grade3.setSchool(relatedSchool3); - } - - const relatedSchool4 = await Schools.findOne({ - offset: Math.floor(Math.random() * (await Schools.count())), - }); - const Grade4 = await Grades.findOne({ - order: [['id', 'ASC']], - offset: 4, - }); - if (Grade4?.setSchool) { - await Grade4.setSchool(relatedSchool4); - } } async function associateStudentWithSchool() { @@ -660,28 +440,6 @@ async function associateStudentWithSchool() { if (Student2?.setSchool) { await Student2.setSchool(relatedSchool2); } - - const relatedSchool3 = await Schools.findOne({ - offset: Math.floor(Math.random() * (await Schools.count())), - }); - const Student3 = await Students.findOne({ - order: [['id', 'ASC']], - offset: 3, - }); - if (Student3?.setSchool) { - await Student3.setSchool(relatedSchool3); - } - - const relatedSchool4 = await Schools.findOne({ - offset: Math.floor(Math.random() * (await Schools.count())), - }); - const Student4 = await Students.findOne({ - order: [['id', 'ASC']], - offset: 4, - }); - if (Student4?.setSchool) { - await Student4.setSchool(relatedSchool4); - } } // Similar logic for "relation_many" @@ -719,28 +477,6 @@ async function associateStudentWithSchool() { if (Student2?.setSchool) { await Student2.setSchool(relatedSchool2); } - - const relatedSchool3 = await Schools.findOne({ - offset: Math.floor(Math.random() * (await Schools.count())), - }); - const Student3 = await Students.findOne({ - order: [['id', 'ASC']], - offset: 3, - }); - if (Student3?.setSchool) { - await Student3.setSchool(relatedSchool3); - } - - const relatedSchool4 = await Schools.findOne({ - offset: Math.floor(Math.random() * (await Schools.count())), - }); - const Student4 = await Students.findOne({ - order: [['id', 'ASC']], - offset: 4, - }); - if (Student4?.setSchool) { - await Student4.setSchool(relatedSchool4); - } } async function associateTeacherWithSchool() { @@ -776,28 +512,6 @@ async function associateTeacherWithSchool() { if (Teacher2?.setSchool) { await Teacher2.setSchool(relatedSchool2); } - - const relatedSchool3 = await Schools.findOne({ - offset: Math.floor(Math.random() * (await Schools.count())), - }); - const Teacher3 = await Teachers.findOne({ - order: [['id', 'ASC']], - offset: 3, - }); - if (Teacher3?.setSchool) { - await Teacher3.setSchool(relatedSchool3); - } - - const relatedSchool4 = await Schools.findOne({ - offset: Math.floor(Math.random() * (await Schools.count())), - }); - const Teacher4 = await Teachers.findOne({ - order: [['id', 'ASC']], - offset: 4, - }); - if (Teacher4?.setSchool) { - await Teacher4.setSchool(relatedSchool4); - } } // Similar logic for "relation_many" @@ -835,28 +549,6 @@ async function associateTeacherWithSchool() { if (Teacher2?.setSchool) { await Teacher2.setSchool(relatedSchool2); } - - const relatedSchool3 = await Schools.findOne({ - offset: Math.floor(Math.random() * (await Schools.count())), - }); - const Teacher3 = await Teachers.findOne({ - order: [['id', 'ASC']], - offset: 3, - }); - if (Teacher3?.setSchool) { - await Teacher3.setSchool(relatedSchool3); - } - - const relatedSchool4 = await Schools.findOne({ - offset: Math.floor(Math.random() * (await Schools.count())), - }); - const Teacher4 = await Teachers.findOne({ - order: [['id', 'ASC']], - offset: 4, - }); - if (Teacher4?.setSchool) { - await Teacher4.setSchool(relatedSchool4); - } } module.exports = { @@ -871,6 +563,8 @@ module.exports = { await Schools.bulkCreate(SchoolsData); + await Organizations.bulkCreate(OrganizationsData); + await Promise.all([ // Similar logic for "relation_many" @@ -912,5 +606,7 @@ module.exports = { await queryInterface.bulkDelete('teachers', null, {}); await queryInterface.bulkDelete('schools', null, {}); + + await queryInterface.bulkDelete('organizations', null, {}); }, }; diff --git a/backend/src/db/seeders/20250903015025.js b/backend/src/db/seeders/20250903015025.js new file mode 100644 index 0000000..32df522 --- /dev/null +++ b/backend/src/db/seeders/20250903015025.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 = ['organizations']; + + 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.super_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 25166fb..48b4525 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -35,6 +35,8 @@ const permissionsRoutes = require('./routes/permissions'); const schoolsRoutes = require('./routes/schools'); +const organizationsRoutes = require('./routes/organizations'); + const getBaseUrl = (url) => { if (!url) return ''; return url.endsWith('/api') ? url.slice(0, -4) : url; @@ -148,6 +150,12 @@ app.use( schoolsRoutes, ); +app.use( + '/api/organizations', + passport.authenticate('jwt', { session: false }), + organizationsRoutes, +); + app.use( '/api/openai', passport.authenticate('jwt', { session: false }), diff --git a/backend/src/routes/organizations.js b/backend/src/routes/organizations.js new file mode 100644 index 0000000..61d72a9 --- /dev/null +++ b/backend/src/routes/organizations.js @@ -0,0 +1,456 @@ +const express = require('express'); + +const OrganizationsService = require('../services/organizations'); +const OrganizationsDBApi = require('../db/api/organizations'); +const wrapAsync = require('../helpers').wrapAsync; + +const config = require('../config'); + +const router = express.Router(); + +const { parse } = require('json2csv'); + +const { checkCrudPermissions } = require('../middlewares/check-permissions'); + +router.use(checkCrudPermissions('organizations')); + +/** + * @swagger + * components: + * schemas: + * Organizations: + * type: object + * properties: + + * name: + * type: string + * default: name + + */ + +/** + * @swagger + * tags: + * name: Organizations + * description: The Organizations managing API + */ + +/** + * @swagger + * /api/organizations: + * post: + * security: + * - bearerAuth: [] + * tags: [Organizations] + * 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/Organizations" + * responses: + * 200: + * description: The item was successfully added + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Organizations" + * 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 OrganizationsService.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: [Organizations] + * 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/Organizations" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Organizations" + * 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 OrganizationsService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/organizations/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Organizations] + * 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/Organizations" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Organizations" + * 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 OrganizationsService.update( + req.body.data, + req.body.id, + req.currentUser, + ); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/organizations/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Organizations] + * 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/Organizations" + * 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 OrganizationsService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/organizations/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Organizations] + * 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/Organizations" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post( + '/deleteByIds', + wrapAsync(async (req, res) => { + await OrganizationsService.deleteByIds(req.body.data, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/organizations: + * get: + * security: + * - bearerAuth: [] + * tags: [Organizations] + * summary: Get all organizations + * description: Get all organizations + * responses: + * 200: + * description: Organizations list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Organizations" + * 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 globalAccess = req.currentUser.app_role.globalAccess; + + const currentUser = req.currentUser; + const payload = await OrganizationsDBApi.findAll(req.query, globalAccess, { + currentUser, + }); + if (filetype && filetype === 'csv') { + const fields = ['id', 'name']; + 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/organizations/count: + * get: + * security: + * - bearerAuth: [] + * tags: [Organizations] + * summary: Count all organizations + * description: Count all organizations + * responses: + * 200: + * description: Organizations count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Organizations" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get( + '/count', + wrapAsync(async (req, res) => { + const globalAccess = req.currentUser.app_role.globalAccess; + + const currentUser = req.currentUser; + const payload = await OrganizationsDBApi.findAll(req.query, globalAccess, { + countOnly: true, + currentUser, + }); + + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/organizations/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [Organizations] + * summary: Find all organizations that match search criteria + * description: Find all organizations that match search criteria + * responses: + * 200: + * description: Organizations list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Organizations" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/autocomplete', async (req, res) => { + const globalAccess = req.currentUser.app_role.globalAccess; + + const organizationId = req.currentUser.organization?.id; + + const payload = await OrganizationsDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + globalAccess, + organizationId, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/organizations/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Organizations] + * 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/Organizations" + * 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 OrganizationsDBApi.findBy({ id: req.params.id }); + + res.status(200).send(payload); + }), +); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/services/organizations.js b/backend/src/services/organizations.js new file mode 100644 index 0000000..453aaf3 --- /dev/null +++ b/backend/src/services/organizations.js @@ -0,0 +1,117 @@ +const db = require('../db/models'); +const OrganizationsDBApi = require('../db/api/organizations'); +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 OrganizationsService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await OrganizationsDBApi.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 OrganizationsDBApi.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 organizations = await OrganizationsDBApi.findBy( + { id }, + { transaction }, + ); + + if (!organizations) { + throw new ValidationError('organizationsNotFound'); + } + + const updatedOrganizations = await OrganizationsDBApi.update(id, data, { + currentUser, + transaction, + }); + + await transaction.commit(); + return updatedOrganizations; + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await OrganizationsDBApi.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 OrganizationsDBApi.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 d60369c..513c94b 100644 --- a/backend/src/services/search.js +++ b/backend/src/services/search.js @@ -50,6 +50,8 @@ module.exports = class SearchService { teachers: ['first_name', 'last_name'], schools: ['name'], + + organizations: ['name'], }; const columnsInt = { grades: ['grade_value'], diff --git a/frontend/json/runtimeError.json b/frontend/json/runtimeError.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/frontend/json/runtimeError.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/frontend/src/components/Organizations/CardOrganizations.tsx b/frontend/src/components/Organizations/CardOrganizations.tsx new file mode 100644 index 0000000..cc0a7cc --- /dev/null +++ b/frontend/src/components/Organizations/CardOrganizations.tsx @@ -0,0 +1,108 @@ +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 = { + organizations: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const CardOrganizations = ({ + organizations, + 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_ORGANIZATIONS', + ); + + return ( +
+ {loading && } +
    + {!loading && + organizations.map((item, index) => ( +
  • +
    + + {item.id} + + +
    + +
    +
    +
    +
    +
    Name
    +
    +
    {item.name}
    +
    +
    +
    +
  • + ))} + {!loading && organizations.length === 0 && ( +
    +

    No data to display

    +
    + )} +
+
+ +
+
+ ); +}; + +export default CardOrganizations; diff --git a/frontend/src/components/Organizations/ListOrganizations.tsx b/frontend/src/components/Organizations/ListOrganizations.tsx new file mode 100644 index 0000000..a9e20aa --- /dev/null +++ b/frontend/src/components/Organizations/ListOrganizations.tsx @@ -0,0 +1,92 @@ +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 = { + organizations: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const ListOrganizations = ({ + organizations, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const currentUser = useAppSelector((state) => state.auth.currentUser); + const hasUpdatePermission = hasPermission( + currentUser, + 'UPDATE_ORGANIZATIONS', + ); + + const corners = useAppSelector((state) => state.style.corners); + const bgColor = useAppSelector((state) => state.style.cardsColor); + + return ( + <> +
+ {loading && } + {!loading && + organizations.map((item) => ( +
+ +
+ dark:divide-dark-700 overflow-x-auto' + } + > +
+

Name

+

{item.name}

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

No data to display

+
+ )} +
+
+ +
+ + ); +}; + +export default ListOrganizations; diff --git a/frontend/src/components/Organizations/TableOrganizations.tsx b/frontend/src/components/Organizations/TableOrganizations.tsx new file mode 100644 index 0000000..4850743 --- /dev/null +++ b/frontend/src/components/Organizations/TableOrganizations.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/organizations/organizationsSlice'; +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 './configureOrganizationsCols'; +import _ from 'lodash'; +import dataFormatter from '../../helpers/dataFormatter'; +import { dataGridStyles } from '../../styles'; + +const perPage = 10; + +const TableSampleOrganizations = ({ + 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 { + organizations, + loading, + count, + notify: organizationsNotify, + refetch, + } = useAppSelector((state) => state.organizations); + 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 (organizationsNotify.showNotification) { + notify( + organizationsNotify.typeNotification, + organizationsNotify.textNotification, + ); + } + }, [organizationsNotify.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, `organizations`, 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={organizations ?? []} + 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 TableSampleOrganizations; diff --git a/frontend/src/components/Organizations/configureOrganizationsCols.tsx b/frontend/src/components/Organizations/configureOrganizationsCols.tsx new file mode 100644 index 0000000..b8b52e0 --- /dev/null +++ b/frontend/src/components/Organizations/configureOrganizationsCols.tsx @@ -0,0 +1,74 @@ +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_ORGANIZATIONS'); + + return [ + { + field: 'name', + headerName: 'Name', + 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/menuAside.ts b/frontend/src/menuAside.ts index 88e4c8b..3b6a900 100644 --- a/frontend/src/menuAside.ts +++ b/frontend/src/menuAside.ts @@ -84,6 +84,14 @@ const menuAside: MenuAsideItem[] = [ icon: icon.mdiTable ?? icon.mdiTable, permissions: 'READ_SCHOOLS', }, + { + href: '/organizations/organizations-list', + label: 'Organizations', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: icon.mdiTable ?? icon.mdiTable, + permissions: 'READ_ORGANIZATIONS', + }, { href: '/profile', label: 'Profile', diff --git a/frontend/src/pages/dashboard.tsx b/frontend/src/pages/dashboard.tsx index 3de821d..892bbd9 100644 --- a/frontend/src/pages/dashboard.tsx +++ b/frontend/src/pages/dashboard.tsx @@ -36,6 +36,7 @@ const Dashboard = () => { const [roles, setRoles] = React.useState(loadingMessage); const [permissions, setPermissions] = React.useState(loadingMessage); const [schools, setSchools] = React.useState(loadingMessage); + const [organizations, setOrganizations] = React.useState(loadingMessage); const [widgetsRole, setWidgetsRole] = React.useState({ role: { value: '', label: '' }, @@ -57,6 +58,7 @@ const Dashboard = () => { 'roles', 'permissions', 'schools', + 'organizations', ]; const fns = [ setUsers, @@ -67,6 +69,7 @@ const Dashboard = () => { setRoles, setPermissions, setSchools, + setOrganizations, ]; const requests = entities.map((entity, index) => { @@ -454,6 +457,38 @@ const Dashboard = () => { )} + + {hasPermission(currentUser, 'READ_ORGANIZATIONS') && ( + +
+
+
+
+ Organizations +
+
+ {organizations} +
+
+
+ +
+
+
+ + )} diff --git a/frontend/src/pages/organizations/[organizationsId].tsx b/frontend/src/pages/organizations/[organizationsId].tsx new file mode 100644 index 0000000..25053b2 --- /dev/null +++ b/frontend/src/pages/organizations/[organizationsId].tsx @@ -0,0 +1,132 @@ +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/organizations/organizationsSlice'; +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'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const EditOrganizations = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + name: '', + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { organizations } = useAppSelector((state) => state.organizations); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { organizationsId } = router.query; + + useEffect(() => { + dispatch(fetch({ id: organizationsId })); + }, [organizationsId]); + + useEffect(() => { + if (typeof organizations === 'object') { + setInitialValues(organizations); + } + }, [organizations]); + + useEffect(() => { + if (typeof organizations === 'object') { + const newInitialVal = { ...initVals }; + + Object.keys(initVals).forEach( + (el) => (newInitialVal[el] = organizations[el]), + ); + + setInitialValues(newInitialVal); + } + }, [organizations]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: organizationsId, data })); + await router.push('/organizations/organizations-list'); + }; + + return ( + <> + + {getPageTitle('Edit organizations')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + + + + + + router.push('/organizations/organizations-list') + } + /> + + +
+
+
+ + ); +}; + +EditOrganizations.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditOrganizations; diff --git a/frontend/src/pages/organizations/organizations-edit.tsx b/frontend/src/pages/organizations/organizations-edit.tsx new file mode 100644 index 0000000..ab8b893 --- /dev/null +++ b/frontend/src/pages/organizations/organizations-edit.tsx @@ -0,0 +1,130 @@ +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/organizations/organizationsSlice'; +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'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const EditOrganizationsPage = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + name: '', + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { organizations } = useAppSelector((state) => state.organizations); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { id } = router.query; + + useEffect(() => { + dispatch(fetch({ id: id })); + }, [id]); + + useEffect(() => { + if (typeof organizations === 'object') { + setInitialValues(organizations); + } + }, [organizations]); + + useEffect(() => { + if (typeof organizations === 'object') { + const newInitialVal = { ...initVals }; + Object.keys(initVals).forEach( + (el) => (newInitialVal[el] = organizations[el]), + ); + setInitialValues(newInitialVal); + } + }, [organizations]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: id, data })); + await router.push('/organizations/organizations-list'); + }; + + return ( + <> + + {getPageTitle('Edit organizations')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + + + + + + router.push('/organizations/organizations-list') + } + /> + + +
+
+
+ + ); +}; + +EditOrganizationsPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditOrganizationsPage; diff --git a/frontend/src/pages/organizations/organizations-list.tsx b/frontend/src/pages/organizations/organizations-list.tsx new file mode 100644 index 0000000..9e0a09b --- /dev/null +++ b/frontend/src/pages/organizations/organizations-list.tsx @@ -0,0 +1,165 @@ +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 TableOrganizations from '../../components/Organizations/TableOrganizations'; +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/organizations/organizationsSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const OrganizationsTablesPage = () => { + 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: 'Name', title: 'name' }]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_ORGANIZATIONS'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getOrganizationsCSV = async () => { + const response = await axios({ + url: '/organizations?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 = 'organizationsCSV.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('Organizations')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
+
+
+
+ + + + +
+ + + + + ); +}; + +OrganizationsTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default OrganizationsTablesPage; diff --git a/frontend/src/pages/organizations/organizations-new.tsx b/frontend/src/pages/organizations/organizations-new.tsx new file mode 100644 index 0000000..2787572 --- /dev/null +++ b/frontend/src/pages/organizations/organizations-new.tsx @@ -0,0 +1,100 @@ +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/organizations/organizationsSlice'; +import { useAppDispatch } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import moment from 'moment'; + +const initialValues = { + name: '', +}; + +const OrganizationsNew = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + + const handleSubmit = async (data) => { + await dispatch(create(data)); + await router.push('/organizations/organizations-list'); + }; + return ( + <> + + {getPageTitle('New Item')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + + + + + + router.push('/organizations/organizations-list') + } + /> + + +
+
+
+ + ); +}; + +OrganizationsNew.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default OrganizationsNew; diff --git a/frontend/src/pages/organizations/organizations-table.tsx b/frontend/src/pages/organizations/organizations-table.tsx new file mode 100644 index 0000000..489c393 --- /dev/null +++ b/frontend/src/pages/organizations/organizations-table.tsx @@ -0,0 +1,164 @@ +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 TableOrganizations from '../../components/Organizations/TableOrganizations'; +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/organizations/organizationsSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const OrganizationsTablesPage = () => { + 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: 'Name', title: 'name' }]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_ORGANIZATIONS'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getOrganizationsCSV = async () => { + const response = await axios({ + url: '/organizations?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 = 'organizationsCSV.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('Organizations')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
+
+
+
+ + + +
+ + + + + ); +}; + +OrganizationsTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default OrganizationsTablesPage; diff --git a/frontend/src/pages/organizations/organizations-view.tsx b/frontend/src/pages/organizations/organizations-view.tsx new file mode 100644 index 0000000..8ce7df9 --- /dev/null +++ b/frontend/src/pages/organizations/organizations-view.tsx @@ -0,0 +1,87 @@ +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/organizations/organizationsSlice'; +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'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const OrganizationsView = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const { organizations } = useAppSelector((state) => state.organizations); + + const { currentUser } = useAppSelector((state) => state.auth); + + 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 organizations')} + + + + + + +
+

Name

+

{organizations?.name}

+
+ + + + router.push('/organizations/organizations-list')} + /> +
+
+ + ); +}; + +OrganizationsView.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default OrganizationsView; diff --git a/frontend/src/stores/organizations/organizationsSlice.ts b/frontend/src/stores/organizations/organizationsSlice.ts new file mode 100644 index 0000000..90bbf2c --- /dev/null +++ b/frontend/src/stores/organizations/organizationsSlice.ts @@ -0,0 +1,250 @@ +import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit'; +import axios from 'axios'; +import { + fulfilledNotify, + rejectNotify, + resetNotify, +} from '../../helpers/notifyStateHandler'; + +interface MainState { + organizations: any; + loading: boolean; + count: number; + refetch: boolean; + rolesWidgets: any[]; + notify: { + showNotification: boolean; + textNotification: string; + typeNotification: string; + }; +} + +const initialState: MainState = { + organizations: [], + loading: false, + count: 0, + refetch: false, + rolesWidgets: [], + notify: { + showNotification: false, + textNotification: '', + typeNotification: 'warn', + }, +}; + +export const fetch = createAsyncThunk( + 'organizations/fetch', + async (data: any) => { + const { id, query } = data; + const result = await axios.get( + `organizations${query || (id ? `/${id}` : '')}`, + ); + return id + ? result.data + : { rows: result.data.rows, count: result.data.count }; + }, +); + +export const deleteItemsByIds = createAsyncThunk( + 'organizations/deleteByIds', + async (data: any, { rejectWithValue }) => { + try { + await axios.post('organizations/deleteByIds', { data }); + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const deleteItem = createAsyncThunk( + 'organizations/deleteOrganizations', + async (id: string, { rejectWithValue }) => { + try { + await axios.delete(`organizations/${id}`); + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const create = createAsyncThunk( + 'organizations/createOrganizations', + async (data: any, { rejectWithValue }) => { + try { + const result = await axios.post('organizations', { data }); + return result.data; + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const uploadCsv = createAsyncThunk( + 'organizations/uploadCsv', + async (file: File, { rejectWithValue }) => { + try { + const data = new FormData(); + data.append('file', file); + data.append('filename', file.name); + + const result = await axios.post('organizations/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( + 'organizations/updateOrganizations', + async (payload: any, { rejectWithValue }) => { + try { + const result = await axios.put(`organizations/${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 organizationsSlice = createSlice({ + name: 'organizations', + 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.organizations = action.payload.rows; + state.count = action.payload.count; + } else { + state.organizations = 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, 'Organizations 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, + `${'Organizations'.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, + `${'Organizations'.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, + `${'Organizations'.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, 'Organizations 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 } = organizationsSlice.actions; + +export default organizationsSlice.reducer; diff --git a/frontend/src/stores/store.ts b/frontend/src/stores/store.ts index 836befe..94a25ae 100644 --- a/frontend/src/stores/store.ts +++ b/frontend/src/stores/store.ts @@ -12,6 +12,7 @@ import teachersSlice from './teachers/teachersSlice'; import rolesSlice from './roles/rolesSlice'; import permissionsSlice from './permissions/permissionsSlice'; import schoolsSlice from './schools/schoolsSlice'; +import organizationsSlice from './organizations/organizationsSlice'; export const store = configureStore({ reducer: { @@ -28,6 +29,7 @@ export const store = configureStore({ roles: rolesSlice, permissions: permissionsSlice, schools: schoolsSlice, + organizations: organizationsSlice, }, });