Autosave: 20260222-225400

This commit is contained in:
Flatlogic Bot 2026-02-22 22:54:00 +00:00
parent 9aaa7b8e76
commit 249c697601
35 changed files with 1372 additions and 1260 deletions

View File

@ -348,6 +348,10 @@ module.exports = class Activity_feed_itemsDBApi {
where.organizationsId = options.currentUser.organizationsId; where.organizationsId = options.currentUser.organizationsId;
} }
} }
if (!globalAccess && options?.currentUser?.tenantId) {
where.tenantId = options.currentUser.tenantId;
}
offset = currentPage * limit; offset = currentPage * limit;

View File

@ -298,6 +298,10 @@ module.exports = class Form_field_choicesDBApi {
where.organizationsId = options.currentUser.organizationsId; where.organizationsId = options.currentUser.organizationsId;
} }
} }
if (!globalAccess && options?.currentUser?.tenantId) {
where.tenantId = options.currentUser.tenantId;
}
offset = currentPage * limit; offset = currentPage * limit;

View File

@ -362,6 +362,10 @@ module.exports = class Form_fieldsDBApi {
where.organizationsId = options.currentUser.organizationsId; where.organizationsId = options.currentUser.organizationsId;
} }
} }
if (!globalAccess && options?.currentUser?.tenantId) {
where.tenantId = options.currentUser.tenantId;
}
offset = currentPage * limit; offset = currentPage * limit;

View File

@ -412,6 +412,10 @@ module.exports = class Form_submissionsDBApi {
where.organizationsId = options.currentUser.organizationsId; where.organizationsId = options.currentUser.organizationsId;
} }
} }
if (!globalAccess && options?.currentUser?.tenantId) {
where.tenantId = options.currentUser.tenantId;
}
offset = currentPage * limit; offset = currentPage * limit;

View File

@ -314,6 +314,10 @@ module.exports = class Form_templatesDBApi {
where.organizationsId = options.currentUser.organizationsId; where.organizationsId = options.currentUser.organizationsId;
} }
} }
if (!globalAccess && options?.currentUser?.tenantId) {
where.tenantId = options.currentUser.tenantId;
}
offset = currentPage * limit; offset = currentPage * limit;

View File

@ -349,6 +349,10 @@ module.exports = class LocationsDBApi {
where.organizationsId = options.currentUser.organizationsId; where.organizationsId = options.currentUser.organizationsId;
} }
} }
if (!globalAccess && options?.currentUser?.tenantId) {
where.tenantId = options.currentUser.tenantId;
}
offset = currentPage * limit; offset = currentPage * limit;

View File

@ -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,12 @@ 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 || 'side',
importHash: data.importHash || null, importHash: data.importHash || null,
createdById: currentUser.id, createdById: currentUser.id,
@ -55,7 +60,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 || 'side',
importHash: item.importHash || null, importHash: item.importHash || null,
createdById: currentUser.id, createdById: currentUser.id,
updatedById: currentUser.id, updatedById: currentUser.id,
@ -74,9 +85,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 +95,12 @@ 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;
updatePayload.updatedById = currentUser.id; updatePayload.updatedById = currentUser.id;
@ -153,8 +170,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 +424,3 @@ module.exports = class OrganizationsDBApi {
}; };

View File

@ -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');
@ -149,9 +148,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(),
@ -324,22 +320,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 +336,11 @@ module.exports = class ProjectsDBApi {
}); });
output.organizations = await projects.getOrganizations({
transaction
});
output.attachments = await projects.getAttachments({ output.attachments = await projects.getAttachments({
transaction transaction
}); });
@ -389,6 +380,10 @@ module.exports = class ProjectsDBApi {
where.organizationsId = options.currentUser.organizationsId; where.organizationsId = options.currentUser.organizationsId;
} }
} }
if (!globalAccess && options?.currentUser?.tenantId) {
where.tenantId = options.currentUser.tenantId;
}
offset = currentPage * limit; offset = currentPage * limit;
@ -556,10 +551,30 @@ 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) {
const listItems = filter.organizations.split('|').map(item => { const listItems = filter.organizations.split('|').map(item => {
return Utils.uuid(item) return Utils.uuid(item)
@ -672,5 +687,4 @@ module.exports = class ProjectsDBApi {
} }
}; };

View File

@ -335,6 +335,10 @@ module.exports = class ReportsDBApi {
where.organizationsId = options.currentUser.organizationsId; where.organizationsId = options.currentUser.organizationsId;
} }
} }
if (!globalAccess && options?.currentUser?.tenantId) {
where.tenantId = options.currentUser.tenantId;
}
offset = currentPage * limit; offset = currentPage * limit;

View File

@ -405,6 +405,10 @@ module.exports = class Submission_valuesDBApi {
where.organizationsId = options.currentUser.organizationsId; where.organizationsId = options.currentUser.organizationsId;
} }
} }
if (!globalAccess && options?.currentUser?.tenantId) {
where.tenantId = options.currentUser.tenantId;
}
offset = currentPage * limit; offset = currentPage * limit;

View File

@ -284,6 +284,10 @@ module.exports = class Trial_typesDBApi {
where.organizationsId = options.currentUser.organizationsId; where.organizationsId = options.currentUser.organizationsId;
} }
} }
if (!globalAccess && options?.currentUser?.tenantId) {
where.tenantId = options.currentUser.tenantId;
}
offset = currentPage * limit; offset = currentPage * limit;

View File

@ -434,6 +434,10 @@ module.exports = class TrialsDBApi {
where.organizationsId = options.currentUser.organizationsId; where.organizationsId = options.currentUser.organizationsId;
} }
} }
if (!globalAccess && options?.currentUser?.tenantId) {
where.tenantId = options.currentUser.tenantId;
}
offset = currentPage * limit; offset = currentPage * limit;

View File

@ -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 || [], {
@ -329,6 +332,12 @@ module.exports = class UsersDBApi {
{ transaction } { transaction }
); );
} }
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
});
output.reports_created_by_user = await user.getReports_created_by_user({
transaction transaction
}); });
return output; return output;
} }
@ -528,6 +505,11 @@ module.exports = class UsersDBApi {
}, },
{
model: db.tenants,
as: 'tenant',
},
{ {
model: db.permissions, model: db.permissions,
@ -1007,5 +989,4 @@ module.exports = class UsersDBApi {
}; };

View 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;
}
}
};

View 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,
});
}
};

View 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
}
};

View File

@ -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');
},
};

View File

@ -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';");
},
};

View File

@ -14,11 +14,34 @@ module.exports = function(sequelize, DataTypes) {
primaryKey: true, primaryKey: true,
}, },
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: 'side',
}, },
importHash: { importHash: {
@ -180,6 +203,4 @@ name: {
return organizations; return organizations;
}; };

View File

@ -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,48 +152,31 @@ provider: {
}); });
}; };
users.beforeCreate((users, options) => {
users.beforeCreate((users, options) => { users = trimStringFields(users);
users = trimStringFields(users);
if (users.provider !== providers.LOCAL && Object.values(providers).indexOf(users.provider) > -1) { if (users.provider !== providers.LOCAL && Object.values(providers).indexOf(users.provider) > -1) {
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
}
}
});
users.beforeUpdate((users, options) => { users.beforeUpdate((users, options) => {
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;
} }

View File

@ -3529,11 +3529,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 +3543,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 +3557,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 +3571,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 +3616,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 +3630,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 +3644,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 +3658,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 +3823,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 +3837,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 +3851,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 +3865,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 +4030,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 +4044,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 +4058,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 +4072,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 +4168,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 +4182,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 +4196,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 +4210,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 +4503,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 +4517,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 +4531,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 +4545,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 +4647,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 +4661,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 +4675,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 +4689,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 +4856,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 +4870,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 +4884,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 +4898,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 +5057,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 +5071,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 +5085,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 +5099,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 +5390,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 +5404,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 +5418,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 +5432,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 +5660,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 +5674,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 +5688,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 +5702,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 +5865,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 +5879,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 +5893,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 +5907,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 +6068,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 +6082,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 +6096,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 +6110,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);
} }
} }

View File

@ -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
@ -145,5 +151,4 @@ function checkCrudPermissions(name) {
module.exports = { module.exports = {
checkPermissions, checkPermissions,
checkCrudPermissions, checkCrudPermissions,
}; };

View File

@ -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');

View File

@ -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';
@ -91,4 +89,4 @@ export default function AsideMenuLayer({ menu, className = '', ...props }: Props
</div> </div>
</aside> </aside>
) )
} }

View File

@ -6,14 +6,16 @@ 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
} }
export default function NavBar({ menu, className = '', children }: Props) { export default function NavBar({ menu, leftMenu = [], className = '', children }: 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);
@ -38,7 +40,16 @@ export default function NavBar({ menu, className = '', children }: Props) {
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} 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 lg:items-stretch ${containerMaxW} ${isScrolled && `border-b border-pavitra-400 dark:border-dark-700`}`}>
<div className="flex flex-1 items-stretch h-14">{children}</div> <div className="flex flex-1 items-stretch h-14">
{children}
{leftMenu.length > 0 && (
<div className="hidden lg:flex items-stretch">
{leftMenu.map((item, index) => (
<NavBarItem key={index} item={item} />
))}
</div>
)}
</div>
<div className="flex-none items-stretch flex h-14 lg:hidden"> <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" />
@ -49,9 +60,16 @@ export default function NavBar({ menu, className = '', children }: Props) {
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`} } 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`}
> >
<NavBarMenuList menu={menu} /> {/* Mobile menu should include both left and right items */}
<div className="lg:hidden w-full">
<NavBarMenuList menu={[...leftMenu, ...menu]} />
</div>
{/* Desktop menu only includes right items */}
<div className="hidden lg:flex items-center">
<NavBarMenuList menu={menu} />
</div>
</div> </div>
</div> </div>
</nav> </nav>
) )
} }

View File

@ -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'
@ -129,4 +128,4 @@ export default function NavBarItem({ item }: Props) {
} }
return <div className={componentClass} ref={excludedRef}>{NavBarItemComponentContents}</div> return <div className={componentClass} ref={excludedRef}>{NavBarItemComponentContents}</div>
} }

View File

@ -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'
@ -106,4 +109,4 @@ export type StyleKey = 'white' | 'basic'
export type UserForm = { export type UserForm = {
name: string name: string
email: string email: string
} }

View File

@ -1,129 +1,168 @@
import React, { ReactNode, useEffect } from 'react' import React, { ReactNode, useState, useMemo } 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'
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)
useEffect(() => { const organizations = currentUser?.organizations || currentUser?.organization;
const handleRouteChangeStart = () => { const orgData = Array.isArray(organizations) ? organizations[0] : organizations;
const navOrientation = orgData?.navOrientation || 'side';
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 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;
});
}, [navOrientation]);
const filteredMenuNavBarLeft = useMemo(() => {
if (navOrientation !== 'top') return [];
return menuNavBar.filter(item => item.isOrientationTopOnly);
}, [navOrientation]);
React.useEffect(() => {
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])
React.useEffect(() => {
if (localStorage.getItem('token') && (!currentUser || !currentUser.organizations)) {
dispatch(findMe())
}
}, [currentUser, dispatch])
const layoutAsidePadding = 'xl:pl-60' React.useEffect(() => {
if (typeof window !== 'undefined') {
const isDarkMode = darkMode || localStorage.getItem('darkMode') === '1'
dispatch(setDarkMode(isDarkMode))
}
}, [darkMode, dispatch])
React.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'
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`} } pt-14 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}
className={`${layoutAsidePadding} ${isAsideMobileExpanded ? 'ml-60 lg:ml-0' : ''}`} className={`${layoutAsidePadding} ${isAsideMobileExpanded ? 'ml-60 lg:ml-0' : ''}`}
> >
<NavBarItemPlain {navOrientation === 'side' && (
display="flex lg:hidden" <>
onClick={() => setIsAsideMobileExpanded(!isAsideMobileExpanded)} <NavBarItemPlain
> display="flex lg:hidden"
<BaseIcon path={isAsideMobileExpanded ? mdiBackburger : mdiForwardburger} size="24" /> onClick={() => setIsAsideMobileExpanded(!isAsideMobileExpanded)}
</NavBarItemPlain> >
<NavBarItemPlain <BaseIcon path={mdiMenu} size="24" />
display="hidden lg:flex xl:hidden" </NavBarItemPlain>
onClick={() => setIsAsideLgActive(true)} <NavBarItemPlain
> display="hidden lg:flex xl:hidden"
<BaseIcon path={mdiMenu} size="24" /> onClick={() => setIsAsideLgActive(true)}
</NavBarItemPlain> >
<NavBarItemPlain useMargin> <BaseIcon path={mdiMenu} size="24" />
<Search /> </NavBarItemPlain>
</NavBarItemPlain> </>
)}
<div className="flex items-center p-3 lg:p-0">
<Search />
</div>
</NavBar> </NavBar>
<AsideMenu {navOrientation === 'side' && (
isAsideMobileExpanded={isAsideMobileExpanded} <AsideMenu
isAsideLgActive={isAsideLgActive} isAsideMobileExpanded={isAsideMobileExpanded}
menu={menuAside} isAsideLgActive={isAsideLgActive}
onAsideLgClose={() => setIsAsideLgActive(false)} menu={filteredMenuAside}
/> onAsideLgClose={() => setIsAsideLgActive(false)}
/>
)}
{children} {children}
<FooterBar>Hand-crafted & Made with </FooterBar> <FooterBar />
</div> </div>
</div> </div>
) )
} }

View File

@ -4,152 +4,105 @@ 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: '/settings/company-preferences',
label: 'Company Preferences',
{ permissions: 'UPDATE_ORGANIZATIONS'
href: '/api-docs', },
target: '_blank', {
label: 'Swagger API', href: '/trial_types/trial_types-list',
icon: icon.mdiFileCode, label: 'Trial Types',
permissions: 'READ_API_DOCS' 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'
},
]
}, },
] ]
export default menuAside export default menuAside

View File

@ -1,19 +1,73 @@
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
},
{
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: [

View File

@ -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,49 +29,27 @@ 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);
async function loadCounts() {
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) => { Promise.allSettled(requests).then((results) => {
@ -79,537 +62,197 @@ const Dashboard = () => {
}); });
}); });
} }
async function getWidgets(roleId) {
await dispatch(fetchWidgets(roleId));
}
React.useEffect(() => { React.useEffect(() => {
if (!currentUser) return; if (!currentUser) return;
loadData().then(); loadCounts().then();
setWidgetsRole({ role: { value: currentUser?.app_role?.id, label: currentUser?.app_role?.name } }); if (hasPermission(currentUser, 'READ_ACTIVITY_FEED_ITEMS')) {
}, [currentUser]); dispatch(fetchActivityItems({ query: '?limit=10&orderBy=occurred_at_DESC' }));
}
}, [currentUser, dispatch]);
React.useEffect(() => { const getItemIcon = (type: string) => {
if (!currentUser || !widgetsRole?.role?.value) return; switch (type) {
getWidgets(widgetsRole?.role?.value || '').then(); case 'project_created': return icon.mdiFolderPlusOutline;
}, [widgetsRole?.role?.value]); case 'trial_created': return icon.mdiFlaskPlusOutline;
case 'submission_created': return icon.mdiClipboardCheckOutline;
return ( case 'user_created': return icon.mdiAccountPlusOutline;
<> case 'attachment_added': return icon.mdiPaperclip;
<Head> default: return icon.mdiBellOutline;
<title> }
{getPageTitle('Overview')} }
</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton
icon={icon.mdiChartTimelineVariant}
title='Overview'
main>
{''}
</SectionTitleLineWithButton>
{hasPermission(currentUser, 'CREATE_ROLES') && <WidgetCreator
currentUser={currentUser}
isFetchingQuery={isFetchingQuery}
setWidgetsRole={setWidgetsRole}
widgetsRole={widgetsRole}
/>}
{!!rolesWidgets.length &&
hasPermission(currentUser, 'CREATE_ROLES') && (
<p className=' text-gray-500 dark:text-gray-400 mb-4'>
{`${widgetsRole?.role?.label || 'Users'}'s widgets`}
</p>
)}
<div className='grid grid-cols-1 gap-6 lg:grid-cols-4 mb-6 grid-flow-dense'> return (
{(isFetchingQuery || loading) && ( <>
<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`}> <Head>
<BaseIcon <title>{getPageTitle('Home')}</title>
className={`${iconsColor} animate-spin mr-5`} </Head>
w='w-16' <SectionMain>
h='h-16' <SectionTitleLineWithButton icon={icon.mdiHomeOutline} title='Home' main>
size={48} {''}
path={icon.mdiLoading} </SectionTitleLineWithButton>
/>{' '}
Loading widgets...
</div>
)}
{ rolesWidgets && {/* Summary Cards */}
rolesWidgets.map((widget) => ( <div className='grid grid-cols-1 gap-6 lg:grid-cols-4 mb-6'>
<SmartWidget <Link href={'/projects/projects-list'}>
key={widget.id} <CardBox className="hover:shadow-lg transition-shadow cursor-pointer">
userId={currentUser?.id} <div className="flex justify-between items-center">
widget={widget} <div>
roleId={widgetsRole?.role?.value || ''} <p className="text-gray-500 dark:text-gray-400">Projects</p>
admin={hasPermission(currentUser, 'CREATE_ROLES')} <h3 className="text-3xl font-bold">{projects}</h3>
/> </div>
))} <BaseIcon path={icon.mdiFolderOutline} size={48} className={iconsColor} />
</div> </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>
{!!rolesWidgets.length && <hr className='my-6 ' />} {/* Activity Feed Section */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div id="dashboard" className='grid grid-cols-1 gap-6 lg:grid-cols-3 mb-6'> <div className="lg:col-span-2">
<SectionTitleLineWithButton icon={icon.mdiTimelineTextOutline} title="Recent Activity" />
{hasPermission(currentUser, 'READ_USERS') && <Link href={'/users/users-list'}> {activityLoading && <p>Loading feed...</p>}
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`} {!activityLoading && activity_feed_items?.length === 0 && (
> <CardBox>
<div className="flex justify-between align-center"> <p className="text-center text-gray-500 py-8">No recent activity found.</p>
<div> </CardBox>
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400"> )}
Users
</div> <div className="space-y-4">
<div className="text-3xl leading-tight font-semibold"> {activity_feed_items?.map((item: any) => (
{users} <CardBox key={item.id} className="hover:border-emerald-500 transition-colors border-l-4 border-l-emerald-500">
</div> <div className="flex items-start space-x-4">
</div> <div className="flex-shrink-0">
<div> <UserAvatar
<BaseIcon username={item.actor_user?.firstName || 'User'}
className={`${iconsColor}`} image={item.actor_user?.avatar}
w="w-16" className="w-12 h-12"
h="h-16" />
size={48} </div>
// eslint-disable-next-line @typescript-eslint/ban-ts-comment <div className="flex-grow">
// @ts-ignore <div className="flex justify-between items-start">
path={icon.mdiAccountGroup || icon.mdiTable} <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 className="mt-3 flex items-center justify-between">
<div className="flex items-center space-x-2">
<BaseIcon
path={getItemIcon(item.item_type)}
size={18}
className="text-emerald-600"
/>
<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> </div>
</div>
</Link>} <div className="space-y-6">
<SectionTitleLineWithButton icon={icon.mdiChartPie} title="Quick Stats" />
{hasPermission(currentUser, 'READ_ROLES') && <Link href={'/roles/roles-list'}> <CardBox>
<div <div className="space-y-4">
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`} <div className="flex justify-between items-center pb-2 border-b dark:border-gray-700">
> <span className="text-gray-500">Active Projects</span>
<div className="flex justify-between align-center"> <span className="font-bold">{projects}</span>
<div> </div>
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400"> <div className="flex justify-between items-center pb-2 border-b dark:border-gray-700">
Roles <span className="text-gray-500">Ongoing Trials</span>
</div> <span className="font-bold">{trials}</span>
<div className="text-3xl leading-tight font-semibold"> </div>
{roles} <div className="flex justify-between items-center">
</div> <span className="text-gray-500">New Submissions</span>
</div> <span className="font-bold text-emerald-600">+{form_submissions}</span>
<div> </div>
<BaseIcon </div>
className={`${iconsColor}`} </CardBox>
w="w-16"
h="h-16" <SectionTitleLineWithButton icon={icon.mdiInformationOutline} title="System Info" />
size={48} <CardBox className="bg-emerald-50 dark:bg-emerald-900/20 border-emerald-200 dark:border-emerald-800">
// eslint-disable-next-line @typescript-eslint/ban-ts-comment <p className="text-sm text-emerald-800 dark:text-emerald-200">
// @ts-ignore <strong>Trial Tracker</strong> is running in multi-tenant mode.
path={icon.mdiShieldAccountVariantOutline || icon.mdiTable} Farm-level data isolation is active for your account.
/> </p>
</div> </CardBox>
</div> </div>
</div> </div>
</Link>} </SectionMain>
</>
{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>
</SectionMain>
</>
)
} }
Dashboard.getLayout = function getLayout(page: ReactElement) { Dashboard.getLayout = function getLayout(page: ReactElement) {
return <LayoutAuthenticated>{page}</LayoutAuthenticated> return <LayoutAuthenticated>{page}</LayoutAuthenticated>
} }
export default Dashboard export default Dashboard

View File

@ -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 return (
useEffect(() => { <div className="min-h-screen bg-white text-gray-900 font-sans selection:bg-emerald-100 selection:text-emerald-900">
async function fetchData() { <Head>
const image = await getPexelsImage(); <title>{getPageTitle('Trial Tracker - Research Simplified')}</title>
const video = await getPexelsVideo(); </Head>
setIllustrationImage(image);
setIllustrationVideo(video);
}
fetchData();
}, []);
const imageBlock = (image) => ( {/* Navigation */}
<div <nav className="fixed top-0 w-full z-50 bg-white/80 backdrop-blur-md border-b border-gray-100">
className='hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3' <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
style={{ <div className="flex justify-between h-16 items-center">
backgroundImage: `${ <div className="flex items-center space-x-2">
image <div className="w-10 h-10 bg-emerald-600 rounded-xl flex items-center justify-center">
? `url(${image?.src?.original})` <BaseIcon path={icon.mdiFlaskOutline} size={24} className="text-white" />
: 'linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5))' </div>
}`, <span className="text-2xl font-bold tracking-tight text-gray-900">{title}</span>
backgroundSize: 'cover', </div>
backgroundPosition: 'left center', <div className="hidden md:flex space-x-8 items-center font-medium">
backgroundRepeat: 'no-repeat', <a href="#features" className="text-gray-600 hover:text-emerald-600 transition-colors">Features</a>
}} <a href="#about" className="text-gray-600 hover:text-emerald-600 transition-colors">About</a>
> <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">
<div className='flex justify-center w-full bg-blue-300/20'> Sign In
<a </Link>
className='text-[8px]' </div>
href={image?.photographer_url} </div>
target='_blank' </div>
rel='noreferrer' </nav>
>
Photo by {image?.photographer} on Pexels {/* Hero Section */}
</a> <header className="relative pt-32 pb-20 overflow-hidden">
</div> <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>
); );
const videoBlock = (video) => {
if (video?.video_files?.length > 0) {
return (
<div className='hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3'>
<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>
<title>{getPageTitle('Starter Page')}</title>
</Head>
<SectionFullScreen bg='violet'>
<div
className={`flex ${
contentPosition === 'right' ? 'flex-row-reverse' : 'flex-row'
} min-h-screen w-full`}
>
{contentType === 'image' && contentPosition !== 'background'
? 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>
<BaseButtons>
<BaseButton
href='/login'
label='Login'
color='info'
className='w-full'
/>
</BaseButtons>
</CardBox>
</div>
</div>
</SectionFullScreen>
<div className='bg-black text-white flex flex-col text-center justify-center md:flex-row'>
<p className='py-6 text-sm'>© 2026 <span>{title}</span>. All rights reserved</p>
<Link className='py-6 ml-4 text-sm' href='/privacy-policy/'>
Privacy Policy
</Link>
</div>
</div>
);
} }
Starter.getLayout = function getLayout(page: ReactElement) { LandingPage.getLayout = function getLayout(page: ReactElement) {
return <LayoutGuest>{page}</LayoutGuest>; return <LayoutGuest>{page}</LayoutGuest>;
}; };

View File

@ -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,10 +60,14 @@ 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) {
router.push('/dashboard'); if (currentUser.app_role?.globalAccess) {
router.push('/tenants/tenants-list');
} else {
router.push('/dashboard');
}
} }
}, [currentUser?.id, router]); }, [currentUser?.id, router]);
// Show error message if there is one // Show error message if there is one
@ -280,4 +282,4 @@ export default function Login() {
Login.getLayout = function getLayout(page: ReactElement) { Login.getLayout = function getLayout(page: ReactElement) {
return <LayoutGuest>{page}</LayoutGuest>; return <LayoutGuest>{page}</LayoutGuest>;
}; };

View File

@ -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';
@ -93,4 +90,4 @@ SearchView.getLayout = function getLayout(page: ReactElement) {
); );
}; };
export default SearchView; export default SearchView;

View 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