diff --git a/app-shell/src/_schema.json b/app-shell/src/_schema.json index d58d012..b88121b 100644 --- a/app-shell/src/_schema.json +++ b/app-shell/src/_schema.json @@ -1,5 +1,6 @@ { "Initial version": "{\"iv\":\"WCoNMD9YQh2OBTEl\",\"encryptedData\":\"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\"}", "v1.01": "{\"iv\":\"T/JOEruewTDuCHrT\",\"encryptedData\":\"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\"}", - "": "{\"iv\":\"FTmNehtF9eqUfh2D\",\"encryptedData\":\"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\"}" + "": "{\"iv\":\"FTmNehtF9eqUfh2D\",\"encryptedData\":\"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\"}", + "3.01": "{\"iv\":\"d/503dMQEAgGiItI\",\"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..3316248 --- /dev/null +++ b/backend/src/db/api/organizations.js @@ -0,0 +1,267 @@ + +const db = require('../models'); +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 }); + + return organizations; + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || {id: null}; + const transaction = (options && options.transaction) || undefined; + + 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, options) { + const limit = filter.limit || 0; + let offset = 0; + let where = {}; + const currentPage = +filter.page; + + const user = (options && options.currentUser) || null; + + 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, + }, + }; + } + } + } + + 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) { + let where = {}; + + if (query) { + where = { + [Op.or]: [ + { ['id']: Utils.uuid(query) }, + Utils.ilike( + 'organizations', + 'name', + query, + ), + ], + }; + } + + const records = await db.organizations.findAll({ + attributes: [ 'id', 'name' ], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + orderBy: [['name', 'ASC']], + }); + + return records.map((record) => ({ + id: record.id, + label: record.name, + })); + } + +}; + diff --git a/backend/src/db/migrations/1756220125378.js b/backend/src/db/migrations/1756220125378.js new file mode 100644 index 0000000..ef158cb --- /dev/null +++ b/backend/src/db/migrations/1756220125378.js @@ -0,0 +1,91 @@ +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', + 'organizationsId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'organizations', + 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( + 'organizations', + 'organizationsId', + { transaction } + ); + + await queryInterface.dropTable('organizations', { transaction }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + } +}; diff --git a/backend/src/db/migrations/1756220134952.js b/backend/src/db/migrations/1756220134952.js new file mode 100644 index 0000000..a8c0cf1 --- /dev/null +++ b/backend/src/db/migrations/1756220134952.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( + '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 transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + } +}; diff --git a/backend/src/db/migrations/1756220141114.js b/backend/src/db/migrations/1756220141114.js new file mode 100644 index 0000000..94e61c5 --- /dev/null +++ b/backend/src/db/migrations/1756220141114.js @@ -0,0 +1,59 @@ +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( + 'users', + 'organizationId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'organizations', + 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( + 'users', + 'organizationId', + { transaction } + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + } +}; diff --git a/backend/src/db/migrations/1756220146280.js b/backend/src/db/migrations/1756220146280.js new file mode 100644 index 0000000..a1c6034 --- /dev/null +++ b/backend/src/db/migrations/1756220146280.js @@ -0,0 +1,38 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + + 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 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..708ca1e --- /dev/null +++ b/backend/src/db/models/organizations.js @@ -0,0 +1,48 @@ +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) => { + + db.organizations.belongsTo(db.users, { + as: 'createdBy', + }); + + db.organizations.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return organizations; +}; + diff --git a/backend/src/index.js b/backend/src/index.js index 8877987..140fe34 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -24,6 +24,8 @@ const check_insRoutes = require('./routes/check_ins'); const exportsRoutes = require('./routes/exports'); +const organizationsRoutes = require('./routes/organizations'); + const getBaseUrl = (url) => { if (!url) return ''; return url.endsWith('/api') ? url.slice(0, -4) : url; @@ -86,6 +88,8 @@ app.use('/api/check_ins', passport.authenticate('jwt', {session: false}), check_ app.use('/api/exports', passport.authenticate('jwt', {session: false}), exportsRoutes); +app.use('/api/organizations', passport.authenticate('jwt', {session: false}), organizationsRoutes); + app.use('/api/contact-form', contactFormRoutes); app.use( diff --git a/backend/src/routes/organizations.js b/backend/src/routes/organizations.js new file mode 100644 index 0000000..536b890 --- /dev/null +++ b/backend/src/routes/organizations.js @@ -0,0 +1,410 @@ + +const express = require('express'); + +const OrganizationsService = require('../services/organizations'); +const OrganizationsDBApi = require('../db/api/organizations'); +const wrapAsync = require('../helpers').wrapAsync; + +const router = express.Router(); + +const { parse } = require('json2csv'); + +/** + * @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 currentUser = req.currentUser; + const payload = await OrganizationsDBApi.findAll( + req.query, { 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 currentUser = req.currentUser; + const payload = await OrganizationsDBApi.findAll( + req.query, + null, + { 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 payload = await OrganizationsDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + ); + + 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..05fec6f --- /dev/null +++ b/backend/src/services/organizations.js @@ -0,0 +1,131 @@ +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 c5e9cdb..13860a6 100644 --- a/backend/src/services/search.js +++ b/backend/src/services/search.js @@ -67,6 +67,12 @@ module.exports = class SearchService { ], + "organizations": [ + + "name", + + ], + }; const columnsInt = { diff --git a/frontend/src/components/Organizations/CardOrganizations.tsx b/frontend/src/components/Organizations/CardOrganizations.tsx new file mode 100644 index 0000000..fbc1be1 --- /dev/null +++ b/frontend/src/components/Organizations/CardOrganizations.tsx @@ -0,0 +1,102 @@ +import React from 'react'; +import ListActionsPopover from '../ListActionsPopover'; +import { useAppSelector } from '../../stores/hooks'; +import dataFormatter from '../../helpers/dataFormatter'; +import { Pagination } from '../Pagination'; +import LoadingSpinner from "../LoadingSpinner"; +import Link from 'next/link'; + +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); + + return ( +
+ {loading && } +
    + {!loading && organizations.map((item, index) => ( +
  • + + {item.name} + + +
    + +
    +
+
+ +
+
Name
+
+
+ { item.name } +
+
+
+ +
+
Members
+
+
+ { item.members } +
+
+
+ +
+ + ))} + {!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..618f998 --- /dev/null +++ b/frontend/src/components/Organizations/ListOrganizations.tsx @@ -0,0 +1,77 @@ +import React from 'react'; +import CardBox from '../CardBox'; +import dataFormatter from '../../helpers/dataFormatter'; +import ListActionsPopover from "../ListActionsPopover"; +import {useAppSelector} from "../../stores/hooks"; +import {Pagination} from "../Pagination"; +import LoadingSpinner from "../LoadingSpinner"; +import Link from 'next/link'; + +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 corners = useAppSelector((state) => state.style.corners); + const bgColor = useAppSelector((state) => state.style.cardsColor); + + return ( + <> +
+ {loading && } + {!loading && organizations.map((item) => ( +
+ +
+ + +
+

Name

+

{ item.name }

+
+ +
+

Members

+

{ item.members }

+
+ + + +
+
+
+ ))} + {!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..10a1742 --- /dev/null +++ b/frontend/src/components/Organizations/TableOrganizations.tsx @@ -0,0 +1,441 @@ +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"; +import axios from 'axios'; + +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(() => { + loadColumns(handleDeleteModalAction, `organizations`).then((newCols) => + setColumns(newCols), + ); + }, []); + + 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 border-gray-700 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..f55efe1 --- /dev/null +++ b/frontend/src/components/Organizations/configureOrganizationsCols.tsx @@ -0,0 +1,78 @@ +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 dataFormatter from '../../helpers/dataFormatter' +import DataGridMultiSelect from "../DataGridMultiSelect"; +import ListActionsPopover from '../ListActionsPopover'; +type Params = (id: string) => void; + +export const loadColumns = async ( + onDelete: Params, + entityName: string, +) => { + async function callOptionsApi(entityName: string) { + try { + const data = await axios(`/${entityName}/autocomplete?limit=100`); + return data.data; + } catch (error) { + console.log(error); + return []; + } + } + return [ + + { + field: 'name', + headerName: 'Name', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + + }, + + { + field: 'members', + headerName: 'Members', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + + }, + + { + field: 'actions', + type: 'actions', + minWidth: 30, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + getActions: (params: GridRowParams) => { + + return [ +
+ +
, + ] + }, + }, + ]; +}; diff --git a/frontend/src/components/Users/CardUsers.tsx b/frontend/src/components/Users/CardUsers.tsx index b98a2e4..2f141aa 100644 --- a/frontend/src/components/Users/CardUsers.tsx +++ b/frontend/src/components/Users/CardUsers.tsx @@ -115,6 +115,15 @@ const CardUsers = ({ +
+
Organization
+
+
+ { dataFormatter.organizationsOneListFormatter(item.organization) } +
+
+
+ ))} diff --git a/frontend/src/components/Users/ListUsers.tsx b/frontend/src/components/Users/ListUsers.tsx index 05ac3f2..3124db1 100644 --- a/frontend/src/components/Users/ListUsers.tsx +++ b/frontend/src/components/Users/ListUsers.tsx @@ -65,6 +65,11 @@ const ListUsers = ({ users, loading, onDelete, currentPage, numPages, onPageChan

{ item.avatar }

+
+

Organization

+

{ dataFormatter.organizationsOneListFormatter(item.organization) }

+
+ value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('organizations'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + + }, + { field: 'actions', type: 'actions', diff --git a/frontend/src/helpers/dataFormatter.js b/frontend/src/helpers/dataFormatter.js index 7af1495..4e00547 100644 --- a/frontend/src/helpers/dataFormatter.js +++ b/frontend/src/helpers/dataFormatter.js @@ -58,4 +58,23 @@ export default { return {label: val.firstName, id: val.id} }, + organizationsManyListFormatter(val) { + if (!val || !val.length) return [] + return val.map((item) => item.name) + }, + organizationsOneListFormatter(val) { + if (!val) return '' + return val.name + }, + organizationsManyListFormatterEdit(val) { + if (!val || !val.length) return [] + return val.map((item) => { + return {id: item.id, label: item.name} + }); + }, + organizationsOneListFormatterEdit(val) { + if (!val) return '' + return {label: val.name, id: val.id} + }, + } diff --git a/frontend/src/menuAside.ts b/frontend/src/menuAside.ts index fa4a07a..366f7f7 100644 --- a/frontend/src/menuAside.ts +++ b/frontend/src/menuAside.ts @@ -40,6 +40,14 @@ const menuAside: MenuAsideItem[] = [ icon: 'mdiFileExport' in icon ? icon['mdiFileExport' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, permissions: 'READ_EXPORTS' }, + { + 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 b482129..b930d37 100644 --- a/frontend/src/pages/dashboard.tsx +++ b/frontend/src/pages/dashboard.tsx @@ -21,10 +21,11 @@ const Dashboard = () => { const [alerts, setAlerts] = React.useState(loadingMessage); const [check_ins, setCheck_ins] = React.useState(loadingMessage); const [exports, setExports] = React.useState(loadingMessage); + const [organizations, setOrganizations] = React.useState(loadingMessage); async function loadData() { - const entities = ['users','alerts','check_ins','exports',]; - const fns = [setUsers,setAlerts,setCheck_ins,setExports,]; + const entities = ['users','alerts','check_ins','exports','organizations',]; + const fns = [setUsers,setAlerts,setCheck_ins,setExports,setOrganizations,]; const requests = entities.map((entity, index) => { return axios.get(`/${entity.toLowerCase()}/count`); @@ -174,6 +175,34 @@ const Dashboard = () => { + +
+
+
+
+ Organizations +
+
+ {organizations} +
+
+
+ +
+
+
+ + diff --git a/frontend/src/pages/organizations/[organizationsId].tsx b/frontend/src/pages/organizations/[organizationsId].tsx new file mode 100644 index 0000000..1b67187 --- /dev/null +++ b/frontend/src/pages/organizations/[organizationsId].tsx @@ -0,0 +1,133 @@ +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 { 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' + +const EditOrganizations = () => { + const router = useRouter() + const dispatch = useAppDispatch() + const initVals = { + + organizations: null, + + 'name': '', + + } + const [initialValues, setInitialValues] = useState(initVals) + + const { organizations } = useAppSelector((state) => state.organizations) + + 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..c7dd547 --- /dev/null +++ b/frontend/src/pages/organizations/organizations-edit.tsx @@ -0,0 +1,131 @@ +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 { 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 dataFormatter from '../../helpers/dataFormatter'; + +const EditOrganizationsPage = () => { + const router = useRouter() + const dispatch = useAppDispatch() + const initVals = { + + organizations: null, + + 'name': '', + + } + const [initialValues, setInitialValues] = useState(initVals) + + const { organizations } = useAppSelector((state) => state.organizations) + + 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..f052e29 --- /dev/null +++ b/frontend/src/pages/organizations/organizations-list.tsx @@ -0,0 +1,128 @@ +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 {useAppDispatch, useAppSelector} from "../../stores/hooks"; +import CardBoxModal from "../../components/CardBoxModal"; +import DragDropFilePicker from "../../components/DragDropFilePicker"; +import {setRefetch, uploadCsv} from '../../stores/organizations/organizationsSlice'; + +const OrganizationsTablesPage = () => { + const [filterItems, setFilterItems] = useState([]); + const [csvFile, setCsvFile] = useState(null); + const [isModalActive, setIsModalActive] = useState(false); + + const dispatch = useAppDispatch(); + + const [filters] = useState([{label: 'Name', title: 'name'}, + + ]); + 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')} + + + + {''} + + + + + + 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..825f1eb --- /dev/null +++ b/frontend/src/pages/organizations/organizations-new.tsx @@ -0,0 +1,95 @@ +import { mdiChartTimelineVariant } 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 { SwitchField } from '../../components/SwitchField' + +import { SelectField } from '../../components/SelectField' +import {RichTextField} from "../../components/RichTextField"; + +import { create } from '../../stores/organizations/organizationsSlice' +import { useAppDispatch } from '../../stores/hooks' +import { useRouter } from 'next/router' + +const initialValues = { + + organizations: '', + + 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..bffc65b --- /dev/null +++ b/frontend/src/pages/organizations/organizations-table.tsx @@ -0,0 +1,129 @@ +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 {useAppDispatch, useAppSelector} from "../../stores/hooks"; +import CardBoxModal from "../../components/CardBoxModal"; +import DragDropFilePicker from "../../components/DragDropFilePicker"; +import {setRefetch, uploadCsv} from '../../stores/organizations/organizationsSlice'; + +const OrganizationsTablesPage = () => { + const [filterItems, setFilterItems] = useState([]); + const [csvFile, setCsvFile] = useState(null); + const [isModalActive, setIsModalActive] = useState(false); + + const dispatch = useAppDispatch(); + + const [filters] = useState([{label: 'Name', title: 'name'}, + + ]); + 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')} + + + + {''} + + + + + + + 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..66cbe0b --- /dev/null +++ b/frontend/src/pages/organizations/organizations-view.tsx @@ -0,0 +1,142 @@ +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 dataFormatter from '../../helpers/dataFormatter'; +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 OrganizationsView = () => { + const router = useRouter() + const dispatch = useAppDispatch() + const { organizations } = useAppSelector((state) => state.organizations) + + 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')} + + + + + + + +
+

organizations

+ +

{organizations?.organizations?.name ?? 'No data'}

+ +
+ +
+

Name

+

{organizations?.name}

+
+ + <> +

Users Organization

+ +
+ + + + + + + + + + + + + + + + + + {organizations.users_organization && Array.isArray(organizations.users_organization) && + organizations.users_organization.map((item: any) => ( + router.push(`/users/users-view/?id=${item.id}`)}> + + + + + + + + + + + + + ))} + +
First NameLast NamePhone NumberE-MailDisabled
+ { item.firstName } + + { item.lastName } + + { item.phoneNumber } + + { item.email } + + { dataFormatter.booleanFormatter(item.disabled) } +
+
+ {!organizations?.users_organization?.length &&
No data
} +
+ + + + + router.push('/organizations/organizations-list')} + /> +
+
+ + ); +}; + +OrganizationsView.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ) +} + +export default OrganizationsView; diff --git a/frontend/src/pages/users/[usersId].tsx b/frontend/src/pages/users/[usersId].tsx index a8be6aa..a6d63c5 100644 --- a/frontend/src/pages/users/[usersId].tsx +++ b/frontend/src/pages/users/[usersId].tsx @@ -42,6 +42,8 @@ const EditUsers = () => { disabled: false, + organization: null, + password: '' } @@ -138,6 +140,19 @@ const EditUsers = () => { > + + + + diff --git a/frontend/src/pages/users/users-edit.tsx b/frontend/src/pages/users/users-edit.tsx index 97158a0..4a87b26 100644 --- a/frontend/src/pages/users/users-edit.tsx +++ b/frontend/src/pages/users/users-edit.tsx @@ -43,6 +43,8 @@ const EditUsersPage = () => { disabled: false, + organization: null, + password: '' } @@ -136,6 +138,19 @@ const EditUsersPage = () => { > + + + + diff --git a/frontend/src/pages/users/users-new.tsx b/frontend/src/pages/users/users-new.tsx index ac3ca0e..a455c62 100644 --- a/frontend/src/pages/users/users-new.tsx +++ b/frontend/src/pages/users/users-new.tsx @@ -35,6 +35,8 @@ const initialValues = { disabled: false, + organization: '', + } const UsersNew = () => { @@ -107,6 +109,10 @@ const UsersNew = () => { > + + + + diff --git a/frontend/src/pages/users/users-view.tsx b/frontend/src/pages/users/users-view.tsx index eebd465..eb33105 100644 --- a/frontend/src/pages/users/users-view.tsx +++ b/frontend/src/pages/users/users-view.tsx @@ -77,6 +77,13 @@ const UsersView = () => { /> +
+

Organization

+ +

{users?.organization?.name ?? 'No data'}

+ +
+ <>

Alerts Athlete

{ + 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 d1bf16b..285f080 100644 --- a/frontend/src/stores/store.ts +++ b/frontend/src/stores/store.ts @@ -7,6 +7,7 @@ import usersSlice from "./users/usersSlice"; import alertsSlice from "./alerts/alertsSlice"; import check_insSlice from "./check_ins/check_insSlice"; import exportsSlice from "./exports/exportsSlice"; +import organizationsSlice from "./organizations/organizationsSlice"; export const store = configureStore({ reducer: { @@ -18,6 +19,7 @@ users: usersSlice, alerts: alertsSlice, check_ins: check_insSlice, exports: exportsSlice, +organizations: organizationsSlice, }, })