From 84db90e7f3ab5ad2937eff3de4b2f100ab8e4422 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Mon, 23 Feb 2026 00:22:54 +0000 Subject: [PATCH] Autosave: 20260223-002254 --- backend/src/db/api/activity_feed_items.js | 2 +- backend/src/db/api/form_field_choices.js | 2 +- backend/src/db/api/form_fields.js | 2 +- backend/src/db/api/form_submissions.js | 2 +- backend/src/db/api/form_templates.js | 2 +- backend/src/db/api/locations.js | 2 +- backend/src/db/api/organizations.js | 9 +- backend/src/db/api/projects.js | 32 +- backend/src/db/api/reports.js | 2 +- backend/src/db/api/submission_values.js | 2 +- backend/src/db/api/tenants.js | 7 +- backend/src/db/api/trial_types.js | 2 +- backend/src/db/api/trials.js | 2 +- ...2000002-add-default-view-and-update-nav.js | 59 + ...20260222000003-add-insights-to-projects.js | 12 + backend/src/db/models/organizations.js | 10 +- backend/src/db/models/projects.js | 11 +- .../db/seeders/20231127130745-sample-data.js | 8 + backend/src/routes/auth.js | 12 + backend/src/services/auth.js | 11 + backend/src/services/form_submissions.js | 68 +- backend/src/services/projects.js | 68 +- backend/src/services/trials.js | 68 +- backend/src/services/users.js | 52 +- .../TableActivity_feed_items.tsx | 14 +- .../configureForm_submissionsCols.tsx | 181 +-- frontend/src/components/NavBar.tsx | 60 +- .../Projects/configureProjectsCols.tsx | 71 +- .../src/components/Tenants/TableTenants.tsx | 37 +- .../Tenants/configureTenantsCols.tsx | 19 +- .../components/Trials/configureTrialsCols.tsx | 50 +- frontend/src/layouts/Authenticated.tsx | 68 +- frontend/src/menuNavBar.ts | 44 +- frontend/src/pages/dashboard.tsx | 27 +- .../organizations/organizations-edit.tsx | 113 +- frontend/src/pages/projects/[projectsId].tsx | 925 ++---------- frontend/src/pages/projects/projects-edit.tsx | 1026 +++---------- frontend/src/pages/projects/projects-list.tsx | 22 +- frontend/src/pages/projects/projects-new.tsx | 658 ++------- frontend/src/pages/projects/projects-view.tsx | 806 +++-------- frontend/src/pages/tenants/tenants-list.tsx | 19 +- frontend/src/pages/trials/[trialsId].tsx | 1278 +++-------------- frontend/src/pages/trials/trials-edit.tsx | 1275 +++------------- frontend/src/pages/trials/trials-list.tsx | 12 +- frontend/src/pages/trials/trials-new.tsx | 591 +------- frontend/src/stores/authSlice.ts | 16 + 46 files changed, 1800 insertions(+), 5959 deletions(-) create mode 100644 backend/src/db/migrations/20260222000002-add-default-view-and-update-nav.js create mode 100644 backend/src/db/migrations/20260222000003-add-insights-to-projects.js diff --git a/backend/src/db/api/activity_feed_items.js b/backend/src/db/api/activity_feed_items.js index 518a8c0..58f51e7 100644 --- a/backend/src/db/api/activity_feed_items.js +++ b/backend/src/db/api/activity_feed_items.js @@ -349,7 +349,7 @@ module.exports = class Activity_feed_itemsDBApi { } } - if (!globalAccess && options?.currentUser?.tenantId) { + if (options?.currentUser?.tenantId) { where.tenantId = options.currentUser.tenantId; } diff --git a/backend/src/db/api/form_field_choices.js b/backend/src/db/api/form_field_choices.js index aa6e385..a3b4fa2 100644 --- a/backend/src/db/api/form_field_choices.js +++ b/backend/src/db/api/form_field_choices.js @@ -299,7 +299,7 @@ module.exports = class Form_field_choicesDBApi { } } - if (!globalAccess && options?.currentUser?.tenantId) { + if (options?.currentUser?.tenantId) { where.tenantId = options.currentUser.tenantId; } diff --git a/backend/src/db/api/form_fields.js b/backend/src/db/api/form_fields.js index efff798..844dba7 100644 --- a/backend/src/db/api/form_fields.js +++ b/backend/src/db/api/form_fields.js @@ -363,7 +363,7 @@ module.exports = class Form_fieldsDBApi { } } - if (!globalAccess && options?.currentUser?.tenantId) { + if (options?.currentUser?.tenantId) { where.tenantId = options.currentUser.tenantId; } diff --git a/backend/src/db/api/form_submissions.js b/backend/src/db/api/form_submissions.js index 2ec268c..dd796bd 100644 --- a/backend/src/db/api/form_submissions.js +++ b/backend/src/db/api/form_submissions.js @@ -413,7 +413,7 @@ module.exports = class Form_submissionsDBApi { } } - if (!globalAccess && options?.currentUser?.tenantId) { + if (options?.currentUser?.tenantId) { where.tenantId = options.currentUser.tenantId; } diff --git a/backend/src/db/api/form_templates.js b/backend/src/db/api/form_templates.js index 82c58eb..415c05c 100644 --- a/backend/src/db/api/form_templates.js +++ b/backend/src/db/api/form_templates.js @@ -315,7 +315,7 @@ module.exports = class Form_templatesDBApi { } } - if (!globalAccess && options?.currentUser?.tenantId) { + if (options?.currentUser?.tenantId) { where.tenantId = options.currentUser.tenantId; } diff --git a/backend/src/db/api/locations.js b/backend/src/db/api/locations.js index c46ea4d..28f7fd0 100644 --- a/backend/src/db/api/locations.js +++ b/backend/src/db/api/locations.js @@ -350,7 +350,7 @@ module.exports = class LocationsDBApi { } } - if (!globalAccess && options?.currentUser?.tenantId) { + if (options?.currentUser?.tenantId) { where.tenantId = options.currentUser.tenantId; } diff --git a/backend/src/db/api/organizations.js b/backend/src/db/api/organizations.js index 7b0420b..1463c52 100644 --- a/backend/src/db/api/organizations.js +++ b/backend/src/db/api/organizations.js @@ -29,7 +29,8 @@ module.exports = class OrganizationsDBApi { state: data.state || null, country: data.country || null, zip: data.zip || null, - navOrientation: data.navOrientation || 'side', + navOrientation: data.navOrientation || 'top', + defaultView: data.defaultView || 'list', importHash: data.importHash || null, createdById: currentUser.id, @@ -65,7 +66,8 @@ module.exports = class OrganizationsDBApi { state: item.state || null, country: item.country || null, zip: item.zip || null, - navOrientation: item.navOrientation || 'side', + navOrientation: item.navOrientation || 'top', + defaultView: item.defaultView || 'list', importHash: item.importHash || null, createdById: currentUser.id, @@ -101,6 +103,7 @@ module.exports = class OrganizationsDBApi { if (data.country !== undefined) updatePayload.country = data.country; if (data.zip !== undefined) updatePayload.zip = data.zip; if (data.navOrientation !== undefined) updatePayload.navOrientation = data.navOrientation; + if (data.defaultView !== undefined) updatePayload.defaultView = data.defaultView; updatePayload.updatedById = currentUser.id; @@ -423,4 +426,4 @@ module.exports = class OrganizationsDBApi { } -}; +}; \ No newline at end of file diff --git a/backend/src/db/api/projects.js b/backend/src/db/api/projects.js index 4412d03..05bfd76 100644 --- a/backend/src/db/api/projects.js +++ b/backend/src/db/api/projects.js @@ -1,6 +1,5 @@ const db = require('../models'); const FileDBApi = require('./file'); -const crypto = require('crypto'); const Utils = require('../utils'); @@ -29,6 +28,11 @@ module.exports = class ProjectsDBApi { || null , + + insights: data.insights + || + null + , start_date: data.start_date || @@ -110,6 +114,11 @@ module.exports = class ProjectsDBApi { description: item.description || null + , + + insights: item.insights + || + null , start_date: item.start_date @@ -166,7 +175,6 @@ module.exports = class ProjectsDBApi { 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 projects = await db.projects.findByPk(id, {}, {transaction}); @@ -179,6 +187,8 @@ module.exports = class ProjectsDBApi { if (data.description !== undefined) updatePayload.description = data.description; + + if (data.insights !== undefined) updatePayload.insights = data.insights; if (data.start_date !== undefined) updatePayload.start_date = data.start_date; @@ -381,15 +391,13 @@ module.exports = class ProjectsDBApi { } } - if (!globalAccess && options?.currentUser?.tenantId) { + if (options?.currentUser?.tenantId) { where.tenantId = options.currentUser.tenantId; } offset = currentPage * limit; - const orderBy = null; - const transaction = (options && options.transaction) || undefined; let include = [ @@ -397,7 +405,7 @@ module.exports = class ProjectsDBApi { { model: db.tenants, as: 'tenant', - + required: !!filter.tenant, where: filter.tenant ? { [Op.or]: [ { id: { [Op.in]: filter.tenant.split('|').map(term => Utils.uuid(term)) } }, @@ -407,14 +415,14 @@ module.exports = class ProjectsDBApi { } }, ] - } : {}, + } : undefined, }, { model: db.locations, as: 'location', - + required: !!filter.location, where: filter.location ? { [Op.or]: [ { id: { [Op.in]: filter.location.split('|').map(term => Utils.uuid(term)) } }, @@ -424,14 +432,14 @@ module.exports = class ProjectsDBApi { } }, ] - } : {}, + } : undefined, }, { model: db.organizations, as: 'organizations', - + required: false, }, @@ -439,11 +447,13 @@ module.exports = class ProjectsDBApi { { model: db.file, as: 'attachments', + required: false, }, { model: db.file, as: 'images', + required: false, }, ]; @@ -628,7 +638,7 @@ module.exports = class ProjectsDBApi { order: filter.field && filter.sort ? [[filter.field, filter.sort]] : [['createdAt', 'desc']], - transaction: options?.transaction, + transaction: transaction, logging: console.log }; diff --git a/backend/src/db/api/reports.js b/backend/src/db/api/reports.js index f1d5d49..6ae913a 100644 --- a/backend/src/db/api/reports.js +++ b/backend/src/db/api/reports.js @@ -336,7 +336,7 @@ module.exports = class ReportsDBApi { } } - if (!globalAccess && options?.currentUser?.tenantId) { + if (options?.currentUser?.tenantId) { where.tenantId = options.currentUser.tenantId; } diff --git a/backend/src/db/api/submission_values.js b/backend/src/db/api/submission_values.js index 850f118..8f9ef16 100644 --- a/backend/src/db/api/submission_values.js +++ b/backend/src/db/api/submission_values.js @@ -406,7 +406,7 @@ module.exports = class Submission_valuesDBApi { } } - if (!globalAccess && options?.currentUser?.tenantId) { + if (options?.currentUser?.tenantId) { where.tenantId = options.currentUser.tenantId; } diff --git a/backend/src/db/api/tenants.js b/backend/src/db/api/tenants.js index 72d9096..fa94635 100644 --- a/backend/src/db/api/tenants.js +++ b/backend/src/db/api/tenants.js @@ -1,4 +1,3 @@ - const db = require('../models'); const FileDBApi = require('./file'); const crypto = require('crypto'); @@ -551,12 +550,13 @@ module.exports = class TenantsDBApi { if (!globalAccess && organizationId) { - where.organizationId = organizationId; + where.organizationsId = organizationId; } if (query) { where = { + ...where, [Op.or]: [ { ['id']: Utils.uuid(query) }, Utils.ilike( @@ -583,5 +583,4 @@ module.exports = class TenantsDBApi { } -}; - +}; \ No newline at end of file diff --git a/backend/src/db/api/trial_types.js b/backend/src/db/api/trial_types.js index 214f66e..b168774 100644 --- a/backend/src/db/api/trial_types.js +++ b/backend/src/db/api/trial_types.js @@ -285,7 +285,7 @@ module.exports = class Trial_typesDBApi { } } - if (!globalAccess && options?.currentUser?.tenantId) { + if (options?.currentUser?.tenantId) { where.tenantId = options.currentUser.tenantId; } diff --git a/backend/src/db/api/trials.js b/backend/src/db/api/trials.js index 6a3be2e..63639bd 100644 --- a/backend/src/db/api/trials.js +++ b/backend/src/db/api/trials.js @@ -435,7 +435,7 @@ module.exports = class TrialsDBApi { } } - if (!globalAccess && options?.currentUser?.tenantId) { + if (options?.currentUser?.tenantId) { where.tenantId = options.currentUser.tenantId; } diff --git a/backend/src/db/migrations/20260222000002-add-default-view-and-update-nav.js b/backend/src/db/migrations/20260222000002-add-default-view-and-update-nav.js new file mode 100644 index 0000000..ef603dd --- /dev/null +++ b/backend/src/db/migrations/20260222000002-add-default-view-and-update-nav.js @@ -0,0 +1,59 @@ +module.exports = { + async up(queryInterface, Sequelize) { + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.addColumn( + 'organizations', + 'defaultView', + { + type: Sequelize.DataTypes.TEXT, + allowNull: false, + defaultValue: 'list', + }, + { transaction } + ); + + await queryInterface.changeColumn( + 'organizations', + 'navOrientation', + { + type: Sequelize.DataTypes.TEXT, + allowNull: false, + defaultValue: 'top', + }, + { transaction } + ); + + await queryInterface.sequelize.query( + "UPDATE organizations SET \"navOrientation\" = 'top', \"defaultView\" = 'list'", + { transaction } + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + + async down(queryInterface, Sequelize) { + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.removeColumn('organizations', 'defaultView', { transaction }); + await queryInterface.changeColumn( + 'organizations', + 'navOrientation', + { + type: Sequelize.DataTypes.TEXT, + allowNull: false, + defaultValue: 'side', + }, + { transaction } + ); + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/20260222000003-add-insights-to-projects.js b/backend/src/db/migrations/20260222000003-add-insights-to-projects.js new file mode 100644 index 0000000..b0a085f --- /dev/null +++ b/backend/src/db/migrations/20260222000003-add-insights-to-projects.js @@ -0,0 +1,12 @@ +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.addColumn('projects', 'insights', { + type: Sequelize.TEXT, + allowNull: true, + }); + }, + + down: async (queryInterface, Sequelize) => { + await queryInterface.removeColumn('projects', 'insights'); + } +}; diff --git a/backend/src/db/models/organizations.js b/backend/src/db/models/organizations.js index 15247f6..8d06c9e 100644 --- a/backend/src/db/models/organizations.js +++ b/backend/src/db/models/organizations.js @@ -41,7 +41,13 @@ module.exports = function(sequelize, DataTypes) { navOrientation: { type: DataTypes.TEXT, allowNull: false, - defaultValue: 'side', + defaultValue: 'top', + }, + + defaultView: { + type: DataTypes.TEXT, + allowNull: false, + defaultValue: 'list', }, importHash: { @@ -203,4 +209,4 @@ module.exports = function(sequelize, DataTypes) { return organizations; -}; \ No newline at end of file +}; diff --git a/backend/src/db/models/projects.js b/backend/src/db/models/projects.js index c379d0a..757a638 100644 --- a/backend/src/db/models/projects.js +++ b/backend/src/db/models/projects.js @@ -26,6 +26,13 @@ description: { + }, + +insights: { + type: DataTypes.TEXT, + + + }, start_date: { @@ -174,6 +181,4 @@ status: { return projects; -}; - - +}; \ No newline at end of file diff --git a/backend/src/db/seeders/20231127130745-sample-data.js b/backend/src/db/seeders/20231127130745-sample-data.js index 765728b..b3906c3 100644 --- a/backend/src/db/seeders/20231127130745-sample-data.js +++ b/backend/src/db/seeders/20231127130745-sample-data.js @@ -68,6 +68,8 @@ const OrganizationsData = [ "name": "Grace Hopper", + "navOrientation": "top", + "defaultView": "list", @@ -79,6 +81,8 @@ const OrganizationsData = [ "name": "Ada Lovelace", + "navOrientation": "top", + "defaultView": "list", @@ -90,6 +94,8 @@ const OrganizationsData = [ "name": "Ada Lovelace", + "navOrientation": "top", + "defaultView": "list", @@ -101,6 +107,8 @@ const OrganizationsData = [ "name": "Ada Lovelace", + "navOrientation": "top", + "defaultView": "list", diff --git a/backend/src/routes/auth.js b/backend/src/routes/auth.js index 31d62cb..0763ef4 100644 --- a/backend/src/routes/auth.js +++ b/backend/src/routes/auth.js @@ -153,6 +153,18 @@ router.post('/signup', wrapAsync(async (req, res) => { res.status(200).send(payload); })); +router.post("/impersonate", passport.authenticate("jwt", {session: false}), wrapAsync(async (req, res) => { + if (!req.currentUser || !req.currentUser.app_role?.globalAccess) { + throw new ForbiddenError(); + } + + const { tenantId } = req.body; + + const user = await AuthService.updateImpersonation(req.currentUser.id, tenantId); + + res.status(200).send(user); +})); + router.put('/profile', passport.authenticate('jwt', {session: false}), wrapAsync(async (req, res) => { if (!req.currentUser || !req.currentUser.id) { throw new ForbiddenError(); diff --git a/backend/src/services/auth.js b/backend/src/services/auth.js index cd2c205..4fe8f81 100644 --- a/backend/src/services/auth.js +++ b/backend/src/services/auth.js @@ -285,6 +285,17 @@ class Auth { ); } + static async updateImpersonation(userId, tenantId) { + const user = await db.users.findByPk(userId); + if (!user) { + throw new ValidationError("auth.userNotFound"); + } + + await user.update({ tenantId: tenantId || null }); + + return UsersDBApi.findBy({ id: userId }); + } + static async updateProfile(data, currentUser) { let transaction = await db.sequelize.transaction(); diff --git a/backend/src/services/form_submissions.js b/backend/src/services/form_submissions.js index 53003ce..8b37bfe 100644 --- a/backend/src/services/form_submissions.js +++ b/backend/src/services/form_submissions.js @@ -1,10 +1,9 @@ const db = require('../db/models'); const Form_submissionsDBApi = require('../db/api/form_submissions'); +const Activity_feed_itemsDBApi = require('../db/api/activity_feed_items'); 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'); @@ -15,7 +14,7 @@ module.exports = class Form_submissionsService { static async create(data, currentUser) { const transaction = await db.sequelize.transaction(); try { - await Form_submissionsDBApi.create( + const submission = await Form_submissionsDBApi.create( data, { currentUser, @@ -23,14 +22,31 @@ module.exports = class Form_submissionsService { }, ); + await Activity_feed_itemsDBApi.create( + { + item_type: 'submission_created', + title: 'Form Submission', + summary: `New form submission was received.`, + occurred_at: new Date(), + link_path: `/form_submissions/form_submissions-view/?id=${submission.id}`, + tenant: submission.tenantId || currentUser.tenantId, + actor_user: currentUser.id, + organizations: submission.organizationsId || currentUser.organizationsId, + }, + { + currentUser, + transaction, + } + ); + await transaction.commit(); } catch (error) { await transaction.rollback(); throw error; } - }; + } - static async bulkImport(req, res, sendInvitationEmails = true, host) { + static async bulkImport(req, res) { const transaction = await db.sequelize.transaction(); try { @@ -51,13 +67,32 @@ module.exports = class Form_submissionsService { .on('error', (error) => reject(error)); }) - await Form_submissionsDBApi.bulkImport(results, { + const submissions = await Form_submissionsDBApi.bulkImport(results, { transaction, ignoreDuplicates: true, validate: true, currentUser: req.currentUser }); + for (const submission of submissions) { + await Activity_feed_itemsDBApi.create( + { + item_type: 'submission_created', + title: 'Form Submission (Bulk)', + summary: `New form submission was imported.`, + occurred_at: new Date(), + link_path: `/form_submissions/form_submissions-view/?id=${submission.id}`, + tenant: submission.tenantId || req.currentUser.tenantId, + actor_user: req.currentUser.id, + organizations: submission.organizationsId || req.currentUser.organizationsId, + }, + { + currentUser: req.currentUser, + transaction, + } + ); + } + await transaction.commit(); } catch (error) { await transaction.rollback(); @@ -88,6 +123,23 @@ module.exports = class Form_submissionsService { }, ); + await Activity_feed_itemsDBApi.create( + { + item_type: 'submission_updated', + title: 'Form Submission Updated', + summary: `Form submission was updated.`, + occurred_at: new Date(), + link_path: `/form_submissions/form_submissions-view/?id=${updatedForm_submissions.id}`, + tenant: updatedForm_submissions.tenantId || currentUser.tenantId, + actor_user: currentUser.id, + organizations: updatedForm_submissions.organizationsId || currentUser.organizationsId, + }, + { + currentUser, + transaction, + } + ); + await transaction.commit(); return updatedForm_submissions; @@ -95,7 +147,7 @@ module.exports = class Form_submissionsService { await transaction.rollback(); throw error; } - }; + } static async deleteByIds(ids, currentUser) { const transaction = await db.sequelize.transaction(); @@ -134,5 +186,3 @@ module.exports = class Form_submissionsService { }; - - diff --git a/backend/src/services/projects.js b/backend/src/services/projects.js index 2e6a714..053d62b 100644 --- a/backend/src/services/projects.js +++ b/backend/src/services/projects.js @@ -1,10 +1,9 @@ const db = require('../db/models'); const ProjectsDBApi = require('../db/api/projects'); +const Activity_feed_itemsDBApi = require('../db/api/activity_feed_items'); 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'); @@ -15,7 +14,7 @@ module.exports = class ProjectsService { static async create(data, currentUser) { const transaction = await db.sequelize.transaction(); try { - await ProjectsDBApi.create( + const project = await ProjectsDBApi.create( data, { currentUser, @@ -23,14 +22,31 @@ module.exports = class ProjectsService { }, ); + await Activity_feed_itemsDBApi.create( + { + item_type: 'project_created', + title: 'Project Created', + summary: `New project "${data.name}" was created.`, + occurred_at: new Date(), + link_path: `/projects/projects-view/?id=${project.id}`, + tenant: data.tenant || currentUser.tenantId, + actor_user: currentUser.id, + organizations: data.organizations || currentUser.organizationsId, + }, + { + currentUser, + transaction, + } + ); + await transaction.commit(); } catch (error) { await transaction.rollback(); throw error; } - }; + } - static async bulkImport(req, res, sendInvitationEmails = true, host) { + static async bulkImport(req, res) { const transaction = await db.sequelize.transaction(); try { @@ -51,13 +67,32 @@ module.exports = class ProjectsService { .on('error', (error) => reject(error)); }) - await ProjectsDBApi.bulkImport(results, { + const projects = await ProjectsDBApi.bulkImport(results, { transaction, ignoreDuplicates: true, validate: true, currentUser: req.currentUser }); + for (const project of projects) { + await Activity_feed_itemsDBApi.create( + { + item_type: 'project_created', + title: 'Project Created (Bulk)', + summary: `New project "${project.name}" was imported.`, + occurred_at: new Date(), + link_path: `/projects/projects-view/?id=${project.id}`, + tenant: project.tenantId || req.currentUser.tenantId, + actor_user: req.currentUser.id, + organizations: project.organizationsId || req.currentUser.organizationsId, + }, + { + currentUser: req.currentUser, + transaction, + } + ); + } + await transaction.commit(); } catch (error) { await transaction.rollback(); @@ -88,6 +123,23 @@ module.exports = class ProjectsService { }, ); + await Activity_feed_itemsDBApi.create( + { + item_type: 'project_updated', + title: 'Project Updated', + summary: `Project "${updatedProjects.name}" was updated.`, + occurred_at: new Date(), + link_path: `/projects/projects-view/?id=${updatedProjects.id}`, + tenant: updatedProjects.tenantId || currentUser.tenantId, + actor_user: currentUser.id, + organizations: updatedProjects.organizationsId || currentUser.organizationsId, + }, + { + currentUser, + transaction, + } + ); + await transaction.commit(); return updatedProjects; @@ -95,7 +147,7 @@ module.exports = class ProjectsService { await transaction.rollback(); throw error; } - }; + } static async deleteByIds(ids, currentUser) { const transaction = await db.sequelize.transaction(); @@ -134,5 +186,3 @@ module.exports = class ProjectsService { }; - - diff --git a/backend/src/services/trials.js b/backend/src/services/trials.js index d980bca..fc8ffd4 100644 --- a/backend/src/services/trials.js +++ b/backend/src/services/trials.js @@ -1,10 +1,9 @@ const db = require('../db/models'); const TrialsDBApi = require('../db/api/trials'); +const Activity_feed_itemsDBApi = require('../db/api/activity_feed_items'); 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'); @@ -15,7 +14,7 @@ module.exports = class TrialsService { static async create(data, currentUser) { const transaction = await db.sequelize.transaction(); try { - await TrialsDBApi.create( + const trial = await TrialsDBApi.create( data, { currentUser, @@ -23,14 +22,31 @@ module.exports = class TrialsService { }, ); + await Activity_feed_itemsDBApi.create( + { + item_type: 'trial_created', + title: 'Trial Created', + summary: `New trial "${data.name}" was created.`, + occurred_at: new Date(), + link_path: `/trials/trials-view/?id=${trial.id}`, + tenant: data.tenant || currentUser.tenantId, + actor_user: currentUser.id, + organizations: data.organizations || currentUser.organizationsId, + }, + { + currentUser, + transaction, + } + ); + await transaction.commit(); } catch (error) { await transaction.rollback(); throw error; } - }; + } - static async bulkImport(req, res, sendInvitationEmails = true, host) { + static async bulkImport(req, res) { const transaction = await db.sequelize.transaction(); try { @@ -51,13 +67,32 @@ module.exports = class TrialsService { .on('error', (error) => reject(error)); }) - await TrialsDBApi.bulkImport(results, { + const trials = await TrialsDBApi.bulkImport(results, { transaction, ignoreDuplicates: true, validate: true, currentUser: req.currentUser }); + for (const trial of trials) { + await Activity_feed_itemsDBApi.create( + { + item_type: 'trial_created', + title: 'Trial Created (Bulk)', + summary: `New trial "${trial.name}" was imported.`, + occurred_at: new Date(), + link_path: `/trials/trials-view/?id=${trial.id}`, + tenant: trial.tenantId || req.currentUser.tenantId, + actor_user: req.currentUser.id, + organizations: trial.organizationsId || req.currentUser.organizationsId, + }, + { + currentUser: req.currentUser, + transaction, + } + ); + } + await transaction.commit(); } catch (error) { await transaction.rollback(); @@ -88,6 +123,23 @@ module.exports = class TrialsService { }, ); + await Activity_feed_itemsDBApi.create( + { + item_type: 'trial_updated', + title: 'Trial Updated', + summary: `Trial "${updatedTrials.name}" was updated.`, + occurred_at: new Date(), + link_path: `/trials/trials-view/?id=${updatedTrials.id}`, + tenant: updatedTrials.tenantId || currentUser.tenantId, + actor_user: currentUser.id, + organizations: updatedTrials.organizationsId || currentUser.organizationsId, + }, + { + currentUser, + transaction, + } + ); + await transaction.commit(); return updatedTrials; @@ -95,7 +147,7 @@ module.exports = class TrialsService { await transaction.rollback(); throw error; } - }; + } static async deleteByIds(ids, currentUser) { const transaction = await db.sequelize.transaction(); @@ -134,5 +186,3 @@ module.exports = class TrialsService { }; - - diff --git a/backend/src/services/users.js b/backend/src/services/users.js index 41f8220..05a6740 100644 --- a/backend/src/services/users.js +++ b/backend/src/services/users.js @@ -1,15 +1,13 @@ const db = require('../db/models'); const UsersDBApi = require('../db/api/users'); +const Activity_feed_itemsDBApi = require('../db/api/activity_feed_items'); 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'); +const config = require('../config'); -const InvitationEmail = require('./email/list/invitation'); -const EmailSender = require('./email'); const AuthService = require('./auth'); module.exports = class UsersService { @@ -28,7 +26,7 @@ module.exports = class UsersService { 'iam.errors.userAlreadyExists', ); } else { - await UsersDBApi.create( + const newUser = await UsersDBApi.create( {data}, globalAccess, @@ -39,6 +37,23 @@ module.exports = class UsersService { }, ); emailsToInvite.push(email); + + await Activity_feed_itemsDBApi.create( + { + item_type: 'user_created', + title: 'New Member', + summary: `${newUser.firstName} ${newUser.lastName} joined the team.`, + occurred_at: new Date(), + link_path: `/users/users-view/?id=${newUser.id}`, + tenant: newUser.tenantId || currentUser.tenantId, + actor_user: currentUser.id, + organizations: newUser.organizationsId || currentUser.organizationsId, + }, + { + currentUser, + transaction, + } + ); } } else { throw new ValidationError('iam.errors.emailRequired') @@ -83,13 +98,32 @@ module.exports = class UsersService { throw new ValidationError('importer.errors.userEmailMissing'); } - await UsersDBApi.bulkImport(results, { + const users = await UsersDBApi.bulkImport(results, { transaction, ignoreDuplicates: true, validate: true, currentUser: req.currentUser }); + for (const user of users) { + await Activity_feed_itemsDBApi.create( + { + item_type: 'user_created', + title: 'New Member (Bulk)', + summary: `${user.firstName} ${user.lastName} was imported.`, + occurred_at: new Date(), + link_path: `/users/users-view/?id=${user.id}`, + tenant: user.tenantId || req.currentUser.tenantId, + actor_user: req.currentUser.id, + organizations: user.organizationsId || req.currentUser.organizationsId, + }, + { + currentUser: req.currentUser, + transaction, + } + ); + } + emailsToInvite = results.map((result) => result.email); await transaction.commit(); @@ -142,7 +176,7 @@ module.exports = class UsersService { await transaction.rollback(); throw error; } - }; + } static async remove(id, currentUser) { const transaction = await db.sequelize.transaction(); @@ -174,6 +208,4 @@ module.exports = class UsersService { throw error; } } -}; - - +}; \ No newline at end of file diff --git a/frontend/src/components/Activity_feed_items/TableActivity_feed_items.tsx b/frontend/src/components/Activity_feed_items/TableActivity_feed_items.tsx index c052e7b..d2df704 100644 --- a/frontend/src/components/Activity_feed_items/TableActivity_feed_items.tsx +++ b/frontend/src/components/Activity_feed_items/TableActivity_feed_items.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState, useMemo } from 'react' +import React, { useEffect, useState, useMemo, useCallback } from 'react' import { createPortal } from 'react-dom'; import { ToastContainer, toast } from 'react-toastify'; import BaseButton from '../BaseButton' @@ -41,6 +41,8 @@ const TableSampleActivity_feed_items = ({ filterItems, setFilterItems, filters, sort: 'desc', }, ]); + + const memoizedSortModel = useMemo(() => JSON.stringify(sortModel), [sortModel]); const { activity_feed_items, loading, count, notify: activity_feed_itemsNotify, refetch } = useAppSelector((state) => state.activity_feed_items) const { currentUser } = useAppSelector((state) => state.auth); @@ -52,14 +54,14 @@ const TableSampleActivity_feed_items = ({ filterItems, setFilterItems, filters, pagesList.push(i); } - const loadData = async (page = currentPage, request = filterRequest) => { + const loadData = useCallback(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 })); - }; + }, [currentPage, filterRequest, sortModel, dispatch]); useEffect(() => { if (activity_feed_itemsNotify.showNotification) { @@ -70,14 +72,14 @@ const TableSampleActivity_feed_items = ({ filterItems, setFilterItems, filters, useEffect(() => { if (!currentUser) return; loadData(); - }, [sortModel, currentUser]); + }, [memoizedSortModel, currentUser, loadData]); useEffect(() => { if (refetch) { loadData(0); dispatch(setRefetch(false)); } - }, [refetch, dispatch]); + }, [refetch, dispatch, loadData]); const [isModalInfoActive, setIsModalInfoActive] = useState(false) const [isModalTrashActive, setIsModalTrashActive] = useState(false) @@ -473,4 +475,4 @@ const TableSampleActivity_feed_items = ({ filterItems, setFilterItems, filters, ) } -export default TableSampleActivity_feed_items +export default TableSampleActivity_feed_items \ No newline at end of file diff --git a/frontend/src/components/Form_submissions/configureForm_submissionsCols.tsx b/frontend/src/components/Form_submissions/configureForm_submissionsCols.tsx index ea8f2ce..ce29137 100644 --- a/frontend/src/components/Form_submissions/configureForm_submissionsCols.tsx +++ b/frontend/src/components/Form_submissions/configureForm_submissionsCols.tsx @@ -38,31 +38,9 @@ export const loadColumns = async ( } const hasUpdatePermission = hasPermission(user, 'UPDATE_FORM_SUBMISSIONS') + const hasTenant = !!(user?.tenant?.id || user?.tenantId); - return [ - - { - field: 'tenant', - headerName: 'Tenant', - flex: 1, - minWidth: 120, - filterable: false, - headerClassName: 'datagrid--header', - cellClassName: 'datagrid--cell', - - - editable: hasUpdatePermission, - - sortable: false, - type: 'singleSelect', - getOptionValue: (value: any) => value?.id, - getOptionLabel: (value: any) => value?.label, - valueOptions: await callOptionsApi('tenants'), - valueGetter: (params: GridValueGetterParams) => - params?.value?.id ?? params?.value, - - }, - + const columns: any[] = [ { field: 'form_template', headerName: 'FormTemplate', @@ -86,22 +64,46 @@ export const loadColumns = async ( }, { - field: 'trials', - headerName: 'Trials', + field: 'project', + headerName: 'Project', flex: 1, minWidth: 120, filterable: false, headerClassName: 'datagrid--header', cellClassName: 'datagrid--cell', - editable: false, + + editable: hasUpdatePermission, + sortable: false, type: 'singleSelect', - valueFormatter: ({ value }) => - dataFormatter.trialsManyListFormatter(value).join(', '), - renderEditCell: (params) => ( - - ), + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('projects'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + + }, + + { + field: 'trial', + headerName: 'Trial', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + + editable: hasUpdatePermission, + + sortable: false, + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('trials'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, }, @@ -127,28 +129,6 @@ export const loadColumns = async ( }, - { - field: 'submitted_by_user', - headerName: 'SubmittedByUser', - flex: 1, - minWidth: 120, - filterable: false, - headerClassName: 'datagrid--header', - cellClassName: 'datagrid--cell', - - - editable: hasUpdatePermission, - - sortable: false, - type: 'singleSelect', - getOptionValue: (value: any) => value?.id, - getOptionLabel: (value: any) => value?.label, - valueOptions: await callOptionsApi('users'), - valueGetter: (params: GridValueGetterParams) => - params?.value?.id ?? params?.value, - - }, - { field: 'submitted_at', headerName: 'SubmittedAt', @@ -168,8 +148,8 @@ export const loadColumns = async ( }, { - field: 'status', - headerName: 'Status', + field: 'organizations', + headerName: 'organizations', flex: 1, minWidth: 120, filterable: false, @@ -179,68 +159,13 @@ export const loadColumns = async ( editable: hasUpdatePermission, - - }, - - { - field: 'notes', - headerName: 'Notes', - flex: 1, - minWidth: 120, - filterable: false, - headerClassName: 'datagrid--header', - cellClassName: 'datagrid--cell', - - - editable: hasUpdatePermission, - - - }, - - { - field: 'attachments', - headerName: 'Attachments', - flex: 1, - minWidth: 120, - filterable: false, - headerClassName: 'datagrid--header', - cellClassName: 'datagrid--cell', - - editable: false, sortable: false, - renderCell: (params: GridValueGetterParams) => ( - <> - {dataFormatter.filesFormatter(params.row.attachments).map(link => ( - - ))} - - ), - - }, - - { - field: 'images', - headerName: 'Images', - flex: 1, - minWidth: 120, - filterable: false, - headerClassName: 'datagrid--header', - cellClassName: 'datagrid--cell', - - editable: false, - sortable: false, - renderCell: (params: GridValueGetterParams) => ( - - ), + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('organizations'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, }, @@ -268,4 +193,26 @@ export const loadColumns = async ( }, }, ]; + + if (!hasTenant) { + columns.unshift({ + field: 'tenant', + headerName: 'Tenant', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + editable: hasUpdatePermission, + sortable: false, + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('tenants'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + }); + } + + return columns; }; diff --git a/frontend/src/components/NavBar.tsx b/frontend/src/components/NavBar.tsx index 625805d..6efcd39 100644 --- a/frontend/src/components/NavBar.tsx +++ b/frontend/src/components/NavBar.tsx @@ -13,9 +13,10 @@ type Props = { leftMenu?: MenuNavBarItem[] className: string children: ReactNode + isTopNav?: boolean } -export default function NavBar({ menu, leftMenu = [], className = '', children }: Props) { +export default function NavBar({ menu, leftMenu = [], className = '', children, isTopNav = false }: Props) { const [isMenuNavBarActive, setIsMenuNavBarActive] = useState(false) const [isScrolled, setIsScrolled] = useState(false); const bgColor = useAppSelector((state) => state.style.bgLayoutColor); @@ -37,37 +38,48 @@ export default function NavBar({ menu, leftMenu = [], className = '', children } return ( diff --git a/frontend/src/components/Projects/configureProjectsCols.tsx b/frontend/src/components/Projects/configureProjectsCols.tsx index f3e820a..5edec37 100644 --- a/frontend/src/components/Projects/configureProjectsCols.tsx +++ b/frontend/src/components/Projects/configureProjectsCols.tsx @@ -1,9 +1,11 @@ +import Link from 'next/link'; import React from 'react'; import BaseIcon from '../BaseIcon'; import { mdiEye, mdiTrashCan, mdiPencilOutline } from '@mdi/js'; import axios from 'axios'; import { GridActionsCellItem, + GridRenderCellParams, GridRowParams, GridValueGetterParams, } from '@mui/x-data-grid'; @@ -38,31 +40,9 @@ export const loadColumns = async ( } const hasUpdatePermission = hasPermission(user, 'UPDATE_PROJECTS') + const hasTenant = !!(user?.tenant?.id || user?.tenantId); - return [ - - { - field: 'tenant', - headerName: 'Tenant', - flex: 1, - minWidth: 120, - filterable: false, - headerClassName: 'datagrid--header', - cellClassName: 'datagrid--cell', - - - editable: hasUpdatePermission, - - sortable: false, - type: 'singleSelect', - getOptionValue: (value: any) => value?.id, - getOptionLabel: (value: any) => value?.label, - valueOptions: await callOptionsApi('tenants'), - valueGetter: (params: GridValueGetterParams) => - params?.value?.id ?? params?.value, - - }, - + const columns: any[] = [ { field: 'name', headerName: 'Name', @@ -74,7 +54,14 @@ export const loadColumns = async ( editable: hasUpdatePermission, - + renderCell: (params: GridRenderCellParams) => { + if (!params.row?.id) return null; + return ( + + {params.row.name || 'Unnamed Project'} + + ) + }, }, @@ -107,7 +94,7 @@ export const loadColumns = async ( type: 'dateTime', valueGetter: (params: GridValueGetterParams) => - new Date(params.row.start_date), + params.row?.start_date ? new Date(params.row.start_date) : null, }, @@ -125,7 +112,7 @@ export const loadColumns = async ( type: 'dateTime', valueGetter: (params: GridValueGetterParams) => - new Date(params.row.estimated_end_date), + params.row?.estimated_end_date ? new Date(params.row.estimated_end_date) : null, }, @@ -177,9 +164,9 @@ export const loadColumns = async ( editable: false, sortable: false, - renderCell: (params: GridValueGetterParams) => ( + renderCell: (params: GridRenderCellParams) => ( <> - {dataFormatter.filesFormatter(params.row.attachments).map(link => ( + {dataFormatter.filesFormatter(params.row?.attachments).map(link => ( - )) :

No Attachments

- } - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-

Images

- {projects?.images?.length - ? ( - - ) :

No Images

- } -
- - - - - - - - - - - - - - - - - - - - - - - - - -
-

organizations

- - - - - - - - -

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

- - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - <> -

Trials Project

- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {projects.trials_project && Array.isArray(projects.trials_project) && - projects.trials_project.map((item: any) => ( - router.push(`/trials/trials-view/?id=${item.id}`)}> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ))} - -
NameStatusStartDateEndDateReferenceCode
- { item.name } - - { item.status } - - { dataFormatter.dateTimeFormatter(item.start_date) } - - { dataFormatter.dateTimeFormatter(item.end_date) } - - { item.reference_code } -
+
+ +
+
+

Name

+

{projectData?.name || 'No name provided'}

+
+ +
+

Status

+ + {projectData?.status || 'No status'} +
- {!projects?.trials_project?.length &&
No data
} - - - - - - - - - - - - +
+

Description

+ {projectData?.description + ?
+ :

No description available

+ } +
- router.push('/projects/projects-list')} - /> - +
+
+

Start Date

+

+ {projectData?.start_date ? dayjs(projectData.start_date).format('MMMM D, YYYY') : 'Not set'} +

+
+
+

Estimated End

+

+ {projectData?.estimated_end_date ? dayjs(projectData.estimated_end_date).format('MMMM D, YYYY') : 'Not set'} +

+
+
+
+ + + +
+
+

+ 💡 Strategic Insights +

+
+ {projectData?.insights ? ( +

{projectData.insights}

+ ) : ( +

No insights recorded for this project yet. Use the Edit page to add strategic observations or AI-generated summaries.

+ )} +
+
+ +
+

Attachments

+
+ {projectData?.attachments?.length ? ( + dataFormatter.filesFormatter(projectData.attachments).map(link => ( + + )) + ) : ( +

No files attached

+ )} +
+
+ +
+

Images

+ {projectData?.images?.length ? ( + + ) : ( +

No images uploaded

+ )} +
+
+
+
+ + +
+

Associated Trials

+ + {projectData?.trials_project?.length || 0} + +
+
+ + + + + + + + + + + {projectData?.trials_project && Array.isArray(projectData.trials_project) && projectData.trials_project.length > 0 ? ( + projectData.trials_project.map((item: any) => ( + router.push(`/trials/trials-view/?id=${item.id}`)} + className="hover:bg-blue-50 cursor-pointer transition-colors group" + > + + + + + + )) + ) : ( + + + + )} + +
Trial NameStatusReferencePeriod
+ {item.name} + + + {item.status} + + + {item.reference_code || '-'} + + {item.start_date ? dayjs(item.start_date).format('MMM D, YY') : '...'} + → + {item.end_date ? dayjs(item.end_date).format('MMM D, YY') : '...'} +
+ No trials associated with this project. +
+
+
+ + router.push('/projects/projects-list')} + /> ); @@ -661,11 +231,7 @@ const ProjectsView = () => { ProjectsView.getLayout = function getLayout(page: ReactElement) { return ( - + {page} ) diff --git a/frontend/src/pages/tenants/tenants-list.tsx b/frontend/src/pages/tenants/tenants-list.tsx index 0ce322e..23ca52c 100644 --- a/frontend/src/pages/tenants/tenants-list.tsx +++ b/frontend/src/pages/tenants/tenants-list.tsx @@ -15,6 +15,7 @@ import {useAppDispatch, useAppSelector} from "../../stores/hooks"; import CardBoxModal from "../../components/CardBoxModal"; import DragDropFilePicker from "../../components/DragDropFilePicker"; import {setRefetch, uploadCsv} from '../../stores/tenants/tenantsSlice'; +import { impersonate } from '../../stores/authSlice'; import {hasPermission} from "../../helpers/userPermissions"; @@ -44,6 +45,8 @@ const TenantsTablesPage = () => { ]); const hasCreatePermission = currentUser && hasPermission(currentUser, 'CREATE_TENANTS'); + const isSuperAdmin = currentUser?.app_role?.globalAccess; + const isImpersonating = isSuperAdmin && (currentUser as any)?.tenantId; const addFilter = () => { @@ -83,6 +86,11 @@ const TenantsTablesPage = () => { setIsModalActive(false); }; + const handleStopImpersonating = async () => { + await dispatch(impersonate(null)); + dispatch(setRefetch(true)); + }; + return ( <> @@ -107,10 +115,19 @@ const TenantsTablesPage = () => { {hasCreatePermission && ( setIsModalActive(true)} /> )} + + {isImpersonating && ( + + )}
@@ -159,4 +176,4 @@ TenantsTablesPage.getLayout = function getLayout(page: ReactElement) { ) } -export default TenantsTablesPage +export default TenantsTablesPage \ No newline at end of file diff --git a/frontend/src/pages/trials/[trialsId].tsx b/frontend/src/pages/trials/[trialsId].tsx index bd6bc27..dfee675 100644 --- a/frontend/src/pages/trials/[trialsId].tsx +++ b/frontend/src/pages/trials/[trialsId].tsx @@ -1,6 +1,6 @@ import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js' import Head from 'next/head' -import React, { ReactElement, useEffect, useState } from 'react' +import React, { ReactElement, useEffect, useState, useMemo } from 'react' import DatePicker from "react-datepicker"; import "react-datepicker/dist/react-datepicker.css"; import dayjs from "dayjs"; @@ -39,409 +39,54 @@ import {hasPermission} from "../../helpers/userPermissions"; const EditTrials = () => { const router = useRouter() const dispatch = useAppDispatch() - const initVals = { - - - - - - - - - - - - - - - - - - - - - - - - + const initVals = useMemo(() => ({ tenant: null, - - - - - - - - - - - - - - - - - - - - - - - - - - - project: null, - - - - - - - - - - - - - - - - - - - - - - - - - - - trial_type: null, - - - - - - 'name': '', - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + name: '', objective: '', - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - status: '', - - - - - - - - - - - - - - - - - - - - - - start_date: new Date(), - - - - - - - - - - - - - - - - - - - - - - - - - - - - end_date: new Date(), - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + start_date: null, + end_date: null, location: null, - - - - - - 'reference_code': '', - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + reference_code: '', attachments: [], - - - - - - - - - - - - - - - - - - - - - - - - - - - - - images: [], - - - - - - - - - - - - - - - - - - - - - - - - - - - - - organizations: null, - + }), []) - - - - } const [initialValues, setInitialValues] = useState(initVals) const { trials } = useAppSelector((state) => state.trials) - const { currentUser } = useAppSelector((state) => state.auth); - - const { trialsId } = router.query useEffect(() => { - dispatch(fetch({ id: trialsId })) - }, [trialsId]) - - useEffect(() => { - if (typeof trials === 'object') { - setInitialValues(trials) + if (trialsId) { + dispatch(fetch({ id: trialsId })) } - }, [trials]) + }, [trialsId, dispatch]) useEffect(() => { - if (typeof trials === 'object') { - + if (trials && typeof trials === 'object' && !Array.isArray(trials) && trials.id === trialsId) { const newInitialVal = {...initVals}; - - Object.keys(initVals).forEach(el => newInitialVal[el] = (trials)[el]) - + Object.keys(initVals).forEach(el => { + if (trials[el] !== undefined) { + newInitialVal[el] = (trials)[el] + } + }) setInitialValues(newInitialVal); } - }, [trials]) + }, [trials, trialsId, initVals]) const handleSubmit = async (data) => { await dispatch(update({ id: trialsId, data })) await router.push('/trials/trials-list') } + const hasTenant = !!(currentUser?.tenant?.id || currentUser?.tenantId); + const hasOrganizations = !!(currentUser?.organizations?.id || currentUser?.organizationsId || currentUser?.tenant?.organizationsId); + return ( <> @@ -457,725 +102,168 @@ const EditTrials = () => { initialValues={initialValues} onSubmit={(values) => handleSubmit(values)} > -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - setInitialValues({...initialValues, 'start_date': date})} - /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - setInitialValues({...initialValues, 'end_date': date})} - /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - router.push('/trials/trials-list')}/> - - + {({ setFieldValue, values }) => ( +
+ + {!hasTenant && ( + + + + )} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + setFieldValue('start_date', date)} + className="w-full border-gray-300 rounded" + /> + + + + setFieldValue('end_date', date)} + className="w-full border-gray-300 rounded" + /> + + + + + + + + + + + + + + + + + + + {!hasOrganizations && ( + + + + )} + + + + + + router.push('/trials/trials-list')}/> + + + )} @@ -1195,4 +283,4 @@ EditTrials.getLayout = function getLayout(page: ReactElement) { ) } -export default EditTrials +export default EditTrials \ No newline at end of file diff --git a/frontend/src/pages/trials/trials-edit.tsx b/frontend/src/pages/trials/trials-edit.tsx index ea453a6..bb18d50 100644 --- a/frontend/src/pages/trials/trials-edit.tsx +++ b/frontend/src/pages/trials/trials-edit.tsx @@ -1,6 +1,6 @@ import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js' import Head from 'next/head' -import React, { ReactElement, useEffect, useState } from 'react' +import React, { ReactElement, useEffect, useState, useMemo } from 'react' import DatePicker from "react-datepicker"; import "react-datepicker/dist/react-datepicker.css"; import dayjs from "dayjs"; @@ -39,406 +39,54 @@ import {hasPermission} from "../../helpers/userPermissions"; const EditTrialsPage = () => { const router = useRouter() const dispatch = useAppDispatch() - const initVals = { - - - - - - - - - - - - - - - - - - - - - - - - + const initVals = useMemo(() => ({ tenant: null, - - - - - - - - - - - - - - - - - - - - - - - - - - - project: null, - - - - - - - - - - - - - - - - - - - - - - - - - - - trial_type: null, - - - - - - 'name': '', - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + name: '', objective: '', - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - status: '', - - - - - - - - - - - - - - - - - - - - - - start_date: new Date(), - - - - - - - - - - - - - - - - - - - - - - - - - - - - end_date: new Date(), - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + start_date: null, + end_date: null, location: null, - - - - - - 'reference_code': '', - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + reference_code: '', attachments: [], - - - - - - - - - - - - - - - - - - - - - - - - - - - - - images: [], - - - - - - - - - - - - - - - - - - - - - - - - - - - - - organizations: null, - + }), []) - - - - } const [initialValues, setInitialValues] = useState(initVals) const { trials } = useAppSelector((state) => state.trials) - const { currentUser } = useAppSelector((state) => state.auth); - - const { id } = router.query useEffect(() => { - dispatch(fetch({ id: id })) - }, [id]) - - useEffect(() => { - if (typeof trials === 'object') { - setInitialValues(trials) + if (id) { + dispatch(fetch({ id: id })) } - }, [trials]) + }, [id, dispatch]) useEffect(() => { - if (typeof trials === 'object') { + if (trials && typeof trials === 'object' && !Array.isArray(trials) && trials.id === id) { const newInitialVal = {...initVals}; - Object.keys(initVals).forEach(el => newInitialVal[el] = (trials)[el]) + Object.keys(initVals).forEach(el => { + if (trials[el] !== undefined) { + newInitialVal[el] = (trials)[el] + } + }) setInitialValues(newInitialVal); } - }, [trials]) + }, [trials, id, initVals]) const handleSubmit = async (data) => { await dispatch(update({ id: id, data })) await router.push('/trials/trials-list') } + const hasTenant = !!(currentUser?.tenant?.id || currentUser?.tenantId); + const hasOrganizations = !!(currentUser?.organizations?.id || currentUser?.organizationsId || currentUser?.tenant?.organizationsId); + return ( <> @@ -454,725 +102,168 @@ const EditTrialsPage = () => { initialValues={initialValues} onSubmit={(values) => handleSubmit(values)} > -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - setInitialValues({...initialValues, 'start_date': date})} - /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - setInitialValues({...initialValues, 'end_date': date})} - /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - router.push('/trials/trials-list')}/> - - + {({ setFieldValue, values }) => ( +
+ + {!hasTenant && ( + + + + )} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + setFieldValue('start_date', date)} + className="w-full border-gray-300 rounded" + /> + + + + setFieldValue('end_date', date)} + className="w-full border-gray-300 rounded" + /> + + + + + + + + + + + + + + + + + + + {!hasOrganizations && ( + + + + )} + + + + + + router.push('/trials/trials-list')}/> + + + )} @@ -1192,4 +283,4 @@ EditTrialsPage.getLayout = function getLayout(page: ReactElement) { ) } -export default EditTrialsPage +export default EditTrialsPage \ No newline at end of file diff --git a/frontend/src/pages/trials/trials-list.tsx b/frontend/src/pages/trials/trials-list.tsx index 0ecd0ff..f94a30c 100644 --- a/frontend/src/pages/trials/trials-list.tsx +++ b/frontend/src/pages/trials/trials-list.tsx @@ -25,7 +25,7 @@ const TrialsTablesPage = () => { const [filterItems, setFilterItems] = useState([]); const [csvFile, setCsvFile] = useState(null); const [isModalActive, setIsModalActive] = useState(false); - const [showTableView, setShowTableView] = useState(false); + const [showTableView, setShowTableView] = useState(true); const { currentUser } = useAppSelector((state) => state.auth); @@ -133,7 +133,11 @@ const TrialsTablesPage = () => {
- Switch to Table + setShowTableView(!showTableView)} + />
@@ -143,7 +147,7 @@ const TrialsTablesPage = () => { filterItems={filterItems} setFilterItems={setFilterItems} filters={filters} - showGrid={false} + showGrid={showTableView} /> @@ -179,4 +183,4 @@ TrialsTablesPage.getLayout = function getLayout(page: ReactElement) { ) } -export default TrialsTablesPage +export default TrialsTablesPage \ No newline at end of file diff --git a/frontend/src/pages/trials/trials-new.tsx b/frontend/src/pages/trials/trials-new.tsx index 368be35..ad2ac4a 100644 --- a/frontend/src/pages/trials/trials-new.tsx +++ b/frontend/src/pages/trials/trials-new.tsx @@ -1,6 +1,6 @@ import { mdiAccount, mdiChartTimelineVariant, mdiMail, mdiUpload } from '@mdi/js' import Head from 'next/head' -import React, { ReactElement } from 'react' +import React, { ReactElement, useMemo } from 'react' import CardBox from '../../components/CardBox' import LayoutAuthenticated from '../../layouts/Authenticated' import SectionMain from '../../components/SectionMain' @@ -23,227 +23,31 @@ import { SelectFieldMany } from "../../components/SelectFieldMany"; import {RichTextField} from "../../components/RichTextField"; import { create } from '../../stores/trials/trialsSlice' -import { useAppDispatch } from '../../stores/hooks' +import { useAppDispatch, useAppSelector } from '../../stores/hooks' import { useRouter } from 'next/router' import moment from 'moment'; const initialValues = { - - - - - - - - - - - - - tenant: '', - - - - - - - - - - - - - - - project: '', - - - - - - - - - - - - - - - trial_type: '', - - - - name: '', - - - - - - - - - - - - - - - - - objective: '', - - - - - - - - - - - - - - - - - - - - - - status: 'draft', - - - - - - - - - - - - start_date: '', - - - - - - - - - - - - - - - end_date: '', - - - - - - - - - - - - - - - - - - - - - location: '', - - - - reference_code: '', - - - - - - - - - - - - - - - - - - - - - - - - attachments: [], - - - - - - - - - - - - - - - - images: [], - - - - - - - - - - - - - - - - organizations: '', - - - } const TrialsNew = () => { const router = useRouter() const dispatch = useAppDispatch() + const { currentUser } = useAppSelector((state) => state.auth) @@ -251,10 +55,27 @@ const TrialsNew = () => { const { dateRangeStart, dateRangeEnd } = router.query + const memoizedInitialValues = useMemo(() => { + return { + ...(dateRangeStart && dateRangeEnd ? + { + ...initialValues, + start_date: moment(dateRangeStart).format('YYYY-MM-DDTHH:mm'), + end_date: moment(dateRangeEnd).format('YYYY-MM-DDTHH:mm'), + } : initialValues), + tenant: currentUser?.tenant?.id || currentUser?.tenantId || '', + organizations: currentUser?.organizations?.id || currentUser?.organizationsId || currentUser?.tenant?.organizationsId || '', + } + }, [currentUser, dateRangeStart, dateRangeEnd]) + const handleSubmit = async (data) => { await dispatch(create(data)) await router.push('/trials/trials-list') } + + const hasTenant = !!(currentUser?.tenant?.id || currentUser?.tenantId) + const hasOrganization = !!(currentUser?.organizations?.id || currentUser?.organizationsId || currentUser?.tenant?.organizationsId) + return ( <> @@ -266,114 +87,26 @@ const TrialsNew = () => { handleSubmit(values)} + enableReinitialize >
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + {!hasTenant && ( + + + + )} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -383,36 +116,6 @@ const TrialsNew = () => { /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - { > - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -473,28 +140,6 @@ const TrialsNew = () => { - - - - - - - - - - - - - - - - - - - - - - @@ -505,32 +150,6 @@ const TrialsNew = () => { /> - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -541,52 +160,10 @@ const TrialsNew = () => { /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -596,55 +173,6 @@ const TrialsNew = () => { /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - { > - - - - - - - - - - - - - - - - - - - - - - - - - { > - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + {!hasOrganization && ( + + + + )} - router.push('/trials/trials-list')}/> + router.push('/trials/trials-list')}/>
@@ -760,4 +237,4 @@ TrialsNew.getLayout = function getLayout(page: ReactElement) { ) } -export default TrialsNew +export default TrialsNew \ No newline at end of file diff --git a/frontend/src/stores/authSlice.ts b/frontend/src/stores/authSlice.ts index 2eaa1f2..e5cfa15 100644 --- a/frontend/src/stores/authSlice.ts +++ b/frontend/src/stores/authSlice.ts @@ -61,6 +61,22 @@ export const passwordReset = createAsyncThunk( }, ); +export const impersonate = createAsyncThunk( + "auth/impersonate", + async (tenantId: string | null, { dispatch, rejectWithValue }) => { + try { + const response = await axios.post("auth/impersonate", { tenantId }); + await dispatch(findMe()); + return response.data; + } catch (error) { + if (!error.response) { + throw error; + } + return rejectWithValue(error.response.data); + } + } +); + export const findMe = createAsyncThunk('auth/findMe', async () => { const response = await axios.get('auth/me'); return response.data;