Autosave: 20260223-002254
This commit is contained in:
parent
249c697601
commit
84db90e7f3
@ -349,7 +349,7 @@ module.exports = class Activity_feed_itemsDBApi {
|
||||
}
|
||||
}
|
||||
|
||||
if (!globalAccess && options?.currentUser?.tenantId) {
|
||||
if (options?.currentUser?.tenantId) {
|
||||
where.tenantId = options.currentUser.tenantId;
|
||||
}
|
||||
|
||||
|
||||
@ -299,7 +299,7 @@ module.exports = class Form_field_choicesDBApi {
|
||||
}
|
||||
}
|
||||
|
||||
if (!globalAccess && options?.currentUser?.tenantId) {
|
||||
if (options?.currentUser?.tenantId) {
|
||||
where.tenantId = options.currentUser.tenantId;
|
||||
}
|
||||
|
||||
|
||||
@ -363,7 +363,7 @@ module.exports = class Form_fieldsDBApi {
|
||||
}
|
||||
}
|
||||
|
||||
if (!globalAccess && options?.currentUser?.tenantId) {
|
||||
if (options?.currentUser?.tenantId) {
|
||||
where.tenantId = options.currentUser.tenantId;
|
||||
}
|
||||
|
||||
|
||||
@ -413,7 +413,7 @@ module.exports = class Form_submissionsDBApi {
|
||||
}
|
||||
}
|
||||
|
||||
if (!globalAccess && options?.currentUser?.tenantId) {
|
||||
if (options?.currentUser?.tenantId) {
|
||||
where.tenantId = options.currentUser.tenantId;
|
||||
}
|
||||
|
||||
|
||||
@ -315,7 +315,7 @@ module.exports = class Form_templatesDBApi {
|
||||
}
|
||||
}
|
||||
|
||||
if (!globalAccess && options?.currentUser?.tenantId) {
|
||||
if (options?.currentUser?.tenantId) {
|
||||
where.tenantId = options.currentUser.tenantId;
|
||||
}
|
||||
|
||||
|
||||
@ -350,7 +350,7 @@ module.exports = class LocationsDBApi {
|
||||
}
|
||||
}
|
||||
|
||||
if (!globalAccess && options?.currentUser?.tenantId) {
|
||||
if (options?.currentUser?.tenantId) {
|
||||
where.tenantId = options.currentUser.tenantId;
|
||||
}
|
||||
|
||||
|
||||
@ -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 {
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
};
|
||||
@ -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
|
||||
};
|
||||
|
||||
|
||||
@ -336,7 +336,7 @@ module.exports = class ReportsDBApi {
|
||||
}
|
||||
}
|
||||
|
||||
if (!globalAccess && options?.currentUser?.tenantId) {
|
||||
if (options?.currentUser?.tenantId) {
|
||||
where.tenantId = options.currentUser.tenantId;
|
||||
}
|
||||
|
||||
|
||||
@ -406,7 +406,7 @@ module.exports = class Submission_valuesDBApi {
|
||||
}
|
||||
}
|
||||
|
||||
if (!globalAccess && options?.currentUser?.tenantId) {
|
||||
if (options?.currentUser?.tenantId) {
|
||||
where.tenantId = options.currentUser.tenantId;
|
||||
}
|
||||
|
||||
|
||||
@ -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 {
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
|
||||
};
|
||||
@ -285,7 +285,7 @@ module.exports = class Trial_typesDBApi {
|
||||
}
|
||||
}
|
||||
|
||||
if (!globalAccess && options?.currentUser?.tenantId) {
|
||||
if (options?.currentUser?.tenantId) {
|
||||
where.tenantId = options.currentUser.tenantId;
|
||||
}
|
||||
|
||||
|
||||
@ -435,7 +435,7 @@ module.exports = class TrialsDBApi {
|
||||
}
|
||||
}
|
||||
|
||||
if (!globalAccess && options?.currentUser?.tenantId) {
|
||||
if (options?.currentUser?.tenantId) {
|
||||
where.tenantId = options.currentUser.tenantId;
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
},
|
||||
};
|
||||
@ -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');
|
||||
}
|
||||
};
|
||||
@ -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;
|
||||
};
|
||||
};
|
||||
|
||||
@ -26,6 +26,13 @@ description: {
|
||||
|
||||
|
||||
|
||||
},
|
||||
|
||||
insights: {
|
||||
type: DataTypes.TEXT,
|
||||
|
||||
|
||||
|
||||
},
|
||||
|
||||
start_date: {
|
||||
@ -174,6 +181,4 @@ status: {
|
||||
|
||||
|
||||
return projects;
|
||||
};
|
||||
|
||||
|
||||
};
|
||||
@ -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",
|
||||
|
||||
|
||||
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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();
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
};
|
||||
@ -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
|
||||
@ -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) => (
|
||||
<DataGridMultiSelect {...params} entityName={'trials'}/>
|
||||
),
|
||||
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 => (
|
||||
<button
|
||||
key={link.publicUrl}
|
||||
onClick={(e) => saveFile(e, link.publicUrl, link.name)}
|
||||
>
|
||||
{link.name}
|
||||
</button>
|
||||
))}
|
||||
</>
|
||||
),
|
||||
|
||||
},
|
||||
|
||||
{
|
||||
field: 'images',
|
||||
headerName: 'Images',
|
||||
flex: 1,
|
||||
minWidth: 120,
|
||||
filterable: false,
|
||||
headerClassName: 'datagrid--header',
|
||||
cellClassName: 'datagrid--cell',
|
||||
|
||||
editable: false,
|
||||
sortable: false,
|
||||
renderCell: (params: GridValueGetterParams) => (
|
||||
<ImageField
|
||||
name={'Avatar'}
|
||||
image={params?.row?.images}
|
||||
className='w-24 h-24 mx-auto lg:w-6 lg:h-6'
|
||||
/>
|
||||
),
|
||||
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;
|
||||
};
|
||||
|
||||
@ -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 (
|
||||
<nav
|
||||
className={`${className} top-0 inset-x-0 fixed ${bgColor} h-14 z-30 transition-position w-screen lg:w-auto dark:bg-dark-800`}
|
||||
className={`${className} top-0 inset-x-0 fixed ${bgColor} ${isTopNav ? 'h-auto lg:h-28' : 'h-14'} z-30 transition-position w-screen lg:w-auto dark:bg-dark-800`}
|
||||
>
|
||||
<div className={`flex lg:items-stretch ${containerMaxW} ${isScrolled && `border-b border-pavitra-400 dark:border-dark-700`}`}>
|
||||
<div className="flex flex-1 items-stretch h-14">
|
||||
{children}
|
||||
{leftMenu.length > 0 && (
|
||||
<div className="hidden lg:flex items-stretch">
|
||||
{leftMenu.map((item, index) => (
|
||||
<NavBarItem key={index} item={item} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-none items-stretch flex h-14 lg:hidden">
|
||||
<NavBarItemPlain onClick={handleMenuNavBarToggleClick}>
|
||||
<BaseIcon path={isMenuNavBarActive ? mdiClose : mdiDotsVertical} size="24" />
|
||||
</NavBarItemPlain>
|
||||
<div className={`flex flex-col ${containerMaxW} ${isScrolled && `border-b border-pavitra-400 dark:border-dark-700`}`}>
|
||||
{/* Row 1: Children and Right Menu (always on Row 1) */}
|
||||
<div className="flex items-stretch h-14 w-full">
|
||||
<div className="flex-1 flex items-stretch overflow-hidden">
|
||||
{children}
|
||||
</div>
|
||||
|
||||
<div className="flex-none items-stretch flex h-14 lg:hidden">
|
||||
<NavBarItemPlain onClick={handleMenuNavBarToggleClick}>
|
||||
<BaseIcon path={isMenuNavBarActive ? mdiClose : mdiDotsVertical} size="24" />
|
||||
</NavBarItemPlain>
|
||||
</div>
|
||||
|
||||
<div className="hidden lg:flex items-center">
|
||||
<NavBarMenuList menu={menu} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Row 2: Navigation (Left Menu) for Top Nav Orientation */}
|
||||
{isTopNav && (
|
||||
<div className="hidden lg:flex items-stretch h-14 border-t border-gray-100 dark:border-dark-700 overflow-x-auto no-scrollbar">
|
||||
{leftMenu.length > 0 && (
|
||||
<div className="flex items-stretch">
|
||||
{leftMenu.map((item, index) => (
|
||||
<NavBarItem key={index} item={item} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Mobile menu dropdown */}
|
||||
<div
|
||||
className={`${
|
||||
isMenuNavBarActive ? 'block' : 'hidden'
|
||||
} flex items-center max-h-screen-menu overflow-y-auto lg:overflow-visible absolute w-screen top-14 left-0 ${bgColor} shadow-lg lg:w-auto lg:flex lg:static lg:shadow-none dark:bg-dark-800`}
|
||||
} lg:hidden flex items-center max-h-screen-menu overflow-y-auto absolute w-screen top-14 left-0 ${bgColor} shadow-lg dark:bg-dark-800`}
|
||||
>
|
||||
{/* Mobile menu should include both left and right items */}
|
||||
<div className="lg:hidden w-full">
|
||||
<div className="w-full">
|
||||
<NavBarMenuList menu={[...leftMenu, ...menu]} />
|
||||
</div>
|
||||
{/* Desktop menu only includes right items */}
|
||||
<div className="hidden lg:flex items-center">
|
||||
<NavBarMenuList menu={menu} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
@ -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 (
|
||||
<Link href={`/projects/projects-view/?id=${params.row.id}`} className="text-blue-600 hover:underline font-semibold">
|
||||
{params.row.name || 'Unnamed Project'}
|
||||
</Link>
|
||||
)
|
||||
},
|
||||
|
||||
},
|
||||
|
||||
@ -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 => (
|
||||
<button
|
||||
key={link.publicUrl}
|
||||
onClick={(e) => saveFile(e, link.publicUrl, link.name)}
|
||||
@ -203,7 +190,7 @@ export const loadColumns = async (
|
||||
|
||||
editable: false,
|
||||
sortable: false,
|
||||
renderCell: (params: GridValueGetterParams) => (
|
||||
renderCell: (params: GridRenderCellParams) => (
|
||||
<ImageField
|
||||
name={'Avatar'}
|
||||
image={params?.row?.images}
|
||||
@ -237,4 +224,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;
|
||||
};
|
||||
@ -1,16 +1,18 @@
|
||||
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'
|
||||
import CardBoxModal from '../CardBoxModal'
|
||||
import CardBox from "../CardBox";
|
||||
import { fetch, update, deleteItem, setRefetch, deleteItemsByIds } from '../../stores/tenants/tenantsSlice'
|
||||
import { impersonate } from '../../stores/authSlice'
|
||||
import { useAppDispatch, useAppSelector } from '../../stores/hooks'
|
||||
import { useRouter } from 'next/router'
|
||||
import { Field, Form, Formik } from "formik";
|
||||
import {
|
||||
DataGrid,
|
||||
GridColDef,
|
||||
GridSortModel,
|
||||
} from '@mui/x-data-grid';
|
||||
import {loadColumns} from "./configureTenantsCols";
|
||||
import _ from 'lodash';
|
||||
@ -33,12 +35,14 @@ const TableSampleTenants = ({ filterItems, setFilterItems, filters, showGrid })
|
||||
const [filterRequest, setFilterRequest] = React.useState('');
|
||||
const [columns, setColumns] = useState<GridColDef[]>([]);
|
||||
const [selectedRows, setSelectedRows] = useState([]);
|
||||
const [sortModel, setSortModel] = useState([
|
||||
const [sortModel, setSortModel] = useState<GridSortModel>([
|
||||
{
|
||||
field: '',
|
||||
sort: 'desc',
|
||||
},
|
||||
]);
|
||||
|
||||
const memoizedSortModel = useMemo(() => JSON.stringify(sortModel), [sortModel]);
|
||||
|
||||
const { tenants, loading, count, notify: tenantsNotify, refetch } = useAppSelector((state) => state.tenants)
|
||||
const { currentUser } = useAppSelector((state) => state.auth);
|
||||
@ -50,14 +54,15 @@ const TableSampleTenants = ({ filterItems, setFilterItems, filters, showGrid })
|
||||
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 sortItem = sortModel[0] || { field: '', sort: 'desc' };
|
||||
const { sort, field } = sortItem;
|
||||
|
||||
const query = `?page=${page}&limit=${perPage}${request}&sort=${sort}&field=${field}`;
|
||||
const query = `?page=${page}&limit=${perPage}${request}&sort=${sort || 'desc'}&field=${field || ''}`;
|
||||
dispatch(fetch({ limit: perPage, page, query }));
|
||||
};
|
||||
}, [dispatch, currentPage, filterRequest, sortModel]);
|
||||
|
||||
useEffect(() => {
|
||||
if (tenantsNotify.showNotification) {
|
||||
@ -68,14 +73,14 @@ const TableSampleTenants = ({ filterItems, setFilterItems, filters, showGrid })
|
||||
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)
|
||||
@ -85,10 +90,6 @@ const TableSampleTenants = ({ filterItems, setFilterItems, filters, showGrid })
|
||||
setIsModalTrashActive(false)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const handleDeleteModalAction = (id: string) => {
|
||||
setId(id)
|
||||
setIsModalTrashActive(true)
|
||||
@ -101,6 +102,11 @@ const TableSampleTenants = ({ filterItems, setFilterItems, filters, showGrid })
|
||||
}
|
||||
};
|
||||
|
||||
const handleImpersonate = async (tenantId: string) => {
|
||||
await dispatch(impersonate(tenantId));
|
||||
router.push('/dashboard');
|
||||
};
|
||||
|
||||
const generateFilterRequests = useMemo(() => {
|
||||
let request = '&';
|
||||
filterItems.forEach((item) => {
|
||||
@ -179,6 +185,7 @@ const TableSampleTenants = ({ filterItems, setFilterItems, filters, showGrid })
|
||||
handleDeleteModalAction,
|
||||
`tenants`,
|
||||
currentUser,
|
||||
handleImpersonate
|
||||
).then((newCols) => setColumns(newCols));
|
||||
}, [currentUser]);
|
||||
|
||||
@ -244,9 +251,7 @@ const TableSampleTenants = ({ filterItems, setFilterItems, filters, showGrid })
|
||||
setSelectedRows(ids)
|
||||
}}
|
||||
onSortModelChange={(params) => {
|
||||
params.length
|
||||
? setSortModel(params)
|
||||
: setSortModel([{ field: '', sort: 'desc' }]);
|
||||
setSortModel(params.length ? params : [{ field: '', sort: 'desc' }]);
|
||||
}}
|
||||
rowCount={count}
|
||||
pageSizeOptions={[10]}
|
||||
@ -460,4 +465,4 @@ const TableSampleTenants = ({ filterItems, setFilterItems, filters, showGrid })
|
||||
)
|
||||
}
|
||||
|
||||
export default TableSampleTenants
|
||||
export default TableSampleTenants
|
||||
@ -21,7 +21,8 @@ export const loadColumns = async (
|
||||
onDelete: Params,
|
||||
entityName: string,
|
||||
|
||||
user
|
||||
user,
|
||||
onImpersonate?: (id: string) => void
|
||||
|
||||
) => {
|
||||
async function callOptionsApi(entityName: string) {
|
||||
@ -38,6 +39,7 @@ export const loadColumns = async (
|
||||
}
|
||||
|
||||
const hasUpdatePermission = hasPermission(user, 'UPDATE_TENANTS')
|
||||
const isSuperAdmin = user?.app_role?.globalAccess;
|
||||
|
||||
return [
|
||||
|
||||
@ -49,9 +51,18 @@ export const loadColumns = async (
|
||||
filterable: false,
|
||||
headerClassName: 'datagrid--header',
|
||||
cellClassName: 'datagrid--cell',
|
||||
|
||||
renderCell: (params: GridValueGetterParams) => {
|
||||
return (
|
||||
<div
|
||||
className={`text-blue-600 dark:text-blue-400 ${isSuperAdmin ? 'cursor-pointer hover:underline font-bold' : ''}`}
|
||||
onClick={() => isSuperAdmin && onImpersonate && onImpersonate(params.row.id)}
|
||||
>
|
||||
{params.row.name}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
|
||||
editable: hasUpdatePermission,
|
||||
editable: hasUpdatePermission && !isSuperAdmin,
|
||||
|
||||
|
||||
},
|
||||
@ -164,4 +175,4 @@ export const loadColumns = async (
|
||||
},
|
||||
},
|
||||
];
|
||||
};
|
||||
};
|
||||
@ -38,31 +38,9 @@ export const loadColumns = async (
|
||||
}
|
||||
|
||||
const hasUpdatePermission = hasPermission(user, 'UPDATE_TRIALS')
|
||||
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: 'project',
|
||||
headerName: 'Project',
|
||||
@ -296,4 +274,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;
|
||||
};
|
||||
@ -1,4 +1,4 @@
|
||||
import React, { ReactNode, useState, useMemo } from 'react'
|
||||
import React, { ReactNode, useState, useMemo, useEffect } from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
import AsideMenu from '../components/AsideMenu'
|
||||
import NavBar from '../components/NavBar'
|
||||
@ -14,6 +14,8 @@ import { useTranslation } from 'next-i18next'
|
||||
import Search from '../components/Search'
|
||||
import NavBarItemPlain from '../components/NavBarItemPlain'
|
||||
import { hasPermission } from '../helpers/userPermissions'
|
||||
import { fetch as fetchOrganizations } from '../stores/organizations/organizationsSlice'
|
||||
import Link from 'next/link'
|
||||
|
||||
type Props = {
|
||||
children: ReactNode
|
||||
@ -27,10 +29,28 @@ export default function LayoutAuthenticated({ children }: Props) {
|
||||
const [isAsideLgActive, setIsAsideLgActive] = useState(false)
|
||||
const router = useRouter()
|
||||
const { currentUser, isFetching } = useAppSelector((state) => state.auth)
|
||||
const { organizations: allOrganizations } = useAppSelector((state) => state.organizations)
|
||||
|
||||
const organizations = currentUser?.organizations || currentUser?.organization;
|
||||
const orgData = Array.isArray(organizations) ? organizations[0] : organizations;
|
||||
const navOrientation = orgData?.navOrientation || 'side';
|
||||
|
||||
// If user has no organization linked, try to find one from the organizations store (for Super Admins)
|
||||
const fallbackOrg = useMemo(() => {
|
||||
if (orgData) return orgData;
|
||||
if (Array.isArray(allOrganizations) && allOrganizations.length > 0) {
|
||||
return allOrganizations[0];
|
||||
}
|
||||
return null;
|
||||
}, [orgData, allOrganizations]);
|
||||
|
||||
const navOrientation = fallbackOrg?.navOrientation || 'side';
|
||||
|
||||
// Fetch organizations if missing (especially for Super Admin to get preferences)
|
||||
useEffect(() => {
|
||||
if (!orgData && currentUser?.app_role?.name === 'Super Administrator' && (!allOrganizations || allOrganizations.length === 0)) {
|
||||
dispatch(fetchOrganizations({ limit: 1 }));
|
||||
}
|
||||
}, [orgData, currentUser, allOrganizations, dispatch]);
|
||||
|
||||
const filteredMenuAside = useMemo(() => {
|
||||
return menuAside.filter(item => {
|
||||
@ -61,6 +81,10 @@ export default function LayoutAuthenticated({ children }: Props) {
|
||||
|
||||
const filteredMenuNavBarRight = useMemo(() => {
|
||||
return menuNavBar.filter(item => {
|
||||
// Filter by permissions
|
||||
if (item.permissions && !hasPermission(currentUser, item.permissions)) {
|
||||
return false;
|
||||
}
|
||||
// Filter by orientation: only those NOT marked as isOrientationTopOnly go to the right menu
|
||||
// EXCEPT when we are in side mode, then we don't show isOrientationTopOnly items at all in NavBar
|
||||
if (navOrientation === 'side' && item.isOrientationTopOnly) {
|
||||
@ -71,14 +95,19 @@ export default function LayoutAuthenticated({ children }: Props) {
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}, [navOrientation]);
|
||||
}, [currentUser, navOrientation]);
|
||||
|
||||
const filteredMenuNavBarLeft = useMemo(() => {
|
||||
if (navOrientation !== 'top') return [];
|
||||
return menuNavBar.filter(item => item.isOrientationTopOnly);
|
||||
}, [navOrientation]);
|
||||
return menuNavBar.filter(item => {
|
||||
if (item.permissions && !hasPermission(currentUser, item.permissions)) {
|
||||
return false;
|
||||
}
|
||||
return item.isOrientationTopOnly;
|
||||
});
|
||||
}, [currentUser, navOrientation]);
|
||||
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
const handleRouteChange = () => {
|
||||
setIsAsideMobileExpanded(false)
|
||||
setIsAsideLgActive(false)
|
||||
@ -91,20 +120,20 @@ export default function LayoutAuthenticated({ children }: Props) {
|
||||
}
|
||||
}, [router.events])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (localStorage.getItem('token') && (!currentUser || !currentUser.organizations)) {
|
||||
useEffect(() => {
|
||||
if (localStorage.getItem('token') && !currentUser && !isFetching) {
|
||||
dispatch(findMe())
|
||||
}
|
||||
}, [currentUser, dispatch])
|
||||
}, [currentUser, dispatch, isFetching])
|
||||
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
const isDarkMode = darkMode || localStorage.getItem('darkMode') === '1'
|
||||
dispatch(setDarkMode(isDarkMode))
|
||||
}
|
||||
}, [darkMode, dispatch])
|
||||
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
if (i18n.language !== router.locale) {
|
||||
i18n.changeLanguage(router.locale)
|
||||
}
|
||||
@ -119,21 +148,24 @@ export default function LayoutAuthenticated({ children }: Props) {
|
||||
}
|
||||
|
||||
const layoutAsidePadding = navOrientation === 'top' ? '' : 'xl:pl-60'
|
||||
const navHeightPadding = navOrientation === 'top' ? 'pt-14 lg:pt-28' : 'pt-14'
|
||||
const organizationName = fallbackOrg?.name;
|
||||
|
||||
return (
|
||||
<div className={`${darkMode ? 'dark' : ''} overflow-hidden lg:overflow-visible`}>
|
||||
<div
|
||||
className={`${layoutAsidePadding} ${
|
||||
isAsideMobileExpanded ? 'ml-60 lg:ml-0' : ''
|
||||
} pt-14 min-h-screen w-screen transition-position lg:w-auto bg-gray-50 dark:bg-dark-800 dark:text-slate-100`}
|
||||
} ${navHeightPadding} min-h-screen w-screen transition-position lg:w-auto bg-gray-50 dark:bg-dark-800 dark:text-slate-100`}
|
||||
>
|
||||
<NavBar
|
||||
menu={filteredMenuNavBarRight}
|
||||
leftMenu={filteredMenuNavBarLeft}
|
||||
isTopNav={navOrientation === 'top'}
|
||||
className={`${layoutAsidePadding} ${isAsideMobileExpanded ? 'ml-60 lg:ml-0' : ''}`}
|
||||
>
|
||||
{navOrientation === 'side' && (
|
||||
<>
|
||||
<div className="flex items-center">
|
||||
<NavBarItemPlain
|
||||
display="flex lg:hidden"
|
||||
onClick={() => setIsAsideMobileExpanded(!isAsideMobileExpanded)}
|
||||
@ -146,9 +178,15 @@ export default function LayoutAuthenticated({ children }: Props) {
|
||||
>
|
||||
<BaseIcon path={mdiMenu} size="24" />
|
||||
</NavBarItemPlain>
|
||||
</>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center p-3 lg:p-0">
|
||||
{navOrientation === 'top' && (
|
||||
<Link href="/dashboard" className="flex items-center px-6">
|
||||
<b className="font-black">Trial Tracker</b>
|
||||
{organizationName && <span className="ml-2 text-sm text-gray-500 hidden md:inline">| {organizationName}</span>}
|
||||
</Link>
|
||||
)}
|
||||
<div className="flex items-center p-3 lg:p-0 flex-1">
|
||||
<Search />
|
||||
</div>
|
||||
</NavBar>
|
||||
|
||||
@ -52,6 +52,48 @@ const menuNavBar: MenuNavBarItem[] = [
|
||||
icon: mdiFileChartOutline,
|
||||
isOrientationTopOnly: true
|
||||
},
|
||||
{
|
||||
label: 'Settings',
|
||||
icon: mdiCogOutline,
|
||||
isOrientationTopOnly: true,
|
||||
menu: [
|
||||
{
|
||||
href: '/settings/company-preferences',
|
||||
label: 'Company Preferences',
|
||||
permissions: 'UPDATE_ORGANIZATIONS'
|
||||
},
|
||||
{
|
||||
href: '/trial_types/trial_types-list',
|
||||
label: 'Trial Types',
|
||||
permissions: 'READ_TRIAL_TYPES'
|
||||
},
|
||||
{
|
||||
href: '/form_templates/form_templates-list',
|
||||
label: 'Form Templates',
|
||||
permissions: 'READ_FORM_TEMPLATES'
|
||||
},
|
||||
{
|
||||
href: '/form_fields/form_fields-list',
|
||||
label: 'Form Fields',
|
||||
permissions: 'READ_FORM_FIELDS'
|
||||
},
|
||||
{
|
||||
href: '/form_field_choices/form_field_choices-list',
|
||||
label: 'Field Choices',
|
||||
permissions: 'READ_FORM_FIELD_CHOICES'
|
||||
},
|
||||
{
|
||||
href: '/locations/locations-list',
|
||||
label: 'Locations',
|
||||
permissions: 'READ_LOCATIONS'
|
||||
},
|
||||
{
|
||||
href: '/submission_values/submission_values-list',
|
||||
label: 'Data Sets',
|
||||
permissions: 'READ_SUBMISSION_VALUES'
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
href: '/tenants/tenants-list',
|
||||
label: 'Tenants',
|
||||
@ -104,4 +146,4 @@ export const webPagesNavBar = [
|
||||
|
||||
];
|
||||
|
||||
export default menuNavBar
|
||||
export default menuNavBar
|
||||
@ -1,6 +1,6 @@
|
||||
import * as icon from '@mdi/js';
|
||||
import Head from 'next/head'
|
||||
import React from 'react'
|
||||
import React, { useCallback, useMemo } from 'react'
|
||||
import axios from 'axios';
|
||||
import type { ReactElement } from 'react'
|
||||
import LayoutAuthenticated from '../layouts/Authenticated'
|
||||
@ -39,7 +39,7 @@ const Dashboard = () => {
|
||||
const { isFetchingQuery } = useAppSelector((state) => state.openAi);
|
||||
const { rolesWidgets, loading } = useAppSelector((state) => state.roles);
|
||||
|
||||
async function loadCounts() {
|
||||
const loadCounts = useCallback(async () => {
|
||||
const entities = ['projects', 'trials', 'form_submissions', 'users'];
|
||||
const fns = [setProjects, setTrials, setForm_submissions, setUsers];
|
||||
|
||||
@ -52,16 +52,17 @@ const Dashboard = () => {
|
||||
}
|
||||
});
|
||||
|
||||
Promise.allSettled(requests).then((results) => {
|
||||
results.forEach((result, i) => {
|
||||
if (result.status === 'fulfilled') {
|
||||
fns[i](result.value.data.count);
|
||||
} else {
|
||||
fns[i](result.reason.message);
|
||||
}
|
||||
});
|
||||
const results = await Promise.allSettled(requests);
|
||||
results.forEach((result, i) => {
|
||||
if (result.status === 'fulfilled') {
|
||||
fns[i](result.value.data.count);
|
||||
} else {
|
||||
fns[i](result.reason.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [currentUser]);
|
||||
|
||||
const currentUserMemo = useMemo(() => currentUser ? JSON.stringify(currentUser.id) : null, [currentUser]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!currentUser) return;
|
||||
@ -69,7 +70,7 @@ const Dashboard = () => {
|
||||
if (hasPermission(currentUser, 'READ_ACTIVITY_FEED_ITEMS')) {
|
||||
dispatch(fetchActivityItems({ query: '?limit=10&orderBy=occurred_at_DESC' }));
|
||||
}
|
||||
}, [currentUser, dispatch]);
|
||||
}, [currentUserMemo, dispatch, loadCounts]);
|
||||
|
||||
const getItemIcon = (type: string) => {
|
||||
switch (type) {
|
||||
@ -255,4 +256,4 @@ Dashboard.getLayout = function getLayout(page: ReactElement) {
|
||||
return <LayoutAuthenticated>{page}</LayoutAuthenticated>
|
||||
}
|
||||
|
||||
export default Dashboard
|
||||
export default Dashboard
|
||||
@ -40,60 +40,30 @@ const EditOrganizationsPage = () => {
|
||||
const router = useRouter()
|
||||
const dispatch = useAppDispatch()
|
||||
const initVals = {
|
||||
|
||||
|
||||
'name': '',
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
'navOrientation': 'top',
|
||||
'defaultView': 'list',
|
||||
}
|
||||
const [initialValues, setInitialValues] = useState(initVals)
|
||||
|
||||
const { organizations } = useAppSelector((state) => state.organizations)
|
||||
|
||||
const { currentUser } = useAppSelector((state) => state.auth);
|
||||
|
||||
|
||||
const { id } = router.query
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetch({ id: id }))
|
||||
if (id) {
|
||||
dispatch(fetch({ id: id }))
|
||||
}
|
||||
}, [id])
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof organizations === 'object') {
|
||||
setInitialValues(organizations)
|
||||
}
|
||||
}, [organizations])
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof organizations === 'object') {
|
||||
if (typeof organizations === 'object' && organizations !== null) {
|
||||
const newInitialVal = {...initVals};
|
||||
Object.keys(initVals).forEach(el => newInitialVal[el] = (organizations)[el])
|
||||
Object.keys(initVals).forEach(el => {
|
||||
if (organizations[el] !== undefined) {
|
||||
newInitialVal[el] = (organizations)[el]
|
||||
}
|
||||
})
|
||||
setInitialValues(newInitialVal);
|
||||
}
|
||||
}, [organizations])
|
||||
@ -119,45 +89,28 @@ const EditOrganizationsPage = () => {
|
||||
onSubmit={(values) => handleSubmit(values)}
|
||||
>
|
||||
<Form>
|
||||
<FormField
|
||||
label="Name"
|
||||
>
|
||||
<Field
|
||||
name="name"
|
||||
placeholder="Name"
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<FormField label="Navigation Orientation" labelFor="navOrientation">
|
||||
<Field name="navOrientation" id="navOrientation" component="select">
|
||||
<option value="side">Side</option>
|
||||
<option value="top">Top</option>
|
||||
</Field>
|
||||
</FormField>
|
||||
|
||||
|
||||
<FormField
|
||||
label="Name"
|
||||
>
|
||||
<Field
|
||||
name="name"
|
||||
placeholder="Name"
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<FormField label="Default Project View" labelFor="defaultView">
|
||||
<Field name="defaultView" id="defaultView" component="select">
|
||||
<option value="list">List</option>
|
||||
<option value="kanban">Kanban</option>
|
||||
</Field>
|
||||
</FormField>
|
||||
|
||||
<BaseDivider />
|
||||
<BaseButtons>
|
||||
@ -176,13 +129,11 @@ const EditOrganizationsPage = () => {
|
||||
EditOrganizationsPage.getLayout = function getLayout(page: ReactElement) {
|
||||
return (
|
||||
<LayoutAuthenticated
|
||||
|
||||
permission={'UPDATE_ORGANIZATIONS'}
|
||||
|
||||
>
|
||||
{page}
|
||||
</LayoutAuthenticated>
|
||||
)
|
||||
}
|
||||
|
||||
export default EditOrganizationsPage
|
||||
export default EditOrganizationsPage
|
||||
@ -40,288 +40,16 @@ const EditProjects = () => {
|
||||
const router = useRouter()
|
||||
const dispatch = useAppDispatch()
|
||||
const initVals = {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
tenant: null,
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
'name': '',
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
description: '',
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
start_date: new Date(),
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
estimated_end_date: new Date(),
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
status: '',
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
location: null,
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
attachments: [],
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
images: [],
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
organizations: null,
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
const [initialValues, setInitialValues] = useState(initVals)
|
||||
|
||||
@ -358,6 +86,9 @@ const EditProjects = () => {
|
||||
await router.push('/projects/projects-list')
|
||||
}
|
||||
|
||||
const hasTenant = !!(currentUser?.tenant?.id || currentUser?.tenantId);
|
||||
const hasOrganizations = !!(currentUser?.organizations?.id || currentUser?.organizationsId || currentUser?.tenant?.organizationsId);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
@ -374,527 +105,131 @@ const EditProjects = () => {
|
||||
onSubmit={(values) => handleSubmit(values)}
|
||||
>
|
||||
<Form>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<FormField label='Tenant' labelFor='tenant'>
|
||||
<Field
|
||||
name='tenant'
|
||||
id='tenant'
|
||||
component={SelectField}
|
||||
options={initialValues.tenant}
|
||||
itemRef={'tenants'}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
showField={'name'}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
></Field>
|
||||
</FormField>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<FormField
|
||||
label="Name"
|
||||
>
|
||||
<Field
|
||||
name="name"
|
||||
placeholder="Name"
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<FormField label='Description' hasTextareaHeight>
|
||||
<Field
|
||||
name='description'
|
||||
id='description'
|
||||
component={RichTextField}
|
||||
></Field>
|
||||
</FormField>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<FormField
|
||||
label="StartDate"
|
||||
>
|
||||
<DatePicker
|
||||
dateFormat="yyyy-MM-dd hh:mm"
|
||||
showTimeSelect
|
||||
selected={initialValues.start_date ?
|
||||
new Date(
|
||||
dayjs(initialValues.start_date).format('YYYY-MM-DD hh:mm'),
|
||||
) : null
|
||||
}
|
||||
onChange={(date) => setInitialValues({...initialValues, 'start_date': date})}
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<FormField
|
||||
label="EstimatedEndDate"
|
||||
>
|
||||
<DatePicker
|
||||
dateFormat="yyyy-MM-dd hh:mm"
|
||||
showTimeSelect
|
||||
selected={initialValues.estimated_end_date ?
|
||||
new Date(
|
||||
dayjs(initialValues.estimated_end_date).format('YYYY-MM-DD hh:mm'),
|
||||
) : null
|
||||
}
|
||||
onChange={(date) => setInitialValues({...initialValues, 'estimated_end_date': date})}
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<FormField label="Status" labelFor="status">
|
||||
<Field name="status" id="status" component="select">
|
||||
|
||||
<option value="planned">planned</option>
|
||||
|
||||
<option value="active">active</option>
|
||||
|
||||
<option value="on_hold">on_hold</option>
|
||||
|
||||
<option value="completed">completed</option>
|
||||
|
||||
<option value="cancelled">cancelled</option>
|
||||
|
||||
</Field>
|
||||
</FormField>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<FormField label='Location' labelFor='location'>
|
||||
<Field
|
||||
name='location'
|
||||
id='location'
|
||||
component={SelectField}
|
||||
options={initialValues.location}
|
||||
itemRef={'locations'}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
showField={'name'}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
></Field>
|
||||
</FormField>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<FormField>
|
||||
<Field
|
||||
label='Attachments'
|
||||
color='info'
|
||||
icon={mdiUpload}
|
||||
path={'projects/attachments'}
|
||||
name='attachments'
|
||||
id='attachments'
|
||||
schema={{
|
||||
size: undefined,
|
||||
formats: undefined,
|
||||
}}
|
||||
component={FormFilePicker}
|
||||
></Field>
|
||||
</FormField>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<FormField>
|
||||
<Field
|
||||
label='Images'
|
||||
color='info'
|
||||
icon={mdiUpload}
|
||||
path={'projects/images'}
|
||||
name='images'
|
||||
id='images'
|
||||
schema={{
|
||||
size: undefined,
|
||||
formats: undefined,
|
||||
}}
|
||||
component={FormImagePicker}
|
||||
></Field>
|
||||
</FormField>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<FormField label='organizations' labelFor='organizations'>
|
||||
<Field
|
||||
name='organizations'
|
||||
id='organizations'
|
||||
component={SelectField}
|
||||
options={initialValues.organizations}
|
||||
itemRef={'organizations'}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
showField={'name'}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
></Field>
|
||||
</FormField>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
{!hasTenant && (
|
||||
<FormField label='Tenant' labelFor='tenant'>
|
||||
<Field
|
||||
name='tenant'
|
||||
id='tenant'
|
||||
component={SelectField}
|
||||
options={initialValues.tenant}
|
||||
itemRef={'tenants'}
|
||||
showField={'name'}
|
||||
></Field>
|
||||
</FormField>
|
||||
)}
|
||||
|
||||
<FormField
|
||||
label="Name"
|
||||
>
|
||||
<Field
|
||||
name="name"
|
||||
placeholder="Name"
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<FormField label='Description' hasTextareaHeight>
|
||||
<Field
|
||||
name='description'
|
||||
id='description'
|
||||
component={RichTextField}
|
||||
></Field>
|
||||
</FormField>
|
||||
|
||||
<FormField
|
||||
label="StartDate"
|
||||
>
|
||||
<DatePicker
|
||||
dateFormat="yyyy-MM-dd hh:mm"
|
||||
showTimeSelect
|
||||
selected={initialValues.start_date ?
|
||||
new Date(
|
||||
dayjs(initialValues.start_date).format('YYYY-MM-DD hh:mm'),
|
||||
) : null
|
||||
}
|
||||
onChange={(date) => setInitialValues({...initialValues, 'start_date': date})}
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<FormField
|
||||
label="EstimatedEndDate"
|
||||
>
|
||||
<DatePicker
|
||||
dateFormat="yyyy-MM-dd hh:mm"
|
||||
showTimeSelect
|
||||
selected={initialValues.estimated_end_date ?
|
||||
new Date(
|
||||
dayjs(initialValues.estimated_end_date).format('YYYY-MM-DD hh:mm'),
|
||||
) : null
|
||||
}
|
||||
onChange={(date) => setInitialValues({...initialValues, 'estimated_end_date': date})}
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<FormField label="Status" labelFor="status">
|
||||
<Field name="status" id="status" component="select">
|
||||
<option value="planned">planned</option>
|
||||
<option value="active">active</option>
|
||||
<option value="on_hold">on_hold</option>
|
||||
<option value="completed">completed</option>
|
||||
<option value="cancelled">cancelled</option>
|
||||
</Field>
|
||||
</FormField>
|
||||
|
||||
<FormField label='Location' labelFor='location'>
|
||||
<Field
|
||||
name='location'
|
||||
id='location'
|
||||
component={SelectField}
|
||||
options={initialValues.location}
|
||||
itemRef={'locations'}
|
||||
showField={'name'}
|
||||
></Field>
|
||||
</FormField>
|
||||
|
||||
<FormField>
|
||||
<Field
|
||||
label='Attachments'
|
||||
color='info'
|
||||
icon={mdiUpload}
|
||||
path={'projects/attachments'}
|
||||
name='attachments'
|
||||
id='attachments'
|
||||
schema={{
|
||||
size: undefined,
|
||||
formats: undefined,
|
||||
}}
|
||||
component={FormFilePicker}
|
||||
></Field>
|
||||
</FormField>
|
||||
|
||||
<FormField>
|
||||
<Field
|
||||
label='Images'
|
||||
color='info'
|
||||
icon={mdiUpload}
|
||||
path={'projects/images'}
|
||||
name='images'
|
||||
id='images'
|
||||
schema={{
|
||||
size: undefined,
|
||||
formats: undefined,
|
||||
}}
|
||||
component={FormImagePicker}
|
||||
></Field>
|
||||
</FormField>
|
||||
|
||||
{!hasOrganizations && (
|
||||
<FormField label='organizations' labelFor='organizations'>
|
||||
<Field
|
||||
name='organizations'
|
||||
id='organizations'
|
||||
component={SelectField}
|
||||
options={initialValues.organizations}
|
||||
itemRef={'organizations'}
|
||||
showField={'name'}
|
||||
></Field>
|
||||
</FormField>
|
||||
)}
|
||||
|
||||
<BaseDivider />
|
||||
<BaseButtons>
|
||||
@ -913,13 +248,11 @@ const EditProjects = () => {
|
||||
EditProjects.getLayout = function getLayout(page: ReactElement) {
|
||||
return (
|
||||
<LayoutAuthenticated
|
||||
|
||||
permission={'UPDATE_PROJECTS'}
|
||||
|
||||
>
|
||||
{page}
|
||||
</LayoutAuthenticated>
|
||||
)
|
||||
}
|
||||
|
||||
export default EditProjects
|
||||
export default EditProjects
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,7 +1,7 @@
|
||||
import { mdiChartTimelineVariant } from '@mdi/js'
|
||||
import Head from 'next/head'
|
||||
import { uniqueId } from 'lodash';
|
||||
import React, { ReactElement, useState } from 'react'
|
||||
import React, { ReactElement, useState, useEffect } from 'react'
|
||||
import CardBox from '../../components/CardBox'
|
||||
import LayoutAuthenticated from '../../layouts/Authenticated'
|
||||
import SectionMain from '../../components/SectionMain'
|
||||
@ -25,10 +25,15 @@ const ProjectsTablesPage = () => {
|
||||
const [filterItems, setFilterItems] = useState([]);
|
||||
const [csvFile, setCsvFile] = useState<File | null>(null);
|
||||
const [isModalActive, setIsModalActive] = useState(false);
|
||||
const [showTableView, setShowTableView] = useState(false);
|
||||
|
||||
|
||||
const { currentUser } = useAppSelector((state) => state.auth);
|
||||
const [showTableView, setShowTableView] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentUser?.organizations?.defaultView) {
|
||||
setShowTableView(currentUser.organizations.defaultView === 'list');
|
||||
}
|
||||
}, [currentUser]);
|
||||
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
@ -125,7 +130,12 @@ const ProjectsTablesPage = () => {
|
||||
</div>
|
||||
|
||||
<div className='md:inline-flex items-center ms-auto'>
|
||||
<Link href={'/projects/projects-table'}>Switch to Table</Link>
|
||||
<BaseButton
|
||||
color='info'
|
||||
outline
|
||||
label={showTableView ? 'Switch to Kanban' : 'Switch to List'}
|
||||
onClick={() => setShowTableView(!showTableView)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
</CardBox>
|
||||
@ -134,7 +144,7 @@ const ProjectsTablesPage = () => {
|
||||
filterItems={filterItems}
|
||||
setFilterItems={setFilterItems}
|
||||
filters={filters}
|
||||
showGrid={false}
|
||||
showGrid={showTableView}
|
||||
/>
|
||||
|
||||
</SectionMain>
|
||||
@ -169,4 +179,4 @@ ProjectsTablesPage.getLayout = function getLayout(page: ReactElement) {
|
||||
)
|
||||
}
|
||||
|
||||
export default ProjectsTablesPage
|
||||
export default ProjectsTablesPage
|
||||
@ -1,6 +1,6 @@
|
||||
import { mdiAccount, mdiChartTimelineVariant, mdiMail, mdiUpload } from '@mdi/js'
|
||||
import { mdiChartTimelineVariant, 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'
|
||||
@ -12,580 +12,170 @@ import FormField from '../../components/FormField'
|
||||
import BaseDivider from '../../components/BaseDivider'
|
||||
import BaseButtons from '../../components/BaseButtons'
|
||||
import BaseButton from '../../components/BaseButton'
|
||||
import FormCheckRadio from '../../components/FormCheckRadio'
|
||||
import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'
|
||||
import FormFilePicker from '../../components/FormFilePicker'
|
||||
import FormImagePicker from '../../components/FormImagePicker'
|
||||
import { SwitchField } from '../../components/SwitchField'
|
||||
|
||||
import { SelectField } from '../../components/SelectField'
|
||||
import { SelectFieldMany } from "../../components/SelectFieldMany";
|
||||
import {RichTextField} from "../../components/RichTextField";
|
||||
|
||||
import { create } from '../../stores/projects/projectsSlice'
|
||||
import { useAppDispatch } from '../../stores/hooks'
|
||||
import { useAppDispatch, useAppSelector } from '../../stores/hooks'
|
||||
import { useRouter } from 'next/router'
|
||||
import moment from 'moment';
|
||||
|
||||
const initialValues = {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
tenant: '',
|
||||
|
||||
|
||||
|
||||
|
||||
tenant: null,
|
||||
name: '',
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
description: '',
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
insights: '',
|
||||
start_date: '',
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
estimated_end_date: '',
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
status: 'planned',
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
location: '',
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
location: null,
|
||||
attachments: [],
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
images: [],
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
organizations: '',
|
||||
|
||||
|
||||
|
||||
organizations: null,
|
||||
}
|
||||
|
||||
|
||||
const ProjectsNew = () => {
|
||||
const router = useRouter()
|
||||
const dispatch = useAppDispatch()
|
||||
const { currentUser } = useAppSelector((state) => state.auth)
|
||||
|
||||
|
||||
|
||||
const memoizedInitialValues = useMemo(() => {
|
||||
return {
|
||||
...initialValues,
|
||||
tenant: currentUser?.tenant?.id || currentUser?.tenantId || null,
|
||||
organizations: currentUser?.organizations?.id || currentUser?.organizationsId || currentUser?.tenant?.organizationsId || null,
|
||||
}
|
||||
}, [currentUser])
|
||||
|
||||
const handleSubmit = async (data) => {
|
||||
await dispatch(create(data))
|
||||
await router.push('/projects/projects-list')
|
||||
}
|
||||
|
||||
const hasTenant = !!(currentUser?.tenant?.id || currentUser?.tenantId)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{getPageTitle('New Item')}</title>
|
||||
<title>{getPageTitle('New Project')}</title>
|
||||
</Head>
|
||||
<SectionMain>
|
||||
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="New Item" main>
|
||||
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="New Project" main>
|
||||
{''}
|
||||
</SectionTitleLineWithButton>
|
||||
<CardBox>
|
||||
<Formik
|
||||
initialValues={
|
||||
|
||||
initialValues
|
||||
|
||||
}
|
||||
initialValues={memoizedInitialValues}
|
||||
onSubmit={(values) => handleSubmit(values)}
|
||||
enableReinitialize
|
||||
>
|
||||
<Form>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<FormField label="Tenant" labelFor="tenant">
|
||||
<Field name="tenant" id="tenant" component={SelectField} options={[]} itemRef={'tenants'}></Field>
|
||||
</FormField>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<FormField
|
||||
label="Name"
|
||||
>
|
||||
<Field
|
||||
name="name"
|
||||
placeholder="Name"
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<FormField label='Description' hasTextareaHeight>
|
||||
<Field
|
||||
name='description'
|
||||
id='description'
|
||||
component={RichTextField}
|
||||
></Field>
|
||||
</FormField>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<FormField
|
||||
label="StartDate"
|
||||
>
|
||||
<Field
|
||||
type="datetime-local"
|
||||
name="start_date"
|
||||
placeholder="StartDate"
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<FormField
|
||||
label="EstimatedEndDate"
|
||||
>
|
||||
<Field
|
||||
type="datetime-local"
|
||||
name="estimated_end_date"
|
||||
placeholder="EstimatedEndDate"
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<FormField label="Status" labelFor="status">
|
||||
<Field name="status" id="status" component="select">
|
||||
|
||||
<option value="planned">planned</option>
|
||||
|
||||
<option value="active">active</option>
|
||||
|
||||
<option value="on_hold">on_hold</option>
|
||||
|
||||
<option value="completed">completed</option>
|
||||
|
||||
<option value="cancelled">cancelled</option>
|
||||
|
||||
</Field>
|
||||
</FormField>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<FormField label="Location" labelFor="location">
|
||||
<Field name="location" id="location" component={SelectField} options={[]} itemRef={'locations'}></Field>
|
||||
</FormField>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<FormField>
|
||||
<Field
|
||||
label='Attachments'
|
||||
color='info'
|
||||
icon={mdiUpload}
|
||||
path={'projects/attachments'}
|
||||
name='attachments'
|
||||
id='attachments'
|
||||
schema={{
|
||||
size: undefined,
|
||||
formats: undefined,
|
||||
}}
|
||||
component={FormFilePicker}
|
||||
></Field>
|
||||
</FormField>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<FormField>
|
||||
<Field
|
||||
label='Images'
|
||||
color='info'
|
||||
icon={mdiUpload}
|
||||
path={'projects/images'}
|
||||
name='images'
|
||||
id='images'
|
||||
schema={{
|
||||
size: undefined,
|
||||
formats: undefined,
|
||||
}}
|
||||
component={FormImagePicker}
|
||||
></Field>
|
||||
</FormField>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<FormField label="organizations" labelFor="organizations">
|
||||
<Field name="organizations" id="organizations" component={SelectField} options={[]} itemRef={'organizations'}></Field>
|
||||
</FormField>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
{!hasTenant && (
|
||||
<FormField label="Tenant" labelFor="tenant">
|
||||
<Field name="tenant" id="tenant" component={SelectField} options={null} itemRef={'tenants'}></Field>
|
||||
</FormField>
|
||||
)}
|
||||
|
||||
<FormField label="Name">
|
||||
<Field name="name" placeholder="Project Name" />
|
||||
</FormField>
|
||||
|
||||
<FormField label='Description' hasTextareaHeight>
|
||||
<Field
|
||||
name='description'
|
||||
id='description'
|
||||
component={RichTextField}
|
||||
></Field>
|
||||
</FormField>
|
||||
|
||||
<FormField label='Strategic Insights' help="Add observations or strategic summaries" hasTextareaHeight>
|
||||
<Field
|
||||
name='insights'
|
||||
id='insights'
|
||||
component='textarea'
|
||||
className="rounded p-3 w-full border-gray-300"
|
||||
rows={4}
|
||||
></Field>
|
||||
</FormField>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<FormField label="Start Date">
|
||||
<Field
|
||||
type="datetime-local"
|
||||
name="start_date"
|
||||
className="w-full border-gray-300 rounded"
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<FormField label="Estimated End Date">
|
||||
<Field
|
||||
type="datetime-local"
|
||||
name="estimated_end_date"
|
||||
className="w-full border-gray-300 rounded"
|
||||
/>
|
||||
</FormField>
|
||||
</div>
|
||||
|
||||
<FormField label="Status" labelFor="status">
|
||||
<Field name="status" id="status" component="select">
|
||||
<option value="planned">planned</option>
|
||||
<option value="active">active</option>
|
||||
<option value="on_hold">on_hold</option>
|
||||
<option value="completed">completed</option>
|
||||
<option value="cancelled">cancelled</option>
|
||||
</Field>
|
||||
</FormField>
|
||||
|
||||
<FormField label="Location" labelFor="location">
|
||||
<Field name="location" id="location" component={SelectField} options={null} itemRef={'locations'}></Field>
|
||||
</FormField>
|
||||
|
||||
<FormField>
|
||||
<Field
|
||||
label='Attachments'
|
||||
color='info'
|
||||
icon={mdiUpload}
|
||||
path={'projects/attachments'}
|
||||
name='attachments'
|
||||
id='attachments'
|
||||
schema={{
|
||||
size: undefined,
|
||||
formats: undefined,
|
||||
}}
|
||||
component={FormFilePicker}
|
||||
></Field>
|
||||
</FormField>
|
||||
|
||||
<FormField>
|
||||
<Field
|
||||
label='Images'
|
||||
color='info'
|
||||
icon={mdiUpload}
|
||||
path={'projects/images'}
|
||||
name='images'
|
||||
id='images'
|
||||
schema={{
|
||||
size: undefined,
|
||||
formats: undefined,
|
||||
}}
|
||||
component={FormImagePicker}
|
||||
></Field>
|
||||
</FormField>
|
||||
|
||||
{!memoizedInitialValues.organizations && (
|
||||
<FormField label="Organizations" labelFor="organizations">
|
||||
<Field name="organizations" id="organizations" component={SelectField} options={null} itemRef={'organizations'}></Field>
|
||||
</FormField>
|
||||
)}
|
||||
|
||||
<BaseDivider />
|
||||
<BaseButtons>
|
||||
<BaseButton type="submit" color="info" label="Submit" />
|
||||
<BaseButton type="reset" color="info" outline label="Reset" />
|
||||
<BaseButton type='reset' color='danger' outline label='Cancel' onClick={() => router.push('/projects/projects-list')}/>
|
||||
<BaseButton color='danger' outline label='Cancel' onClick={() => router.push('/projects/projects-list')}/>
|
||||
</BaseButtons>
|
||||
</Form>
|
||||
</Formik>
|
||||
@ -597,14 +187,10 @@ const ProjectsNew = () => {
|
||||
|
||||
ProjectsNew.getLayout = function getLayout(page: ReactElement) {
|
||||
return (
|
||||
<LayoutAuthenticated
|
||||
|
||||
permission={'CREATE_PROJECTS'}
|
||||
|
||||
>
|
||||
<LayoutAuthenticated>
|
||||
{page}
|
||||
</LayoutAuthenticated>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProjectsNew
|
||||
export default ProjectsNew
|
||||
@ -1,6 +1,5 @@
|
||||
import React, { ReactElement, useEffect } from 'react';
|
||||
import React, { ReactElement, useEffect, useMemo } 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";
|
||||
@ -15,645 +14,216 @@ import SectionTitleLineWithButton from "../../components/SectionTitleLineWithBut
|
||||
import SectionMain from "../../components/SectionMain";
|
||||
import CardBox from "../../components/CardBox";
|
||||
import BaseButton from "../../components/BaseButton";
|
||||
import BaseDivider from "../../components/BaseDivider";
|
||||
import {mdiChartTimelineVariant} from "@mdi/js";
|
||||
import {SwitchField} from "../../components/SwitchField";
|
||||
import FormField from "../../components/FormField";
|
||||
|
||||
import {hasPermission} from "../../helpers/userPermissions";
|
||||
|
||||
|
||||
const ProjectsView = () => {
|
||||
const router = useRouter()
|
||||
const dispatch = useAppDispatch()
|
||||
const { projects } = useAppSelector((state) => state.projects)
|
||||
|
||||
const { currentUser } = useAppSelector((state) => state.auth);
|
||||
|
||||
|
||||
const { projects: projectsRaw, loading } = useAppSelector((state) => state.projects)
|
||||
const { id } = router.query;
|
||||
|
||||
function removeLastCharacter(str) {
|
||||
console.log(str,`str`)
|
||||
return str.slice(0, -1);
|
||||
}
|
||||
|
||||
// Ensure we are working with the correct project object
|
||||
const projects = useMemo(() => {
|
||||
if (Array.isArray(projectsRaw)) {
|
||||
return projectsRaw.find(p => p.id === id) || null;
|
||||
}
|
||||
return projectsRaw;
|
||||
}, [projectsRaw, id]);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetch({ id }));
|
||||
if (id) {
|
||||
dispatch(fetch({ id }));
|
||||
}
|
||||
}, [dispatch, id]);
|
||||
|
||||
const projectData = projects && !Array.isArray(projects) ? projects : null;
|
||||
|
||||
if (loading && !projectData) {
|
||||
return (
|
||||
<LayoutAuthenticated>
|
||||
<SectionMain>
|
||||
<div className="flex justify-center items-center h-64">
|
||||
<p className="text-gray-500 animate-pulse">Loading project details...</p>
|
||||
</div>
|
||||
</SectionMain>
|
||||
</LayoutAuthenticated>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{getPageTitle('View projects')}</title>
|
||||
<title>{getPageTitle('View Project')}</title>
|
||||
</Head>
|
||||
<SectionMain>
|
||||
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={removeLastCharacter('View projects')} main>
|
||||
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={'Project Details'} main>
|
||||
<BaseButton
|
||||
color='info'
|
||||
label='Edit'
|
||||
href={`/projects/projects-edit/?id=${id}`}
|
||||
/>
|
||||
</SectionTitleLineWithButton>
|
||||
<CardBox>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div className={'mb-4'}>
|
||||
<p className={'block font-bold mb-2'}>Tenant</p>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<p>{projects?.tenant?.name ?? 'No data'}</p>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div className={'mb-4'}>
|
||||
<p className={'block font-bold mb-2'}>Name</p>
|
||||
<p>{projects?.name}</p>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div className={'mb-4'}>
|
||||
<p className={'block font-bold mb-2'}>Description</p>
|
||||
{projects.description
|
||||
? <p dangerouslySetInnerHTML={{__html: projects.description}}/>
|
||||
: <p>No data</p>
|
||||
}
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<FormField label='StartDate'>
|
||||
{projects.start_date ? <DatePicker
|
||||
dateFormat="yyyy-MM-dd hh:mm"
|
||||
showTimeSelect
|
||||
selected={projects.start_date ?
|
||||
new Date(
|
||||
dayjs(projects.start_date).format('YYYY-MM-DD hh:mm'),
|
||||
) : null
|
||||
}
|
||||
disabled
|
||||
/> : <p>No StartDate</p>}
|
||||
</FormField>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<FormField label='EstimatedEndDate'>
|
||||
{projects.estimated_end_date ? <DatePicker
|
||||
dateFormat="yyyy-MM-dd hh:mm"
|
||||
showTimeSelect
|
||||
selected={projects.estimated_end_date ?
|
||||
new Date(
|
||||
dayjs(projects.estimated_end_date).format('YYYY-MM-DD hh:mm'),
|
||||
) : null
|
||||
}
|
||||
disabled
|
||||
/> : <p>No EstimatedEndDate</p>}
|
||||
</FormField>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div className={'mb-4'}>
|
||||
<p className={'block font-bold mb-2'}>Status</p>
|
||||
<p>{projects?.status ?? 'No data'}</p>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div className={'mb-4'}>
|
||||
<p className={'block font-bold mb-2'}>Location</p>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<p>{projects?.location?.name ?? 'No data'}</p>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div className={'mb-4'}>
|
||||
<p className={'block font-bold mb-2'}>Attachments</p>
|
||||
{projects?.attachments?.length
|
||||
? dataFormatter.filesFormatter(projects.attachments).map(link => (
|
||||
<button
|
||||
key={link.publicUrl}
|
||||
onClick={(e) => saveFile(e, link.publicUrl, link.name)}
|
||||
>
|
||||
{link.name}
|
||||
</button>
|
||||
)) : <p>No Attachments</p>
|
||||
}
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div className={'mb-4'}>
|
||||
<p className={'block font-bold mb-2'}>Images</p>
|
||||
{projects?.images?.length
|
||||
? (
|
||||
<ImageField
|
||||
name={'images'}
|
||||
image={projects?.images}
|
||||
className='w-20 h-20'
|
||||
/>
|
||||
) : <p>No Images</p>
|
||||
}
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div className={'mb-4'}>
|
||||
<p className={'block font-bold mb-2'}>organizations</p>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<p>{projects?.organizations?.name ?? 'No data'}</p>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<>
|
||||
<p className={'block font-bold mb-2'}>Trials Project</p>
|
||||
<CardBox
|
||||
className='mb-6 border border-gray-300 rounded overflow-hidden'
|
||||
hasTable
|
||||
>
|
||||
<div className='overflow-x-auto'>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<th>Name</th>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<th>Status</th>
|
||||
|
||||
|
||||
|
||||
<th>StartDate</th>
|
||||
|
||||
|
||||
|
||||
<th>EndDate</th>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<th>ReferenceCode</th>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{projects.trials_project && Array.isArray(projects.trials_project) &&
|
||||
projects.trials_project.map((item: any) => (
|
||||
<tr key={item.id} onClick={() => router.push(`/trials/trials-view/?id=${item.id}`)}>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<td data-label="name">
|
||||
{ item.name }
|
||||
</td>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<td data-label="status">
|
||||
{ item.status }
|
||||
</td>
|
||||
|
||||
|
||||
|
||||
<td data-label="start_date">
|
||||
{ dataFormatter.dateTimeFormatter(item.start_date) }
|
||||
</td>
|
||||
|
||||
|
||||
|
||||
<td data-label="end_date">
|
||||
{ dataFormatter.dateTimeFormatter(item.end_date) }
|
||||
</td>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<td data-label="reference_code">
|
||||
{ item.reference_code }
|
||||
</td>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
|
||||
<CardBox className="h-full">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<p className="text-gray-500 text-sm font-semibold uppercase tracking-wider">Name</p>
|
||||
<p className="text-xl font-bold">{projectData?.name || 'No name provided'}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="text-gray-500 text-sm font-semibold uppercase tracking-wider">Status</p>
|
||||
<span className={`px-3 py-1 rounded-full text-xs font-bold uppercase ${
|
||||
projectData?.status === 'active' ? 'bg-green-100 text-green-800' :
|
||||
projectData?.status === 'completed' ? 'bg-blue-100 text-blue-800' :
|
||||
projectData?.status === 'on_hold' ? 'bg-yellow-100 text-yellow-800' :
|
||||
projectData?.status === 'planned' ? 'bg-purple-100 text-purple-800' :
|
||||
'bg-gray-100 text-gray-800'
|
||||
}`}>
|
||||
{projectData?.status || 'No status'}
|
||||
</span>
|
||||
</div>
|
||||
{!projects?.trials_project?.length && <div className={'text-center py-4'}>No data</div>}
|
||||
</CardBox>
|
||||
</>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<BaseDivider />
|
||||
<div>
|
||||
<p className="text-gray-500 text-sm font-semibold uppercase tracking-wider">Description</p>
|
||||
{projectData?.description
|
||||
? <div className="prose prose-sm max-w-none text-gray-700 mt-1" dangerouslySetInnerHTML={{__html: projectData.description}}/>
|
||||
: <p className="text-gray-400 italic">No description available</p>
|
||||
}
|
||||
</div>
|
||||
|
||||
<BaseButton
|
||||
color='info'
|
||||
label='Back'
|
||||
onClick={() => router.push('/projects/projects-list')}
|
||||
/>
|
||||
</CardBox>
|
||||
<div className="grid grid-cols-2 gap-4 pt-2">
|
||||
<div>
|
||||
<p className="text-gray-500 text-sm font-semibold uppercase tracking-wider">Start Date</p>
|
||||
<p className="font-medium">
|
||||
{projectData?.start_date ? dayjs(projectData.start_date).format('MMMM D, YYYY') : 'Not set'}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-gray-500 text-sm font-semibold uppercase tracking-wider">Estimated End</p>
|
||||
<p className="font-medium">
|
||||
{projectData?.estimated_end_date ? dayjs(projectData.estimated_end_date).format('MMMM D, YYYY') : 'Not set'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardBox>
|
||||
|
||||
<CardBox className="h-full">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<p className="text-gray-500 text-sm font-semibold uppercase tracking-wider flex items-center">
|
||||
<span className="mr-2">💡</span> Strategic Insights
|
||||
</p>
|
||||
<div className="bg-blue-50 border-l-4 border-blue-400 p-4 mt-2">
|
||||
{projectData?.insights ? (
|
||||
<p className="text-blue-700 whitespace-pre-wrap">{projectData.insights}</p>
|
||||
) : (
|
||||
<p className="text-blue-500 italic text-sm">No insights recorded for this project yet. Use the Edit page to add strategic observations or AI-generated summaries.</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="text-gray-500 text-sm font-semibold uppercase tracking-wider mb-2">Attachments</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{projectData?.attachments?.length ? (
|
||||
dataFormatter.filesFormatter(projectData.attachments).map(link => (
|
||||
<button
|
||||
key={link.publicUrl}
|
||||
onClick={(e) => saveFile(e, link.publicUrl, link.name)}
|
||||
className="px-3 py-1 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded text-xs border border-gray-300 transition-colors flex items-center"
|
||||
>
|
||||
<span className="mr-1 text-base">📄</span> {link.name}
|
||||
</button>
|
||||
))
|
||||
) : (
|
||||
<p className="text-gray-400 text-sm italic">No files attached</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="text-gray-500 text-sm font-semibold uppercase tracking-wider mb-2">Images</p>
|
||||
{projectData?.images?.length ? (
|
||||
<ImageField
|
||||
name={'images'}
|
||||
image={projectData?.images}
|
||||
className='w-full max-h-40 object-cover rounded shadow-sm'
|
||||
/>
|
||||
) : (
|
||||
<p className="text-gray-400 text-sm italic">No images uploaded</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardBox>
|
||||
</div>
|
||||
|
||||
<CardBox hasTable className="mb-6 overflow-hidden">
|
||||
<div className="p-4 border-b border-gray-100 bg-gray-50 flex justify-between items-center">
|
||||
<h3 className="font-bold text-gray-700">Associated Trials</h3>
|
||||
<span className="bg-blue-600 text-white px-2 py-0.5 rounded-full text-xs font-bold">
|
||||
{projectData?.trials_project?.length || 0}
|
||||
</span>
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm text-left">
|
||||
<thead className="bg-gray-50 text-gray-500 uppercase text-[10px] tracking-wider">
|
||||
<tr>
|
||||
<th className="px-6 py-3 font-bold">Trial Name</th>
|
||||
<th className="px-6 py-3 font-bold text-center">Status</th>
|
||||
<th className="px-6 py-3 font-bold text-center">Reference</th>
|
||||
<th className="px-6 py-3 font-bold text-right">Period</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-100">
|
||||
{projectData?.trials_project && Array.isArray(projectData.trials_project) && projectData.trials_project.length > 0 ? (
|
||||
projectData.trials_project.map((item: any) => (
|
||||
<tr
|
||||
key={item.id}
|
||||
onClick={() => router.push(`/trials/trials-view/?id=${item.id}`)}
|
||||
className="hover:bg-blue-50 cursor-pointer transition-colors group"
|
||||
>
|
||||
<td className="px-6 py-4 font-semibold text-blue-600 group-hover:underline">
|
||||
{item.name}
|
||||
</td>
|
||||
<td className="px-6 py-4 text-center">
|
||||
<span className={`px-2 py-0.5 rounded-full text-[10px] font-bold uppercase ${
|
||||
item.status === 'active' ? 'bg-green-100 text-green-800' :
|
||||
item.status === 'completed' ? 'bg-gray-100 text-gray-800' :
|
||||
'bg-yellow-100 text-yellow-800'
|
||||
}`}>
|
||||
{item.status}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 text-center font-mono text-gray-400 text-xs">
|
||||
{item.reference_code || '-'}
|
||||
</td>
|
||||
<td className="px-6 py-4 text-right text-gray-500 text-xs">
|
||||
{item.start_date ? dayjs(item.start_date).format('MMM D, YY') : '...'}
|
||||
<span className="mx-1 opacity-50">→</span>
|
||||
{item.end_date ? dayjs(item.end_date).format('MMM D, YY') : '...'}
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
) : (
|
||||
<tr>
|
||||
<td colSpan={4} className="px-6 py-10 text-center text-gray-400 italic">
|
||||
No trials associated with this project.
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</CardBox>
|
||||
|
||||
<BaseButton
|
||||
color='info'
|
||||
label='Back to List'
|
||||
onClick={() => router.push('/projects/projects-list')}
|
||||
/>
|
||||
</SectionMain>
|
||||
</>
|
||||
);
|
||||
@ -661,11 +231,7 @@ const ProjectsView = () => {
|
||||
|
||||
ProjectsView.getLayout = function getLayout(page: ReactElement) {
|
||||
return (
|
||||
<LayoutAuthenticated
|
||||
|
||||
permission={'READ_PROJECTS'}
|
||||
|
||||
>
|
||||
<LayoutAuthenticated>
|
||||
{page}
|
||||
</LayoutAuthenticated>
|
||||
)
|
||||
|
||||
@ -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 (
|
||||
<>
|
||||
<Head>
|
||||
@ -107,10 +115,19 @@ const TenantsTablesPage = () => {
|
||||
{hasCreatePermission && (
|
||||
<BaseButton
|
||||
color='info'
|
||||
className={'mr-3'}
|
||||
label='Upload CSV'
|
||||
onClick={() => setIsModalActive(true)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isImpersonating && (
|
||||
<BaseButton
|
||||
color='danger'
|
||||
label='Stop Impersonating'
|
||||
onClick={handleStopImpersonating}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className='md:inline-flex items-center ms-auto'>
|
||||
<div id='delete-rows-button'></div>
|
||||
@ -159,4 +176,4 @@ TenantsTablesPage.getLayout = function getLayout(page: ReactElement) {
|
||||
)
|
||||
}
|
||||
|
||||
export default TenantsTablesPage
|
||||
export default TenantsTablesPage
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -25,7 +25,7 @@ const TrialsTablesPage = () => {
|
||||
const [filterItems, setFilterItems] = useState([]);
|
||||
const [csvFile, setCsvFile] = useState<File | null>(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 = () => {
|
||||
</div>
|
||||
|
||||
<div className='md:inline-flex items-center ms-auto'>
|
||||
<Link href={'/trials/trials-table'}>Switch to Table</Link>
|
||||
<BaseButton
|
||||
color='info'
|
||||
label={showTableView ? 'Switch to Calendar' : 'Switch to Table'}
|
||||
onClick={() => setShowTableView(!showTableView)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
</CardBox>
|
||||
@ -143,7 +147,7 @@ const TrialsTablesPage = () => {
|
||||
filterItems={filterItems}
|
||||
setFilterItems={setFilterItems}
|
||||
filters={filters}
|
||||
showGrid={false}
|
||||
showGrid={showTableView}
|
||||
/>
|
||||
</CardBox>
|
||||
|
||||
@ -179,4 +183,4 @@ TrialsTablesPage.getLayout = function getLayout(page: ReactElement) {
|
||||
)
|
||||
}
|
||||
|
||||
export default TrialsTablesPage
|
||||
export default TrialsTablesPage
|
||||
@ -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 (
|
||||
<>
|
||||
<Head>
|
||||
@ -266,114 +87,26 @@ const TrialsNew = () => {
|
||||
</SectionTitleLineWithButton>
|
||||
<CardBox>
|
||||
<Formik
|
||||
initialValues={
|
||||
|
||||
|
||||
|
||||
dateRangeStart && dateRangeEnd ?
|
||||
{
|
||||
...initialValues,
|
||||
start_date: moment(dateRangeStart).format('YYYY-MM-DDTHH:mm'),
|
||||
end_date: moment(dateRangeEnd).format('YYYY-MM-DDTHH:mm'),
|
||||
} : initialValues
|
||||
|
||||
}
|
||||
initialValues={memoizedInitialValues}
|
||||
onSubmit={(values) => handleSubmit(values)}
|
||||
enableReinitialize
|
||||
>
|
||||
<Form>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<FormField label="Tenant" labelFor="tenant">
|
||||
<Field name="tenant" id="tenant" component={SelectField} options={[]} itemRef={'tenants'}></Field>
|
||||
</FormField>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
{!hasTenant && (
|
||||
<FormField label="Tenant" labelFor="tenant">
|
||||
<Field name="tenant" id="tenant" component={SelectField} options={[]} itemRef={'tenants'}></Field>
|
||||
</FormField>
|
||||
)}
|
||||
|
||||
<FormField label="Project" labelFor="project">
|
||||
<Field name="project" id="project" component={SelectField} options={[]} itemRef={'projects'}></Field>
|
||||
</FormField>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<FormField label="TrialType" labelFor="trial_type">
|
||||
<Field name="trial_type" id="trial_type" component={SelectField} options={[]} itemRef={'trial_types'}></Field>
|
||||
</FormField>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<FormField
|
||||
label="Name"
|
||||
>
|
||||
@ -383,36 +116,6 @@ const TrialsNew = () => {
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<FormField label='Objective' hasTextareaHeight>
|
||||
<Field
|
||||
name='objective'
|
||||
@ -421,42 +124,6 @@ const TrialsNew = () => {
|
||||
></Field>
|
||||
</FormField>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<FormField label="Status" labelFor="status">
|
||||
<Field name="status" id="status" component="select">
|
||||
|
||||
@ -473,28 +140,6 @@ const TrialsNew = () => {
|
||||
</Field>
|
||||
</FormField>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<FormField
|
||||
label="StartDate"
|
||||
>
|
||||
@ -505,32 +150,6 @@ const TrialsNew = () => {
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<FormField
|
||||
label="EndDate"
|
||||
>
|
||||
@ -541,52 +160,10 @@ const TrialsNew = () => {
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<FormField label="Location" labelFor="location">
|
||||
<Field name="location" id="location" component={SelectField} options={[]} itemRef={'locations'}></Field>
|
||||
</FormField>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<FormField
|
||||
label="ReferenceCode"
|
||||
>
|
||||
@ -596,55 +173,6 @@ const TrialsNew = () => {
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<FormField>
|
||||
<Field
|
||||
label='Attachments'
|
||||
@ -661,31 +189,6 @@ const TrialsNew = () => {
|
||||
></Field>
|
||||
</FormField>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<FormField>
|
||||
<Field
|
||||
label='Images'
|
||||
@ -702,43 +205,17 @@ const TrialsNew = () => {
|
||||
></Field>
|
||||
</FormField>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<FormField label="organizations" labelFor="organizations">
|
||||
<Field name="organizations" id="organizations" component={SelectField} options={[]} itemRef={'organizations'}></Field>
|
||||
</FormField>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
{!hasOrganization && (
|
||||
<FormField label="organizations" labelFor="organizations">
|
||||
<Field name="organizations" id="organizations" component={SelectField} options={[]} itemRef={'organizations'}></Field>
|
||||
</FormField>
|
||||
)}
|
||||
|
||||
<BaseDivider />
|
||||
<BaseButtons>
|
||||
<BaseButton type="submit" color="info" label="Submit" />
|
||||
<BaseButton type="reset" color="info" outline label="Reset" />
|
||||
<BaseButton type='reset' color='danger' outline label='Cancel' onClick={() => router.push('/trials/trials-list')}/>
|
||||
<BaseButton color='danger' outline label='Cancel' onClick={() => router.push('/trials/trials-list')}/>
|
||||
</BaseButtons>
|
||||
</Form>
|
||||
</Formik>
|
||||
@ -760,4 +237,4 @@ TrialsNew.getLayout = function getLayout(page: ReactElement) {
|
||||
)
|
||||
}
|
||||
|
||||
export default TrialsNew
|
||||
export default TrialsNew
|
||||
@ -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;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user