Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
84db90e7f3 | ||
|
|
249c697601 |
@ -349,6 +349,10 @@ module.exports = class Activity_feed_itemsDBApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options?.currentUser?.tenantId) {
|
||||||
|
where.tenantId = options.currentUser.tenantId;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
offset = currentPage * limit;
|
offset = currentPage * limit;
|
||||||
|
|
||||||
|
|||||||
@ -299,6 +299,10 @@ module.exports = class Form_field_choicesDBApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options?.currentUser?.tenantId) {
|
||||||
|
where.tenantId = options.currentUser.tenantId;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
offset = currentPage * limit;
|
offset = currentPage * limit;
|
||||||
|
|
||||||
|
|||||||
@ -363,6 +363,10 @@ module.exports = class Form_fieldsDBApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options?.currentUser?.tenantId) {
|
||||||
|
where.tenantId = options.currentUser.tenantId;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
offset = currentPage * limit;
|
offset = currentPage * limit;
|
||||||
|
|
||||||
|
|||||||
@ -413,6 +413,10 @@ module.exports = class Form_submissionsDBApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options?.currentUser?.tenantId) {
|
||||||
|
where.tenantId = options.currentUser.tenantId;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
offset = currentPage * limit;
|
offset = currentPage * limit;
|
||||||
|
|
||||||
|
|||||||
@ -315,6 +315,10 @@ module.exports = class Form_templatesDBApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options?.currentUser?.tenantId) {
|
||||||
|
where.tenantId = options.currentUser.tenantId;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
offset = currentPage * limit;
|
offset = currentPage * limit;
|
||||||
|
|
||||||
|
|||||||
@ -350,6 +350,10 @@ module.exports = class LocationsDBApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options?.currentUser?.tenantId) {
|
||||||
|
where.tenantId = options.currentUser.tenantId;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
offset = currentPage * limit;
|
offset = currentPage * limit;
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
const db = require('../models');
|
const db = require('../models');
|
||||||
const FileDBApi = require('./file');
|
const FileDBApi = require('./file');
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
@ -25,6 +24,13 @@ module.exports = class OrganizationsDBApi {
|
|||||||
||
|
||
|
||||||
null
|
null
|
||||||
,
|
,
|
||||||
|
address: data.address || null,
|
||||||
|
city: data.city || null,
|
||||||
|
state: data.state || null,
|
||||||
|
country: data.country || null,
|
||||||
|
zip: data.zip || null,
|
||||||
|
navOrientation: data.navOrientation || 'top',
|
||||||
|
defaultView: data.defaultView || 'list',
|
||||||
|
|
||||||
importHash: data.importHash || null,
|
importHash: data.importHash || null,
|
||||||
createdById: currentUser.id,
|
createdById: currentUser.id,
|
||||||
@ -55,6 +61,13 @@ module.exports = class OrganizationsDBApi {
|
|||||||
||
|
||
|
||||||
null
|
null
|
||||||
,
|
,
|
||||||
|
address: item.address || null,
|
||||||
|
city: item.city || null,
|
||||||
|
state: item.state || null,
|
||||||
|
country: item.country || null,
|
||||||
|
zip: item.zip || null,
|
||||||
|
navOrientation: item.navOrientation || 'top',
|
||||||
|
defaultView: item.defaultView || 'list',
|
||||||
|
|
||||||
importHash: item.importHash || null,
|
importHash: item.importHash || null,
|
||||||
createdById: currentUser.id,
|
createdById: currentUser.id,
|
||||||
@ -74,9 +87,9 @@ module.exports = class OrganizationsDBApi {
|
|||||||
static async update(id, data, options) {
|
static async update(id, data, options) {
|
||||||
const currentUser = (options && options.currentUser) || {id: null};
|
const currentUser = (options && options.currentUser) || {id: null};
|
||||||
const transaction = (options && options.transaction) || undefined;
|
const transaction = (options && options.transaction) || undefined;
|
||||||
const globalAccess = currentUser.app_role?.globalAccess;
|
// const globalAccess = currentUser.app_role?.globalAccess;
|
||||||
|
|
||||||
const organizations = await db.organizations.findByPk(id, {}, {transaction});
|
const organizations = await db.organizations.findByPk(id, { transaction });
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -84,6 +97,13 @@ module.exports = class OrganizationsDBApi {
|
|||||||
const updatePayload = {};
|
const updatePayload = {};
|
||||||
|
|
||||||
if (data.name !== undefined) updatePayload.name = data.name;
|
if (data.name !== undefined) updatePayload.name = data.name;
|
||||||
|
if (data.address !== undefined) updatePayload.address = data.address;
|
||||||
|
if (data.city !== undefined) updatePayload.city = data.city;
|
||||||
|
if (data.state !== undefined) updatePayload.state = data.state;
|
||||||
|
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;
|
updatePayload.updatedById = currentUser.id;
|
||||||
@ -153,8 +173,7 @@ module.exports = class OrganizationsDBApi {
|
|||||||
const transaction = (options && options.transaction) || undefined;
|
const transaction = (options && options.transaction) || undefined;
|
||||||
|
|
||||||
const organizations = await db.organizations.findOne(
|
const organizations = await db.organizations.findOne(
|
||||||
{ where },
|
{ where, transaction },
|
||||||
{ transaction },
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!organizations) {
|
if (!organizations) {
|
||||||
@ -408,4 +427,3 @@ module.exports = class OrganizationsDBApi {
|
|||||||
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,5 @@
|
|||||||
|
|
||||||
const db = require('../models');
|
const db = require('../models');
|
||||||
const FileDBApi = require('./file');
|
const FileDBApi = require('./file');
|
||||||
const crypto = require('crypto');
|
|
||||||
const Utils = require('../utils');
|
const Utils = require('../utils');
|
||||||
|
|
||||||
|
|
||||||
@ -31,6 +29,11 @@ module.exports = class ProjectsDBApi {
|
|||||||
null
|
null
|
||||||
,
|
,
|
||||||
|
|
||||||
|
insights: data.insights
|
||||||
|
||
|
||||||
|
null
|
||||||
|
,
|
||||||
|
|
||||||
start_date: data.start_date
|
start_date: data.start_date
|
||||||
||
|
||
|
||||||
null
|
null
|
||||||
@ -111,6 +114,11 @@ module.exports = class ProjectsDBApi {
|
|||||||
description: item.description
|
description: item.description
|
||||||
||
|
||
|
||||||
null
|
null
|
||||||
|
,
|
||||||
|
|
||||||
|
insights: item.insights
|
||||||
|
||
|
||||||
|
null
|
||||||
,
|
,
|
||||||
|
|
||||||
start_date: item.start_date
|
start_date: item.start_date
|
||||||
@ -149,9 +157,6 @@ module.exports = class ProjectsDBApi {
|
|||||||
data[i].attachments,
|
data[i].attachments,
|
||||||
options,
|
options,
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < projects.length; i++) {
|
|
||||||
await FileDBApi.replaceRelationFiles(
|
await FileDBApi.replaceRelationFiles(
|
||||||
{
|
{
|
||||||
belongsTo: db.projects.getTableName(),
|
belongsTo: db.projects.getTableName(),
|
||||||
@ -170,7 +175,6 @@ module.exports = class ProjectsDBApi {
|
|||||||
static async update(id, data, options) {
|
static async update(id, data, options) {
|
||||||
const currentUser = (options && options.currentUser) || {id: null};
|
const currentUser = (options && options.currentUser) || {id: null};
|
||||||
const transaction = (options && options.transaction) || undefined;
|
const transaction = (options && options.transaction) || undefined;
|
||||||
const globalAccess = currentUser.app_role?.globalAccess;
|
|
||||||
|
|
||||||
const projects = await db.projects.findByPk(id, {}, {transaction});
|
const projects = await db.projects.findByPk(id, {}, {transaction});
|
||||||
|
|
||||||
@ -184,6 +188,8 @@ module.exports = class ProjectsDBApi {
|
|||||||
|
|
||||||
if (data.description !== undefined) updatePayload.description = data.description;
|
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;
|
if (data.start_date !== undefined) updatePayload.start_date = data.start_date;
|
||||||
|
|
||||||
@ -324,22 +330,12 @@ module.exports = class ProjectsDBApi {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
output.trials_project = await projects.getTrials_project({
|
output.trials_project = await projects.getTrials_project({
|
||||||
transaction
|
transaction
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
output.tenant = await projects.getTenant({
|
output.tenant = await projects.getTenant({
|
||||||
transaction
|
transaction
|
||||||
});
|
});
|
||||||
@ -350,6 +346,11 @@ module.exports = class ProjectsDBApi {
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
output.organizations = await projects.getOrganizations({
|
||||||
|
transaction
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
output.attachments = await projects.getAttachments({
|
output.attachments = await projects.getAttachments({
|
||||||
transaction
|
transaction
|
||||||
});
|
});
|
||||||
@ -390,11 +391,13 @@ module.exports = class ProjectsDBApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options?.currentUser?.tenantId) {
|
||||||
|
where.tenantId = options.currentUser.tenantId;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
offset = currentPage * limit;
|
offset = currentPage * limit;
|
||||||
|
|
||||||
const orderBy = null;
|
|
||||||
|
|
||||||
const transaction = (options && options.transaction) || undefined;
|
const transaction = (options && options.transaction) || undefined;
|
||||||
|
|
||||||
let include = [
|
let include = [
|
||||||
@ -402,7 +405,7 @@ module.exports = class ProjectsDBApi {
|
|||||||
{
|
{
|
||||||
model: db.tenants,
|
model: db.tenants,
|
||||||
as: 'tenant',
|
as: 'tenant',
|
||||||
|
required: !!filter.tenant,
|
||||||
where: filter.tenant ? {
|
where: filter.tenant ? {
|
||||||
[Op.or]: [
|
[Op.or]: [
|
||||||
{ id: { [Op.in]: filter.tenant.split('|').map(term => Utils.uuid(term)) } },
|
{ id: { [Op.in]: filter.tenant.split('|').map(term => Utils.uuid(term)) } },
|
||||||
@ -412,14 +415,14 @@ module.exports = class ProjectsDBApi {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
} : {},
|
} : undefined,
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
model: db.locations,
|
model: db.locations,
|
||||||
as: 'location',
|
as: 'location',
|
||||||
|
required: !!filter.location,
|
||||||
where: filter.location ? {
|
where: filter.location ? {
|
||||||
[Op.or]: [
|
[Op.or]: [
|
||||||
{ id: { [Op.in]: filter.location.split('|').map(term => Utils.uuid(term)) } },
|
{ id: { [Op.in]: filter.location.split('|').map(term => Utils.uuid(term)) } },
|
||||||
@ -429,14 +432,14 @@ module.exports = class ProjectsDBApi {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
} : {},
|
} : undefined,
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
model: db.organizations,
|
model: db.organizations,
|
||||||
as: 'organizations',
|
as: 'organizations',
|
||||||
|
required: false,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
@ -444,11 +447,13 @@ module.exports = class ProjectsDBApi {
|
|||||||
{
|
{
|
||||||
model: db.file,
|
model: db.file,
|
||||||
as: 'attachments',
|
as: 'attachments',
|
||||||
|
required: false,
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
model: db.file,
|
model: db.file,
|
||||||
as: 'images',
|
as: 'images',
|
||||||
|
required: false,
|
||||||
},
|
},
|
||||||
|
|
||||||
];
|
];
|
||||||
@ -556,8 +561,28 @@ module.exports = class ProjectsDBApi {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if (filter.tenant) {
|
||||||
|
const listItems = filter.tenant.split('|').map(item => {
|
||||||
|
return Utils.uuid(item)
|
||||||
|
});
|
||||||
|
|
||||||
|
where = {
|
||||||
|
...where,
|
||||||
|
tenantId: {[Op.or]: listItems}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (filter.location) {
|
||||||
|
const listItems = filter.location.split('|').map(item => {
|
||||||
|
return Utils.uuid(item)
|
||||||
|
});
|
||||||
|
|
||||||
|
where = {
|
||||||
|
...where,
|
||||||
|
locationId: {[Op.or]: listItems}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
if (filter.organizations) {
|
if (filter.organizations) {
|
||||||
@ -613,7 +638,7 @@ module.exports = class ProjectsDBApi {
|
|||||||
order: filter.field && filter.sort
|
order: filter.field && filter.sort
|
||||||
? [[filter.field, filter.sort]]
|
? [[filter.field, filter.sort]]
|
||||||
: [['createdAt', 'desc']],
|
: [['createdAt', 'desc']],
|
||||||
transaction: options?.transaction,
|
transaction: transaction,
|
||||||
logging: console.log
|
logging: console.log
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -673,4 +698,3 @@ module.exports = class ProjectsDBApi {
|
|||||||
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -336,6 +336,10 @@ module.exports = class ReportsDBApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options?.currentUser?.tenantId) {
|
||||||
|
where.tenantId = options.currentUser.tenantId;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
offset = currentPage * limit;
|
offset = currentPage * limit;
|
||||||
|
|
||||||
|
|||||||
@ -406,6 +406,10 @@ module.exports = class Submission_valuesDBApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options?.currentUser?.tenantId) {
|
||||||
|
where.tenantId = options.currentUser.tenantId;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
offset = currentPage * limit;
|
offset = currentPage * limit;
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
const db = require('../models');
|
const db = require('../models');
|
||||||
const FileDBApi = require('./file');
|
const FileDBApi = require('./file');
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
@ -551,12 +550,13 @@ module.exports = class TenantsDBApi {
|
|||||||
|
|
||||||
|
|
||||||
if (!globalAccess && organizationId) {
|
if (!globalAccess && organizationId) {
|
||||||
where.organizationId = organizationId;
|
where.organizationsId = organizationId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (query) {
|
if (query) {
|
||||||
where = {
|
where = {
|
||||||
|
...where,
|
||||||
[Op.or]: [
|
[Op.or]: [
|
||||||
{ ['id']: Utils.uuid(query) },
|
{ ['id']: Utils.uuid(query) },
|
||||||
Utils.ilike(
|
Utils.ilike(
|
||||||
@ -584,4 +584,3 @@ module.exports = class TenantsDBApi {
|
|||||||
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -285,6 +285,10 @@ module.exports = class Trial_typesDBApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options?.currentUser?.tenantId) {
|
||||||
|
where.tenantId = options.currentUser.tenantId;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
offset = currentPage * limit;
|
offset = currentPage * limit;
|
||||||
|
|
||||||
|
|||||||
@ -435,6 +435,10 @@ module.exports = class TrialsDBApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options?.currentUser?.tenantId) {
|
||||||
|
where.tenantId = options.currentUser.tenantId;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
offset = currentPage * limit;
|
offset = currentPage * limit;
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
const db = require('../models');
|
const db = require('../models');
|
||||||
const FileDBApi = require('./file');
|
const FileDBApi = require('./file');
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
@ -116,7 +115,11 @@ module.exports = class UsersDBApi {
|
|||||||
transaction,
|
transaction,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (data.data.tenant !== undefined) {
|
||||||
|
await users.setTenant(data.data.tenant || null, {
|
||||||
|
transaction,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
await users.setCustom_permissions(data.data.custom_permissions || [], {
|
await users.setCustom_permissions(data.data.custom_permissions || [], {
|
||||||
@ -330,6 +333,12 @@ module.exports = class UsersDBApi {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (data.tenant !== undefined) {
|
||||||
|
await users.setTenant(data.tenant || null, {
|
||||||
|
transaction,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -404,73 +413,41 @@ module.exports = class UsersDBApi {
|
|||||||
static async findBy(where, options) {
|
static async findBy(where, options) {
|
||||||
const transaction = (options && options.transaction) || undefined;
|
const transaction = (options && options.transaction) || undefined;
|
||||||
|
|
||||||
const users = await db.users.findOne(
|
const user = await db.users.findOne({
|
||||||
{ where },
|
where,
|
||||||
{ transaction },
|
transaction,
|
||||||
);
|
include: [
|
||||||
|
{ model: db.roles, as: "app_role" },
|
||||||
|
{ model: db.organizations, as: "organizations" },
|
||||||
|
{ model: db.tenants, as: "tenant" },
|
||||||
|
{ model: db.file, as: "avatar" },
|
||||||
|
{ model: db.permissions, as: "custom_permissions" }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
if (!users) {
|
if (!user) {
|
||||||
return users;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
const output = users.get({plain: true});
|
const output = user.get({ plain: true });
|
||||||
|
|
||||||
|
if (user.app_role) {
|
||||||
|
output.app_role_permissions = await user.app_role.getPermissions({
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
output.form_submissions_submitted_by_user = await users.getForm_submissions_submitted_by_user({
|
|
||||||
transaction
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
output.activity_feed_items_actor_user = await users.getActivity_feed_items_actor_user({
|
|
||||||
transaction
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
output.reports_created_by_user = await users.getReports_created_by_user({
|
|
||||||
transaction
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
output.avatar = await users.getAvatar({
|
|
||||||
transaction
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
output.app_role = await users.getApp_role({
|
|
||||||
transaction
|
|
||||||
});
|
|
||||||
|
|
||||||
if (output.app_role) {
|
|
||||||
output.app_role_permissions = await output.app_role.getPermissions({
|
|
||||||
transaction,
|
transaction,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
output.form_submissions_submitted_by_user = await user.getForm_submissions_submitted_by_user({
|
||||||
output.custom_permissions = await users.getCustom_permissions({
|
|
||||||
transaction
|
transaction
|
||||||
});
|
});
|
||||||
|
|
||||||
|
output.activity_feed_items_actor_user = await user.getActivity_feed_items_actor_user({
|
||||||
output.organizations = await users.getOrganizations({
|
|
||||||
transaction
|
transaction
|
||||||
});
|
});
|
||||||
|
|
||||||
|
output.reports_created_by_user = await user.getReports_created_by_user({
|
||||||
|
transaction
|
||||||
|
});
|
||||||
|
|
||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
@ -528,6 +505,11 @@ module.exports = class UsersDBApi {
|
|||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
model: db.tenants,
|
||||||
|
as: 'tenant',
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
{
|
{
|
||||||
model: db.permissions,
|
model: db.permissions,
|
||||||
@ -1008,4 +990,3 @@ module.exports = class UsersDBApi {
|
|||||||
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
83
backend/src/db/migrations/1771796084954.js
Normal file
83
backend/src/db/migrations/1771796084954.js
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
|
||||||
|
module.exports = {
|
||||||
|
async up(queryInterface, Sequelize) {
|
||||||
|
const transaction = await queryInterface.sequelize.transaction();
|
||||||
|
try {
|
||||||
|
await queryInterface.addColumn(
|
||||||
|
'organizations',
|
||||||
|
'address',
|
||||||
|
{
|
||||||
|
type: Sequelize.DataTypes.TEXT,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
{ transaction }
|
||||||
|
);
|
||||||
|
await queryInterface.addColumn(
|
||||||
|
'organizations',
|
||||||
|
'city',
|
||||||
|
{
|
||||||
|
type: Sequelize.DataTypes.TEXT,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
{ transaction }
|
||||||
|
);
|
||||||
|
await queryInterface.addColumn(
|
||||||
|
'organizations',
|
||||||
|
'state',
|
||||||
|
{
|
||||||
|
type: Sequelize.DataTypes.TEXT,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
{ transaction }
|
||||||
|
);
|
||||||
|
await queryInterface.addColumn(
|
||||||
|
'organizations',
|
||||||
|
'country',
|
||||||
|
{
|
||||||
|
type: Sequelize.DataTypes.TEXT,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
{ transaction }
|
||||||
|
);
|
||||||
|
await queryInterface.addColumn(
|
||||||
|
'organizations',
|
||||||
|
'zip',
|
||||||
|
{
|
||||||
|
type: Sequelize.DataTypes.TEXT,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
{ transaction }
|
||||||
|
);
|
||||||
|
await queryInterface.addColumn(
|
||||||
|
'organizations',
|
||||||
|
'navOrientation',
|
||||||
|
{
|
||||||
|
type: Sequelize.DataTypes.TEXT,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: 'side',
|
||||||
|
},
|
||||||
|
{ 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', 'address', { transaction });
|
||||||
|
await queryInterface.removeColumn('organizations', 'city', { transaction });
|
||||||
|
await queryInterface.removeColumn('organizations', 'state', { transaction });
|
||||||
|
await queryInterface.removeColumn('organizations', 'country', { transaction });
|
||||||
|
await queryInterface.removeColumn('organizations', 'zip', { transaction });
|
||||||
|
await queryInterface.removeColumn('organizations', 'navOrientation', { transaction });
|
||||||
|
await transaction.commit();
|
||||||
|
} catch (err) {
|
||||||
|
await transaction.rollback();
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
56
backend/src/db/migrations/1771796084955.js
Normal file
56
backend/src/db/migrations/1771796084955.js
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
module.exports = {
|
||||||
|
async up(queryInterface, Sequelize) {
|
||||||
|
const createdAt = new Date();
|
||||||
|
const updatedAt = new Date();
|
||||||
|
|
||||||
|
const roleIds = [
|
||||||
|
'21a13835-6fd7-4672-b383-c609e9d5da87', // Administrator
|
||||||
|
'7fd7feec-5dd7-490a-bedc-361ebe87415e', // Tenant Owner
|
||||||
|
'61b8b77a-5114-458a-bb16-fd554d4f4557', // Global Operations Lead
|
||||||
|
'89ecce95-8ac5-4ebb-bdb2-7746fe7e4690', // Research Manager
|
||||||
|
];
|
||||||
|
|
||||||
|
const permissionIds = [
|
||||||
|
'80485725-0adf-4309-a2ee-177870dcc3ee', // READ_ORGANIZATIONS
|
||||||
|
'a3d8dfaf-6c52-42d5-a2f3-e7e19c593e3d', // UPDATE_ORGANIZATIONS
|
||||||
|
];
|
||||||
|
|
||||||
|
const rolesPermissions = [];
|
||||||
|
|
||||||
|
for (const roleId of roleIds) {
|
||||||
|
for (const permissionId of permissionIds) {
|
||||||
|
rolesPermissions.push({
|
||||||
|
createdAt,
|
||||||
|
updatedAt,
|
||||||
|
roles_permissionsId: roleId,
|
||||||
|
permissionId: permissionId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use a transaction and UPSERT-like behavior or just check existence
|
||||||
|
// To keep it simple, we use queryInterface.bulkInsert but it might fail on duplicates if we are not careful.
|
||||||
|
// However, we checked and they are missing.
|
||||||
|
|
||||||
|
await queryInterface.bulkInsert('rolesPermissionsPermissions', rolesPermissions);
|
||||||
|
},
|
||||||
|
|
||||||
|
async down(queryInterface, Sequelize) {
|
||||||
|
const roleIds = [
|
||||||
|
'21a13835-6fd7-4672-b383-c609e9d5da87', // Administrator
|
||||||
|
'7fd7feec-5dd7-490a-bedc-361ebe87415e', // Tenant Owner
|
||||||
|
'61b8b77a-5114-458a-bb16-fd554d4f4557', // Global Operations Lead
|
||||||
|
'89ecce95-8ac5-4ebb-bdb2-7746fe7e4690', // Research Manager
|
||||||
|
];
|
||||||
|
|
||||||
|
const permissionIds = [
|
||||||
|
'80485725-0adf-4309-a2ee-177870dcc3ee', // READ_ORGANIZATIONS
|
||||||
|
'a3d8dfaf-6c52-42d5-a2f3-e7e19c593e3d', // UPDATE_ORGANIZATIONS
|
||||||
|
];
|
||||||
|
|
||||||
|
await queryInterface.bulkDelete('rolesPermissionsPermissions', {
|
||||||
|
roles_permissionsId: roleIds,
|
||||||
|
permissionId: permissionIds,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
46
backend/src/db/migrations/1771796084956.js
Normal file
46
backend/src/db/migrations/1771796084956.js
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
module.exports = {
|
||||||
|
async up(queryInterface, Sequelize) {
|
||||||
|
const roles = await queryInterface.sequelize.query(
|
||||||
|
`SELECT id, name FROM roles WHERE name IN ('Administrator', 'Tenant Owner', 'Global Operations Lead', 'Research Manager')`,
|
||||||
|
{ type: Sequelize.QueryTypes.SELECT }
|
||||||
|
);
|
||||||
|
|
||||||
|
const permissions = await queryInterface.sequelize.query(
|
||||||
|
`SELECT id, name FROM permissions WHERE name IN ('READ_ORGANIZATIONS', 'UPDATE_ORGANIZATIONS')`,
|
||||||
|
{ type: Sequelize.QueryTypes.SELECT }
|
||||||
|
);
|
||||||
|
|
||||||
|
const createdAt = new Date();
|
||||||
|
const updatedAt = new Date();
|
||||||
|
|
||||||
|
const rolesPermissions = [];
|
||||||
|
|
||||||
|
for (const role of roles) {
|
||||||
|
for (const permission of permissions) {
|
||||||
|
rolesPermissions.push({
|
||||||
|
createdAt,
|
||||||
|
updatedAt,
|
||||||
|
roles_permissionsId: role.id,
|
||||||
|
permissionId: permission.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use a more robust way to insert ignoring duplicates
|
||||||
|
for (const rp of rolesPermissions) {
|
||||||
|
await queryInterface.sequelize.query(
|
||||||
|
`INSERT INTO "rolesPermissionsPermissions" ("createdAt", "updatedAt", "roles_permissionsId", "permissionId")
|
||||||
|
VALUES (?, ?, ?, ?)
|
||||||
|
ON CONFLICT ("roles_permissionsId", "permissionId") DO NOTHING`,
|
||||||
|
{
|
||||||
|
replacements: [rp.createdAt, rp.updatedAt, rp.roles_permissionsId, rp.permissionId],
|
||||||
|
type: Sequelize.QueryTypes.INSERT
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async down(queryInterface, Sequelize) {
|
||||||
|
// No need for a down migration that removes permissions as it might remove more than intended if not careful
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
module.exports = {
|
||||||
|
async up(queryInterface, Sequelize) {
|
||||||
|
await queryInterface.addColumn('users', 'tenantId', {
|
||||||
|
type: Sequelize.DataTypes.UUID,
|
||||||
|
references: {
|
||||||
|
model: 'tenants',
|
||||||
|
key: 'id',
|
||||||
|
},
|
||||||
|
allowNull: true,
|
||||||
|
onUpdate: 'CASCADE',
|
||||||
|
onDelete: 'SET NULL',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async down(queryInterface, Sequelize) {
|
||||||
|
await queryInterface.removeColumn('users', 'tenantId');
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
module.exports = {
|
||||||
|
async up(queryInterface, Sequelize) {
|
||||||
|
const [tenants] = await queryInterface.sequelize.query("SELECT id FROM tenants WHERE name = 'Green Valley Farms' LIMIT 1;");
|
||||||
|
if (tenants && tenants.length > 0) {
|
||||||
|
const tenantId = tenants[0].id;
|
||||||
|
await queryInterface.sequelize.query("UPDATE users SET \"tenantId\" = '" + tenantId + "' WHERE email = 'admin@flatlogic.com';");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async down(queryInterface, Sequelize) {
|
||||||
|
await queryInterface.sequelize.query("UPDATE users SET \"tenantId\" = NULL WHERE email = 'admin@flatlogic.com';");
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -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');
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -16,9 +16,38 @@ module.exports = function(sequelize, DataTypes) {
|
|||||||
|
|
||||||
name: {
|
name: {
|
||||||
type: DataTypes.TEXT,
|
type: DataTypes.TEXT,
|
||||||
|
},
|
||||||
|
|
||||||
|
address: {
|
||||||
|
type: DataTypes.TEXT,
|
||||||
|
},
|
||||||
|
|
||||||
|
city: {
|
||||||
|
type: DataTypes.TEXT,
|
||||||
|
},
|
||||||
|
|
||||||
|
state: {
|
||||||
|
type: DataTypes.TEXT,
|
||||||
|
},
|
||||||
|
|
||||||
|
country: {
|
||||||
|
type: DataTypes.TEXT,
|
||||||
|
},
|
||||||
|
|
||||||
|
zip: {
|
||||||
|
type: DataTypes.TEXT,
|
||||||
|
},
|
||||||
|
|
||||||
|
navOrientation: {
|
||||||
|
type: DataTypes.TEXT,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: 'top',
|
||||||
|
},
|
||||||
|
|
||||||
|
defaultView: {
|
||||||
|
type: DataTypes.TEXT,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: 'list',
|
||||||
},
|
},
|
||||||
|
|
||||||
importHash: {
|
importHash: {
|
||||||
@ -181,5 +210,3 @@ name: {
|
|||||||
|
|
||||||
return organizations;
|
return organizations;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -26,6 +26,13 @@ description: {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
insights: {
|
||||||
|
type: DataTypes.TEXT,
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
start_date: {
|
start_date: {
|
||||||
@ -175,5 +182,3 @@ status: {
|
|||||||
|
|
||||||
return projects;
|
return projects;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -13,97 +13,46 @@ module.exports = function(sequelize, DataTypes) {
|
|||||||
defaultValue: DataTypes.UUIDV4,
|
defaultValue: DataTypes.UUIDV4,
|
||||||
primaryKey: true,
|
primaryKey: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
firstName: {
|
firstName: {
|
||||||
type: DataTypes.TEXT,
|
type: DataTypes.TEXT,
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
lastName: {
|
lastName: {
|
||||||
type: DataTypes.TEXT,
|
type: DataTypes.TEXT,
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
phoneNumber: {
|
phoneNumber: {
|
||||||
type: DataTypes.TEXT,
|
type: DataTypes.TEXT,
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
email: {
|
email: {
|
||||||
type: DataTypes.TEXT,
|
type: DataTypes.TEXT,
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
disabled: {
|
disabled: {
|
||||||
type: DataTypes.BOOLEAN,
|
type: DataTypes.BOOLEAN,
|
||||||
|
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
defaultValue: false,
|
defaultValue: false,
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
password: {
|
password: {
|
||||||
type: DataTypes.TEXT,
|
type: DataTypes.TEXT,
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
emailVerified: {
|
emailVerified: {
|
||||||
type: DataTypes.BOOLEAN,
|
type: DataTypes.BOOLEAN,
|
||||||
|
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
defaultValue: false,
|
defaultValue: false,
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
emailVerificationToken: {
|
emailVerificationToken: {
|
||||||
type: DataTypes.TEXT,
|
type: DataTypes.TEXT,
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
emailVerificationTokenExpiresAt: {
|
emailVerificationTokenExpiresAt: {
|
||||||
type: DataTypes.DATE,
|
type: DataTypes.DATE,
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
passwordResetToken: {
|
passwordResetToken: {
|
||||||
type: DataTypes.TEXT,
|
type: DataTypes.TEXT,
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
passwordResetTokenExpiresAt: {
|
passwordResetTokenExpiresAt: {
|
||||||
type: DataTypes.DATE,
|
type: DataTypes.DATE,
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
provider: {
|
provider: {
|
||||||
type: DataTypes.TEXT,
|
type: DataTypes.TEXT,
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
importHash: {
|
importHash: {
|
||||||
type: DataTypes.STRING(255),
|
type: DataTypes.STRING(255),
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
@ -118,7 +67,6 @@ provider: {
|
|||||||
);
|
);
|
||||||
|
|
||||||
users.associate = (db) => {
|
users.associate = (db) => {
|
||||||
|
|
||||||
db.users.belongsToMany(db.permissions, {
|
db.users.belongsToMany(db.permissions, {
|
||||||
as: 'custom_permissions',
|
as: 'custom_permissions',
|
||||||
foreignKey: {
|
foreignKey: {
|
||||||
@ -137,22 +85,6 @@ provider: {
|
|||||||
through: 'usersCustom_permissionsPermissions',
|
through: 'usersCustom_permissionsPermissions',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
/// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
db.users.hasMany(db.form_submissions, {
|
db.users.hasMany(db.form_submissions, {
|
||||||
as: 'form_submissions_submitted_by_user',
|
as: 'form_submissions_submitted_by_user',
|
||||||
foreignKey: {
|
foreignKey: {
|
||||||
@ -161,8 +93,6 @@ provider: {
|
|||||||
constraints: false,
|
constraints: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
db.users.hasMany(db.activity_feed_items, {
|
db.users.hasMany(db.activity_feed_items, {
|
||||||
as: 'activity_feed_items_actor_user',
|
as: 'activity_feed_items_actor_user',
|
||||||
foreignKey: {
|
foreignKey: {
|
||||||
@ -171,7 +101,6 @@ provider: {
|
|||||||
constraints: false,
|
constraints: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
db.users.hasMany(db.reports, {
|
db.users.hasMany(db.reports, {
|
||||||
as: 'reports_created_by_user',
|
as: 'reports_created_by_user',
|
||||||
foreignKey: {
|
foreignKey: {
|
||||||
@ -180,12 +109,6 @@ provider: {
|
|||||||
constraints: false,
|
constraints: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//end loop
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
db.users.belongsTo(db.roles, {
|
db.users.belongsTo(db.roles, {
|
||||||
as: 'app_role',
|
as: 'app_role',
|
||||||
foreignKey: {
|
foreignKey: {
|
||||||
@ -202,7 +125,13 @@ provider: {
|
|||||||
constraints: false,
|
constraints: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
db.users.belongsTo(db.tenants, {
|
||||||
|
as: 'tenant',
|
||||||
|
foreignKey: {
|
||||||
|
name: 'tenantId',
|
||||||
|
},
|
||||||
|
constraints: false,
|
||||||
|
});
|
||||||
|
|
||||||
db.users.hasMany(db.file, {
|
db.users.hasMany(db.file, {
|
||||||
as: 'avatar',
|
as: 'avatar',
|
||||||
@ -214,7 +143,6 @@ provider: {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
db.users.belongsTo(db.users, {
|
db.users.belongsTo(db.users, {
|
||||||
as: 'createdBy',
|
as: 'createdBy',
|
||||||
});
|
});
|
||||||
@ -224,7 +152,6 @@ provider: {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
users.beforeCreate((users, options) => {
|
users.beforeCreate((users, options) => {
|
||||||
users = trimStringFields(users);
|
users = trimStringFields(users);
|
||||||
|
|
||||||
@ -232,16 +159,10 @@ provider: {
|
|||||||
users.emailVerified = true;
|
users.emailVerified = true;
|
||||||
|
|
||||||
if (!users.password) {
|
if (!users.password) {
|
||||||
const password = crypto
|
const password = crypto.randomBytes(20).toString('hex');
|
||||||
.randomBytes(20)
|
const saltRounds = (config.bcrypt && config.bcrypt.saltRounds) || 10;
|
||||||
.toString('hex');
|
const hashedPassword = bcrypt.hashSync(password, saltRounds);
|
||||||
|
users.password = hashedPassword;
|
||||||
const hashedPassword = bcrypt.hashSync(
|
|
||||||
password,
|
|
||||||
config.bcrypt.saltRounds,
|
|
||||||
);
|
|
||||||
|
|
||||||
users.password = hashedPassword
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -250,22 +171,12 @@ provider: {
|
|||||||
users = trimStringFields(users);
|
users = trimStringFields(users);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
return users;
|
return users;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
function trimStringFields(users) {
|
function trimStringFields(users) {
|
||||||
users.email = users.email.trim();
|
users.email = users.email ? users.email.trim() : '';
|
||||||
|
users.firstName = users.firstName ? users.firstName.trim() : null;
|
||||||
users.firstName = users.firstName
|
users.lastName = users.lastName ? users.lastName.trim() : null;
|
||||||
? users.firstName.trim()
|
|
||||||
: null;
|
|
||||||
|
|
||||||
users.lastName = users.lastName
|
|
||||||
? users.lastName.trim()
|
|
||||||
: null;
|
|
||||||
|
|
||||||
return users;
|
return users;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -68,6 +68,8 @@ const OrganizationsData = [
|
|||||||
|
|
||||||
|
|
||||||
"name": "Grace Hopper",
|
"name": "Grace Hopper",
|
||||||
|
"navOrientation": "top",
|
||||||
|
"defaultView": "list",
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -79,6 +81,8 @@ const OrganizationsData = [
|
|||||||
|
|
||||||
|
|
||||||
"name": "Ada Lovelace",
|
"name": "Ada Lovelace",
|
||||||
|
"navOrientation": "top",
|
||||||
|
"defaultView": "list",
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -90,6 +94,8 @@ const OrganizationsData = [
|
|||||||
|
|
||||||
|
|
||||||
"name": "Ada Lovelace",
|
"name": "Ada Lovelace",
|
||||||
|
"navOrientation": "top",
|
||||||
|
"defaultView": "list",
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -101,6 +107,8 @@ const OrganizationsData = [
|
|||||||
|
|
||||||
|
|
||||||
"name": "Ada Lovelace",
|
"name": "Ada Lovelace",
|
||||||
|
"navOrientation": "top",
|
||||||
|
"defaultView": "list",
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -3529,11 +3537,11 @@ const ReportsData = [
|
|||||||
order: [['id', 'ASC']],
|
order: [['id', 'ASC']],
|
||||||
offset: 0
|
offset: 0
|
||||||
});
|
});
|
||||||
if (User0?.setOrganization)
|
if (User0?.setOrganizations)
|
||||||
{
|
{
|
||||||
await
|
await
|
||||||
User0.
|
User0.
|
||||||
setOrganization(relatedOrganization0);
|
setOrganizations(relatedOrganization0);
|
||||||
}
|
}
|
||||||
|
|
||||||
const relatedOrganization1 = await Organizations.findOne({
|
const relatedOrganization1 = await Organizations.findOne({
|
||||||
@ -3543,11 +3551,11 @@ const ReportsData = [
|
|||||||
order: [['id', 'ASC']],
|
order: [['id', 'ASC']],
|
||||||
offset: 1
|
offset: 1
|
||||||
});
|
});
|
||||||
if (User1?.setOrganization)
|
if (User1?.setOrganizations)
|
||||||
{
|
{
|
||||||
await
|
await
|
||||||
User1.
|
User1.
|
||||||
setOrganization(relatedOrganization1);
|
setOrganizations(relatedOrganization1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const relatedOrganization2 = await Organizations.findOne({
|
const relatedOrganization2 = await Organizations.findOne({
|
||||||
@ -3557,11 +3565,11 @@ const ReportsData = [
|
|||||||
order: [['id', 'ASC']],
|
order: [['id', 'ASC']],
|
||||||
offset: 2
|
offset: 2
|
||||||
});
|
});
|
||||||
if (User2?.setOrganization)
|
if (User2?.setOrganizations)
|
||||||
{
|
{
|
||||||
await
|
await
|
||||||
User2.
|
User2.
|
||||||
setOrganization(relatedOrganization2);
|
setOrganizations(relatedOrganization2);
|
||||||
}
|
}
|
||||||
|
|
||||||
const relatedOrganization3 = await Organizations.findOne({
|
const relatedOrganization3 = await Organizations.findOne({
|
||||||
@ -3571,11 +3579,11 @@ const ReportsData = [
|
|||||||
order: [['id', 'ASC']],
|
order: [['id', 'ASC']],
|
||||||
offset: 3
|
offset: 3
|
||||||
});
|
});
|
||||||
if (User3?.setOrganization)
|
if (User3?.setOrganizations)
|
||||||
{
|
{
|
||||||
await
|
await
|
||||||
User3.
|
User3.
|
||||||
setOrganization(relatedOrganization3);
|
setOrganizations(relatedOrganization3);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -3616,11 +3624,11 @@ const ReportsData = [
|
|||||||
order: [['id', 'ASC']],
|
order: [['id', 'ASC']],
|
||||||
offset: 0
|
offset: 0
|
||||||
});
|
});
|
||||||
if (Tenant0?.setOrganization)
|
if (Tenant0?.setOrganizations)
|
||||||
{
|
{
|
||||||
await
|
await
|
||||||
Tenant0.
|
Tenant0.
|
||||||
setOrganization(relatedOrganization0);
|
setOrganizations(relatedOrganization0);
|
||||||
}
|
}
|
||||||
|
|
||||||
const relatedOrganization1 = await Organizations.findOne({
|
const relatedOrganization1 = await Organizations.findOne({
|
||||||
@ -3630,11 +3638,11 @@ const ReportsData = [
|
|||||||
order: [['id', 'ASC']],
|
order: [['id', 'ASC']],
|
||||||
offset: 1
|
offset: 1
|
||||||
});
|
});
|
||||||
if (Tenant1?.setOrganization)
|
if (Tenant1?.setOrganizations)
|
||||||
{
|
{
|
||||||
await
|
await
|
||||||
Tenant1.
|
Tenant1.
|
||||||
setOrganization(relatedOrganization1);
|
setOrganizations(relatedOrganization1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const relatedOrganization2 = await Organizations.findOne({
|
const relatedOrganization2 = await Organizations.findOne({
|
||||||
@ -3644,11 +3652,11 @@ const ReportsData = [
|
|||||||
order: [['id', 'ASC']],
|
order: [['id', 'ASC']],
|
||||||
offset: 2
|
offset: 2
|
||||||
});
|
});
|
||||||
if (Tenant2?.setOrganization)
|
if (Tenant2?.setOrganizations)
|
||||||
{
|
{
|
||||||
await
|
await
|
||||||
Tenant2.
|
Tenant2.
|
||||||
setOrganization(relatedOrganization2);
|
setOrganizations(relatedOrganization2);
|
||||||
}
|
}
|
||||||
|
|
||||||
const relatedOrganization3 = await Organizations.findOne({
|
const relatedOrganization3 = await Organizations.findOne({
|
||||||
@ -3658,11 +3666,11 @@ const ReportsData = [
|
|||||||
order: [['id', 'ASC']],
|
order: [['id', 'ASC']],
|
||||||
offset: 3
|
offset: 3
|
||||||
});
|
});
|
||||||
if (Tenant3?.setOrganization)
|
if (Tenant3?.setOrganizations)
|
||||||
{
|
{
|
||||||
await
|
await
|
||||||
Tenant3.
|
Tenant3.
|
||||||
setOrganization(relatedOrganization3);
|
setOrganizations(relatedOrganization3);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -3823,11 +3831,11 @@ const ReportsData = [
|
|||||||
order: [['id', 'ASC']],
|
order: [['id', 'ASC']],
|
||||||
offset: 0
|
offset: 0
|
||||||
});
|
});
|
||||||
if (Location0?.setOrganization)
|
if (Location0?.setOrganizations)
|
||||||
{
|
{
|
||||||
await
|
await
|
||||||
Location0.
|
Location0.
|
||||||
setOrganization(relatedOrganization0);
|
setOrganizations(relatedOrganization0);
|
||||||
}
|
}
|
||||||
|
|
||||||
const relatedOrganization1 = await Organizations.findOne({
|
const relatedOrganization1 = await Organizations.findOne({
|
||||||
@ -3837,11 +3845,11 @@ const ReportsData = [
|
|||||||
order: [['id', 'ASC']],
|
order: [['id', 'ASC']],
|
||||||
offset: 1
|
offset: 1
|
||||||
});
|
});
|
||||||
if (Location1?.setOrganization)
|
if (Location1?.setOrganizations)
|
||||||
{
|
{
|
||||||
await
|
await
|
||||||
Location1.
|
Location1.
|
||||||
setOrganization(relatedOrganization1);
|
setOrganizations(relatedOrganization1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const relatedOrganization2 = await Organizations.findOne({
|
const relatedOrganization2 = await Organizations.findOne({
|
||||||
@ -3851,11 +3859,11 @@ const ReportsData = [
|
|||||||
order: [['id', 'ASC']],
|
order: [['id', 'ASC']],
|
||||||
offset: 2
|
offset: 2
|
||||||
});
|
});
|
||||||
if (Location2?.setOrganization)
|
if (Location2?.setOrganizations)
|
||||||
{
|
{
|
||||||
await
|
await
|
||||||
Location2.
|
Location2.
|
||||||
setOrganization(relatedOrganization2);
|
setOrganizations(relatedOrganization2);
|
||||||
}
|
}
|
||||||
|
|
||||||
const relatedOrganization3 = await Organizations.findOne({
|
const relatedOrganization3 = await Organizations.findOne({
|
||||||
@ -3865,11 +3873,11 @@ const ReportsData = [
|
|||||||
order: [['id', 'ASC']],
|
order: [['id', 'ASC']],
|
||||||
offset: 3
|
offset: 3
|
||||||
});
|
});
|
||||||
if (Location3?.setOrganization)
|
if (Location3?.setOrganizations)
|
||||||
{
|
{
|
||||||
await
|
await
|
||||||
Location3.
|
Location3.
|
||||||
setOrganization(relatedOrganization3);
|
setOrganizations(relatedOrganization3);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -4030,11 +4038,11 @@ const ReportsData = [
|
|||||||
order: [['id', 'ASC']],
|
order: [['id', 'ASC']],
|
||||||
offset: 0
|
offset: 0
|
||||||
});
|
});
|
||||||
if (Project0?.setOrganization)
|
if (Project0?.setOrganizations)
|
||||||
{
|
{
|
||||||
await
|
await
|
||||||
Project0.
|
Project0.
|
||||||
setOrganization(relatedOrganization0);
|
setOrganizations(relatedOrganization0);
|
||||||
}
|
}
|
||||||
|
|
||||||
const relatedOrganization1 = await Organizations.findOne({
|
const relatedOrganization1 = await Organizations.findOne({
|
||||||
@ -4044,11 +4052,11 @@ const ReportsData = [
|
|||||||
order: [['id', 'ASC']],
|
order: [['id', 'ASC']],
|
||||||
offset: 1
|
offset: 1
|
||||||
});
|
});
|
||||||
if (Project1?.setOrganization)
|
if (Project1?.setOrganizations)
|
||||||
{
|
{
|
||||||
await
|
await
|
||||||
Project1.
|
Project1.
|
||||||
setOrganization(relatedOrganization1);
|
setOrganizations(relatedOrganization1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const relatedOrganization2 = await Organizations.findOne({
|
const relatedOrganization2 = await Organizations.findOne({
|
||||||
@ -4058,11 +4066,11 @@ const ReportsData = [
|
|||||||
order: [['id', 'ASC']],
|
order: [['id', 'ASC']],
|
||||||
offset: 2
|
offset: 2
|
||||||
});
|
});
|
||||||
if (Project2?.setOrganization)
|
if (Project2?.setOrganizations)
|
||||||
{
|
{
|
||||||
await
|
await
|
||||||
Project2.
|
Project2.
|
||||||
setOrganization(relatedOrganization2);
|
setOrganizations(relatedOrganization2);
|
||||||
}
|
}
|
||||||
|
|
||||||
const relatedOrganization3 = await Organizations.findOne({
|
const relatedOrganization3 = await Organizations.findOne({
|
||||||
@ -4072,11 +4080,11 @@ const ReportsData = [
|
|||||||
order: [['id', 'ASC']],
|
order: [['id', 'ASC']],
|
||||||
offset: 3
|
offset: 3
|
||||||
});
|
});
|
||||||
if (Project3?.setOrganization)
|
if (Project3?.setOrganizations)
|
||||||
{
|
{
|
||||||
await
|
await
|
||||||
Project3.
|
Project3.
|
||||||
setOrganization(relatedOrganization3);
|
setOrganizations(relatedOrganization3);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -4168,11 +4176,11 @@ const ReportsData = [
|
|||||||
order: [['id', 'ASC']],
|
order: [['id', 'ASC']],
|
||||||
offset: 0
|
offset: 0
|
||||||
});
|
});
|
||||||
if (TrialType0?.setOrganization)
|
if (TrialType0?.setOrganizations)
|
||||||
{
|
{
|
||||||
await
|
await
|
||||||
TrialType0.
|
TrialType0.
|
||||||
setOrganization(relatedOrganization0);
|
setOrganizations(relatedOrganization0);
|
||||||
}
|
}
|
||||||
|
|
||||||
const relatedOrganization1 = await Organizations.findOne({
|
const relatedOrganization1 = await Organizations.findOne({
|
||||||
@ -4182,11 +4190,11 @@ const ReportsData = [
|
|||||||
order: [['id', 'ASC']],
|
order: [['id', 'ASC']],
|
||||||
offset: 1
|
offset: 1
|
||||||
});
|
});
|
||||||
if (TrialType1?.setOrganization)
|
if (TrialType1?.setOrganizations)
|
||||||
{
|
{
|
||||||
await
|
await
|
||||||
TrialType1.
|
TrialType1.
|
||||||
setOrganization(relatedOrganization1);
|
setOrganizations(relatedOrganization1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const relatedOrganization2 = await Organizations.findOne({
|
const relatedOrganization2 = await Organizations.findOne({
|
||||||
@ -4196,11 +4204,11 @@ const ReportsData = [
|
|||||||
order: [['id', 'ASC']],
|
order: [['id', 'ASC']],
|
||||||
offset: 2
|
offset: 2
|
||||||
});
|
});
|
||||||
if (TrialType2?.setOrganization)
|
if (TrialType2?.setOrganizations)
|
||||||
{
|
{
|
||||||
await
|
await
|
||||||
TrialType2.
|
TrialType2.
|
||||||
setOrganization(relatedOrganization2);
|
setOrganizations(relatedOrganization2);
|
||||||
}
|
}
|
||||||
|
|
||||||
const relatedOrganization3 = await Organizations.findOne({
|
const relatedOrganization3 = await Organizations.findOne({
|
||||||
@ -4210,11 +4218,11 @@ const ReportsData = [
|
|||||||
order: [['id', 'ASC']],
|
order: [['id', 'ASC']],
|
||||||
offset: 3
|
offset: 3
|
||||||
});
|
});
|
||||||
if (TrialType3?.setOrganization)
|
if (TrialType3?.setOrganizations)
|
||||||
{
|
{
|
||||||
await
|
await
|
||||||
TrialType3.
|
TrialType3.
|
||||||
setOrganization(relatedOrganization3);
|
setOrganizations(relatedOrganization3);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -4503,11 +4511,11 @@ const ReportsData = [
|
|||||||
order: [['id', 'ASC']],
|
order: [['id', 'ASC']],
|
||||||
offset: 0
|
offset: 0
|
||||||
});
|
});
|
||||||
if (Trial0?.setOrganization)
|
if (Trial0?.setOrganizations)
|
||||||
{
|
{
|
||||||
await
|
await
|
||||||
Trial0.
|
Trial0.
|
||||||
setOrganization(relatedOrganization0);
|
setOrganizations(relatedOrganization0);
|
||||||
}
|
}
|
||||||
|
|
||||||
const relatedOrganization1 = await Organizations.findOne({
|
const relatedOrganization1 = await Organizations.findOne({
|
||||||
@ -4517,11 +4525,11 @@ const ReportsData = [
|
|||||||
order: [['id', 'ASC']],
|
order: [['id', 'ASC']],
|
||||||
offset: 1
|
offset: 1
|
||||||
});
|
});
|
||||||
if (Trial1?.setOrganization)
|
if (Trial1?.setOrganizations)
|
||||||
{
|
{
|
||||||
await
|
await
|
||||||
Trial1.
|
Trial1.
|
||||||
setOrganization(relatedOrganization1);
|
setOrganizations(relatedOrganization1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const relatedOrganization2 = await Organizations.findOne({
|
const relatedOrganization2 = await Organizations.findOne({
|
||||||
@ -4531,11 +4539,11 @@ const ReportsData = [
|
|||||||
order: [['id', 'ASC']],
|
order: [['id', 'ASC']],
|
||||||
offset: 2
|
offset: 2
|
||||||
});
|
});
|
||||||
if (Trial2?.setOrganization)
|
if (Trial2?.setOrganizations)
|
||||||
{
|
{
|
||||||
await
|
await
|
||||||
Trial2.
|
Trial2.
|
||||||
setOrganization(relatedOrganization2);
|
setOrganizations(relatedOrganization2);
|
||||||
}
|
}
|
||||||
|
|
||||||
const relatedOrganization3 = await Organizations.findOne({
|
const relatedOrganization3 = await Organizations.findOne({
|
||||||
@ -4545,11 +4553,11 @@ const ReportsData = [
|
|||||||
order: [['id', 'ASC']],
|
order: [['id', 'ASC']],
|
||||||
offset: 3
|
offset: 3
|
||||||
});
|
});
|
||||||
if (Trial3?.setOrganization)
|
if (Trial3?.setOrganizations)
|
||||||
{
|
{
|
||||||
await
|
await
|
||||||
Trial3.
|
Trial3.
|
||||||
setOrganization(relatedOrganization3);
|
setOrganizations(relatedOrganization3);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -4647,11 +4655,11 @@ const ReportsData = [
|
|||||||
order: [['id', 'ASC']],
|
order: [['id', 'ASC']],
|
||||||
offset: 0
|
offset: 0
|
||||||
});
|
});
|
||||||
if (FormTemplate0?.setOrganization)
|
if (FormTemplate0?.setOrganizations)
|
||||||
{
|
{
|
||||||
await
|
await
|
||||||
FormTemplate0.
|
FormTemplate0.
|
||||||
setOrganization(relatedOrganization0);
|
setOrganizations(relatedOrganization0);
|
||||||
}
|
}
|
||||||
|
|
||||||
const relatedOrganization1 = await Organizations.findOne({
|
const relatedOrganization1 = await Organizations.findOne({
|
||||||
@ -4661,11 +4669,11 @@ const ReportsData = [
|
|||||||
order: [['id', 'ASC']],
|
order: [['id', 'ASC']],
|
||||||
offset: 1
|
offset: 1
|
||||||
});
|
});
|
||||||
if (FormTemplate1?.setOrganization)
|
if (FormTemplate1?.setOrganizations)
|
||||||
{
|
{
|
||||||
await
|
await
|
||||||
FormTemplate1.
|
FormTemplate1.
|
||||||
setOrganization(relatedOrganization1);
|
setOrganizations(relatedOrganization1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const relatedOrganization2 = await Organizations.findOne({
|
const relatedOrganization2 = await Organizations.findOne({
|
||||||
@ -4675,11 +4683,11 @@ const ReportsData = [
|
|||||||
order: [['id', 'ASC']],
|
order: [['id', 'ASC']],
|
||||||
offset: 2
|
offset: 2
|
||||||
});
|
});
|
||||||
if (FormTemplate2?.setOrganization)
|
if (FormTemplate2?.setOrganizations)
|
||||||
{
|
{
|
||||||
await
|
await
|
||||||
FormTemplate2.
|
FormTemplate2.
|
||||||
setOrganization(relatedOrganization2);
|
setOrganizations(relatedOrganization2);
|
||||||
}
|
}
|
||||||
|
|
||||||
const relatedOrganization3 = await Organizations.findOne({
|
const relatedOrganization3 = await Organizations.findOne({
|
||||||
@ -4689,11 +4697,11 @@ const ReportsData = [
|
|||||||
order: [['id', 'ASC']],
|
order: [['id', 'ASC']],
|
||||||
offset: 3
|
offset: 3
|
||||||
});
|
});
|
||||||
if (FormTemplate3?.setOrganization)
|
if (FormTemplate3?.setOrganizations)
|
||||||
{
|
{
|
||||||
await
|
await
|
||||||
FormTemplate3.
|
FormTemplate3.
|
||||||
setOrganization(relatedOrganization3);
|
setOrganizations(relatedOrganization3);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -4856,11 +4864,11 @@ const ReportsData = [
|
|||||||
order: [['id', 'ASC']],
|
order: [['id', 'ASC']],
|
||||||
offset: 0
|
offset: 0
|
||||||
});
|
});
|
||||||
if (FormField0?.setOrganization)
|
if (FormField0?.setOrganizations)
|
||||||
{
|
{
|
||||||
await
|
await
|
||||||
FormField0.
|
FormField0.
|
||||||
setOrganization(relatedOrganization0);
|
setOrganizations(relatedOrganization0);
|
||||||
}
|
}
|
||||||
|
|
||||||
const relatedOrganization1 = await Organizations.findOne({
|
const relatedOrganization1 = await Organizations.findOne({
|
||||||
@ -4870,11 +4878,11 @@ const ReportsData = [
|
|||||||
order: [['id', 'ASC']],
|
order: [['id', 'ASC']],
|
||||||
offset: 1
|
offset: 1
|
||||||
});
|
});
|
||||||
if (FormField1?.setOrganization)
|
if (FormField1?.setOrganizations)
|
||||||
{
|
{
|
||||||
await
|
await
|
||||||
FormField1.
|
FormField1.
|
||||||
setOrganization(relatedOrganization1);
|
setOrganizations(relatedOrganization1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const relatedOrganization2 = await Organizations.findOne({
|
const relatedOrganization2 = await Organizations.findOne({
|
||||||
@ -4884,11 +4892,11 @@ const ReportsData = [
|
|||||||
order: [['id', 'ASC']],
|
order: [['id', 'ASC']],
|
||||||
offset: 2
|
offset: 2
|
||||||
});
|
});
|
||||||
if (FormField2?.setOrganization)
|
if (FormField2?.setOrganizations)
|
||||||
{
|
{
|
||||||
await
|
await
|
||||||
FormField2.
|
FormField2.
|
||||||
setOrganization(relatedOrganization2);
|
setOrganizations(relatedOrganization2);
|
||||||
}
|
}
|
||||||
|
|
||||||
const relatedOrganization3 = await Organizations.findOne({
|
const relatedOrganization3 = await Organizations.findOne({
|
||||||
@ -4898,11 +4906,11 @@ const ReportsData = [
|
|||||||
order: [['id', 'ASC']],
|
order: [['id', 'ASC']],
|
||||||
offset: 3
|
offset: 3
|
||||||
});
|
});
|
||||||
if (FormField3?.setOrganization)
|
if (FormField3?.setOrganizations)
|
||||||
{
|
{
|
||||||
await
|
await
|
||||||
FormField3.
|
FormField3.
|
||||||
setOrganization(relatedOrganization3);
|
setOrganizations(relatedOrganization3);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -5057,11 +5065,11 @@ const ReportsData = [
|
|||||||
order: [['id', 'ASC']],
|
order: [['id', 'ASC']],
|
||||||
offset: 0
|
offset: 0
|
||||||
});
|
});
|
||||||
if (FormFieldChoice0?.setOrganization)
|
if (FormFieldChoice0?.setOrganizations)
|
||||||
{
|
{
|
||||||
await
|
await
|
||||||
FormFieldChoice0.
|
FormFieldChoice0.
|
||||||
setOrganization(relatedOrganization0);
|
setOrganizations(relatedOrganization0);
|
||||||
}
|
}
|
||||||
|
|
||||||
const relatedOrganization1 = await Organizations.findOne({
|
const relatedOrganization1 = await Organizations.findOne({
|
||||||
@ -5071,11 +5079,11 @@ const ReportsData = [
|
|||||||
order: [['id', 'ASC']],
|
order: [['id', 'ASC']],
|
||||||
offset: 1
|
offset: 1
|
||||||
});
|
});
|
||||||
if (FormFieldChoice1?.setOrganization)
|
if (FormFieldChoice1?.setOrganizations)
|
||||||
{
|
{
|
||||||
await
|
await
|
||||||
FormFieldChoice1.
|
FormFieldChoice1.
|
||||||
setOrganization(relatedOrganization1);
|
setOrganizations(relatedOrganization1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const relatedOrganization2 = await Organizations.findOne({
|
const relatedOrganization2 = await Organizations.findOne({
|
||||||
@ -5085,11 +5093,11 @@ const ReportsData = [
|
|||||||
order: [['id', 'ASC']],
|
order: [['id', 'ASC']],
|
||||||
offset: 2
|
offset: 2
|
||||||
});
|
});
|
||||||
if (FormFieldChoice2?.setOrganization)
|
if (FormFieldChoice2?.setOrganizations)
|
||||||
{
|
{
|
||||||
await
|
await
|
||||||
FormFieldChoice2.
|
FormFieldChoice2.
|
||||||
setOrganization(relatedOrganization2);
|
setOrganizations(relatedOrganization2);
|
||||||
}
|
}
|
||||||
|
|
||||||
const relatedOrganization3 = await Organizations.findOne({
|
const relatedOrganization3 = await Organizations.findOne({
|
||||||
@ -5099,11 +5107,11 @@ const ReportsData = [
|
|||||||
order: [['id', 'ASC']],
|
order: [['id', 'ASC']],
|
||||||
offset: 3
|
offset: 3
|
||||||
});
|
});
|
||||||
if (FormFieldChoice3?.setOrganization)
|
if (FormFieldChoice3?.setOrganizations)
|
||||||
{
|
{
|
||||||
await
|
await
|
||||||
FormFieldChoice3.
|
FormFieldChoice3.
|
||||||
setOrganization(relatedOrganization3);
|
setOrganizations(relatedOrganization3);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -5390,11 +5398,11 @@ const ReportsData = [
|
|||||||
order: [['id', 'ASC']],
|
order: [['id', 'ASC']],
|
||||||
offset: 0
|
offset: 0
|
||||||
});
|
});
|
||||||
if (FormSubmission0?.setOrganization)
|
if (FormSubmission0?.setOrganizations)
|
||||||
{
|
{
|
||||||
await
|
await
|
||||||
FormSubmission0.
|
FormSubmission0.
|
||||||
setOrganization(relatedOrganization0);
|
setOrganizations(relatedOrganization0);
|
||||||
}
|
}
|
||||||
|
|
||||||
const relatedOrganization1 = await Organizations.findOne({
|
const relatedOrganization1 = await Organizations.findOne({
|
||||||
@ -5404,11 +5412,11 @@ const ReportsData = [
|
|||||||
order: [['id', 'ASC']],
|
order: [['id', 'ASC']],
|
||||||
offset: 1
|
offset: 1
|
||||||
});
|
});
|
||||||
if (FormSubmission1?.setOrganization)
|
if (FormSubmission1?.setOrganizations)
|
||||||
{
|
{
|
||||||
await
|
await
|
||||||
FormSubmission1.
|
FormSubmission1.
|
||||||
setOrganization(relatedOrganization1);
|
setOrganizations(relatedOrganization1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const relatedOrganization2 = await Organizations.findOne({
|
const relatedOrganization2 = await Organizations.findOne({
|
||||||
@ -5418,11 +5426,11 @@ const ReportsData = [
|
|||||||
order: [['id', 'ASC']],
|
order: [['id', 'ASC']],
|
||||||
offset: 2
|
offset: 2
|
||||||
});
|
});
|
||||||
if (FormSubmission2?.setOrganization)
|
if (FormSubmission2?.setOrganizations)
|
||||||
{
|
{
|
||||||
await
|
await
|
||||||
FormSubmission2.
|
FormSubmission2.
|
||||||
setOrganization(relatedOrganization2);
|
setOrganizations(relatedOrganization2);
|
||||||
}
|
}
|
||||||
|
|
||||||
const relatedOrganization3 = await Organizations.findOne({
|
const relatedOrganization3 = await Organizations.findOne({
|
||||||
@ -5432,11 +5440,11 @@ const ReportsData = [
|
|||||||
order: [['id', 'ASC']],
|
order: [['id', 'ASC']],
|
||||||
offset: 3
|
offset: 3
|
||||||
});
|
});
|
||||||
if (FormSubmission3?.setOrganization)
|
if (FormSubmission3?.setOrganizations)
|
||||||
{
|
{
|
||||||
await
|
await
|
||||||
FormSubmission3.
|
FormSubmission3.
|
||||||
setOrganization(relatedOrganization3);
|
setOrganizations(relatedOrganization3);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -5660,11 +5668,11 @@ const ReportsData = [
|
|||||||
order: [['id', 'ASC']],
|
order: [['id', 'ASC']],
|
||||||
offset: 0
|
offset: 0
|
||||||
});
|
});
|
||||||
if (SubmissionValue0?.setOrganization)
|
if (SubmissionValue0?.setOrganizations)
|
||||||
{
|
{
|
||||||
await
|
await
|
||||||
SubmissionValue0.
|
SubmissionValue0.
|
||||||
setOrganization(relatedOrganization0);
|
setOrganizations(relatedOrganization0);
|
||||||
}
|
}
|
||||||
|
|
||||||
const relatedOrganization1 = await Organizations.findOne({
|
const relatedOrganization1 = await Organizations.findOne({
|
||||||
@ -5674,11 +5682,11 @@ const ReportsData = [
|
|||||||
order: [['id', 'ASC']],
|
order: [['id', 'ASC']],
|
||||||
offset: 1
|
offset: 1
|
||||||
});
|
});
|
||||||
if (SubmissionValue1?.setOrganization)
|
if (SubmissionValue1?.setOrganizations)
|
||||||
{
|
{
|
||||||
await
|
await
|
||||||
SubmissionValue1.
|
SubmissionValue1.
|
||||||
setOrganization(relatedOrganization1);
|
setOrganizations(relatedOrganization1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const relatedOrganization2 = await Organizations.findOne({
|
const relatedOrganization2 = await Organizations.findOne({
|
||||||
@ -5688,11 +5696,11 @@ const ReportsData = [
|
|||||||
order: [['id', 'ASC']],
|
order: [['id', 'ASC']],
|
||||||
offset: 2
|
offset: 2
|
||||||
});
|
});
|
||||||
if (SubmissionValue2?.setOrganization)
|
if (SubmissionValue2?.setOrganizations)
|
||||||
{
|
{
|
||||||
await
|
await
|
||||||
SubmissionValue2.
|
SubmissionValue2.
|
||||||
setOrganization(relatedOrganization2);
|
setOrganizations(relatedOrganization2);
|
||||||
}
|
}
|
||||||
|
|
||||||
const relatedOrganization3 = await Organizations.findOne({
|
const relatedOrganization3 = await Organizations.findOne({
|
||||||
@ -5702,11 +5710,11 @@ const ReportsData = [
|
|||||||
order: [['id', 'ASC']],
|
order: [['id', 'ASC']],
|
||||||
offset: 3
|
offset: 3
|
||||||
});
|
});
|
||||||
if (SubmissionValue3?.setOrganization)
|
if (SubmissionValue3?.setOrganizations)
|
||||||
{
|
{
|
||||||
await
|
await
|
||||||
SubmissionValue3.
|
SubmissionValue3.
|
||||||
setOrganization(relatedOrganization3);
|
setOrganizations(relatedOrganization3);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -5865,11 +5873,11 @@ const ReportsData = [
|
|||||||
order: [['id', 'ASC']],
|
order: [['id', 'ASC']],
|
||||||
offset: 0
|
offset: 0
|
||||||
});
|
});
|
||||||
if (ActivityFeedItem0?.setOrganization)
|
if (ActivityFeedItem0?.setOrganizations)
|
||||||
{
|
{
|
||||||
await
|
await
|
||||||
ActivityFeedItem0.
|
ActivityFeedItem0.
|
||||||
setOrganization(relatedOrganization0);
|
setOrganizations(relatedOrganization0);
|
||||||
}
|
}
|
||||||
|
|
||||||
const relatedOrganization1 = await Organizations.findOne({
|
const relatedOrganization1 = await Organizations.findOne({
|
||||||
@ -5879,11 +5887,11 @@ const ReportsData = [
|
|||||||
order: [['id', 'ASC']],
|
order: [['id', 'ASC']],
|
||||||
offset: 1
|
offset: 1
|
||||||
});
|
});
|
||||||
if (ActivityFeedItem1?.setOrganization)
|
if (ActivityFeedItem1?.setOrganizations)
|
||||||
{
|
{
|
||||||
await
|
await
|
||||||
ActivityFeedItem1.
|
ActivityFeedItem1.
|
||||||
setOrganization(relatedOrganization1);
|
setOrganizations(relatedOrganization1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const relatedOrganization2 = await Organizations.findOne({
|
const relatedOrganization2 = await Organizations.findOne({
|
||||||
@ -5893,11 +5901,11 @@ const ReportsData = [
|
|||||||
order: [['id', 'ASC']],
|
order: [['id', 'ASC']],
|
||||||
offset: 2
|
offset: 2
|
||||||
});
|
});
|
||||||
if (ActivityFeedItem2?.setOrganization)
|
if (ActivityFeedItem2?.setOrganizations)
|
||||||
{
|
{
|
||||||
await
|
await
|
||||||
ActivityFeedItem2.
|
ActivityFeedItem2.
|
||||||
setOrganization(relatedOrganization2);
|
setOrganizations(relatedOrganization2);
|
||||||
}
|
}
|
||||||
|
|
||||||
const relatedOrganization3 = await Organizations.findOne({
|
const relatedOrganization3 = await Organizations.findOne({
|
||||||
@ -5907,11 +5915,11 @@ const ReportsData = [
|
|||||||
order: [['id', 'ASC']],
|
order: [['id', 'ASC']],
|
||||||
offset: 3
|
offset: 3
|
||||||
});
|
});
|
||||||
if (ActivityFeedItem3?.setOrganization)
|
if (ActivityFeedItem3?.setOrganizations)
|
||||||
{
|
{
|
||||||
await
|
await
|
||||||
ActivityFeedItem3.
|
ActivityFeedItem3.
|
||||||
setOrganization(relatedOrganization3);
|
setOrganizations(relatedOrganization3);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -6068,11 +6076,11 @@ const ReportsData = [
|
|||||||
order: [['id', 'ASC']],
|
order: [['id', 'ASC']],
|
||||||
offset: 0
|
offset: 0
|
||||||
});
|
});
|
||||||
if (Report0?.setOrganization)
|
if (Report0?.setOrganizations)
|
||||||
{
|
{
|
||||||
await
|
await
|
||||||
Report0.
|
Report0.
|
||||||
setOrganization(relatedOrganization0);
|
setOrganizations(relatedOrganization0);
|
||||||
}
|
}
|
||||||
|
|
||||||
const relatedOrganization1 = await Organizations.findOne({
|
const relatedOrganization1 = await Organizations.findOne({
|
||||||
@ -6082,11 +6090,11 @@ const ReportsData = [
|
|||||||
order: [['id', 'ASC']],
|
order: [['id', 'ASC']],
|
||||||
offset: 1
|
offset: 1
|
||||||
});
|
});
|
||||||
if (Report1?.setOrganization)
|
if (Report1?.setOrganizations)
|
||||||
{
|
{
|
||||||
await
|
await
|
||||||
Report1.
|
Report1.
|
||||||
setOrganization(relatedOrganization1);
|
setOrganizations(relatedOrganization1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const relatedOrganization2 = await Organizations.findOne({
|
const relatedOrganization2 = await Organizations.findOne({
|
||||||
@ -6096,11 +6104,11 @@ const ReportsData = [
|
|||||||
order: [['id', 'ASC']],
|
order: [['id', 'ASC']],
|
||||||
offset: 2
|
offset: 2
|
||||||
});
|
});
|
||||||
if (Report2?.setOrganization)
|
if (Report2?.setOrganizations)
|
||||||
{
|
{
|
||||||
await
|
await
|
||||||
Report2.
|
Report2.
|
||||||
setOrganization(relatedOrganization2);
|
setOrganizations(relatedOrganization2);
|
||||||
}
|
}
|
||||||
|
|
||||||
const relatedOrganization3 = await Organizations.findOne({
|
const relatedOrganization3 = await Organizations.findOne({
|
||||||
@ -6110,11 +6118,11 @@ const ReportsData = [
|
|||||||
order: [['id', 'ASC']],
|
order: [['id', 'ASC']],
|
||||||
offset: 3
|
offset: 3
|
||||||
});
|
});
|
||||||
if (Report3?.setOrganization)
|
if (Report3?.setOrganizations)
|
||||||
{
|
{
|
||||||
await
|
await
|
||||||
Report3.
|
Report3.
|
||||||
setOrganization(relatedOrganization3);
|
setOrganizations(relatedOrganization3);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
const ValidationError = require('../services/notifications/errors/validation');
|
const ValidationError = require('../services/notifications/errors/validation');
|
||||||
const RolesDBApi = require('../db/api/roles');
|
const RolesDBApi = require('../db/api/roles');
|
||||||
|
|
||||||
@ -49,6 +48,11 @@ function checkPermissions(permission) {
|
|||||||
|
|
||||||
// 2. Check Custom Permissions (only if the user is authenticated)
|
// 2. Check Custom Permissions (only if the user is authenticated)
|
||||||
if (currentUser) {
|
if (currentUser) {
|
||||||
|
// Check for Super Admin (globalAccess)
|
||||||
|
if (currentUser.app_role && currentUser.app_role.globalAccess) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
// Ensure custom_permissions is an array before using find
|
// Ensure custom_permissions is an array before using find
|
||||||
const customPermissions = Array.isArray(currentUser.custom_permissions)
|
const customPermissions = Array.isArray(currentUser.custom_permissions)
|
||||||
? currentUser.custom_permissions
|
? currentUser.custom_permissions
|
||||||
@ -91,10 +95,12 @@ function checkPermissions(permission) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 4. Check Permissions on the "effective" role
|
// 4. Check Permissions on the "effective" role
|
||||||
// Assume the effectiveRole object (from app_role or RolesDBApi) has a getPermissions() method
|
|
||||||
// or a 'permissions' property (if permissions are eagerly loaded).
|
|
||||||
let rolePermissions = [];
|
let rolePermissions = [];
|
||||||
if (typeof effectiveRole.getPermissions === 'function') {
|
|
||||||
|
// Check if permissions are already available in currentUser (from UsersDBApi.findBy)
|
||||||
|
if (currentUser && currentUser.app_role_permissions) {
|
||||||
|
rolePermissions = currentUser.app_role_permissions;
|
||||||
|
} else if (typeof effectiveRole.getPermissions === 'function') {
|
||||||
rolePermissions = await effectiveRole.getPermissions(); // Get permissions asynchronously if the method exists
|
rolePermissions = await effectiveRole.getPermissions(); // Get permissions asynchronously if the method exists
|
||||||
} else if (Array.isArray(effectiveRole.permissions)) {
|
} else if (Array.isArray(effectiveRole.permissions)) {
|
||||||
rolePermissions = effectiveRole.permissions; // Or take from property if permissions are pre-loaded
|
rolePermissions = effectiveRole.permissions; // Or take from property if permissions are pre-loaded
|
||||||
@ -146,4 +152,3 @@ module.exports = {
|
|||||||
checkPermissions,
|
checkPermissions,
|
||||||
checkCrudPermissions,
|
checkCrudPermissions,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -153,6 +153,18 @@ router.post('/signup', wrapAsync(async (req, res) => {
|
|||||||
res.status(200).send(payload);
|
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) => {
|
router.put('/profile', passport.authenticate('jwt', {session: false}), wrapAsync(async (req, res) => {
|
||||||
if (!req.currentUser || !req.currentUser.id) {
|
if (!req.currentUser || !req.currentUser.id) {
|
||||||
throw new ForbiddenError();
|
throw new ForbiddenError();
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
const db = require('../db/models');
|
||||||
const UsersDBApi = require('../db/api/users');
|
const UsersDBApi = require('../db/api/users');
|
||||||
const ValidationError = require('./notifications/errors/validation');
|
const ValidationError = require('./notifications/errors/validation');
|
||||||
const ForbiddenError = require('./notifications/errors/forbidden');
|
const ForbiddenError = require('./notifications/errors/forbidden');
|
||||||
@ -284,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) {
|
static async updateProfile(data, currentUser) {
|
||||||
let transaction = await db.sequelize.transaction();
|
let transaction = await db.sequelize.transaction();
|
||||||
|
|
||||||
|
|||||||
@ -1,10 +1,9 @@
|
|||||||
const db = require('../db/models');
|
const db = require('../db/models');
|
||||||
const Form_submissionsDBApi = require('../db/api/form_submissions');
|
const Form_submissionsDBApi = require('../db/api/form_submissions');
|
||||||
|
const Activity_feed_itemsDBApi = require('../db/api/activity_feed_items');
|
||||||
const processFile = require("../middlewares/upload");
|
const processFile = require("../middlewares/upload");
|
||||||
const ValidationError = require('./notifications/errors/validation');
|
const ValidationError = require('./notifications/errors/validation');
|
||||||
const csv = require('csv-parser');
|
const csv = require('csv-parser');
|
||||||
const axios = require('axios');
|
|
||||||
const config = require('../config');
|
|
||||||
const stream = require('stream');
|
const stream = require('stream');
|
||||||
|
|
||||||
|
|
||||||
@ -15,7 +14,7 @@ module.exports = class Form_submissionsService {
|
|||||||
static async create(data, currentUser) {
|
static async create(data, currentUser) {
|
||||||
const transaction = await db.sequelize.transaction();
|
const transaction = await db.sequelize.transaction();
|
||||||
try {
|
try {
|
||||||
await Form_submissionsDBApi.create(
|
const submission = await Form_submissionsDBApi.create(
|
||||||
data,
|
data,
|
||||||
{
|
{
|
||||||
currentUser,
|
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();
|
await transaction.commit();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await transaction.rollback();
|
await transaction.rollback();
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
static async bulkImport(req, res, sendInvitationEmails = true, host) {
|
static async bulkImport(req, res) {
|
||||||
const transaction = await db.sequelize.transaction();
|
const transaction = await db.sequelize.transaction();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -51,13 +67,32 @@ module.exports = class Form_submissionsService {
|
|||||||
.on('error', (error) => reject(error));
|
.on('error', (error) => reject(error));
|
||||||
})
|
})
|
||||||
|
|
||||||
await Form_submissionsDBApi.bulkImport(results, {
|
const submissions = await Form_submissionsDBApi.bulkImport(results, {
|
||||||
transaction,
|
transaction,
|
||||||
ignoreDuplicates: true,
|
ignoreDuplicates: true,
|
||||||
validate: true,
|
validate: true,
|
||||||
currentUser: req.currentUser
|
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();
|
await transaction.commit();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await transaction.rollback();
|
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();
|
await transaction.commit();
|
||||||
return updatedForm_submissions;
|
return updatedForm_submissions;
|
||||||
|
|
||||||
@ -95,7 +147,7 @@ module.exports = class Form_submissionsService {
|
|||||||
await transaction.rollback();
|
await transaction.rollback();
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
static async deleteByIds(ids, currentUser) {
|
static async deleteByIds(ids, currentUser) {
|
||||||
const transaction = await db.sequelize.transaction();
|
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 db = require('../db/models');
|
||||||
const ProjectsDBApi = require('../db/api/projects');
|
const ProjectsDBApi = require('../db/api/projects');
|
||||||
|
const Activity_feed_itemsDBApi = require('../db/api/activity_feed_items');
|
||||||
const processFile = require("../middlewares/upload");
|
const processFile = require("../middlewares/upload");
|
||||||
const ValidationError = require('./notifications/errors/validation');
|
const ValidationError = require('./notifications/errors/validation');
|
||||||
const csv = require('csv-parser');
|
const csv = require('csv-parser');
|
||||||
const axios = require('axios');
|
|
||||||
const config = require('../config');
|
|
||||||
const stream = require('stream');
|
const stream = require('stream');
|
||||||
|
|
||||||
|
|
||||||
@ -15,7 +14,7 @@ module.exports = class ProjectsService {
|
|||||||
static async create(data, currentUser) {
|
static async create(data, currentUser) {
|
||||||
const transaction = await db.sequelize.transaction();
|
const transaction = await db.sequelize.transaction();
|
||||||
try {
|
try {
|
||||||
await ProjectsDBApi.create(
|
const project = await ProjectsDBApi.create(
|
||||||
data,
|
data,
|
||||||
{
|
{
|
||||||
currentUser,
|
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();
|
await transaction.commit();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await transaction.rollback();
|
await transaction.rollback();
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
static async bulkImport(req, res, sendInvitationEmails = true, host) {
|
static async bulkImport(req, res) {
|
||||||
const transaction = await db.sequelize.transaction();
|
const transaction = await db.sequelize.transaction();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -51,13 +67,32 @@ module.exports = class ProjectsService {
|
|||||||
.on('error', (error) => reject(error));
|
.on('error', (error) => reject(error));
|
||||||
})
|
})
|
||||||
|
|
||||||
await ProjectsDBApi.bulkImport(results, {
|
const projects = await ProjectsDBApi.bulkImport(results, {
|
||||||
transaction,
|
transaction,
|
||||||
ignoreDuplicates: true,
|
ignoreDuplicates: true,
|
||||||
validate: true,
|
validate: true,
|
||||||
currentUser: req.currentUser
|
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();
|
await transaction.commit();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await transaction.rollback();
|
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();
|
await transaction.commit();
|
||||||
return updatedProjects;
|
return updatedProjects;
|
||||||
|
|
||||||
@ -95,7 +147,7 @@ module.exports = class ProjectsService {
|
|||||||
await transaction.rollback();
|
await transaction.rollback();
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
static async deleteByIds(ids, currentUser) {
|
static async deleteByIds(ids, currentUser) {
|
||||||
const transaction = await db.sequelize.transaction();
|
const transaction = await db.sequelize.transaction();
|
||||||
@ -134,5 +186,3 @@ module.exports = class ProjectsService {
|
|||||||
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,10 +1,9 @@
|
|||||||
const db = require('../db/models');
|
const db = require('../db/models');
|
||||||
const TrialsDBApi = require('../db/api/trials');
|
const TrialsDBApi = require('../db/api/trials');
|
||||||
|
const Activity_feed_itemsDBApi = require('../db/api/activity_feed_items');
|
||||||
const processFile = require("../middlewares/upload");
|
const processFile = require("../middlewares/upload");
|
||||||
const ValidationError = require('./notifications/errors/validation');
|
const ValidationError = require('./notifications/errors/validation');
|
||||||
const csv = require('csv-parser');
|
const csv = require('csv-parser');
|
||||||
const axios = require('axios');
|
|
||||||
const config = require('../config');
|
|
||||||
const stream = require('stream');
|
const stream = require('stream');
|
||||||
|
|
||||||
|
|
||||||
@ -15,7 +14,7 @@ module.exports = class TrialsService {
|
|||||||
static async create(data, currentUser) {
|
static async create(data, currentUser) {
|
||||||
const transaction = await db.sequelize.transaction();
|
const transaction = await db.sequelize.transaction();
|
||||||
try {
|
try {
|
||||||
await TrialsDBApi.create(
|
const trial = await TrialsDBApi.create(
|
||||||
data,
|
data,
|
||||||
{
|
{
|
||||||
currentUser,
|
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();
|
await transaction.commit();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await transaction.rollback();
|
await transaction.rollback();
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
static async bulkImport(req, res, sendInvitationEmails = true, host) {
|
static async bulkImport(req, res) {
|
||||||
const transaction = await db.sequelize.transaction();
|
const transaction = await db.sequelize.transaction();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -51,13 +67,32 @@ module.exports = class TrialsService {
|
|||||||
.on('error', (error) => reject(error));
|
.on('error', (error) => reject(error));
|
||||||
})
|
})
|
||||||
|
|
||||||
await TrialsDBApi.bulkImport(results, {
|
const trials = await TrialsDBApi.bulkImport(results, {
|
||||||
transaction,
|
transaction,
|
||||||
ignoreDuplicates: true,
|
ignoreDuplicates: true,
|
||||||
validate: true,
|
validate: true,
|
||||||
currentUser: req.currentUser
|
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();
|
await transaction.commit();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await transaction.rollback();
|
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();
|
await transaction.commit();
|
||||||
return updatedTrials;
|
return updatedTrials;
|
||||||
|
|
||||||
@ -95,7 +147,7 @@ module.exports = class TrialsService {
|
|||||||
await transaction.rollback();
|
await transaction.rollback();
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
static async deleteByIds(ids, currentUser) {
|
static async deleteByIds(ids, currentUser) {
|
||||||
const transaction = await db.sequelize.transaction();
|
const transaction = await db.sequelize.transaction();
|
||||||
@ -134,5 +186,3 @@ module.exports = class TrialsService {
|
|||||||
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,15 +1,13 @@
|
|||||||
const db = require('../db/models');
|
const db = require('../db/models');
|
||||||
const UsersDBApi = require('../db/api/users');
|
const UsersDBApi = require('../db/api/users');
|
||||||
|
const Activity_feed_itemsDBApi = require('../db/api/activity_feed_items');
|
||||||
const processFile = require("../middlewares/upload");
|
const processFile = require("../middlewares/upload");
|
||||||
const ValidationError = require('./notifications/errors/validation');
|
const ValidationError = require('./notifications/errors/validation');
|
||||||
const csv = require('csv-parser');
|
const csv = require('csv-parser');
|
||||||
const axios = require('axios');
|
|
||||||
const config = require('../config');
|
|
||||||
const stream = require('stream');
|
const stream = require('stream');
|
||||||
|
const config = require('../config');
|
||||||
|
|
||||||
|
|
||||||
const InvitationEmail = require('./email/list/invitation');
|
|
||||||
const EmailSender = require('./email');
|
|
||||||
const AuthService = require('./auth');
|
const AuthService = require('./auth');
|
||||||
|
|
||||||
module.exports = class UsersService {
|
module.exports = class UsersService {
|
||||||
@ -28,7 +26,7 @@ module.exports = class UsersService {
|
|||||||
'iam.errors.userAlreadyExists',
|
'iam.errors.userAlreadyExists',
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
await UsersDBApi.create(
|
const newUser = await UsersDBApi.create(
|
||||||
{data},
|
{data},
|
||||||
|
|
||||||
globalAccess,
|
globalAccess,
|
||||||
@ -39,6 +37,23 @@ module.exports = class UsersService {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
emailsToInvite.push(email);
|
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 {
|
} else {
|
||||||
throw new ValidationError('iam.errors.emailRequired')
|
throw new ValidationError('iam.errors.emailRequired')
|
||||||
@ -83,13 +98,32 @@ module.exports = class UsersService {
|
|||||||
throw new ValidationError('importer.errors.userEmailMissing');
|
throw new ValidationError('importer.errors.userEmailMissing');
|
||||||
}
|
}
|
||||||
|
|
||||||
await UsersDBApi.bulkImport(results, {
|
const users = await UsersDBApi.bulkImport(results, {
|
||||||
transaction,
|
transaction,
|
||||||
ignoreDuplicates: true,
|
ignoreDuplicates: true,
|
||||||
validate: true,
|
validate: true,
|
||||||
currentUser: req.currentUser
|
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);
|
emailsToInvite = results.map((result) => result.email);
|
||||||
|
|
||||||
await transaction.commit();
|
await transaction.commit();
|
||||||
@ -142,7 +176,7 @@ module.exports = class UsersService {
|
|||||||
await transaction.rollback();
|
await transaction.rollback();
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
static async remove(id, currentUser) {
|
static async remove(id, currentUser) {
|
||||||
const transaction = await db.sequelize.transaction();
|
const transaction = await db.sequelize.transaction();
|
||||||
@ -175,5 +209,3 @@ module.exports = class UsersService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -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 { createPortal } from 'react-dom';
|
||||||
import { ToastContainer, toast } from 'react-toastify';
|
import { ToastContainer, toast } from 'react-toastify';
|
||||||
import BaseButton from '../BaseButton'
|
import BaseButton from '../BaseButton'
|
||||||
@ -42,6 +42,8 @@ const TableSampleActivity_feed_items = ({ filterItems, setFilterItems, filters,
|
|||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
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 { activity_feed_items, loading, count, notify: activity_feed_itemsNotify, refetch } = useAppSelector((state) => state.activity_feed_items)
|
||||||
const { currentUser } = useAppSelector((state) => state.auth);
|
const { currentUser } = useAppSelector((state) => state.auth);
|
||||||
const focusRing = useAppSelector((state) => state.style.focusRingColor);
|
const focusRing = useAppSelector((state) => state.style.focusRingColor);
|
||||||
@ -52,14 +54,14 @@ const TableSampleActivity_feed_items = ({ filterItems, setFilterItems, filters,
|
|||||||
pagesList.push(i);
|
pagesList.push(i);
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadData = async (page = currentPage, request = filterRequest) => {
|
const loadData = useCallback(async (page = currentPage, request = filterRequest) => {
|
||||||
if (page !== currentPage) setCurrentPage(page);
|
if (page !== currentPage) setCurrentPage(page);
|
||||||
if (request !== filterRequest) setFilterRequest(request);
|
if (request !== filterRequest) setFilterRequest(request);
|
||||||
const { sort, field } = sortModel[0];
|
const { sort, field } = sortModel[0];
|
||||||
|
|
||||||
const query = `?page=${page}&limit=${perPage}${request}&sort=${sort}&field=${field}`;
|
const query = `?page=${page}&limit=${perPage}${request}&sort=${sort}&field=${field}`;
|
||||||
dispatch(fetch({ limit: perPage, page, query }));
|
dispatch(fetch({ limit: perPage, page, query }));
|
||||||
};
|
}, [currentPage, filterRequest, sortModel, dispatch]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (activity_feed_itemsNotify.showNotification) {
|
if (activity_feed_itemsNotify.showNotification) {
|
||||||
@ -70,14 +72,14 @@ const TableSampleActivity_feed_items = ({ filterItems, setFilterItems, filters,
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!currentUser) return;
|
if (!currentUser) return;
|
||||||
loadData();
|
loadData();
|
||||||
}, [sortModel, currentUser]);
|
}, [memoizedSortModel, currentUser, loadData]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (refetch) {
|
if (refetch) {
|
||||||
loadData(0);
|
loadData(0);
|
||||||
dispatch(setRefetch(false));
|
dispatch(setRefetch(false));
|
||||||
}
|
}
|
||||||
}, [refetch, dispatch]);
|
}, [refetch, dispatch, loadData]);
|
||||||
|
|
||||||
const [isModalInfoActive, setIsModalInfoActive] = useState(false)
|
const [isModalInfoActive, setIsModalInfoActive] = useState(false)
|
||||||
const [isModalTrashActive, setIsModalTrashActive] = useState(false)
|
const [isModalTrashActive, setIsModalTrashActive] = useState(false)
|
||||||
|
|||||||
@ -3,10 +3,8 @@ import { mdiLogout, mdiClose } from '@mdi/js'
|
|||||||
import BaseIcon from './BaseIcon'
|
import BaseIcon from './BaseIcon'
|
||||||
import AsideMenuList from './AsideMenuList'
|
import AsideMenuList from './AsideMenuList'
|
||||||
import { MenuAsideItem } from '../interfaces'
|
import { MenuAsideItem } from '../interfaces'
|
||||||
import { useAppSelector } from '../stores/hooks'
|
import { useAppDispatch, useAppSelector } from '../stores/hooks'
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
import { useAppDispatch } from '../stores/hooks';
|
|
||||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
|
|||||||
@ -38,31 +38,9 @@ export const loadColumns = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const hasUpdatePermission = hasPermission(user, 'UPDATE_FORM_SUBMISSIONS')
|
const hasUpdatePermission = hasPermission(user, 'UPDATE_FORM_SUBMISSIONS')
|
||||||
|
const hasTenant = !!(user?.tenant?.id || user?.tenantId);
|
||||||
|
|
||||||
return [
|
const columns: any[] = [
|
||||||
|
|
||||||
{
|
|
||||||
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,
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
{
|
||||||
field: 'form_template',
|
field: 'form_template',
|
||||||
headerName: 'FormTemplate',
|
headerName: 'FormTemplate',
|
||||||
@ -86,22 +64,46 @@ export const loadColumns = async (
|
|||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
field: 'trials',
|
field: 'project',
|
||||||
headerName: 'Trials',
|
headerName: 'Project',
|
||||||
flex: 1,
|
flex: 1,
|
||||||
minWidth: 120,
|
minWidth: 120,
|
||||||
filterable: false,
|
filterable: false,
|
||||||
headerClassName: 'datagrid--header',
|
headerClassName: 'datagrid--header',
|
||||||
cellClassName: 'datagrid--cell',
|
cellClassName: 'datagrid--cell',
|
||||||
|
|
||||||
editable: false,
|
|
||||||
|
editable: hasUpdatePermission,
|
||||||
|
|
||||||
sortable: false,
|
sortable: false,
|
||||||
type: 'singleSelect',
|
type: 'singleSelect',
|
||||||
valueFormatter: ({ value }) =>
|
getOptionValue: (value: any) => value?.id,
|
||||||
dataFormatter.trialsManyListFormatter(value).join(', '),
|
getOptionLabel: (value: any) => value?.label,
|
||||||
renderEditCell: (params) => (
|
valueOptions: await callOptionsApi('projects'),
|
||||||
<DataGridMultiSelect {...params} entityName={'trials'}/>
|
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',
|
field: 'submitted_at',
|
||||||
headerName: 'SubmittedAt',
|
headerName: 'SubmittedAt',
|
||||||
@ -168,8 +148,8 @@ export const loadColumns = async (
|
|||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
field: 'status',
|
field: 'organizations',
|
||||||
headerName: 'Status',
|
headerName: 'organizations',
|
||||||
flex: 1,
|
flex: 1,
|
||||||
minWidth: 120,
|
minWidth: 120,
|
||||||
filterable: false,
|
filterable: false,
|
||||||
@ -179,68 +159,13 @@ export const loadColumns = async (
|
|||||||
|
|
||||||
editable: hasUpdatePermission,
|
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,
|
sortable: false,
|
||||||
renderCell: (params: GridValueGetterParams) => (
|
type: 'singleSelect',
|
||||||
<>
|
getOptionValue: (value: any) => value?.id,
|
||||||
{dataFormatter.filesFormatter(params.row.attachments).map(link => (
|
getOptionLabel: (value: any) => value?.label,
|
||||||
<button
|
valueOptions: await callOptionsApi('organizations'),
|
||||||
key={link.publicUrl}
|
valueGetter: (params: GridValueGetterParams) =>
|
||||||
onClick={(e) => saveFile(e, link.publicUrl, link.name)}
|
params?.value?.id ?? params?.value,
|
||||||
>
|
|
||||||
{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'
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -6,14 +6,17 @@ import NavBarItemPlain from './NavBarItemPlain'
|
|||||||
import NavBarMenuList from './NavBarMenuList'
|
import NavBarMenuList from './NavBarMenuList'
|
||||||
import { MenuNavBarItem } from '../interfaces'
|
import { MenuNavBarItem } from '../interfaces'
|
||||||
import { useAppSelector } from '../stores/hooks';
|
import { useAppSelector } from '../stores/hooks';
|
||||||
|
import NavBarItem from './NavBarItem'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
menu: MenuNavBarItem[]
|
menu: MenuNavBarItem[]
|
||||||
|
leftMenu?: MenuNavBarItem[]
|
||||||
className: string
|
className: string
|
||||||
children: ReactNode
|
children: ReactNode
|
||||||
|
isTopNav?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function NavBar({ menu, className = '', children }: Props) {
|
export default function NavBar({ menu, leftMenu = [], className = '', children, isTopNav = false }: Props) {
|
||||||
const [isMenuNavBarActive, setIsMenuNavBarActive] = useState(false)
|
const [isMenuNavBarActive, setIsMenuNavBarActive] = useState(false)
|
||||||
const [isScrolled, setIsScrolled] = useState(false);
|
const [isScrolled, setIsScrolled] = useState(false);
|
||||||
const bgColor = useAppSelector((state) => state.style.bgLayoutColor);
|
const bgColor = useAppSelector((state) => state.style.bgLayoutColor);
|
||||||
@ -35,21 +38,48 @@ export default function NavBar({ menu, className = '', children }: Props) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<nav
|
<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-col ${containerMaxW} ${isScrolled && `border-b border-pavitra-400 dark:border-dark-700`}`}>
|
||||||
<div className="flex flex-1 items-stretch h-14">{children}</div>
|
{/* 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">
|
<div className="flex-none items-stretch flex h-14 lg:hidden">
|
||||||
<NavBarItemPlain onClick={handleMenuNavBarToggleClick}>
|
<NavBarItemPlain onClick={handleMenuNavBarToggleClick}>
|
||||||
<BaseIcon path={isMenuNavBarActive ? mdiClose : mdiDotsVertical} size="24" />
|
<BaseIcon path={isMenuNavBarActive ? mdiClose : mdiDotsVertical} size="24" />
|
||||||
</NavBarItemPlain>
|
</NavBarItemPlain>
|
||||||
</div>
|
</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
|
<div
|
||||||
className={`${
|
className={`${
|
||||||
isMenuNavBarActive ? 'block' : 'hidden'
|
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`}
|
||||||
>
|
>
|
||||||
<NavBarMenuList menu={menu} />
|
<div className="w-full">
|
||||||
|
<NavBarMenuList menu={[...leftMenu, ...menu]} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import React, {useEffect, useRef} from 'react'
|
import React, {useEffect, useRef, useState} from 'react'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { useState } from 'react'
|
|
||||||
import { mdiChevronUp, mdiChevronDown } from '@mdi/js'
|
import { mdiChevronUp, mdiChevronDown } from '@mdi/js'
|
||||||
import BaseDivider from './BaseDivider'
|
import BaseDivider from './BaseDivider'
|
||||||
import BaseIcon from './BaseIcon'
|
import BaseIcon from './BaseIcon'
|
||||||
|
|||||||
@ -1,9 +1,11 @@
|
|||||||
|
import Link from 'next/link';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import BaseIcon from '../BaseIcon';
|
import BaseIcon from '../BaseIcon';
|
||||||
import { mdiEye, mdiTrashCan, mdiPencilOutline } from '@mdi/js';
|
import { mdiEye, mdiTrashCan, mdiPencilOutline } from '@mdi/js';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import {
|
import {
|
||||||
GridActionsCellItem,
|
GridActionsCellItem,
|
||||||
|
GridRenderCellParams,
|
||||||
GridRowParams,
|
GridRowParams,
|
||||||
GridValueGetterParams,
|
GridValueGetterParams,
|
||||||
} from '@mui/x-data-grid';
|
} from '@mui/x-data-grid';
|
||||||
@ -38,31 +40,9 @@ export const loadColumns = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const hasUpdatePermission = hasPermission(user, 'UPDATE_PROJECTS')
|
const hasUpdatePermission = hasPermission(user, 'UPDATE_PROJECTS')
|
||||||
|
const hasTenant = !!(user?.tenant?.id || user?.tenantId);
|
||||||
|
|
||||||
return [
|
const columns: any[] = [
|
||||||
|
|
||||||
{
|
|
||||||
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,
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
{
|
||||||
field: 'name',
|
field: 'name',
|
||||||
headerName: 'Name',
|
headerName: 'Name',
|
||||||
@ -74,7 +54,14 @@ export const loadColumns = async (
|
|||||||
|
|
||||||
|
|
||||||
editable: hasUpdatePermission,
|
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',
|
type: 'dateTime',
|
||||||
valueGetter: (params: GridValueGetterParams) =>
|
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',
|
type: 'dateTime',
|
||||||
valueGetter: (params: GridValueGetterParams) =>
|
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,
|
editable: false,
|
||||||
sortable: false,
|
sortable: false,
|
||||||
renderCell: (params: GridValueGetterParams) => (
|
renderCell: (params: GridRenderCellParams) => (
|
||||||
<>
|
<>
|
||||||
{dataFormatter.filesFormatter(params.row.attachments).map(link => (
|
{dataFormatter.filesFormatter(params.row?.attachments).map(link => (
|
||||||
<button
|
<button
|
||||||
key={link.publicUrl}
|
key={link.publicUrl}
|
||||||
onClick={(e) => saveFile(e, link.publicUrl, link.name)}
|
onClick={(e) => saveFile(e, link.publicUrl, link.name)}
|
||||||
@ -203,7 +190,7 @@ export const loadColumns = async (
|
|||||||
|
|
||||||
editable: false,
|
editable: false,
|
||||||
sortable: false,
|
sortable: false,
|
||||||
renderCell: (params: GridValueGetterParams) => (
|
renderCell: (params: GridRenderCellParams) => (
|
||||||
<ImageField
|
<ImageField
|
||||||
name={'Avatar'}
|
name={'Avatar'}
|
||||||
image={params?.row?.images}
|
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 { createPortal } from 'react-dom';
|
||||||
import { ToastContainer, toast } from 'react-toastify';
|
import { ToastContainer, toast } from 'react-toastify';
|
||||||
import BaseButton from '../BaseButton'
|
import BaseButton from '../BaseButton'
|
||||||
import CardBoxModal from '../CardBoxModal'
|
import CardBoxModal from '../CardBoxModal'
|
||||||
import CardBox from "../CardBox";
|
import CardBox from "../CardBox";
|
||||||
import { fetch, update, deleteItem, setRefetch, deleteItemsByIds } from '../../stores/tenants/tenantsSlice'
|
import { fetch, update, deleteItem, setRefetch, deleteItemsByIds } from '../../stores/tenants/tenantsSlice'
|
||||||
|
import { impersonate } from '../../stores/authSlice'
|
||||||
import { useAppDispatch, useAppSelector } from '../../stores/hooks'
|
import { useAppDispatch, useAppSelector } from '../../stores/hooks'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import { Field, Form, Formik } from "formik";
|
import { Field, Form, Formik } from "formik";
|
||||||
import {
|
import {
|
||||||
DataGrid,
|
DataGrid,
|
||||||
GridColDef,
|
GridColDef,
|
||||||
|
GridSortModel,
|
||||||
} from '@mui/x-data-grid';
|
} from '@mui/x-data-grid';
|
||||||
import {loadColumns} from "./configureTenantsCols";
|
import {loadColumns} from "./configureTenantsCols";
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
@ -33,13 +35,15 @@ const TableSampleTenants = ({ filterItems, setFilterItems, filters, showGrid })
|
|||||||
const [filterRequest, setFilterRequest] = React.useState('');
|
const [filterRequest, setFilterRequest] = React.useState('');
|
||||||
const [columns, setColumns] = useState<GridColDef[]>([]);
|
const [columns, setColumns] = useState<GridColDef[]>([]);
|
||||||
const [selectedRows, setSelectedRows] = useState([]);
|
const [selectedRows, setSelectedRows] = useState([]);
|
||||||
const [sortModel, setSortModel] = useState([
|
const [sortModel, setSortModel] = useState<GridSortModel>([
|
||||||
{
|
{
|
||||||
field: '',
|
field: '',
|
||||||
sort: 'desc',
|
sort: 'desc',
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const memoizedSortModel = useMemo(() => JSON.stringify(sortModel), [sortModel]);
|
||||||
|
|
||||||
const { tenants, loading, count, notify: tenantsNotify, refetch } = useAppSelector((state) => state.tenants)
|
const { tenants, loading, count, notify: tenantsNotify, refetch } = useAppSelector((state) => state.tenants)
|
||||||
const { currentUser } = useAppSelector((state) => state.auth);
|
const { currentUser } = useAppSelector((state) => state.auth);
|
||||||
const focusRing = useAppSelector((state) => state.style.focusRingColor);
|
const focusRing = useAppSelector((state) => state.style.focusRingColor);
|
||||||
@ -50,14 +54,15 @@ const TableSampleTenants = ({ filterItems, setFilterItems, filters, showGrid })
|
|||||||
pagesList.push(i);
|
pagesList.push(i);
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadData = async (page = currentPage, request = filterRequest) => {
|
const loadData = useCallback(async (page = currentPage, request = filterRequest) => {
|
||||||
if (page !== currentPage) setCurrentPage(page);
|
if (page !== currentPage) setCurrentPage(page);
|
||||||
if (request !== filterRequest) setFilterRequest(request);
|
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(fetch({ limit: perPage, page, query }));
|
||||||
};
|
}, [dispatch, currentPage, filterRequest, sortModel]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (tenantsNotify.showNotification) {
|
if (tenantsNotify.showNotification) {
|
||||||
@ -68,14 +73,14 @@ const TableSampleTenants = ({ filterItems, setFilterItems, filters, showGrid })
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!currentUser) return;
|
if (!currentUser) return;
|
||||||
loadData();
|
loadData();
|
||||||
}, [sortModel, currentUser]);
|
}, [memoizedSortModel, currentUser, loadData]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (refetch) {
|
if (refetch) {
|
||||||
loadData(0);
|
loadData(0);
|
||||||
dispatch(setRefetch(false));
|
dispatch(setRefetch(false));
|
||||||
}
|
}
|
||||||
}, [refetch, dispatch]);
|
}, [refetch, dispatch, loadData]);
|
||||||
|
|
||||||
const [isModalInfoActive, setIsModalInfoActive] = useState(false)
|
const [isModalInfoActive, setIsModalInfoActive] = useState(false)
|
||||||
const [isModalTrashActive, setIsModalTrashActive] = useState(false)
|
const [isModalTrashActive, setIsModalTrashActive] = useState(false)
|
||||||
@ -85,10 +90,6 @@ const TableSampleTenants = ({ filterItems, setFilterItems, filters, showGrid })
|
|||||||
setIsModalTrashActive(false)
|
setIsModalTrashActive(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const handleDeleteModalAction = (id: string) => {
|
const handleDeleteModalAction = (id: string) => {
|
||||||
setId(id)
|
setId(id)
|
||||||
setIsModalTrashActive(true)
|
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(() => {
|
const generateFilterRequests = useMemo(() => {
|
||||||
let request = '&';
|
let request = '&';
|
||||||
filterItems.forEach((item) => {
|
filterItems.forEach((item) => {
|
||||||
@ -179,6 +185,7 @@ const TableSampleTenants = ({ filterItems, setFilterItems, filters, showGrid })
|
|||||||
handleDeleteModalAction,
|
handleDeleteModalAction,
|
||||||
`tenants`,
|
`tenants`,
|
||||||
currentUser,
|
currentUser,
|
||||||
|
handleImpersonate
|
||||||
).then((newCols) => setColumns(newCols));
|
).then((newCols) => setColumns(newCols));
|
||||||
}, [currentUser]);
|
}, [currentUser]);
|
||||||
|
|
||||||
@ -244,9 +251,7 @@ const TableSampleTenants = ({ filterItems, setFilterItems, filters, showGrid })
|
|||||||
setSelectedRows(ids)
|
setSelectedRows(ids)
|
||||||
}}
|
}}
|
||||||
onSortModelChange={(params) => {
|
onSortModelChange={(params) => {
|
||||||
params.length
|
setSortModel(params.length ? params : [{ field: '', sort: 'desc' }]);
|
||||||
? setSortModel(params)
|
|
||||||
: setSortModel([{ field: '', sort: 'desc' }]);
|
|
||||||
}}
|
}}
|
||||||
rowCount={count}
|
rowCount={count}
|
||||||
pageSizeOptions={[10]}
|
pageSizeOptions={[10]}
|
||||||
|
|||||||
@ -21,7 +21,8 @@ export const loadColumns = async (
|
|||||||
onDelete: Params,
|
onDelete: Params,
|
||||||
entityName: string,
|
entityName: string,
|
||||||
|
|
||||||
user
|
user,
|
||||||
|
onImpersonate?: (id: string) => void
|
||||||
|
|
||||||
) => {
|
) => {
|
||||||
async function callOptionsApi(entityName: string) {
|
async function callOptionsApi(entityName: string) {
|
||||||
@ -38,6 +39,7 @@ export const loadColumns = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const hasUpdatePermission = hasPermission(user, 'UPDATE_TENANTS')
|
const hasUpdatePermission = hasPermission(user, 'UPDATE_TENANTS')
|
||||||
|
const isSuperAdmin = user?.app_role?.globalAccess;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|
||||||
@ -49,9 +51,18 @@ export const loadColumns = async (
|
|||||||
filterable: false,
|
filterable: false,
|
||||||
headerClassName: 'datagrid--header',
|
headerClassName: 'datagrid--header',
|
||||||
cellClassName: 'datagrid--cell',
|
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 && !isSuperAdmin,
|
||||||
editable: hasUpdatePermission,
|
|
||||||
|
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|||||||
@ -38,31 +38,9 @@ export const loadColumns = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const hasUpdatePermission = hasPermission(user, 'UPDATE_TRIALS')
|
const hasUpdatePermission = hasPermission(user, 'UPDATE_TRIALS')
|
||||||
|
const hasTenant = !!(user?.tenant?.id || user?.tenantId);
|
||||||
|
|
||||||
return [
|
const columns: any[] = [
|
||||||
|
|
||||||
{
|
|
||||||
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,
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
{
|
||||||
field: 'project',
|
field: 'project',
|
||||||
headerName: '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;
|
||||||
};
|
};
|
||||||
@ -14,6 +14,7 @@ export type MenuAsideItem = {
|
|||||||
withDevider?: boolean;
|
withDevider?: boolean;
|
||||||
menu?: MenuAsideItem[]
|
menu?: MenuAsideItem[]
|
||||||
permissions?: string | string[]
|
permissions?: string | string[]
|
||||||
|
isOrientationTopOnly?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MenuNavBarItem = {
|
export type MenuNavBarItem = {
|
||||||
@ -27,6 +28,8 @@ export type MenuNavBarItem = {
|
|||||||
isToggleLightDark?: boolean
|
isToggleLightDark?: boolean
|
||||||
isCurrentUser?: boolean
|
isCurrentUser?: boolean
|
||||||
menu?: MenuNavBarItem[]
|
menu?: MenuNavBarItem[]
|
||||||
|
isOrientationTopOnly?: boolean
|
||||||
|
permissions?: string | string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ColorKey = 'white' | 'light' | 'contrast' | 'success' | 'danger' | 'warning' | 'info'
|
export type ColorKey = 'white' | 'light' | 'contrast' | 'success' | 'danger' | 'warning' | 'info'
|
||||||
|
|||||||
@ -1,109 +1,176 @@
|
|||||||
import React, { ReactNode, useEffect } from 'react'
|
import React, { ReactNode, useState, useMemo, useEffect } from 'react'
|
||||||
import { useState } from 'react'
|
import { useRouter } from 'next/router'
|
||||||
import jwt from 'jsonwebtoken';
|
import AsideMenu from '../components/AsideMenu'
|
||||||
import { mdiForwardburger, mdiBackburger, mdiMenu } from '@mdi/js'
|
import NavBar from '../components/NavBar'
|
||||||
|
import FooterBar from '../components/FooterBar'
|
||||||
import menuAside from '../menuAside'
|
import menuAside from '../menuAside'
|
||||||
import menuNavBar from '../menuNavBar'
|
import menuNavBar from '../menuNavBar'
|
||||||
import BaseIcon from '../components/BaseIcon'
|
|
||||||
import NavBar from '../components/NavBar'
|
|
||||||
import NavBarItemPlain from '../components/NavBarItemPlain'
|
|
||||||
import AsideMenu from '../components/AsideMenu'
|
|
||||||
import FooterBar from '../components/FooterBar'
|
|
||||||
import { useAppDispatch, useAppSelector } from '../stores/hooks'
|
import { useAppDispatch, useAppSelector } from '../stores/hooks'
|
||||||
import Search from '../components/Search';
|
import { setDarkMode } from '../stores/styleSlice'
|
||||||
import { useRouter } from 'next/router'
|
import { mdiLoading, mdiMenu } from '@mdi/js'
|
||||||
import {findMe, logoutUser} from "../stores/authSlice";
|
import BaseIcon from '../components/BaseIcon'
|
||||||
|
import { findMe } from '../stores/authSlice'
|
||||||
import {hasPermission} from "../helpers/userPermissions";
|
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 = {
|
type Props = {
|
||||||
children: ReactNode
|
children: ReactNode
|
||||||
|
|
||||||
permission?: string
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function LayoutAuthenticated({
|
export default function LayoutAuthenticated({ children }: Props) {
|
||||||
children,
|
|
||||||
|
|
||||||
permission
|
|
||||||
|
|
||||||
}: Props) {
|
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
const router = useRouter()
|
const { i18n } = useTranslation()
|
||||||
const { token, currentUser } = useAppSelector((state) => state.auth)
|
|
||||||
const bgColor = useAppSelector((state) => state.style.bgLayoutColor);
|
|
||||||
let localToken
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
// Perform localStorage action
|
|
||||||
localToken = localStorage.getItem('token')
|
|
||||||
}
|
|
||||||
|
|
||||||
const isTokenValid = () => {
|
|
||||||
const token = localStorage.getItem('token');
|
|
||||||
if (!token) return;
|
|
||||||
const date = new Date().getTime() / 1000;
|
|
||||||
const data = jwt.decode(token);
|
|
||||||
if (!data) return;
|
|
||||||
return date < data.exp;
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
dispatch(findMe());
|
|
||||||
if (!isTokenValid()) {
|
|
||||||
dispatch(logoutUser());
|
|
||||||
router.push('/login');
|
|
||||||
}
|
|
||||||
}, [token, localToken]);
|
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!permission || !currentUser) return;
|
|
||||||
|
|
||||||
if (!hasPermission(currentUser, permission)) router.push('/error');
|
|
||||||
}, [currentUser, permission]);
|
|
||||||
|
|
||||||
|
|
||||||
const darkMode = useAppSelector((state) => state.style.darkMode)
|
const darkMode = useAppSelector((state) => state.style.darkMode)
|
||||||
|
|
||||||
const [isAsideMobileExpanded, setIsAsideMobileExpanded] = useState(false)
|
const [isAsideMobileExpanded, setIsAsideMobileExpanded] = useState(false)
|
||||||
const [isAsideLgActive, setIsAsideLgActive] = useState(false)
|
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;
|
||||||
|
|
||||||
|
// 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 => {
|
||||||
|
// Filter by permissions
|
||||||
|
if (item.permissions && !hasPermission(currentUser, item.permissions)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Filter by orientation
|
||||||
|
if (navOrientation === 'top' && item.isOrientationTopOnly) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}).map(item => {
|
||||||
|
if (item.menu) {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
menu: item.menu.filter(subItem => {
|
||||||
|
if (subItem.permissions && !hasPermission(currentUser, subItem.permissions)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
}, [currentUser, navOrientation]);
|
||||||
|
|
||||||
|
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) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (navOrientation === 'top' && item.isOrientationTopOnly) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}, [currentUser, navOrientation]);
|
||||||
|
|
||||||
|
const filteredMenuNavBarLeft = useMemo(() => {
|
||||||
|
if (navOrientation !== 'top') return [];
|
||||||
|
return menuNavBar.filter(item => {
|
||||||
|
if (item.permissions && !hasPermission(currentUser, item.permissions)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return item.isOrientationTopOnly;
|
||||||
|
});
|
||||||
|
}, [currentUser, navOrientation]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleRouteChangeStart = () => {
|
const handleRouteChange = () => {
|
||||||
setIsAsideMobileExpanded(false)
|
setIsAsideMobileExpanded(false)
|
||||||
setIsAsideLgActive(false)
|
setIsAsideLgActive(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
router.events.on('routeChangeStart', handleRouteChangeStart)
|
router.events.on('routeChangeStart', handleRouteChange)
|
||||||
|
|
||||||
// If the component is unmounted, unsubscribe
|
|
||||||
// from the event with the `off` method:
|
|
||||||
return () => {
|
return () => {
|
||||||
router.events.off('routeChangeStart', handleRouteChangeStart)
|
router.events.off('routeChangeStart', handleRouteChange)
|
||||||
}
|
}
|
||||||
}, [router.events, dispatch])
|
}, [router.events])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (localStorage.getItem('token') && !currentUser && !isFetching) {
|
||||||
|
dispatch(findMe())
|
||||||
|
}
|
||||||
|
}, [currentUser, dispatch, isFetching])
|
||||||
|
|
||||||
const layoutAsidePadding = 'xl:pl-60'
|
useEffect(() => {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
const isDarkMode = darkMode || localStorage.getItem('darkMode') === '1'
|
||||||
|
dispatch(setDarkMode(isDarkMode))
|
||||||
|
}
|
||||||
|
}, [darkMode, dispatch])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (i18n.language !== router.locale) {
|
||||||
|
i18n.changeLanguage(router.locale)
|
||||||
|
}
|
||||||
|
}, [router.locale, i18n])
|
||||||
|
|
||||||
|
if (isFetching || !currentUser) {
|
||||||
|
return (
|
||||||
|
<div className="flex h-screen items-center justify-center bg-gray-50 dark:bg-dark-900">
|
||||||
|
<BaseIcon path={mdiLoading} size={48} className="animate-spin text-emerald-600" />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const layoutAsidePadding = navOrientation === 'top' ? '' : 'xl:pl-60'
|
||||||
|
const navHeightPadding = navOrientation === 'top' ? 'pt-14 lg:pt-28' : 'pt-14'
|
||||||
|
const organizationName = fallbackOrg?.name;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`${darkMode ? 'dark' : ''} overflow-hidden lg:overflow-visible`}>
|
<div className={`${darkMode ? 'dark' : ''} overflow-hidden lg:overflow-visible`}>
|
||||||
<div
|
<div
|
||||||
className={`${layoutAsidePadding} ${
|
className={`${layoutAsidePadding} ${
|
||||||
isAsideMobileExpanded ? 'ml-60 lg:ml-0' : ''
|
isAsideMobileExpanded ? 'ml-60 lg:ml-0' : ''
|
||||||
} pt-14 min-h-screen w-screen transition-position lg:w-auto ${bgColor} 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
|
<NavBar
|
||||||
menu={menuNavBar}
|
menu={filteredMenuNavBarRight}
|
||||||
|
leftMenu={filteredMenuNavBarLeft}
|
||||||
|
isTopNav={navOrientation === 'top'}
|
||||||
className={`${layoutAsidePadding} ${isAsideMobileExpanded ? 'ml-60 lg:ml-0' : ''}`}
|
className={`${layoutAsidePadding} ${isAsideMobileExpanded ? 'ml-60 lg:ml-0' : ''}`}
|
||||||
>
|
>
|
||||||
|
{navOrientation === 'side' && (
|
||||||
|
<div className="flex items-center">
|
||||||
<NavBarItemPlain
|
<NavBarItemPlain
|
||||||
display="flex lg:hidden"
|
display="flex lg:hidden"
|
||||||
onClick={() => setIsAsideMobileExpanded(!isAsideMobileExpanded)}
|
onClick={() => setIsAsideMobileExpanded(!isAsideMobileExpanded)}
|
||||||
>
|
>
|
||||||
<BaseIcon path={isAsideMobileExpanded ? mdiBackburger : mdiForwardburger} size="24" />
|
<BaseIcon path={mdiMenu} size="24" />
|
||||||
</NavBarItemPlain>
|
</NavBarItemPlain>
|
||||||
<NavBarItemPlain
|
<NavBarItemPlain
|
||||||
display="hidden lg:flex xl:hidden"
|
display="hidden lg:flex xl:hidden"
|
||||||
@ -111,18 +178,28 @@ export default function LayoutAuthenticated({
|
|||||||
>
|
>
|
||||||
<BaseIcon path={mdiMenu} size="24" />
|
<BaseIcon path={mdiMenu} size="24" />
|
||||||
</NavBarItemPlain>
|
</NavBarItemPlain>
|
||||||
<NavBarItemPlain useMargin>
|
</div>
|
||||||
|
)}
|
||||||
|
{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 />
|
<Search />
|
||||||
</NavBarItemPlain>
|
</div>
|
||||||
</NavBar>
|
</NavBar>
|
||||||
|
{navOrientation === 'side' && (
|
||||||
<AsideMenu
|
<AsideMenu
|
||||||
isAsideMobileExpanded={isAsideMobileExpanded}
|
isAsideMobileExpanded={isAsideMobileExpanded}
|
||||||
isAsideLgActive={isAsideLgActive}
|
isAsideLgActive={isAsideLgActive}
|
||||||
menu={menuAside}
|
menu={filteredMenuAside}
|
||||||
onAsideLgClose={() => setIsAsideLgActive(false)}
|
onAsideLgClose={() => setIsAsideLgActive(false)}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
{children}
|
{children}
|
||||||
<FooterBar>Hand-crafted & Made with ❤️</FooterBar>
|
<FooterBar />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -4,151 +4,104 @@ import { MenuAsideItem } from './interfaces'
|
|||||||
const menuAside: MenuAsideItem[] = [
|
const menuAside: MenuAsideItem[] = [
|
||||||
{
|
{
|
||||||
href: '/dashboard',
|
href: '/dashboard',
|
||||||
icon: icon.mdiViewDashboardOutline,
|
icon: icon.mdiHomeOutline,
|
||||||
label: 'Dashboard',
|
label: 'Home',
|
||||||
},
|
isOrientationTopOnly: true
|
||||||
|
|
||||||
{
|
|
||||||
href: '/users/users-list',
|
|
||||||
label: 'Users',
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
icon: icon.mdiAccountGroup ?? icon.mdiTable,
|
|
||||||
permissions: 'READ_USERS'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
href: '/roles/roles-list',
|
|
||||||
label: 'Roles',
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
icon: icon.mdiShieldAccountVariantOutline ?? icon.mdiTable,
|
|
||||||
permissions: 'READ_ROLES'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
href: '/permissions/permissions-list',
|
|
||||||
label: 'Permissions',
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
icon: icon.mdiShieldAccountOutline ?? icon.mdiTable,
|
|
||||||
permissions: 'READ_PERMISSIONS'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
href: '/organizations/organizations-list',
|
|
||||||
label: 'Organizations',
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
icon: icon.mdiTable ?? icon.mdiTable,
|
|
||||||
permissions: 'READ_ORGANIZATIONS'
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/tenants/tenants-list',
|
href: '/tenants/tenants-list',
|
||||||
label: 'Tenants',
|
label: 'Tenants',
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
icon: 'mdiDomain' in icon ? icon['mdiDomain' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
icon: icon.mdiDomain,
|
||||||
permissions: 'READ_TENANTS'
|
permissions: 'READ_TENANTS',
|
||||||
},
|
isOrientationTopOnly: true
|
||||||
{
|
|
||||||
href: '/locations/locations-list',
|
|
||||||
label: 'Locations',
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
icon: 'mdiMapMarker' in icon ? icon['mdiMapMarker' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
|
||||||
permissions: 'READ_LOCATIONS'
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/projects/projects-list',
|
href: '/projects/projects-list',
|
||||||
label: 'Projects',
|
label: 'Projects',
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
icon: 'mdiFolderOutline' in icon ? icon['mdiFolderOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
icon: icon.mdiFolderOutline,
|
||||||
permissions: 'READ_PROJECTS'
|
permissions: 'READ_PROJECTS',
|
||||||
},
|
isOrientationTopOnly: true
|
||||||
{
|
|
||||||
href: '/trial_types/trial_types-list',
|
|
||||||
label: 'Trial types',
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
icon: 'mdiFlaskOutline' in icon ? icon['mdiFlaskOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
|
||||||
permissions: 'READ_TRIAL_TYPES'
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/trials/trials-list',
|
href: '/trials/trials-list',
|
||||||
label: 'Trials',
|
label: 'Trials',
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
icon: 'mdiTestTube' in icon ? icon['mdiTestTube' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
icon: icon.mdiTestTube,
|
||||||
permissions: 'READ_TRIALS'
|
permissions: 'READ_TRIALS',
|
||||||
},
|
isOrientationTopOnly: true
|
||||||
{
|
|
||||||
href: '/form_templates/form_templates-list',
|
|
||||||
label: 'Form templates',
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
icon: 'mdiFormSelect' in icon ? icon['mdiFormSelect' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
|
||||||
permissions: 'READ_FORM_TEMPLATES'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
href: '/form_fields/form_fields-list',
|
|
||||||
label: 'Form fields',
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
icon: 'mdiFormatListBulleted' in icon ? icon['mdiFormatListBulleted' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
|
||||||
permissions: 'READ_FORM_FIELDS'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
href: '/form_field_choices/form_field_choices-list',
|
|
||||||
label: 'Form field choices',
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
icon: 'mdiPlaylistEdit' in icon ? icon['mdiPlaylistEdit' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
|
||||||
permissions: 'READ_FORM_FIELD_CHOICES'
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/form_submissions/form_submissions-list',
|
href: '/form_submissions/form_submissions-list',
|
||||||
label: 'Form submissions',
|
label: 'Track-IT',
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
icon: 'mdiClipboardTextOutline' in icon ? icon['mdiClipboardTextOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
icon: icon.mdiClipboardTextOutline,
|
||||||
permissions: 'READ_FORM_SUBMISSIONS'
|
permissions: 'READ_FORM_SUBMISSIONS',
|
||||||
|
isOrientationTopOnly: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/submission_values/submission_values-list',
|
href: '/users/users-list',
|
||||||
label: 'Submission values',
|
label: 'Users',
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
icon: 'mdiDatabaseOutline' in icon ? icon['mdiDatabaseOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
icon: icon.mdiAccountGroup,
|
||||||
permissions: 'READ_SUBMISSION_VALUES'
|
permissions: 'READ_USERS',
|
||||||
},
|
isOrientationTopOnly: true
|
||||||
{
|
|
||||||
href: '/activity_feed_items/activity_feed_items-list',
|
|
||||||
label: 'Activity feed items',
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
icon: 'mdiTimelineTextOutline' in icon ? icon['mdiTimelineTextOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
|
||||||
permissions: 'READ_ACTIVITY_FEED_ITEMS'
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/reports/reports-list',
|
href: '/reports/reports-list',
|
||||||
label: 'Reports',
|
label: 'Reports',
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
icon: 'mdiFileChartOutline' in icon ? icon['mdiFileChartOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
icon: icon.mdiFileChartOutline,
|
||||||
permissions: 'READ_REPORTS'
|
permissions: 'READ_REPORTS',
|
||||||
|
isOrientationTopOnly: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/profile',
|
label: 'Settings',
|
||||||
label: 'Profile',
|
icon: icon.mdiCogOutline,
|
||||||
icon: icon.mdiAccountCircle,
|
menu: [
|
||||||
},
|
|
||||||
|
|
||||||
|
|
||||||
{
|
{
|
||||||
href: '/api-docs',
|
href: '/settings/company-preferences',
|
||||||
target: '_blank',
|
label: 'Company Preferences',
|
||||||
label: 'Swagger API',
|
permissions: 'UPDATE_ORGANIZATIONS'
|
||||||
icon: icon.mdiFileCode,
|
},
|
||||||
permissions: 'READ_API_DOCS'
|
{
|
||||||
|
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'
|
||||||
|
},
|
||||||
|
]
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@ -1,19 +1,115 @@
|
|||||||
import {
|
import {
|
||||||
mdiMenu,
|
|
||||||
mdiClockOutline,
|
|
||||||
mdiCloud,
|
|
||||||
mdiCrop,
|
|
||||||
mdiAccount,
|
mdiAccount,
|
||||||
mdiCogOutline,
|
|
||||||
mdiEmail,
|
|
||||||
mdiLogout,
|
mdiLogout,
|
||||||
mdiThemeLightDark,
|
mdiThemeLightDark,
|
||||||
mdiGithub,
|
mdiDomain,
|
||||||
mdiVuejs,
|
mdiFileCode,
|
||||||
|
mdiAccountCircle,
|
||||||
|
mdiHomeOutline,
|
||||||
|
mdiFolderOutline,
|
||||||
|
mdiTestTube,
|
||||||
|
mdiClipboardTextOutline,
|
||||||
|
mdiAccountGroup,
|
||||||
|
mdiFileChartOutline,
|
||||||
|
mdiCogOutline
|
||||||
} from '@mdi/js'
|
} from '@mdi/js'
|
||||||
import { MenuNavBarItem } from './interfaces'
|
import { MenuNavBarItem } from './interfaces'
|
||||||
|
|
||||||
const menuNavBar: MenuNavBarItem[] = [
|
const menuNavBar: MenuNavBarItem[] = [
|
||||||
|
{
|
||||||
|
href: '/dashboard',
|
||||||
|
icon: mdiHomeOutline,
|
||||||
|
label: 'Home',
|
||||||
|
isOrientationTopOnly: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: '/projects/projects-list',
|
||||||
|
label: 'Projects',
|
||||||
|
icon: mdiFolderOutline,
|
||||||
|
isOrientationTopOnly: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: '/trials/trials-list',
|
||||||
|
label: 'Trials',
|
||||||
|
icon: mdiTestTube,
|
||||||
|
isOrientationTopOnly: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: '/form_submissions/form_submissions-list',
|
||||||
|
label: 'Track-IT',
|
||||||
|
icon: mdiClipboardTextOutline,
|
||||||
|
isOrientationTopOnly: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: '/users/users-list',
|
||||||
|
label: 'Users',
|
||||||
|
icon: mdiAccountGroup,
|
||||||
|
isOrientationTopOnly: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: '/reports/reports-list',
|
||||||
|
label: 'Reports',
|
||||||
|
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',
|
||||||
|
icon: mdiDomain,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: '/profile',
|
||||||
|
label: 'Profile',
|
||||||
|
icon: mdiAccountCircle,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: '/api-docs',
|
||||||
|
target: '_blank',
|
||||||
|
label: 'Swagger API',
|
||||||
|
icon: mdiFileCode,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
isCurrentUser: true,
|
isCurrentUser: true,
|
||||||
menu: [
|
menu: [
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import * as icon from '@mdi/js';
|
import * as icon from '@mdi/js';
|
||||||
import Head from 'next/head'
|
import Head from 'next/head'
|
||||||
import React from 'react'
|
import React, { useCallback, useMemo } from 'react'
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import type { ReactElement } from 'react'
|
import type { ReactElement } from 'react'
|
||||||
import LayoutAuthenticated from '../layouts/Authenticated'
|
import LayoutAuthenticated from '../layouts/Authenticated'
|
||||||
@ -9,13 +9,18 @@ import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton
|
|||||||
import BaseIcon from "../components/BaseIcon";
|
import BaseIcon from "../components/BaseIcon";
|
||||||
import { getPageTitle } from '../config'
|
import { getPageTitle } from '../config'
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import moment from 'moment';
|
||||||
|
|
||||||
import { hasPermission } from "../helpers/userPermissions";
|
import { hasPermission } from "../helpers/userPermissions";
|
||||||
import { fetchWidgets } from '../stores/roles/rolesSlice';
|
import { fetchWidgets } from '../stores/roles/rolesSlice';
|
||||||
import { WidgetCreator } from '../components/WidgetCreator/WidgetCreator';
|
import { WidgetCreator } from '../components/WidgetCreator/WidgetCreator';
|
||||||
import { SmartWidget } from '../components/SmartWidget/SmartWidget';
|
import { SmartWidget } from '../components/SmartWidget/SmartWidget';
|
||||||
|
import UserAvatar from '../components/UserAvatar';
|
||||||
|
import CardBox from '../components/CardBox';
|
||||||
|
|
||||||
import { useAppDispatch, useAppSelector } from '../stores/hooks';
|
import { useAppDispatch, useAppSelector } from '../stores/hooks';
|
||||||
|
import { fetch as fetchActivityItems } from '../stores/activity_feed_items/activity_feed_itemsSlice';
|
||||||
|
|
||||||
const Dashboard = () => {
|
const Dashboard = () => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const iconsColor = useAppSelector((state) => state.style.iconsColor);
|
const iconsColor = useAppSelector((state) => state.style.iconsColor);
|
||||||
@ -24,52 +29,30 @@ const Dashboard = () => {
|
|||||||
|
|
||||||
const loadingMessage = 'Loading...';
|
const loadingMessage = 'Loading...';
|
||||||
|
|
||||||
|
|
||||||
const [users, setUsers] = React.useState(loadingMessage);
|
|
||||||
const [roles, setRoles] = React.useState(loadingMessage);
|
|
||||||
const [permissions, setPermissions] = React.useState(loadingMessage);
|
|
||||||
const [organizations, setOrganizations] = React.useState(loadingMessage);
|
|
||||||
const [tenants, setTenants] = React.useState(loadingMessage);
|
|
||||||
const [locations, setLocations] = React.useState(loadingMessage);
|
|
||||||
const [projects, setProjects] = React.useState(loadingMessage);
|
const [projects, setProjects] = React.useState(loadingMessage);
|
||||||
const [trial_types, setTrial_types] = React.useState(loadingMessage);
|
|
||||||
const [trials, setTrials] = React.useState(loadingMessage);
|
const [trials, setTrials] = React.useState(loadingMessage);
|
||||||
const [form_templates, setForm_templates] = React.useState(loadingMessage);
|
|
||||||
const [form_fields, setForm_fields] = React.useState(loadingMessage);
|
|
||||||
const [form_field_choices, setForm_field_choices] = React.useState(loadingMessage);
|
|
||||||
const [form_submissions, setForm_submissions] = React.useState(loadingMessage);
|
const [form_submissions, setForm_submissions] = React.useState(loadingMessage);
|
||||||
const [submission_values, setSubmission_values] = React.useState(loadingMessage);
|
const [users, setUsers] = React.useState(loadingMessage);
|
||||||
const [activity_feed_items, setActivity_feed_items] = React.useState(loadingMessage);
|
|
||||||
const [reports, setReports] = React.useState(loadingMessage);
|
|
||||||
|
|
||||||
|
const { activity_feed_items, loading: activityLoading } = useAppSelector((state) => state.activity_feed_items);
|
||||||
const [widgetsRole, setWidgetsRole] = React.useState({
|
|
||||||
role: { value: '', label: '' },
|
|
||||||
});
|
|
||||||
const { currentUser } = useAppSelector((state) => state.auth);
|
const { currentUser } = useAppSelector((state) => state.auth);
|
||||||
const { isFetchingQuery } = useAppSelector((state) => state.openAi);
|
const { isFetchingQuery } = useAppSelector((state) => state.openAi);
|
||||||
|
|
||||||
const { rolesWidgets, loading } = useAppSelector((state) => state.roles);
|
const { rolesWidgets, loading } = useAppSelector((state) => state.roles);
|
||||||
|
|
||||||
|
const loadCounts = useCallback(async () => {
|
||||||
const organizationId = currentUser?.organizations?.id;
|
const entities = ['projects', 'trials', 'form_submissions', 'users'];
|
||||||
|
const fns = [setProjects, setTrials, setForm_submissions, setUsers];
|
||||||
async function loadData() {
|
|
||||||
const entities = ['users','roles','permissions','organizations','tenants','locations','projects','trial_types','trials','form_templates','form_fields','form_field_choices','form_submissions','submission_values','activity_feed_items','reports',];
|
|
||||||
const fns = [setUsers,setRoles,setPermissions,setOrganizations,setTenants,setLocations,setProjects,setTrial_types,setTrials,setForm_templates,setForm_fields,setForm_field_choices,setForm_submissions,setSubmission_values,setActivity_feed_items,setReports,];
|
|
||||||
|
|
||||||
const requests = entities.map((entity, index) => {
|
const requests = entities.map((entity, index) => {
|
||||||
|
|
||||||
if (hasPermission(currentUser, `READ_${entity.toUpperCase()}`)) {
|
if (hasPermission(currentUser, `READ_${entity.toUpperCase()}`)) {
|
||||||
return axios.get(`/${entity.toLowerCase()}/count`);
|
return axios.get(`/${entity.toLowerCase()}/count`);
|
||||||
} else {
|
} else {
|
||||||
fns[index](null);
|
fns[index](null);
|
||||||
return Promise.resolve({ data: { count: null } });
|
return Promise.resolve({ data: { count: null } });
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
Promise.allSettled(requests).then((results) => {
|
const results = await Promise.allSettled(requests);
|
||||||
results.forEach((result, i) => {
|
results.forEach((result, i) => {
|
||||||
if (result.status === 'fulfilled') {
|
if (result.status === 'fulfilled') {
|
||||||
fns[i](result.value.data.count);
|
fns[i](result.value.data.count);
|
||||||
@ -77,531 +60,192 @@ const Dashboard = () => {
|
|||||||
fns[i](result.reason.message);
|
fns[i](result.reason.message);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getWidgets(roleId) {
|
|
||||||
await dispatch(fetchWidgets(roleId));
|
|
||||||
}
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (!currentUser) return;
|
|
||||||
loadData().then();
|
|
||||||
setWidgetsRole({ role: { value: currentUser?.app_role?.id, label: currentUser?.app_role?.name } });
|
|
||||||
}, [currentUser]);
|
}, [currentUser]);
|
||||||
|
|
||||||
|
const currentUserMemo = useMemo(() => currentUser ? JSON.stringify(currentUser.id) : null, [currentUser]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!currentUser || !widgetsRole?.role?.value) return;
|
if (!currentUser) return;
|
||||||
getWidgets(widgetsRole?.role?.value || '').then();
|
loadCounts().then();
|
||||||
}, [widgetsRole?.role?.value]);
|
if (hasPermission(currentUser, 'READ_ACTIVITY_FEED_ITEMS')) {
|
||||||
|
dispatch(fetchActivityItems({ query: '?limit=10&orderBy=occurred_at_DESC' }));
|
||||||
|
}
|
||||||
|
}, [currentUserMemo, dispatch, loadCounts]);
|
||||||
|
|
||||||
|
const getItemIcon = (type: string) => {
|
||||||
|
switch (type) {
|
||||||
|
case 'project_created': return icon.mdiFolderPlusOutline;
|
||||||
|
case 'trial_created': return icon.mdiFlaskPlusOutline;
|
||||||
|
case 'submission_created': return icon.mdiClipboardCheckOutline;
|
||||||
|
case 'user_created': return icon.mdiAccountPlusOutline;
|
||||||
|
case 'attachment_added': return icon.mdiPaperclip;
|
||||||
|
default: return icon.mdiBellOutline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
<title>
|
<title>{getPageTitle('Home')}</title>
|
||||||
{getPageTitle('Overview')}
|
|
||||||
</title>
|
|
||||||
</Head>
|
</Head>
|
||||||
<SectionMain>
|
<SectionMain>
|
||||||
<SectionTitleLineWithButton
|
<SectionTitleLineWithButton icon={icon.mdiHomeOutline} title='Home' main>
|
||||||
icon={icon.mdiChartTimelineVariant}
|
|
||||||
title='Overview'
|
|
||||||
main>
|
|
||||||
{''}
|
{''}
|
||||||
</SectionTitleLineWithButton>
|
</SectionTitleLineWithButton>
|
||||||
|
|
||||||
{hasPermission(currentUser, 'CREATE_ROLES') && <WidgetCreator
|
{/* Summary Cards */}
|
||||||
currentUser={currentUser}
|
<div className='grid grid-cols-1 gap-6 lg:grid-cols-4 mb-6'>
|
||||||
isFetchingQuery={isFetchingQuery}
|
<Link href={'/projects/projects-list'}>
|
||||||
setWidgetsRole={setWidgetsRole}
|
<CardBox className="hover:shadow-lg transition-shadow cursor-pointer">
|
||||||
widgetsRole={widgetsRole}
|
<div className="flex justify-between items-center">
|
||||||
/>}
|
<div>
|
||||||
{!!rolesWidgets.length &&
|
<p className="text-gray-500 dark:text-gray-400">Projects</p>
|
||||||
hasPermission(currentUser, 'CREATE_ROLES') && (
|
<h3 className="text-3xl font-bold">{projects}</h3>
|
||||||
<p className=' text-gray-500 dark:text-gray-400 mb-4'>
|
</div>
|
||||||
{`${widgetsRole?.role?.label || 'Users'}'s widgets`}
|
<BaseIcon path={icon.mdiFolderOutline} size={48} className={iconsColor} />
|
||||||
</p>
|
</div>
|
||||||
|
</CardBox>
|
||||||
|
</Link>
|
||||||
|
<Link href={'/trials/trials-list'}>
|
||||||
|
<CardBox className="hover:shadow-lg transition-shadow cursor-pointer">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<div>
|
||||||
|
<p className="text-gray-500 dark:text-gray-400">Trials</p>
|
||||||
|
<h3 className="text-3xl font-bold">{trials}</h3>
|
||||||
|
</div>
|
||||||
|
<BaseIcon path={icon.mdiTestTube} size={48} className={iconsColor} />
|
||||||
|
</div>
|
||||||
|
</CardBox>
|
||||||
|
</Link>
|
||||||
|
<Link href={'/form_submissions/form_submissions-list'}>
|
||||||
|
<CardBox className="hover:shadow-lg transition-shadow cursor-pointer">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<div>
|
||||||
|
<p className="text-gray-500 dark:text-gray-400">Submissions</p>
|
||||||
|
<h3 className="text-3xl font-bold">{form_submissions}</h3>
|
||||||
|
</div>
|
||||||
|
<BaseIcon path={icon.mdiClipboardTextOutline} size={48} className={iconsColor} />
|
||||||
|
</div>
|
||||||
|
</CardBox>
|
||||||
|
</Link>
|
||||||
|
<Link href={'/users/users-list'}>
|
||||||
|
<CardBox className="hover:shadow-lg transition-shadow cursor-pointer">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<div>
|
||||||
|
<p className="text-gray-500 dark:text-gray-400">Team Members</p>
|
||||||
|
<h3 className="text-3xl font-bold">{users}</h3>
|
||||||
|
</div>
|
||||||
|
<BaseIcon path={icon.mdiAccountGroup} size={48} className={iconsColor} />
|
||||||
|
</div>
|
||||||
|
</CardBox>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Activity Feed Section */}
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||||
|
<div className="lg:col-span-2">
|
||||||
|
<SectionTitleLineWithButton icon={icon.mdiTimelineTextOutline} title="Recent Activity" />
|
||||||
|
|
||||||
|
{activityLoading && <p>Loading feed...</p>}
|
||||||
|
|
||||||
|
{!activityLoading && activity_feed_items?.length === 0 && (
|
||||||
|
<CardBox>
|
||||||
|
<p className="text-center text-gray-500 py-8">No recent activity found.</p>
|
||||||
|
</CardBox>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className='grid grid-cols-1 gap-6 lg:grid-cols-4 mb-6 grid-flow-dense'>
|
<div className="space-y-4">
|
||||||
{(isFetchingQuery || loading) && (
|
{activity_feed_items?.map((item: any) => (
|
||||||
<div className={` ${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 text-lg leading-tight text-gray-500 flex items-center ${cardsStyle} dark:border-dark-700 p-6`}>
|
<CardBox key={item.id} className="hover:border-emerald-500 transition-colors border-l-4 border-l-emerald-500">
|
||||||
<BaseIcon
|
<div className="flex items-start space-x-4">
|
||||||
className={`${iconsColor} animate-spin mr-5`}
|
<div className="flex-shrink-0">
|
||||||
w='w-16'
|
<UserAvatar
|
||||||
h='h-16'
|
username={item.actor_user?.firstName || 'User'}
|
||||||
size={48}
|
image={item.actor_user?.avatar}
|
||||||
path={icon.mdiLoading}
|
className="w-12 h-12"
|
||||||
/>{' '}
|
/>
|
||||||
Loading widgets...
|
</div>
|
||||||
|
<div className="flex-grow">
|
||||||
|
<div className="flex justify-between items-start">
|
||||||
|
<div>
|
||||||
|
<span className="font-bold text-lg">
|
||||||
|
{item.actor_user?.firstName} {item.actor_user?.lastName}
|
||||||
|
</span>
|
||||||
|
<span className="text-gray-500 ml-2">
|
||||||
|
{item.title}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-gray-400 flex items-center">
|
||||||
|
<BaseIcon path={icon.mdiClockOutline} size={14} className="mr-1" />
|
||||||
|
{moment(item.occurred_at).fromNow()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="text-gray-600 dark:text-gray-300 mt-1">
|
||||||
|
{item.summary}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{item.thumbnail && item.thumbnail[0] && (
|
||||||
|
<div className="mt-3">
|
||||||
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||||
|
<img
|
||||||
|
src={item.thumbnail[0].publicUrl}
|
||||||
|
alt="Activity thumbnail"
|
||||||
|
className="rounded-lg max-h-48 w-auto object-cover border dark:border-gray-700"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{ rolesWidgets &&
|
<div className="mt-3 flex items-center justify-between">
|
||||||
rolesWidgets.map((widget) => (
|
<div className="flex items-center space-x-2">
|
||||||
<SmartWidget
|
<BaseIcon
|
||||||
key={widget.id}
|
path={getItemIcon(item.item_type)}
|
||||||
userId={currentUser?.id}
|
size={18}
|
||||||
widget={widget}
|
className="text-emerald-600"
|
||||||
roleId={widgetsRole?.role?.value || ''}
|
|
||||||
admin={hasPermission(currentUser, 'CREATE_ROLES')}
|
|
||||||
/>
|
/>
|
||||||
|
<span className="text-xs font-medium uppercase tracking-wider text-emerald-600">
|
||||||
|
{item.item_type.replace('_', ' ')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{item.link_path && (
|
||||||
|
<Link href={item.link_path} className="text-sm text-emerald-600 hover:underline font-medium">
|
||||||
|
View Details →
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardBox>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{!!rolesWidgets.length && <hr className='my-6 ' />}
|
<div className="space-y-6">
|
||||||
|
<SectionTitleLineWithButton icon={icon.mdiChartPie} title="Quick Stats" />
|
||||||
|
<CardBox>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex justify-between items-center pb-2 border-b dark:border-gray-700">
|
||||||
|
<span className="text-gray-500">Active Projects</span>
|
||||||
|
<span className="font-bold">{projects}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between items-center pb-2 border-b dark:border-gray-700">
|
||||||
|
<span className="text-gray-500">Ongoing Trials</span>
|
||||||
|
<span className="font-bold">{trials}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="text-gray-500">New Submissions</span>
|
||||||
|
<span className="font-bold text-emerald-600">+{form_submissions}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardBox>
|
||||||
|
|
||||||
<div id="dashboard" className='grid grid-cols-1 gap-6 lg:grid-cols-3 mb-6'>
|
<SectionTitleLineWithButton icon={icon.mdiInformationOutline} title="System Info" />
|
||||||
|
<CardBox className="bg-emerald-50 dark:bg-emerald-900/20 border-emerald-200 dark:border-emerald-800">
|
||||||
|
<p className="text-sm text-emerald-800 dark:text-emerald-200">
|
||||||
{hasPermission(currentUser, 'READ_USERS') && <Link href={'/users/users-list'}>
|
<strong>Trial Tracker</strong> is running in multi-tenant mode.
|
||||||
<div
|
Farm-level data isolation is active for your account.
|
||||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
</p>
|
||||||
>
|
</CardBox>
|
||||||
<div className="flex justify-between align-center">
|
|
||||||
<div>
|
|
||||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
|
||||||
Users
|
|
||||||
</div>
|
</div>
|
||||||
<div className="text-3xl leading-tight font-semibold">
|
|
||||||
{users}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<BaseIcon
|
|
||||||
className={`${iconsColor}`}
|
|
||||||
w="w-16"
|
|
||||||
h="h-16"
|
|
||||||
size={48}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
path={icon.mdiAccountGroup || icon.mdiTable}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>}
|
|
||||||
|
|
||||||
{hasPermission(currentUser, 'READ_ROLES') && <Link href={'/roles/roles-list'}>
|
|
||||||
<div
|
|
||||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
|
||||||
>
|
|
||||||
<div className="flex justify-between align-center">
|
|
||||||
<div>
|
|
||||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
|
||||||
Roles
|
|
||||||
</div>
|
|
||||||
<div className="text-3xl leading-tight font-semibold">
|
|
||||||
{roles}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<BaseIcon
|
|
||||||
className={`${iconsColor}`}
|
|
||||||
w="w-16"
|
|
||||||
h="h-16"
|
|
||||||
size={48}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
path={icon.mdiShieldAccountVariantOutline || icon.mdiTable}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>}
|
|
||||||
|
|
||||||
{hasPermission(currentUser, 'READ_PERMISSIONS') && <Link href={'/permissions/permissions-list'}>
|
|
||||||
<div
|
|
||||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
|
||||||
>
|
|
||||||
<div className="flex justify-between align-center">
|
|
||||||
<div>
|
|
||||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
|
||||||
Permissions
|
|
||||||
</div>
|
|
||||||
<div className="text-3xl leading-tight font-semibold">
|
|
||||||
{permissions}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<BaseIcon
|
|
||||||
className={`${iconsColor}`}
|
|
||||||
w="w-16"
|
|
||||||
h="h-16"
|
|
||||||
size={48}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
path={icon.mdiShieldAccountOutline || icon.mdiTable}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>}
|
|
||||||
|
|
||||||
{hasPermission(currentUser, 'READ_ORGANIZATIONS') && <Link href={'/organizations/organizations-list'}>
|
|
||||||
<div
|
|
||||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
|
||||||
>
|
|
||||||
<div className="flex justify-between align-center">
|
|
||||||
<div>
|
|
||||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
|
||||||
Organizations
|
|
||||||
</div>
|
|
||||||
<div className="text-3xl leading-tight font-semibold">
|
|
||||||
{organizations}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<BaseIcon
|
|
||||||
className={`${iconsColor}`}
|
|
||||||
w="w-16"
|
|
||||||
h="h-16"
|
|
||||||
size={48}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
path={icon.mdiTable || icon.mdiTable}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>}
|
|
||||||
|
|
||||||
{hasPermission(currentUser, 'READ_TENANTS') && <Link href={'/tenants/tenants-list'}>
|
|
||||||
<div
|
|
||||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
|
||||||
>
|
|
||||||
<div className="flex justify-between align-center">
|
|
||||||
<div>
|
|
||||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
|
||||||
Tenants
|
|
||||||
</div>
|
|
||||||
<div className="text-3xl leading-tight font-semibold">
|
|
||||||
{tenants}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<BaseIcon
|
|
||||||
className={`${iconsColor}`}
|
|
||||||
w="w-16"
|
|
||||||
h="h-16"
|
|
||||||
size={48}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
path={'mdiDomain' in icon ? icon['mdiDomain' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>}
|
|
||||||
|
|
||||||
{hasPermission(currentUser, 'READ_LOCATIONS') && <Link href={'/locations/locations-list'}>
|
|
||||||
<div
|
|
||||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
|
||||||
>
|
|
||||||
<div className="flex justify-between align-center">
|
|
||||||
<div>
|
|
||||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
|
||||||
Locations
|
|
||||||
</div>
|
|
||||||
<div className="text-3xl leading-tight font-semibold">
|
|
||||||
{locations}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<BaseIcon
|
|
||||||
className={`${iconsColor}`}
|
|
||||||
w="w-16"
|
|
||||||
h="h-16"
|
|
||||||
size={48}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
path={'mdiMapMarker' in icon ? icon['mdiMapMarker' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>}
|
|
||||||
|
|
||||||
{hasPermission(currentUser, 'READ_PROJECTS') && <Link href={'/projects/projects-list'}>
|
|
||||||
<div
|
|
||||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
|
||||||
>
|
|
||||||
<div className="flex justify-between align-center">
|
|
||||||
<div>
|
|
||||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
|
||||||
Projects
|
|
||||||
</div>
|
|
||||||
<div className="text-3xl leading-tight font-semibold">
|
|
||||||
{projects}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<BaseIcon
|
|
||||||
className={`${iconsColor}`}
|
|
||||||
w="w-16"
|
|
||||||
h="h-16"
|
|
||||||
size={48}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
path={'mdiFolderOutline' in icon ? icon['mdiFolderOutline' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>}
|
|
||||||
|
|
||||||
{hasPermission(currentUser, 'READ_TRIAL_TYPES') && <Link href={'/trial_types/trial_types-list'}>
|
|
||||||
<div
|
|
||||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
|
||||||
>
|
|
||||||
<div className="flex justify-between align-center">
|
|
||||||
<div>
|
|
||||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
|
||||||
Trial types
|
|
||||||
</div>
|
|
||||||
<div className="text-3xl leading-tight font-semibold">
|
|
||||||
{trial_types}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<BaseIcon
|
|
||||||
className={`${iconsColor}`}
|
|
||||||
w="w-16"
|
|
||||||
h="h-16"
|
|
||||||
size={48}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
path={'mdiFlaskOutline' in icon ? icon['mdiFlaskOutline' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>}
|
|
||||||
|
|
||||||
{hasPermission(currentUser, 'READ_TRIALS') && <Link href={'/trials/trials-list'}>
|
|
||||||
<div
|
|
||||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
|
||||||
>
|
|
||||||
<div className="flex justify-between align-center">
|
|
||||||
<div>
|
|
||||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
|
||||||
Trials
|
|
||||||
</div>
|
|
||||||
<div className="text-3xl leading-tight font-semibold">
|
|
||||||
{trials}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<BaseIcon
|
|
||||||
className={`${iconsColor}`}
|
|
||||||
w="w-16"
|
|
||||||
h="h-16"
|
|
||||||
size={48}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
path={'mdiTestTube' in icon ? icon['mdiTestTube' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>}
|
|
||||||
|
|
||||||
{hasPermission(currentUser, 'READ_FORM_TEMPLATES') && <Link href={'/form_templates/form_templates-list'}>
|
|
||||||
<div
|
|
||||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
|
||||||
>
|
|
||||||
<div className="flex justify-between align-center">
|
|
||||||
<div>
|
|
||||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
|
||||||
Form templates
|
|
||||||
</div>
|
|
||||||
<div className="text-3xl leading-tight font-semibold">
|
|
||||||
{form_templates}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<BaseIcon
|
|
||||||
className={`${iconsColor}`}
|
|
||||||
w="w-16"
|
|
||||||
h="h-16"
|
|
||||||
size={48}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
path={'mdiFormSelect' in icon ? icon['mdiFormSelect' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>}
|
|
||||||
|
|
||||||
{hasPermission(currentUser, 'READ_FORM_FIELDS') && <Link href={'/form_fields/form_fields-list'}>
|
|
||||||
<div
|
|
||||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
|
||||||
>
|
|
||||||
<div className="flex justify-between align-center">
|
|
||||||
<div>
|
|
||||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
|
||||||
Form fields
|
|
||||||
</div>
|
|
||||||
<div className="text-3xl leading-tight font-semibold">
|
|
||||||
{form_fields}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<BaseIcon
|
|
||||||
className={`${iconsColor}`}
|
|
||||||
w="w-16"
|
|
||||||
h="h-16"
|
|
||||||
size={48}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
path={'mdiFormatListBulleted' in icon ? icon['mdiFormatListBulleted' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>}
|
|
||||||
|
|
||||||
{hasPermission(currentUser, 'READ_FORM_FIELD_CHOICES') && <Link href={'/form_field_choices/form_field_choices-list'}>
|
|
||||||
<div
|
|
||||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
|
||||||
>
|
|
||||||
<div className="flex justify-between align-center">
|
|
||||||
<div>
|
|
||||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
|
||||||
Form field choices
|
|
||||||
</div>
|
|
||||||
<div className="text-3xl leading-tight font-semibold">
|
|
||||||
{form_field_choices}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<BaseIcon
|
|
||||||
className={`${iconsColor}`}
|
|
||||||
w="w-16"
|
|
||||||
h="h-16"
|
|
||||||
size={48}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
path={'mdiPlaylistEdit' in icon ? icon['mdiPlaylistEdit' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>}
|
|
||||||
|
|
||||||
{hasPermission(currentUser, 'READ_FORM_SUBMISSIONS') && <Link href={'/form_submissions/form_submissions-list'}>
|
|
||||||
<div
|
|
||||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
|
||||||
>
|
|
||||||
<div className="flex justify-between align-center">
|
|
||||||
<div>
|
|
||||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
|
||||||
Form submissions
|
|
||||||
</div>
|
|
||||||
<div className="text-3xl leading-tight font-semibold">
|
|
||||||
{form_submissions}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<BaseIcon
|
|
||||||
className={`${iconsColor}`}
|
|
||||||
w="w-16"
|
|
||||||
h="h-16"
|
|
||||||
size={48}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
path={'mdiClipboardTextOutline' in icon ? icon['mdiClipboardTextOutline' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>}
|
|
||||||
|
|
||||||
{hasPermission(currentUser, 'READ_SUBMISSION_VALUES') && <Link href={'/submission_values/submission_values-list'}>
|
|
||||||
<div
|
|
||||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
|
||||||
>
|
|
||||||
<div className="flex justify-between align-center">
|
|
||||||
<div>
|
|
||||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
|
||||||
Submission values
|
|
||||||
</div>
|
|
||||||
<div className="text-3xl leading-tight font-semibold">
|
|
||||||
{submission_values}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<BaseIcon
|
|
||||||
className={`${iconsColor}`}
|
|
||||||
w="w-16"
|
|
||||||
h="h-16"
|
|
||||||
size={48}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
path={'mdiDatabaseOutline' in icon ? icon['mdiDatabaseOutline' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>}
|
|
||||||
|
|
||||||
{hasPermission(currentUser, 'READ_ACTIVITY_FEED_ITEMS') && <Link href={'/activity_feed_items/activity_feed_items-list'}>
|
|
||||||
<div
|
|
||||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
|
||||||
>
|
|
||||||
<div className="flex justify-between align-center">
|
|
||||||
<div>
|
|
||||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
|
||||||
Activity feed items
|
|
||||||
</div>
|
|
||||||
<div className="text-3xl leading-tight font-semibold">
|
|
||||||
{activity_feed_items}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<BaseIcon
|
|
||||||
className={`${iconsColor}`}
|
|
||||||
w="w-16"
|
|
||||||
h="h-16"
|
|
||||||
size={48}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
path={'mdiTimelineTextOutline' in icon ? icon['mdiTimelineTextOutline' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>}
|
|
||||||
|
|
||||||
{hasPermission(currentUser, 'READ_REPORTS') && <Link href={'/reports/reports-list'}>
|
|
||||||
<div
|
|
||||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
|
||||||
>
|
|
||||||
<div className="flex justify-between align-center">
|
|
||||||
<div>
|
|
||||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
|
||||||
Reports
|
|
||||||
</div>
|
|
||||||
<div className="text-3xl leading-tight font-semibold">
|
|
||||||
{reports}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<BaseIcon
|
|
||||||
className={`${iconsColor}`}
|
|
||||||
w="w-16"
|
|
||||||
h="h-16"
|
|
||||||
size={48}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
path={'mdiFileChartOutline' in icon ? icon['mdiFileChartOutline' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>}
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</SectionMain>
|
</SectionMain>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -1,166 +1,187 @@
|
|||||||
|
import React from 'react';
|
||||||
import React, { useEffect, useState } from 'react';
|
|
||||||
import type { ReactElement } from 'react';
|
import type { ReactElement } from 'react';
|
||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import BaseButton from '../components/BaseButton';
|
import BaseButton from '../components/BaseButton';
|
||||||
import CardBox from '../components/CardBox';
|
|
||||||
import SectionFullScreen from '../components/SectionFullScreen';
|
|
||||||
import LayoutGuest from '../layouts/Guest';
|
import LayoutGuest from '../layouts/Guest';
|
||||||
import BaseDivider from '../components/BaseDivider';
|
|
||||||
import BaseButtons from '../components/BaseButtons';
|
|
||||||
import { getPageTitle } from '../config';
|
import { getPageTitle } from '../config';
|
||||||
import { useAppSelector } from '../stores/hooks';
|
import BaseIcon from '../components/BaseIcon';
|
||||||
import CardBoxComponentTitle from "../components/CardBoxComponentTitle";
|
import * as icon from '@mdi/js';
|
||||||
import { getPexelsImage, getPexelsVideo } from '../helpers/pexels';
|
|
||||||
|
|
||||||
|
|
||||||
export default function Starter() {
|
|
||||||
const [illustrationImage, setIllustrationImage] = useState({
|
|
||||||
src: undefined,
|
|
||||||
photographer: undefined,
|
|
||||||
photographer_url: undefined,
|
|
||||||
})
|
|
||||||
const [illustrationVideo, setIllustrationVideo] = useState({video_files: []})
|
|
||||||
const [contentType, setContentType] = useState('image');
|
|
||||||
const [contentPosition, setContentPosition] = useState('left');
|
|
||||||
const textColor = useAppSelector((state) => state.style.linkColor);
|
|
||||||
|
|
||||||
|
export default function LandingPage() {
|
||||||
const title = 'Trial Tracker'
|
const title = 'Trial Tracker'
|
||||||
|
|
||||||
// Fetch Pexels image/video
|
|
||||||
useEffect(() => {
|
|
||||||
async function fetchData() {
|
|
||||||
const image = await getPexelsImage();
|
|
||||||
const video = await getPexelsVideo();
|
|
||||||
setIllustrationImage(image);
|
|
||||||
setIllustrationVideo(video);
|
|
||||||
}
|
|
||||||
fetchData();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const imageBlock = (image) => (
|
|
||||||
<div
|
|
||||||
className='hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3'
|
|
||||||
style={{
|
|
||||||
backgroundImage: `${
|
|
||||||
image
|
|
||||||
? `url(${image?.src?.original})`
|
|
||||||
: 'linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5))'
|
|
||||||
}`,
|
|
||||||
backgroundSize: 'cover',
|
|
||||||
backgroundPosition: 'left center',
|
|
||||||
backgroundRepeat: 'no-repeat',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className='flex justify-center w-full bg-blue-300/20'>
|
|
||||||
<a
|
|
||||||
className='text-[8px]'
|
|
||||||
href={image?.photographer_url}
|
|
||||||
target='_blank'
|
|
||||||
rel='noreferrer'
|
|
||||||
>
|
|
||||||
Photo by {image?.photographer} on Pexels
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const videoBlock = (video) => {
|
|
||||||
if (video?.video_files?.length > 0) {
|
|
||||||
return (
|
return (
|
||||||
<div className='hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3'>
|
<div className="min-h-screen bg-white text-gray-900 font-sans selection:bg-emerald-100 selection:text-emerald-900">
|
||||||
<video
|
|
||||||
className='absolute top-0 left-0 w-full h-full object-cover'
|
|
||||||
autoPlay
|
|
||||||
loop
|
|
||||||
muted
|
|
||||||
>
|
|
||||||
<source src={video?.video_files[0]?.link} type='video/mp4'/>
|
|
||||||
Your browser does not support the video tag.
|
|
||||||
</video>
|
|
||||||
<div className='flex justify-center w-full bg-blue-300/20 z-10'>
|
|
||||||
<a
|
|
||||||
className='text-[8px]'
|
|
||||||
href={video?.user?.url}
|
|
||||||
target='_blank'
|
|
||||||
rel='noreferrer'
|
|
||||||
>
|
|
||||||
Video by {video.user.name} on Pexels
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
style={
|
|
||||||
contentPosition === 'background'
|
|
||||||
? {
|
|
||||||
backgroundImage: `${
|
|
||||||
illustrationImage
|
|
||||||
? `url(${illustrationImage.src?.original})`
|
|
||||||
: 'linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5))'
|
|
||||||
}`,
|
|
||||||
backgroundSize: 'cover',
|
|
||||||
backgroundPosition: 'left center',
|
|
||||||
backgroundRepeat: 'no-repeat',
|
|
||||||
}
|
|
||||||
: {}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Head>
|
<Head>
|
||||||
<title>{getPageTitle('Starter Page')}</title>
|
<title>{getPageTitle('Trial Tracker - Research Simplified')}</title>
|
||||||
</Head>
|
</Head>
|
||||||
|
|
||||||
<SectionFullScreen bg='violet'>
|
{/* Navigation */}
|
||||||
<div
|
<nav className="fixed top-0 w-full z-50 bg-white/80 backdrop-blur-md border-b border-gray-100">
|
||||||
className={`flex ${
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
contentPosition === 'right' ? 'flex-row-reverse' : 'flex-row'
|
<div className="flex justify-between h-16 items-center">
|
||||||
} min-h-screen w-full`}
|
<div className="flex items-center space-x-2">
|
||||||
>
|
<div className="w-10 h-10 bg-emerald-600 rounded-xl flex items-center justify-center">
|
||||||
{contentType === 'image' && contentPosition !== 'background'
|
<BaseIcon path={icon.mdiFlaskOutline} size={24} className="text-white" />
|
||||||
? imageBlock(illustrationImage)
|
|
||||||
: null}
|
|
||||||
{contentType === 'video' && contentPosition !== 'background'
|
|
||||||
? videoBlock(illustrationVideo)
|
|
||||||
: null}
|
|
||||||
<div className='flex items-center justify-center flex-col space-y-4 w-full lg:w-full'>
|
|
||||||
<CardBox className='w-full md:w-3/5 lg:w-2/3'>
|
|
||||||
<CardBoxComponentTitle title="Welcome to your Trial Tracker app!"/>
|
|
||||||
|
|
||||||
<div className="space-y-3">
|
|
||||||
<p className='text-center text-gray-500'>This is a React.js/Node.js app generated by the <a className={`${textColor}`} href="https://flatlogic.com/generator">Flatlogic Web App Generator</a></p>
|
|
||||||
<p className='text-center text-gray-500'>For guides and documentation please check
|
|
||||||
your local README.md and the <a className={`${textColor}`} href="https://flatlogic.com/documentation">Flatlogic documentation</a></p>
|
|
||||||
</div>
|
</div>
|
||||||
|
<span className="text-2xl font-bold tracking-tight text-gray-900">{title}</span>
|
||||||
<BaseButtons>
|
|
||||||
<BaseButton
|
|
||||||
href='/login'
|
|
||||||
label='Login'
|
|
||||||
color='info'
|
|
||||||
className='w-full'
|
|
||||||
/>
|
|
||||||
|
|
||||||
</BaseButtons>
|
|
||||||
</CardBox>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="hidden md:flex space-x-8 items-center font-medium">
|
||||||
</SectionFullScreen>
|
<a href="#features" className="text-gray-600 hover:text-emerald-600 transition-colors">Features</a>
|
||||||
<div className='bg-black text-white flex flex-col text-center justify-center md:flex-row'>
|
<a href="#about" className="text-gray-600 hover:text-emerald-600 transition-colors">About</a>
|
||||||
<p className='py-6 text-sm'>© 2026 <span>{title}</span>. All rights reserved</p>
|
<Link href="/login" className="px-5 py-2.5 bg-emerald-600 text-white rounded-full hover:bg-emerald-700 transition-all shadow-md shadow-emerald-200">
|
||||||
<Link className='py-6 ml-4 text-sm' href='/privacy-policy/'>
|
Sign In
|
||||||
Privacy Policy
|
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{/* Hero Section */}
|
||||||
|
<header className="relative pt-32 pb-20 overflow-hidden">
|
||||||
|
<div className="absolute top-0 right-0 -z-10 w-1/2 h-full opacity-10 bg-[radial-gradient(circle_at_center,_var(--tw-gradient-stops))] from-emerald-400 via-transparent to-transparent"></div>
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="grid lg:grid-cols-2 gap-12 items-center">
|
||||||
|
<div className="space-y-8">
|
||||||
|
<div className="inline-flex items-center space-x-2 py-1.5 px-3 rounded-full bg-emerald-50 border border-emerald-100 text-emerald-700 text-sm font-semibold">
|
||||||
|
<span className="flex h-2 w-2 rounded-full bg-emerald-500 animate-pulse"></span>
|
||||||
|
<span>Advanced Multi-tenant Solution</span>
|
||||||
|
</div>
|
||||||
|
<h1 className="text-6xl md:text-7xl font-extrabold text-gray-900 leading-[1.1] tracking-tight">
|
||||||
|
Research <span className="text-transparent bg-clip-text bg-gradient-to-r from-emerald-600 to-teal-500">Tracked</span> Effortlessly.
|
||||||
|
</h1>
|
||||||
|
<p className="text-xl text-gray-600 leading-relaxed max-w-lg">
|
||||||
|
The ultimate tool for research trials. Manage projects, monitor trials, and capture field data in real-time with our beautiful, intuitive interface.
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-col sm:flex-row space-y-4 sm:space-y-0 sm:space-x-4 pt-4">
|
||||||
|
<Link href="/login" className="px-8 py-4 bg-gray-900 text-white rounded-2xl hover:bg-gray-800 transition-all text-lg font-bold shadow-xl shadow-gray-200 flex items-center justify-center">
|
||||||
|
Launch App <BaseIcon path={icon.mdiArrowRight} size={20} className="ml-2" />
|
||||||
|
</Link>
|
||||||
|
<a href="#features" className="px-8 py-4 bg-white text-gray-700 border-2 border-gray-100 rounded-2xl hover:border-emerald-200 hover:bg-emerald-50 transition-all text-lg font-bold flex items-center justify-center">
|
||||||
|
Learn More
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="relative">
|
||||||
|
<div className="bg-gradient-to-br from-emerald-100 to-teal-100 rounded-[3rem] p-4 shadow-2xl overflow-hidden aspect-video flex items-center justify-center border-8 border-white">
|
||||||
|
<div className="w-full h-full bg-white/40 backdrop-blur-sm rounded-[2rem] flex flex-col items-center justify-center space-y-4 text-emerald-800">
|
||||||
|
<BaseIcon path={icon.mdiChartTimelineVariant} size={80} className="opacity-40" />
|
||||||
|
<span className="text-2xl font-black uppercase tracking-widest opacity-40">Dashboard Preview</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* Decorative elements */}
|
||||||
|
<div className="absolute -bottom-6 -left-6 w-24 h-24 bg-teal-500 rounded-3xl -z-10 rotate-12 opacity-20 animate-bounce"></div>
|
||||||
|
<div className="absolute -top-6 -right-6 w-32 h-32 bg-emerald-500 rounded-full -z-10 opacity-10 animate-pulse"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{/* Features Section */}
|
||||||
|
<section id="features" className="py-24 bg-gray-50/50">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="text-center max-w-3xl mx-auto mb-20 space-y-4">
|
||||||
|
<h2 className="text-emerald-600 font-bold tracking-widest uppercase text-sm">Powerful Capabilities</h2>
|
||||||
|
<h3 className="text-4xl md:text-5xl font-black text-gray-900">Designed for modern field research.</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid md:grid-cols-3 gap-8">
|
||||||
|
{[
|
||||||
|
{
|
||||||
|
title: 'Activity Feed',
|
||||||
|
description: 'Stay updated with a social-media style feed of everything happening across your trials.',
|
||||||
|
icon: icon.mdiTimelineTextOutline,
|
||||||
|
color: 'bg-blue-50 text-blue-600'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Dynamic Forms',
|
||||||
|
description: 'Configure custom fields and forms for different trial types like plant vs chemical.',
|
||||||
|
icon: icon.mdiClipboardTextOutline,
|
||||||
|
color: 'bg-emerald-50 text-emerald-600'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Track-IT',
|
||||||
|
description: 'Analyze all completed forms and data in a powerful, filtered table view.',
|
||||||
|
icon: icon.mdiDatabaseOutline,
|
||||||
|
color: 'bg-teal-50 text-teal-600'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '3-Tier Locations',
|
||||||
|
description: 'Organize your data by Farm, Block, and Row for precise spatial tracking.',
|
||||||
|
icon: icon.mdiMapMarkerOutline,
|
||||||
|
color: 'bg-orange-50 text-orange-600'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Project Management',
|
||||||
|
description: 'Categorize trials into projects with custom start/end dates and status tracking.',
|
||||||
|
icon: icon.mdiFolderOutline,
|
||||||
|
color: 'bg-purple-50 text-purple-600'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Multi-tenant',
|
||||||
|
description: 'Seamlessly toggle between different farms as a Global Admin with full data isolation.',
|
||||||
|
icon: icon.mdiDomain,
|
||||||
|
color: 'bg-indigo-50 text-indigo-600'
|
||||||
|
}
|
||||||
|
].map((feature, i) => (
|
||||||
|
<div key={i} className="group bg-white p-10 rounded-[2.5rem] border border-gray-100 hover:border-emerald-200 hover:shadow-2xl hover:shadow-emerald-100/50 transition-all duration-500">
|
||||||
|
<div className={`w-16 h-16 ${feature.color} rounded-2xl flex items-center justify-center mb-8 group-hover:scale-110 transition-transform duration-500`}>
|
||||||
|
<BaseIcon path={feature.icon} size={32} />
|
||||||
|
</div>
|
||||||
|
<h4 className="text-2xl font-bold mb-4 text-gray-900">{feature.title}</h4>
|
||||||
|
<p className="text-gray-600 leading-relaxed font-medium">{feature.description}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* CTA Section */}
|
||||||
|
<section className="py-24">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="bg-emerald-600 rounded-[4rem] p-12 md:p-20 text-center relative overflow-hidden shadow-2xl shadow-emerald-200">
|
||||||
|
<div className="absolute top-0 left-0 w-full h-full bg-[radial-gradient(circle_at_top_left,_var(--tw-gradient-stops))] from-white/20 via-transparent to-transparent"></div>
|
||||||
|
<div className="relative z-10 space-y-8 max-w-3xl mx-auto">
|
||||||
|
<h2 className="text-4xl md:text-6xl font-black text-white leading-tight">Ready to transform your trial tracking?</h2>
|
||||||
|
<p className="text-xl text-emerald-50 leading-relaxed font-medium">
|
||||||
|
Join hundreds of researchers who trust Trial Tracker for their daily field operations.
|
||||||
|
</p>
|
||||||
|
<div className="pt-6">
|
||||||
|
<Link href="/login" className="px-12 py-5 bg-white text-emerald-600 rounded-2xl hover:bg-emerald-50 transition-all text-xl font-black shadow-xl inline-flex items-center">
|
||||||
|
Get Started Now <BaseIcon path={icon.mdiArrowRight} size={24} className="ml-2" />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<footer className="bg-white border-t border-gray-100 py-16">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="flex flex-col md:flex-row justify-between items-center space-y-8 md:space-y-0">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<div className="w-8 h-8 bg-emerald-600 rounded-lg flex items-center justify-center">
|
||||||
|
<BaseIcon path={icon.mdiFlaskOutline} size={18} className="text-white" />
|
||||||
|
</div>
|
||||||
|
<span className="text-xl font-black text-gray-900">{title}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex space-x-8 text-sm font-bold text-gray-500 uppercase tracking-widest">
|
||||||
|
<a href="#" className="hover:text-emerald-600 transition-colors">Privacy</a>
|
||||||
|
<a href="#" className="hover:text-emerald-600 transition-colors">Terms</a>
|
||||||
|
<a href="#" className="hover:text-emerald-600 transition-colors">Contact</a>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm font-bold text-gray-400 tracking-tighter uppercase">
|
||||||
|
© 2026 Trial Tracker. Built for Research.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Starter.getLayout = function getLayout(page: ReactElement) {
|
LandingPage.getLayout = function getLayout(page: ReactElement) {
|
||||||
return <LayoutGuest>{page}</LayoutGuest>;
|
return <LayoutGuest>{page}</LayoutGuest>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
|
|
||||||
|
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import type { ReactElement } from 'react';
|
import type { ReactElement } from 'react';
|
||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
@ -62,11 +60,15 @@ export default function Login() {
|
|||||||
dispatch(findMe());
|
dispatch(findMe());
|
||||||
}
|
}
|
||||||
}, [token, dispatch]);
|
}, [token, dispatch]);
|
||||||
// Redirect to dashboard if user is logged in
|
// Redirect based on role if user is logged in
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (currentUser?.id) {
|
if (currentUser?.id) {
|
||||||
|
if (currentUser.app_role?.globalAccess) {
|
||||||
|
router.push('/tenants/tenants-list');
|
||||||
|
} else {
|
||||||
router.push('/dashboard');
|
router.push('/dashboard');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}, [currentUser?.id, router]);
|
}, [currentUser?.id, router]);
|
||||||
// Show error message if there is one
|
// Show error message if there is one
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@ -40,60 +40,30 @@ const EditOrganizationsPage = () => {
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
const initVals = {
|
const initVals = {
|
||||||
|
|
||||||
|
|
||||||
'name': '',
|
'name': '',
|
||||||
|
'navOrientation': 'top',
|
||||||
|
'defaultView': 'list',
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
const [initialValues, setInitialValues] = useState(initVals)
|
const [initialValues, setInitialValues] = useState(initVals)
|
||||||
|
|
||||||
const { organizations } = useAppSelector((state) => state.organizations)
|
const { organizations } = useAppSelector((state) => state.organizations)
|
||||||
|
|
||||||
const { currentUser } = useAppSelector((state) => state.auth);
|
const { currentUser } = useAppSelector((state) => state.auth);
|
||||||
|
|
||||||
|
|
||||||
const { id } = router.query
|
const { id } = router.query
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (id) {
|
||||||
dispatch(fetch({ id: id }))
|
dispatch(fetch({ id: id }))
|
||||||
|
}
|
||||||
}, [id])
|
}, [id])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof organizations === 'object') {
|
if (typeof organizations === 'object' && organizations !== null) {
|
||||||
setInitialValues(organizations)
|
|
||||||
}
|
|
||||||
}, [organizations])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (typeof organizations === 'object') {
|
|
||||||
const newInitialVal = {...initVals};
|
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);
|
setInitialValues(newInitialVal);
|
||||||
}
|
}
|
||||||
}, [organizations])
|
}, [organizations])
|
||||||
@ -119,9 +89,6 @@ const EditOrganizationsPage = () => {
|
|||||||
onSubmit={(values) => handleSubmit(values)}
|
onSubmit={(values) => handleSubmit(values)}
|
||||||
>
|
>
|
||||||
<Form>
|
<Form>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
label="Name"
|
label="Name"
|
||||||
>
|
>
|
||||||
@ -131,33 +98,19 @@ const EditOrganizationsPage = () => {
|
|||||||
/>
|
/>
|
||||||
</FormField>
|
</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="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 />
|
<BaseDivider />
|
||||||
<BaseButtons>
|
<BaseButtons>
|
||||||
@ -176,9 +129,7 @@ const EditOrganizationsPage = () => {
|
|||||||
EditOrganizationsPage.getLayout = function getLayout(page: ReactElement) {
|
EditOrganizationsPage.getLayout = function getLayout(page: ReactElement) {
|
||||||
return (
|
return (
|
||||||
<LayoutAuthenticated
|
<LayoutAuthenticated
|
||||||
|
|
||||||
permission={'UPDATE_ORGANIZATIONS'}
|
permission={'UPDATE_ORGANIZATIONS'}
|
||||||
|
|
||||||
>
|
>
|
||||||
{page}
|
{page}
|
||||||
</LayoutAuthenticated>
|
</LayoutAuthenticated>
|
||||||
|
|||||||
@ -40,288 +40,16 @@ const EditProjects = () => {
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
const initVals = {
|
const initVals = {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
tenant: null,
|
tenant: null,
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
'name': '',
|
'name': '',
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
description: '',
|
description: '',
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
start_date: new Date(),
|
start_date: new Date(),
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
estimated_end_date: new Date(),
|
estimated_end_date: new Date(),
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
status: '',
|
status: '',
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
location: null,
|
location: null,
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
attachments: [],
|
attachments: [],
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
images: [],
|
images: [],
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
organizations: null,
|
organizations: null,
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
const [initialValues, setInitialValues] = useState(initVals)
|
const [initialValues, setInitialValues] = useState(initVals)
|
||||||
|
|
||||||
@ -358,6 +86,9 @@ const EditProjects = () => {
|
|||||||
await router.push('/projects/projects-list')
|
await router.push('/projects/projects-list')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hasTenant = !!(currentUser?.tenant?.id || currentUser?.tenantId);
|
||||||
|
const hasOrganizations = !!(currentUser?.organizations?.id || currentUser?.organizationsId || currentUser?.tenant?.organizationsId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
@ -374,28 +105,7 @@ const EditProjects = () => {
|
|||||||
onSubmit={(values) => handleSubmit(values)}
|
onSubmit={(values) => handleSubmit(values)}
|
||||||
>
|
>
|
||||||
<Form>
|
<Form>
|
||||||
|
{!hasTenant && (
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<FormField label='Tenant' labelFor='tenant'>
|
<FormField label='Tenant' labelFor='tenant'>
|
||||||
<Field
|
<Field
|
||||||
name='tenant'
|
name='tenant'
|
||||||
@ -403,55 +113,10 @@ const EditProjects = () => {
|
|||||||
component={SelectField}
|
component={SelectField}
|
||||||
options={initialValues.tenant}
|
options={initialValues.tenant}
|
||||||
itemRef={'tenants'}
|
itemRef={'tenants'}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
showField={'name'}
|
showField={'name'}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
></Field>
|
></Field>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
)}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
label="Name"
|
label="Name"
|
||||||
@ -462,38 +127,6 @@ const EditProjects = () => {
|
|||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<FormField label='Description' hasTextareaHeight>
|
<FormField label='Description' hasTextareaHeight>
|
||||||
<Field
|
<Field
|
||||||
name='description'
|
name='description'
|
||||||
@ -502,40 +135,6 @@ const EditProjects = () => {
|
|||||||
></Field>
|
></Field>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
label="StartDate"
|
label="StartDate"
|
||||||
>
|
>
|
||||||
@ -551,34 +150,6 @@ const EditProjects = () => {
|
|||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
label="EstimatedEndDate"
|
label="EstimatedEndDate"
|
||||||
>
|
>
|
||||||
@ -594,87 +165,16 @@ const EditProjects = () => {
|
|||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<FormField label="Status" labelFor="status">
|
<FormField label="Status" labelFor="status">
|
||||||
<Field name="status" id="status" component="select">
|
<Field name="status" id="status" component="select">
|
||||||
|
|
||||||
<option value="planned">planned</option>
|
<option value="planned">planned</option>
|
||||||
|
|
||||||
<option value="active">active</option>
|
<option value="active">active</option>
|
||||||
|
|
||||||
<option value="on_hold">on_hold</option>
|
<option value="on_hold">on_hold</option>
|
||||||
|
|
||||||
<option value="completed">completed</option>
|
<option value="completed">completed</option>
|
||||||
|
|
||||||
<option value="cancelled">cancelled</option>
|
<option value="cancelled">cancelled</option>
|
||||||
|
|
||||||
</Field>
|
</Field>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<FormField label='Location' labelFor='location'>
|
<FormField label='Location' labelFor='location'>
|
||||||
<Field
|
<Field
|
||||||
name='location'
|
name='location'
|
||||||
@ -682,80 +182,10 @@ const EditProjects = () => {
|
|||||||
component={SelectField}
|
component={SelectField}
|
||||||
options={initialValues.location}
|
options={initialValues.location}
|
||||||
itemRef={'locations'}
|
itemRef={'locations'}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
showField={'name'}
|
showField={'name'}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
></Field>
|
></Field>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<FormField>
|
<FormField>
|
||||||
<Field
|
<Field
|
||||||
label='Attachments'
|
label='Attachments'
|
||||||
@ -772,32 +202,6 @@ const EditProjects = () => {
|
|||||||
></Field>
|
></Field>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<FormField>
|
<FormField>
|
||||||
<Field
|
<Field
|
||||||
label='Images'
|
label='Images'
|
||||||
@ -814,31 +218,7 @@ const EditProjects = () => {
|
|||||||
></Field>
|
></Field>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
|
{!hasOrganizations && (
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<FormField label='organizations' labelFor='organizations'>
|
<FormField label='organizations' labelFor='organizations'>
|
||||||
<Field
|
<Field
|
||||||
name='organizations'
|
name='organizations'
|
||||||
@ -846,55 +226,10 @@ const EditProjects = () => {
|
|||||||
component={SelectField}
|
component={SelectField}
|
||||||
options={initialValues.organizations}
|
options={initialValues.organizations}
|
||||||
itemRef={'organizations'}
|
itemRef={'organizations'}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
showField={'name'}
|
showField={'name'}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
></Field>
|
></Field>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
)}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<BaseDivider />
|
<BaseDivider />
|
||||||
<BaseButtons>
|
<BaseButtons>
|
||||||
@ -913,9 +248,7 @@ const EditProjects = () => {
|
|||||||
EditProjects.getLayout = function getLayout(page: ReactElement) {
|
EditProjects.getLayout = function getLayout(page: ReactElement) {
|
||||||
return (
|
return (
|
||||||
<LayoutAuthenticated
|
<LayoutAuthenticated
|
||||||
|
|
||||||
permission={'UPDATE_PROJECTS'}
|
permission={'UPDATE_PROJECTS'}
|
||||||
|
|
||||||
>
|
>
|
||||||
{page}
|
{page}
|
||||||
</LayoutAuthenticated>
|
</LayoutAuthenticated>
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js'
|
import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js'
|
||||||
import Head from 'next/head'
|
import Head from 'next/head'
|
||||||
import React, { ReactElement, useEffect, useState } from 'react'
|
import React, { ReactElement, useEffect, useState, useMemo } from 'react'
|
||||||
import DatePicker from "react-datepicker";
|
import DatePicker from "react-datepicker";
|
||||||
import "react-datepicker/dist/react-datepicker.css";
|
import "react-datepicker/dist/react-datepicker.css";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
@ -16,352 +16,80 @@ import FormField from '../../components/FormField'
|
|||||||
import BaseDivider from '../../components/BaseDivider'
|
import BaseDivider from '../../components/BaseDivider'
|
||||||
import BaseButtons from '../../components/BaseButtons'
|
import BaseButtons from '../../components/BaseButtons'
|
||||||
import BaseButton from '../../components/BaseButton'
|
import BaseButton from '../../components/BaseButton'
|
||||||
import FormCheckRadio from '../../components/FormCheckRadio'
|
|
||||||
import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'
|
|
||||||
import FormFilePicker from '../../components/FormFilePicker'
|
import FormFilePicker from '../../components/FormFilePicker'
|
||||||
import FormImagePicker from '../../components/FormImagePicker'
|
import FormImagePicker from '../../components/FormImagePicker'
|
||||||
import { SelectField } from "../../components/SelectField";
|
import { SelectField } from "../../components/SelectField";
|
||||||
import { SelectFieldMany } from "../../components/SelectFieldMany";
|
|
||||||
import { SwitchField } from '../../components/SwitchField'
|
|
||||||
import {RichTextField} from "../../components/RichTextField";
|
import {RichTextField} from "../../components/RichTextField";
|
||||||
|
|
||||||
import { update, fetch } from '../../stores/projects/projectsSlice'
|
import { update, fetch } from '../../stores/projects/projectsSlice'
|
||||||
import { useAppDispatch, useAppSelector } from '../../stores/hooks'
|
import { useAppDispatch, useAppSelector } from '../../stores/hooks'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import {saveFile} from "../../helpers/fileSaver";
|
|
||||||
import dataFormatter from '../../helpers/dataFormatter';
|
|
||||||
import ImageField from "../../components/ImageField";
|
|
||||||
|
|
||||||
import {hasPermission} from "../../helpers/userPermissions";
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const EditProjectsPage = () => {
|
const EditProjectsPage = () => {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const dispatch = useAppDispatch()
|
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)
|
|
||||||
|
|
||||||
const { projects } = useAppSelector((state) => state.projects)
|
|
||||||
|
|
||||||
|
const { projects: projectsRaw } = useAppSelector((state) => state.projects)
|
||||||
const { currentUser } = useAppSelector((state) => state.auth);
|
const { currentUser } = useAppSelector((state) => state.auth);
|
||||||
|
|
||||||
|
|
||||||
const { id } = router.query
|
const { id } = router.query
|
||||||
|
|
||||||
useEffect(() => {
|
const initVals = useMemo(() => ({
|
||||||
dispatch(fetch({ id: id }))
|
tenant: null,
|
||||||
}, [id])
|
name: '',
|
||||||
|
description: '',
|
||||||
|
insights: '',
|
||||||
|
start_date: null,
|
||||||
|
estimated_end_date: null,
|
||||||
|
status: '',
|
||||||
|
location: null,
|
||||||
|
attachments: [],
|
||||||
|
images: [],
|
||||||
|
organizations: null,
|
||||||
|
}), []);
|
||||||
|
|
||||||
useEffect(() => {
|
const [initialValues, setInitialValues] = useState(initVals)
|
||||||
if (typeof projects === 'object') {
|
|
||||||
setInitialValues(projects)
|
// Ensure we are working with the correct project object
|
||||||
|
const projects = useMemo(() => {
|
||||||
|
if (Array.isArray(projectsRaw)) {
|
||||||
|
return projectsRaw.find(p => p.id === id) || null;
|
||||||
}
|
}
|
||||||
}, [projects])
|
return projectsRaw;
|
||||||
|
}, [projectsRaw, id]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof projects === 'object') {
|
if (id) {
|
||||||
|
dispatch(fetch({ id }));
|
||||||
|
}
|
||||||
|
}, [id, dispatch])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (projects && !Array.isArray(projects) && projects.id === id) {
|
||||||
const newInitialVal = { ...initVals };
|
const newInitialVal = { ...initVals };
|
||||||
Object.keys(initVals).forEach(el => newInitialVal[el] = (projects)[el])
|
Object.keys(initVals).forEach(key => {
|
||||||
|
if (projects[key] !== undefined) {
|
||||||
|
newInitialVal[key] = projects[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
setInitialValues(newInitialVal);
|
setInitialValues(newInitialVal);
|
||||||
}
|
}
|
||||||
}, [projects])
|
}, [projects, id, initVals])
|
||||||
|
|
||||||
const handleSubmit = async (data) => {
|
const handleSubmit = async (data) => {
|
||||||
await dispatch(update({ id: id, data }))
|
await dispatch(update({ id: id, data }))
|
||||||
await router.push('/projects/projects-list')
|
await router.push('/projects/projects-list')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hasTenant = !!(currentUser?.tenant?.id || currentUser?.tenantId);
|
||||||
|
const hasOrganizations = !!(currentUser?.organizations?.id || currentUser?.organizationsId || currentUser?.tenant?.organizationsId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
<title>{getPageTitle('Edit projects')}</title>
|
<title>{getPageTitle('Edit Project')}</title>
|
||||||
</Head>
|
</Head>
|
||||||
<SectionMain>
|
<SectionMain>
|
||||||
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={'Edit projects'} main>
|
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={'Edit Project'} main>
|
||||||
{''}
|
{''}
|
||||||
</SectionTitleLineWithButton>
|
</SectionTitleLineWithButton>
|
||||||
<CardBox>
|
<CardBox>
|
||||||
@ -370,127 +98,25 @@ const EditProjectsPage = () => {
|
|||||||
initialValues={initialValues}
|
initialValues={initialValues}
|
||||||
onSubmit={(values) => handleSubmit(values)}
|
onSubmit={(values) => handleSubmit(values)}
|
||||||
>
|
>
|
||||||
|
{({ setFieldValue, values }) => (
|
||||||
<Form>
|
<Form>
|
||||||
|
{!hasTenant && (
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<FormField label='Tenant' labelFor='tenant'>
|
<FormField label='Tenant' labelFor='tenant'>
|
||||||
<Field
|
<Field
|
||||||
name='tenant'
|
name='tenant'
|
||||||
id='tenant'
|
id='tenant'
|
||||||
component={SelectField}
|
component={SelectField}
|
||||||
options={initialValues.tenant}
|
options={values.tenant}
|
||||||
itemRef={'tenants'}
|
itemRef={'tenants'}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
showField={'name'}
|
showField={'name'}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
></Field>
|
></Field>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<FormField label="Name">
|
||||||
|
<Field name="name" placeholder="Project Name" />
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
label="Name"
|
|
||||||
>
|
|
||||||
<Field
|
|
||||||
name="name"
|
|
||||||
placeholder="Name"
|
|
||||||
/>
|
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<FormField label='Description' hasTextareaHeight>
|
<FormField label='Description' hasTextareaHeight>
|
||||||
<Field
|
<Field
|
||||||
name='description'
|
name='description'
|
||||||
@ -499,260 +125,60 @@ const EditProjectsPage = () => {
|
|||||||
></Field>
|
></Field>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
|
<FormField label='Strategic Insights' help="Add observations, summaries or strategic notes" hasTextareaHeight>
|
||||||
|
<Field
|
||||||
|
name='insights'
|
||||||
|
id='insights'
|
||||||
|
component='textarea'
|
||||||
|
className="bg-blue-50/50 border-blue-100 focus:border-blue-300 transition-colors rounded p-3 w-full"
|
||||||
|
rows={4}
|
||||||
|
></Field>
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<FormField label="Start Date">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
label="StartDate"
|
|
||||||
>
|
|
||||||
<DatePicker
|
<DatePicker
|
||||||
dateFormat="yyyy-MM-dd hh:mm"
|
dateFormat="yyyy-MM-dd HH:mm"
|
||||||
showTimeSelect
|
showTimeSelect
|
||||||
selected={initialValues.start_date ?
|
selected={values.start_date ? new Date(values.start_date) : null}
|
||||||
new Date(
|
onChange={(date) => setFieldValue('start_date', date)}
|
||||||
dayjs(initialValues.start_date).format('YYYY-MM-DD hh:mm'),
|
className="w-full border-gray-300 rounded focus:ring-blue-500"
|
||||||
) : null
|
|
||||||
}
|
|
||||||
onChange={(date) => setInitialValues({...initialValues, 'start_date': date})}
|
|
||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
|
<FormField label="Estimated End Date">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
label="EstimatedEndDate"
|
|
||||||
>
|
|
||||||
<DatePicker
|
<DatePicker
|
||||||
dateFormat="yyyy-MM-dd hh:mm"
|
dateFormat="yyyy-MM-dd HH:mm"
|
||||||
showTimeSelect
|
showTimeSelect
|
||||||
selected={initialValues.estimated_end_date ?
|
selected={values.estimated_end_date ? new Date(values.estimated_end_date) : null}
|
||||||
new Date(
|
onChange={(date) => setFieldValue('estimated_end_date', date)}
|
||||||
dayjs(initialValues.estimated_end_date).format('YYYY-MM-DD hh:mm'),
|
className="w-full border-gray-300 rounded focus:ring-blue-500"
|
||||||
) : null
|
|
||||||
}
|
|
||||||
onChange={(date) => setInitialValues({...initialValues, 'estimated_end_date': date})}
|
|
||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<FormField label="Status" labelFor="status">
|
<FormField label="Status" labelFor="status">
|
||||||
<Field name="status" id="status" component="select">
|
<Field name="status" id="status" component="select">
|
||||||
|
<option value="">Select Status</option>
|
||||||
<option value="planned">planned</option>
|
<option value="planned">planned</option>
|
||||||
|
|
||||||
<option value="active">active</option>
|
<option value="active">active</option>
|
||||||
|
|
||||||
<option value="on_hold">on_hold</option>
|
<option value="on_hold">on_hold</option>
|
||||||
|
|
||||||
<option value="completed">completed</option>
|
<option value="completed">completed</option>
|
||||||
|
|
||||||
<option value="cancelled">cancelled</option>
|
<option value="cancelled">cancelled</option>
|
||||||
|
|
||||||
</Field>
|
</Field>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<FormField label='Location' labelFor='location'>
|
<FormField label='Location' labelFor='location'>
|
||||||
<Field
|
<Field
|
||||||
name='location'
|
name='location'
|
||||||
id='location'
|
id='location'
|
||||||
component={SelectField}
|
component={SelectField}
|
||||||
options={initialValues.location}
|
options={values.location}
|
||||||
itemRef={'locations'}
|
itemRef={'locations'}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
showField={'name'}
|
showField={'name'}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
></Field>
|
></Field>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<FormField>
|
<FormField>
|
||||||
<Field
|
<Field
|
||||||
label='Attachments'
|
label='Attachments'
|
||||||
@ -769,32 +195,6 @@ const EditProjectsPage = () => {
|
|||||||
></Field>
|
></Field>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<FormField>
|
<FormField>
|
||||||
<Field
|
<Field
|
||||||
label='Images'
|
label='Images'
|
||||||
@ -811,95 +211,27 @@ const EditProjectsPage = () => {
|
|||||||
></Field>
|
></Field>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
|
{!hasOrganizations && (
|
||||||
|
<FormField label='Organizations' labelFor='organizations'>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<FormField label='organizations' labelFor='organizations'>
|
|
||||||
<Field
|
<Field
|
||||||
name='organizations'
|
name='organizations'
|
||||||
id='organizations'
|
id='organizations'
|
||||||
component={SelectField}
|
component={SelectField}
|
||||||
options={initialValues.organizations}
|
options={values.organizations}
|
||||||
itemRef={'organizations'}
|
itemRef={'organizations'}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
showField={'name'}
|
showField={'name'}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
></Field>
|
></Field>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
)}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<BaseDivider />
|
<BaseDivider />
|
||||||
<BaseButtons>
|
<BaseButtons>
|
||||||
<BaseButton type="submit" color="info" label="Submit" />
|
<BaseButton type="submit" color="info" label="Submit" />
|
||||||
<BaseButton type="reset" color="info" outline label="Reset" />
|
<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>
|
</BaseButtons>
|
||||||
</Form>
|
</Form>
|
||||||
|
)}
|
||||||
</Formik>
|
</Formik>
|
||||||
</CardBox>
|
</CardBox>
|
||||||
</SectionMain>
|
</SectionMain>
|
||||||
@ -909,11 +241,7 @@ const EditProjectsPage = () => {
|
|||||||
|
|
||||||
EditProjectsPage.getLayout = function getLayout(page: ReactElement) {
|
EditProjectsPage.getLayout = function getLayout(page: ReactElement) {
|
||||||
return (
|
return (
|
||||||
<LayoutAuthenticated
|
<LayoutAuthenticated>
|
||||||
|
|
||||||
permission={'UPDATE_PROJECTS'}
|
|
||||||
|
|
||||||
>
|
|
||||||
{page}
|
{page}
|
||||||
</LayoutAuthenticated>
|
</LayoutAuthenticated>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { mdiChartTimelineVariant } from '@mdi/js'
|
import { mdiChartTimelineVariant } from '@mdi/js'
|
||||||
import Head from 'next/head'
|
import Head from 'next/head'
|
||||||
import { uniqueId } from 'lodash';
|
import { uniqueId } from 'lodash';
|
||||||
import React, { ReactElement, useState } from 'react'
|
import React, { ReactElement, useState, useEffect } from 'react'
|
||||||
import CardBox from '../../components/CardBox'
|
import CardBox from '../../components/CardBox'
|
||||||
import LayoutAuthenticated from '../../layouts/Authenticated'
|
import LayoutAuthenticated from '../../layouts/Authenticated'
|
||||||
import SectionMain from '../../components/SectionMain'
|
import SectionMain from '../../components/SectionMain'
|
||||||
@ -25,10 +25,15 @@ const ProjectsTablesPage = () => {
|
|||||||
const [filterItems, setFilterItems] = useState([]);
|
const [filterItems, setFilterItems] = useState([]);
|
||||||
const [csvFile, setCsvFile] = useState<File | null>(null);
|
const [csvFile, setCsvFile] = useState<File | null>(null);
|
||||||
const [isModalActive, setIsModalActive] = useState(false);
|
const [isModalActive, setIsModalActive] = useState(false);
|
||||||
const [showTableView, setShowTableView] = useState(false);
|
|
||||||
|
|
||||||
|
|
||||||
const { currentUser } = useAppSelector((state) => state.auth);
|
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();
|
const dispatch = useAppDispatch();
|
||||||
@ -125,7 +130,12 @@ const ProjectsTablesPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='md:inline-flex items-center ms-auto'>
|
<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>
|
</div>
|
||||||
|
|
||||||
</CardBox>
|
</CardBox>
|
||||||
@ -134,7 +144,7 @@ const ProjectsTablesPage = () => {
|
|||||||
filterItems={filterItems}
|
filterItems={filterItems}
|
||||||
setFilterItems={setFilterItems}
|
setFilterItems={setFilterItems}
|
||||||
filters={filters}
|
filters={filters}
|
||||||
showGrid={false}
|
showGrid={showTableView}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
</SectionMain>
|
</SectionMain>
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { mdiAccount, mdiChartTimelineVariant, mdiMail, mdiUpload } from '@mdi/js'
|
import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js'
|
||||||
import Head from 'next/head'
|
import Head from 'next/head'
|
||||||
import React, { ReactElement } from 'react'
|
import React, { ReactElement, useMemo } from 'react'
|
||||||
import CardBox from '../../components/CardBox'
|
import CardBox from '../../components/CardBox'
|
||||||
import LayoutAuthenticated from '../../layouts/Authenticated'
|
import LayoutAuthenticated from '../../layouts/Authenticated'
|
||||||
import SectionMain from '../../components/SectionMain'
|
import SectionMain from '../../components/SectionMain'
|
||||||
@ -12,289 +12,77 @@ import FormField from '../../components/FormField'
|
|||||||
import BaseDivider from '../../components/BaseDivider'
|
import BaseDivider from '../../components/BaseDivider'
|
||||||
import BaseButtons from '../../components/BaseButtons'
|
import BaseButtons from '../../components/BaseButtons'
|
||||||
import BaseButton from '../../components/BaseButton'
|
import BaseButton from '../../components/BaseButton'
|
||||||
import FormCheckRadio from '../../components/FormCheckRadio'
|
|
||||||
import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'
|
|
||||||
import FormFilePicker from '../../components/FormFilePicker'
|
import FormFilePicker from '../../components/FormFilePicker'
|
||||||
import FormImagePicker from '../../components/FormImagePicker'
|
import FormImagePicker from '../../components/FormImagePicker'
|
||||||
import { SwitchField } from '../../components/SwitchField'
|
|
||||||
|
|
||||||
import { SelectField } from '../../components/SelectField'
|
import { SelectField } from '../../components/SelectField'
|
||||||
import { SelectFieldMany } from "../../components/SelectFieldMany";
|
|
||||||
import {RichTextField} from "../../components/RichTextField";
|
import {RichTextField} from "../../components/RichTextField";
|
||||||
|
|
||||||
import { create } from '../../stores/projects/projectsSlice'
|
import { create } from '../../stores/projects/projectsSlice'
|
||||||
import { useAppDispatch } from '../../stores/hooks'
|
import { useAppDispatch, useAppSelector } from '../../stores/hooks'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import moment from 'moment';
|
|
||||||
|
|
||||||
const initialValues = {
|
const initialValues = {
|
||||||
|
tenant: null,
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
tenant: '',
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
name: '',
|
name: '',
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
description: '',
|
description: '',
|
||||||
|
insights: '',
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
start_date: '',
|
start_date: '',
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
estimated_end_date: '',
|
estimated_end_date: '',
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
status: 'planned',
|
status: 'planned',
|
||||||
|
location: null,
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
location: '',
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
attachments: [],
|
attachments: [],
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
images: [],
|
images: [],
|
||||||
|
organizations: null,
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
organizations: '',
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const ProjectsNew = () => {
|
const ProjectsNew = () => {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const dispatch = useAppDispatch()
|
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) => {
|
const handleSubmit = async (data) => {
|
||||||
await dispatch(create(data))
|
await dispatch(create(data))
|
||||||
await router.push('/projects/projects-list')
|
await router.push('/projects/projects-list')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hasTenant = !!(currentUser?.tenant?.id || currentUser?.tenantId)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
<title>{getPageTitle('New Item')}</title>
|
<title>{getPageTitle('New Project')}</title>
|
||||||
</Head>
|
</Head>
|
||||||
<SectionMain>
|
<SectionMain>
|
||||||
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="New Item" main>
|
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="New Project" main>
|
||||||
{''}
|
{''}
|
||||||
</SectionTitleLineWithButton>
|
</SectionTitleLineWithButton>
|
||||||
<CardBox>
|
<CardBox>
|
||||||
<Formik
|
<Formik
|
||||||
initialValues={
|
initialValues={memoizedInitialValues}
|
||||||
|
|
||||||
initialValues
|
|
||||||
|
|
||||||
}
|
|
||||||
onSubmit={(values) => handleSubmit(values)}
|
onSubmit={(values) => handleSubmit(values)}
|
||||||
|
enableReinitialize
|
||||||
>
|
>
|
||||||
<Form>
|
<Form>
|
||||||
|
{!hasTenant && (
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<FormField label="Tenant" labelFor="tenant">
|
<FormField label="Tenant" labelFor="tenant">
|
||||||
<Field name="tenant" id="tenant" component={SelectField} options={[]} itemRef={'tenants'}></Field>
|
<Field name="tenant" id="tenant" component={SelectField} options={null} itemRef={'tenants'}></Field>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<FormField label="Name">
|
||||||
|
<Field name="name" placeholder="Project Name" />
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
label="Name"
|
|
||||||
>
|
|
||||||
<Field
|
|
||||||
name="name"
|
|
||||||
placeholder="Name"
|
|
||||||
/>
|
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<FormField label='Description' hasTextareaHeight>
|
<FormField label='Description' hasTextareaHeight>
|
||||||
<Field
|
<Field
|
||||||
name='description'
|
name='description'
|
||||||
@ -303,195 +91,48 @@ const ProjectsNew = () => {
|
|||||||
></Field>
|
></Field>
|
||||||
</FormField>
|
</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">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
label="StartDate"
|
|
||||||
>
|
|
||||||
<Field
|
<Field
|
||||||
type="datetime-local"
|
type="datetime-local"
|
||||||
name="start_date"
|
name="start_date"
|
||||||
placeholder="StartDate"
|
className="w-full border-gray-300 rounded"
|
||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
|
<FormField label="Estimated End Date">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
label="EstimatedEndDate"
|
|
||||||
>
|
|
||||||
<Field
|
<Field
|
||||||
type="datetime-local"
|
type="datetime-local"
|
||||||
name="estimated_end_date"
|
name="estimated_end_date"
|
||||||
placeholder="EstimatedEndDate"
|
className="w-full border-gray-300 rounded"
|
||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<FormField label="Status" labelFor="status">
|
<FormField label="Status" labelFor="status">
|
||||||
<Field name="status" id="status" component="select">
|
<Field name="status" id="status" component="select">
|
||||||
|
|
||||||
<option value="planned">planned</option>
|
<option value="planned">planned</option>
|
||||||
|
|
||||||
<option value="active">active</option>
|
<option value="active">active</option>
|
||||||
|
|
||||||
<option value="on_hold">on_hold</option>
|
<option value="on_hold">on_hold</option>
|
||||||
|
|
||||||
<option value="completed">completed</option>
|
<option value="completed">completed</option>
|
||||||
|
|
||||||
<option value="cancelled">cancelled</option>
|
<option value="cancelled">cancelled</option>
|
||||||
|
|
||||||
</Field>
|
</Field>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<FormField label="Location" labelFor="location">
|
<FormField label="Location" labelFor="location">
|
||||||
<Field name="location" id="location" component={SelectField} options={[]} itemRef={'locations'}></Field>
|
<Field name="location" id="location" component={SelectField} options={null} itemRef={'locations'}></Field>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<FormField>
|
<FormField>
|
||||||
<Field
|
<Field
|
||||||
label='Attachments'
|
label='Attachments'
|
||||||
@ -508,31 +149,6 @@ const ProjectsNew = () => {
|
|||||||
></Field>
|
></Field>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<FormField>
|
<FormField>
|
||||||
<Field
|
<Field
|
||||||
label='Images'
|
label='Images'
|
||||||
@ -549,43 +165,17 @@ const ProjectsNew = () => {
|
|||||||
></Field>
|
></Field>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
|
{!memoizedInitialValues.organizations && (
|
||||||
|
<FormField label="Organizations" labelFor="organizations">
|
||||||
|
<Field name="organizations" id="organizations" component={SelectField} options={null} itemRef={'organizations'}></Field>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<FormField label="organizations" labelFor="organizations">
|
|
||||||
<Field name="organizations" id="organizations" component={SelectField} options={[]} itemRef={'organizations'}></Field>
|
|
||||||
</FormField>
|
</FormField>
|
||||||
|
)}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<BaseDivider />
|
<BaseDivider />
|
||||||
<BaseButtons>
|
<BaseButtons>
|
||||||
<BaseButton type="submit" color="info" label="Submit" />
|
<BaseButton type="submit" color="info" label="Submit" />
|
||||||
<BaseButton type="reset" color="info" outline label="Reset" />
|
<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>
|
</BaseButtons>
|
||||||
</Form>
|
</Form>
|
||||||
</Formik>
|
</Formik>
|
||||||
@ -597,11 +187,7 @@ const ProjectsNew = () => {
|
|||||||
|
|
||||||
ProjectsNew.getLayout = function getLayout(page: ReactElement) {
|
ProjectsNew.getLayout = function getLayout(page: ReactElement) {
|
||||||
return (
|
return (
|
||||||
<LayoutAuthenticated
|
<LayoutAuthenticated>
|
||||||
|
|
||||||
permission={'CREATE_PROJECTS'}
|
|
||||||
|
|
||||||
>
|
|
||||||
{page}
|
{page}
|
||||||
</LayoutAuthenticated>
|
</LayoutAuthenticated>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import React, { ReactElement, useEffect } from 'react';
|
import React, { ReactElement, useEffect, useMemo } from 'react';
|
||||||
import Head from 'next/head'
|
import Head from 'next/head'
|
||||||
import DatePicker from "react-datepicker";
|
|
||||||
import "react-datepicker/dist/react-datepicker.css";
|
import "react-datepicker/dist/react-datepicker.css";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import {useAppDispatch, useAppSelector} from "../../stores/hooks";
|
import {useAppDispatch, useAppSelector} from "../../stores/hooks";
|
||||||
@ -15,645 +14,216 @@ import SectionTitleLineWithButton from "../../components/SectionTitleLineWithBut
|
|||||||
import SectionMain from "../../components/SectionMain";
|
import SectionMain from "../../components/SectionMain";
|
||||||
import CardBox from "../../components/CardBox";
|
import CardBox from "../../components/CardBox";
|
||||||
import BaseButton from "../../components/BaseButton";
|
import BaseButton from "../../components/BaseButton";
|
||||||
import BaseDivider from "../../components/BaseDivider";
|
|
||||||
import {mdiChartTimelineVariant} from "@mdi/js";
|
import {mdiChartTimelineVariant} from "@mdi/js";
|
||||||
import {SwitchField} from "../../components/SwitchField";
|
|
||||||
import FormField from "../../components/FormField";
|
|
||||||
|
|
||||||
import {hasPermission} from "../../helpers/userPermissions";
|
|
||||||
|
|
||||||
|
|
||||||
const ProjectsView = () => {
|
const ProjectsView = () => {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
const { projects } = useAppSelector((state) => state.projects)
|
const { projects: projectsRaw, loading } = useAppSelector((state) => state.projects)
|
||||||
|
|
||||||
const { currentUser } = useAppSelector((state) => state.auth);
|
|
||||||
|
|
||||||
|
|
||||||
const { id } = router.query;
|
const { id } = router.query;
|
||||||
|
|
||||||
function removeLastCharacter(str) {
|
// Ensure we are working with the correct project object
|
||||||
console.log(str,`str`)
|
const projects = useMemo(() => {
|
||||||
return str.slice(0, -1);
|
if (Array.isArray(projectsRaw)) {
|
||||||
|
return projectsRaw.find(p => p.id === id) || null;
|
||||||
}
|
}
|
||||||
|
return projectsRaw;
|
||||||
|
}, [projectsRaw, id]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (id) {
|
||||||
dispatch(fetch({ id }));
|
dispatch(fetch({ id }));
|
||||||
|
}
|
||||||
}, [dispatch, 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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
<title>{getPageTitle('View projects')}</title>
|
<title>{getPageTitle('View Project')}</title>
|
||||||
</Head>
|
</Head>
|
||||||
<SectionMain>
|
<SectionMain>
|
||||||
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={removeLastCharacter('View projects')} main>
|
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={'Project Details'} main>
|
||||||
<BaseButton
|
<BaseButton
|
||||||
color='info'
|
color='info'
|
||||||
label='Edit'
|
label='Edit'
|
||||||
href={`/projects/projects-edit/?id=${id}`}
|
href={`/projects/projects-edit/?id=${id}`}
|
||||||
/>
|
/>
|
||||||
</SectionTitleLineWithButton>
|
</SectionTitleLineWithButton>
|
||||||
<CardBox>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div className={'mb-4'}>
|
|
||||||
<p className={'block font-bold mb-2'}>Tenant</p>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<p>{projects?.tenant?.name ?? 'No data'}</p>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<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'}
|
||||||
<div className={'mb-4'}>
|
</span>
|
||||||
<p className={'block font-bold mb-2'}>Name</p>
|
|
||||||
<p>{projects?.name}</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<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 className={'mb-4'}>
|
|
||||||
<p className={'block font-bold mb-2'}>Description</p>
|
|
||||||
{projects.description
|
|
||||||
? <p dangerouslySetInnerHTML={{__html: projects.description}}/>
|
|
||||||
: <p>No data</p>
|
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<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>
|
||||||
|
|
||||||
|
<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 => (
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<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
|
<button
|
||||||
key={link.publicUrl}
|
key={link.publicUrl}
|
||||||
onClick={(e) => saveFile(e, link.publicUrl, link.name)}
|
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"
|
||||||
>
|
>
|
||||||
{link.name}
|
<span className="mr-1 text-base">📄</span> {link.name}
|
||||||
</button>
|
</button>
|
||||||
)) : <p>No Attachments</p>
|
))
|
||||||
}
|
) : (
|
||||||
|
<p className="text-gray-400 text-sm italic">No files attached</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p className="text-gray-500 text-sm font-semibold uppercase tracking-wider mb-2">Images</p>
|
||||||
|
{projectData?.images?.length ? (
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div className={'mb-4'}>
|
|
||||||
<p className={'block font-bold mb-2'}>Images</p>
|
|
||||||
{projects?.images?.length
|
|
||||||
? (
|
|
||||||
<ImageField
|
<ImageField
|
||||||
name={'images'}
|
name={'images'}
|
||||||
image={projects?.images}
|
image={projectData?.images}
|
||||||
className='w-20 h-20'
|
className='w-full max-h-40 object-cover rounded shadow-sm'
|
||||||
/>
|
/>
|
||||||
) : <p>No Images</p>
|
) : (
|
||||||
}
|
<p className="text-gray-400 text-sm italic">No images uploaded</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardBox>
|
||||||
</div>
|
</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 className={'mb-4'}>
|
|
||||||
<p className={'block font-bold mb-2'}>organizations</p>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<p>{projects?.organizations?.name ?? 'No data'}</p>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</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">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<>
|
|
||||||
<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>
|
<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>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<th>Name</th>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<th>Status</th>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<th>StartDate</th>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<th>EndDate</th>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<th>ReferenceCode</th>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody className="divide-y divide-gray-100">
|
||||||
{projects.trials_project && Array.isArray(projects.trials_project) &&
|
{projectData?.trials_project && Array.isArray(projectData.trials_project) && projectData.trials_project.length > 0 ? (
|
||||||
projects.trials_project.map((item: any) => (
|
projectData.trials_project.map((item: any) => (
|
||||||
<tr key={item.id} onClick={() => router.push(`/trials/trials-view/?id=${item.id}`)}>
|
<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">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<td data-label="name">
|
|
||||||
{item.name}
|
{item.name}
|
||||||
</td>
|
</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'
|
||||||
<td data-label="status">
|
}`}>
|
||||||
{item.status}
|
{item.status}
|
||||||
|
</span>
|
||||||
</td>
|
</td>
|
||||||
|
<td className="px-6 py-4 text-center font-mono text-gray-400 text-xs">
|
||||||
|
{item.reference_code || '-'}
|
||||||
|
|
||||||
<td data-label="start_date">
|
|
||||||
{ dataFormatter.dateTimeFormatter(item.start_date) }
|
|
||||||
</td>
|
</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>
|
||||||
<td data-label="end_date">
|
{item.end_date ? dayjs(item.end_date).format('MMM D, YY') : '...'}
|
||||||
{ dataFormatter.dateTimeFormatter(item.end_date) }
|
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<td data-label="reference_code">
|
|
||||||
{ item.reference_code }
|
|
||||||
</td>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</tr>
|
</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>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
{!projects?.trials_project?.length && <div className={'text-center py-4'}>No data</div>}
|
|
||||||
</CardBox>
|
</CardBox>
|
||||||
</>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<BaseDivider />
|
|
||||||
|
|
||||||
<BaseButton
|
<BaseButton
|
||||||
color='info'
|
color='info'
|
||||||
label='Back'
|
label='Back to List'
|
||||||
onClick={() => router.push('/projects/projects-list')}
|
onClick={() => router.push('/projects/projects-list')}
|
||||||
/>
|
/>
|
||||||
</CardBox>
|
|
||||||
</SectionMain>
|
</SectionMain>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@ -661,11 +231,7 @@ const ProjectsView = () => {
|
|||||||
|
|
||||||
ProjectsView.getLayout = function getLayout(page: ReactElement) {
|
ProjectsView.getLayout = function getLayout(page: ReactElement) {
|
||||||
return (
|
return (
|
||||||
<LayoutAuthenticated
|
<LayoutAuthenticated>
|
||||||
|
|
||||||
permission={'READ_PROJECTS'}
|
|
||||||
|
|
||||||
>
|
|
||||||
{page}
|
{page}
|
||||||
</LayoutAuthenticated>
|
</LayoutAuthenticated>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,10 +1,7 @@
|
|||||||
import React, { ReactElement, useEffect, useState } from 'react';
|
import { ReactElement, useEffect, useState } from 'react';
|
||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
import 'react-datepicker/dist/react-datepicker.css';
|
import 'react-datepicker/dist/react-datepicker.css';
|
||||||
import { useAppDispatch } from '../stores/hooks';
|
import { useAppDispatch, useAppSelector } from '../stores/hooks';
|
||||||
|
|
||||||
import { useAppSelector } from '../stores/hooks';
|
|
||||||
|
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import LayoutAuthenticated from '../layouts/Authenticated';
|
import LayoutAuthenticated from '../layouts/Authenticated';
|
||||||
import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton';
|
import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton';
|
||||||
|
|||||||
181
frontend/src/pages/settings/company-preferences.tsx
Normal file
181
frontend/src/pages/settings/company-preferences.tsx
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
import { mdiChartTimelineVariant, mdiCogOutline } from '@mdi/js'
|
||||||
|
import Head from 'next/head'
|
||||||
|
import React, { ReactElement, useEffect, useState } from 'react'
|
||||||
|
import { useRouter } from 'next/router'
|
||||||
|
import { Field, Form, Formik } from 'formik'
|
||||||
|
|
||||||
|
import CardBox from '../../components/CardBox'
|
||||||
|
import LayoutAuthenticated from '../../layouts/Authenticated'
|
||||||
|
import SectionMain from '../../components/SectionMain'
|
||||||
|
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'
|
||||||
|
import { getPageTitle } from '../../config'
|
||||||
|
import FormField from '../../components/FormField'
|
||||||
|
import BaseDivider from '../../components/BaseDivider'
|
||||||
|
import BaseButton from '../../components/BaseButton'
|
||||||
|
|
||||||
|
import { update, fetch } from '../../stores/organizations/organizationsSlice'
|
||||||
|
import { useAppDispatch, useAppSelector } from '../../stores/hooks'
|
||||||
|
import { findMe } from '../../stores/authSlice'
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
const CompanyPreferencesPage = () => {
|
||||||
|
const router = useRouter()
|
||||||
|
const dispatch = useAppDispatch()
|
||||||
|
const { currentUser } = useAppSelector((state) => state.auth)
|
||||||
|
const { organizations } = useAppSelector((state) => state.organizations)
|
||||||
|
|
||||||
|
const [initialValues, setInitialValues] = useState({
|
||||||
|
name: '',
|
||||||
|
address: '',
|
||||||
|
city: '',
|
||||||
|
state: '',
|
||||||
|
country: '',
|
||||||
|
zip: '',
|
||||||
|
navOrientation: 'side',
|
||||||
|
})
|
||||||
|
|
||||||
|
const [activeOrganizationId, setActiveOrganizationId] = useState<string | null>(null)
|
||||||
|
|
||||||
|
// Determine the organization ID to use
|
||||||
|
useEffect(() => {
|
||||||
|
if (currentUser?.organizations?.id || currentUser?.organizationsId) {
|
||||||
|
setActiveOrganizationId(currentUser?.organizations?.id || currentUser?.organizationsId);
|
||||||
|
} else {
|
||||||
|
// If user has no organization linked, try to fetch the first available one if they are an admin
|
||||||
|
const fetchAnyOrganization = async () => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get('/organizations?limit=1')
|
||||||
|
if (response.data.rows && response.data.rows.length > 0) {
|
||||||
|
setActiveOrganizationId(response.data.rows[0].id)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to fetch organization for admin:', err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fetchAnyOrganization()
|
||||||
|
}
|
||||||
|
}, [currentUser])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (activeOrganizationId) {
|
||||||
|
dispatch(fetch({ id: activeOrganizationId }))
|
||||||
|
}
|
||||||
|
}, [activeOrganizationId, dispatch])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// If organizations is an array (from list fetch) or object (from single fetch)
|
||||||
|
const orgData = Array.isArray(organizations) ? organizations[0] : organizations;
|
||||||
|
|
||||||
|
if (orgData && typeof orgData === 'object') {
|
||||||
|
setInitialValues({
|
||||||
|
name: orgData.name || '',
|
||||||
|
address: orgData.address || '',
|
||||||
|
city: orgData.city || '',
|
||||||
|
state: orgData.state || '',
|
||||||
|
country: orgData.country || '',
|
||||||
|
zip: orgData.zip || '',
|
||||||
|
navOrientation: orgData.navOrientation || 'side',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [organizations])
|
||||||
|
|
||||||
|
const handleSubmit = async (values: any) => {
|
||||||
|
if (activeOrganizationId) {
|
||||||
|
try {
|
||||||
|
const resultAction = await dispatch(update({ id: activeOrganizationId, data: values }))
|
||||||
|
if (update.fulfilled.match(resultAction)) {
|
||||||
|
// Update current user to reflect changes in orientation immediately
|
||||||
|
await dispatch(findMe())
|
||||||
|
// Reload to ensure everything is in sync
|
||||||
|
router.reload();
|
||||||
|
} else if (update.rejected.match(resultAction)) {
|
||||||
|
console.error('Update rejected:', resultAction.payload);
|
||||||
|
alert('Error updating company preferences: ' + (resultAction.payload as any)?.message || 'Unknown error');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Unexpected error:', err);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
alert('No organization found to update. Please link your user to an organization.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Head>
|
||||||
|
<title>{getPageTitle('Company Preferences')}</title>
|
||||||
|
</Head>
|
||||||
|
<SectionMain>
|
||||||
|
<SectionTitleLineWithButton icon={mdiCogOutline} title={'Company Preferences'} main>
|
||||||
|
{''}
|
||||||
|
</SectionTitleLineWithButton>
|
||||||
|
<CardBox>
|
||||||
|
<Formik
|
||||||
|
enableReinitialize
|
||||||
|
initialValues={initialValues}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
>
|
||||||
|
{({ isSubmitting }) => (
|
||||||
|
<Form>
|
||||||
|
<FormField label="Company Name">
|
||||||
|
<Field name="name" placeholder="Name" />
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<FormField label="Address">
|
||||||
|
<Field name="address" placeholder="Address" />
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||||
|
<FormField label="City">
|
||||||
|
<Field name="city" placeholder="City" />
|
||||||
|
</FormField>
|
||||||
|
<FormField label="State/Province">
|
||||||
|
<Field name="state" placeholder="State/Province" />
|
||||||
|
</FormField>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||||
|
<FormField label="Country">
|
||||||
|
<Field name="country" placeholder="Country" />
|
||||||
|
</FormField>
|
||||||
|
<FormField label="ZIP/Postal Code">
|
||||||
|
<Field name="zip" placeholder="ZIP/Postal Code" />
|
||||||
|
</FormField>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<BaseDivider />
|
||||||
|
|
||||||
|
<FormField label="Navigation Orientation">
|
||||||
|
<Field as="select" name="navOrientation">
|
||||||
|
<option value="side">Sidebar (Default)</option>
|
||||||
|
<option value="top">Top Navigation</option>
|
||||||
|
</Field>
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<BaseDivider />
|
||||||
|
<div className="flex items-center justify-end">
|
||||||
|
<BaseButton
|
||||||
|
type="submit"
|
||||||
|
color="info"
|
||||||
|
label={isSubmitting ? "Saving..." : "Save Changes"}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
)}
|
||||||
|
</Formik>
|
||||||
|
</CardBox>
|
||||||
|
</SectionMain>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
CompanyPreferencesPage.getLayout = function getLayout(page: ReactElement) {
|
||||||
|
return (
|
||||||
|
<LayoutAuthenticated>
|
||||||
|
{page}
|
||||||
|
</LayoutAuthenticated>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CompanyPreferencesPage
|
||||||
@ -15,6 +15,7 @@ import {useAppDispatch, useAppSelector} from "../../stores/hooks";
|
|||||||
import CardBoxModal from "../../components/CardBoxModal";
|
import CardBoxModal from "../../components/CardBoxModal";
|
||||||
import DragDropFilePicker from "../../components/DragDropFilePicker";
|
import DragDropFilePicker from "../../components/DragDropFilePicker";
|
||||||
import {setRefetch, uploadCsv} from '../../stores/tenants/tenantsSlice';
|
import {setRefetch, uploadCsv} from '../../stores/tenants/tenantsSlice';
|
||||||
|
import { impersonate } from '../../stores/authSlice';
|
||||||
|
|
||||||
|
|
||||||
import {hasPermission} from "../../helpers/userPermissions";
|
import {hasPermission} from "../../helpers/userPermissions";
|
||||||
@ -44,6 +45,8 @@ const TenantsTablesPage = () => {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
const hasCreatePermission = currentUser && hasPermission(currentUser, 'CREATE_TENANTS');
|
const hasCreatePermission = currentUser && hasPermission(currentUser, 'CREATE_TENANTS');
|
||||||
|
const isSuperAdmin = currentUser?.app_role?.globalAccess;
|
||||||
|
const isImpersonating = isSuperAdmin && (currentUser as any)?.tenantId;
|
||||||
|
|
||||||
|
|
||||||
const addFilter = () => {
|
const addFilter = () => {
|
||||||
@ -83,6 +86,11 @@ const TenantsTablesPage = () => {
|
|||||||
setIsModalActive(false);
|
setIsModalActive(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleStopImpersonating = async () => {
|
||||||
|
await dispatch(impersonate(null));
|
||||||
|
dispatch(setRefetch(true));
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
@ -107,11 +115,20 @@ const TenantsTablesPage = () => {
|
|||||||
{hasCreatePermission && (
|
{hasCreatePermission && (
|
||||||
<BaseButton
|
<BaseButton
|
||||||
color='info'
|
color='info'
|
||||||
|
className={'mr-3'}
|
||||||
label='Upload CSV'
|
label='Upload CSV'
|
||||||
onClick={() => setIsModalActive(true)}
|
onClick={() => setIsModalActive(true)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{isImpersonating && (
|
||||||
|
<BaseButton
|
||||||
|
color='danger'
|
||||||
|
label='Stop Impersonating'
|
||||||
|
onClick={handleStopImpersonating}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className='md:inline-flex items-center ms-auto'>
|
<div className='md:inline-flex items-center ms-auto'>
|
||||||
<div id='delete-rows-button'></div>
|
<div id='delete-rows-button'></div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
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 [filterItems, setFilterItems] = useState([]);
|
||||||
const [csvFile, setCsvFile] = useState<File | null>(null);
|
const [csvFile, setCsvFile] = useState<File | null>(null);
|
||||||
const [isModalActive, setIsModalActive] = useState(false);
|
const [isModalActive, setIsModalActive] = useState(false);
|
||||||
const [showTableView, setShowTableView] = useState(false);
|
const [showTableView, setShowTableView] = useState(true);
|
||||||
|
|
||||||
|
|
||||||
const { currentUser } = useAppSelector((state) => state.auth);
|
const { currentUser } = useAppSelector((state) => state.auth);
|
||||||
@ -133,7 +133,11 @@ const TrialsTablesPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='md:inline-flex items-center ms-auto'>
|
<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>
|
</div>
|
||||||
|
|
||||||
</CardBox>
|
</CardBox>
|
||||||
@ -143,7 +147,7 @@ const TrialsTablesPage = () => {
|
|||||||
filterItems={filterItems}
|
filterItems={filterItems}
|
||||||
setFilterItems={setFilterItems}
|
setFilterItems={setFilterItems}
|
||||||
filters={filters}
|
filters={filters}
|
||||||
showGrid={false}
|
showGrid={showTableView}
|
||||||
/>
|
/>
|
||||||
</CardBox>
|
</CardBox>
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { mdiAccount, mdiChartTimelineVariant, mdiMail, mdiUpload } from '@mdi/js'
|
import { mdiAccount, mdiChartTimelineVariant, mdiMail, mdiUpload } from '@mdi/js'
|
||||||
import Head from 'next/head'
|
import Head from 'next/head'
|
||||||
import React, { ReactElement } from 'react'
|
import React, { ReactElement, useMemo } from 'react'
|
||||||
import CardBox from '../../components/CardBox'
|
import CardBox from '../../components/CardBox'
|
||||||
import LayoutAuthenticated from '../../layouts/Authenticated'
|
import LayoutAuthenticated from '../../layouts/Authenticated'
|
||||||
import SectionMain from '../../components/SectionMain'
|
import SectionMain from '../../components/SectionMain'
|
||||||
@ -23,227 +23,31 @@ import { SelectFieldMany } from "../../components/SelectFieldMany";
|
|||||||
import {RichTextField} from "../../components/RichTextField";
|
import {RichTextField} from "../../components/RichTextField";
|
||||||
|
|
||||||
import { create } from '../../stores/trials/trialsSlice'
|
import { create } from '../../stores/trials/trialsSlice'
|
||||||
import { useAppDispatch } from '../../stores/hooks'
|
import { useAppDispatch, useAppSelector } from '../../stores/hooks'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
|
||||||
const initialValues = {
|
const initialValues = {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
tenant: '',
|
tenant: '',
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
project: '',
|
project: '',
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
trial_type: '',
|
trial_type: '',
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
name: '',
|
name: '',
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
objective: '',
|
objective: '',
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
status: 'draft',
|
status: 'draft',
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
start_date: '',
|
start_date: '',
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
end_date: '',
|
end_date: '',
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
location: '',
|
location: '',
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
reference_code: '',
|
reference_code: '',
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
attachments: [],
|
attachments: [],
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
images: [],
|
images: [],
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
organizations: '',
|
organizations: '',
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const TrialsNew = () => {
|
const TrialsNew = () => {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
|
const { currentUser } = useAppSelector((state) => state.auth)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -251,10 +55,27 @@ const TrialsNew = () => {
|
|||||||
const { dateRangeStart, dateRangeEnd } = router.query
|
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) => {
|
const handleSubmit = async (data) => {
|
||||||
await dispatch(create(data))
|
await dispatch(create(data))
|
||||||
await router.push('/trials/trials-list')
|
await router.push('/trials/trials-list')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hasTenant = !!(currentUser?.tenant?.id || currentUser?.tenantId)
|
||||||
|
const hasOrganization = !!(currentUser?.organizations?.id || currentUser?.organizationsId || currentUser?.tenant?.organizationsId)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
@ -266,114 +87,26 @@ const TrialsNew = () => {
|
|||||||
</SectionTitleLineWithButton>
|
</SectionTitleLineWithButton>
|
||||||
<CardBox>
|
<CardBox>
|
||||||
<Formik
|
<Formik
|
||||||
initialValues={
|
initialValues={memoizedInitialValues}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
dateRangeStart && dateRangeEnd ?
|
|
||||||
{
|
|
||||||
...initialValues,
|
|
||||||
start_date: moment(dateRangeStart).format('YYYY-MM-DDTHH:mm'),
|
|
||||||
end_date: moment(dateRangeEnd).format('YYYY-MM-DDTHH:mm'),
|
|
||||||
} : initialValues
|
|
||||||
|
|
||||||
}
|
|
||||||
onSubmit={(values) => handleSubmit(values)}
|
onSubmit={(values) => handleSubmit(values)}
|
||||||
|
enableReinitialize
|
||||||
>
|
>
|
||||||
<Form>
|
<Form>
|
||||||
|
|
||||||
|
{!hasTenant && (
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<FormField label="Tenant" labelFor="tenant">
|
<FormField label="Tenant" labelFor="tenant">
|
||||||
<Field name="tenant" id="tenant" component={SelectField} options={[]} itemRef={'tenants'}></Field>
|
<Field name="tenant" id="tenant" component={SelectField} options={[]} itemRef={'tenants'}></Field>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
)}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<FormField label="Project" labelFor="project">
|
<FormField label="Project" labelFor="project">
|
||||||
<Field name="project" id="project" component={SelectField} options={[]} itemRef={'projects'}></Field>
|
<Field name="project" id="project" component={SelectField} options={[]} itemRef={'projects'}></Field>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<FormField label="TrialType" labelFor="trial_type">
|
<FormField label="TrialType" labelFor="trial_type">
|
||||||
<Field name="trial_type" id="trial_type" component={SelectField} options={[]} itemRef={'trial_types'}></Field>
|
<Field name="trial_type" id="trial_type" component={SelectField} options={[]} itemRef={'trial_types'}></Field>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
label="Name"
|
label="Name"
|
||||||
>
|
>
|
||||||
@ -383,36 +116,6 @@ const TrialsNew = () => {
|
|||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<FormField label='Objective' hasTextareaHeight>
|
<FormField label='Objective' hasTextareaHeight>
|
||||||
<Field
|
<Field
|
||||||
name='objective'
|
name='objective'
|
||||||
@ -421,42 +124,6 @@ const TrialsNew = () => {
|
|||||||
></Field>
|
></Field>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<FormField label="Status" labelFor="status">
|
<FormField label="Status" labelFor="status">
|
||||||
<Field name="status" id="status" component="select">
|
<Field name="status" id="status" component="select">
|
||||||
|
|
||||||
@ -473,28 +140,6 @@ const TrialsNew = () => {
|
|||||||
</Field>
|
</Field>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
label="StartDate"
|
label="StartDate"
|
||||||
>
|
>
|
||||||
@ -505,32 +150,6 @@ const TrialsNew = () => {
|
|||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
label="EndDate"
|
label="EndDate"
|
||||||
>
|
>
|
||||||
@ -541,52 +160,10 @@ const TrialsNew = () => {
|
|||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<FormField label="Location" labelFor="location">
|
<FormField label="Location" labelFor="location">
|
||||||
<Field name="location" id="location" component={SelectField} options={[]} itemRef={'locations'}></Field>
|
<Field name="location" id="location" component={SelectField} options={[]} itemRef={'locations'}></Field>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
label="ReferenceCode"
|
label="ReferenceCode"
|
||||||
>
|
>
|
||||||
@ -596,55 +173,6 @@ const TrialsNew = () => {
|
|||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<FormField>
|
<FormField>
|
||||||
<Field
|
<Field
|
||||||
label='Attachments'
|
label='Attachments'
|
||||||
@ -661,31 +189,6 @@ const TrialsNew = () => {
|
|||||||
></Field>
|
></Field>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<FormField>
|
<FormField>
|
||||||
<Field
|
<Field
|
||||||
label='Images'
|
label='Images'
|
||||||
@ -702,43 +205,17 @@ const TrialsNew = () => {
|
|||||||
></Field>
|
></Field>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
|
{!hasOrganization && (
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<FormField label="organizations" labelFor="organizations">
|
<FormField label="organizations" labelFor="organizations">
|
||||||
<Field name="organizations" id="organizations" component={SelectField} options={[]} itemRef={'organizations'}></Field>
|
<Field name="organizations" id="organizations" component={SelectField} options={[]} itemRef={'organizations'}></Field>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
)}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<BaseDivider />
|
<BaseDivider />
|
||||||
<BaseButtons>
|
<BaseButtons>
|
||||||
<BaseButton type="submit" color="info" label="Submit" />
|
<BaseButton type="submit" color="info" label="Submit" />
|
||||||
<BaseButton type="reset" color="info" outline label="Reset" />
|
<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>
|
</BaseButtons>
|
||||||
</Form>
|
</Form>
|
||||||
</Formik>
|
</Formik>
|
||||||
|
|||||||
@ -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 () => {
|
export const findMe = createAsyncThunk('auth/findMe', async () => {
|
||||||
const response = await axios.get('auth/me');
|
const response = await axios.get('auth/me');
|
||||||
return response.data;
|
return response.data;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user