From a3f902faafc03e666b696a8524fd9e1c854ff957 Mon Sep 17 00:00:00 2001
From: Flatlogic Bot
Date: Sun, 6 Jul 2025 20:09:22 +0000
Subject: [PATCH] Updated via schema editor on 2025-07-06 20:08
---
.gitignore | 5 +
app-shell/src/_schema.json | 7 +-
backend/src/db/api/field_visits.js | 60 ++-
backend/src/db/api/organizations.js | 276 ++++++++++
backend/src/db/api/payments.js | 42 ++
backend/src/db/api/projects.js | 73 ++-
backend/src/db/api/roles.js | 14 +
backend/src/db/api/users.js | 42 ++
backend/src/db/migrations/1751832486825.js | 173 +++++++
backend/src/db/models/field_visits.js | 8 +
backend/src/db/models/organizations.js | 81 +++
backend/src/db/models/payments.js | 8 +
backend/src/db/models/projects.js | 12 +
backend/src/db/models/roles.js | 7 +
backend/src/db/models/users.js | 8 +
.../db/seeders/20200430130760-user-roles.js | 26 +
.../db/seeders/20231127130745-sample-data.js | 332 +++++++++++-
backend/src/db/seeders/20250706200806.js | 87 ++++
backend/src/index.js | 8 +
backend/src/routes/organizations.js | 444 ++++++++++++++++
backend/src/services/organizations.js | 117 +++++
backend/src/services/search.js | 4 +
frontend/json/runtimeError.json | 1 +
.../Field_visits/TableField_visits.tsx | 31 +-
.../Organizations/CardOrganizations.tsx | 108 ++++
.../Organizations/ListOrganizations.tsx | 90 ++++
.../Organizations/TableOrganizations.tsx | 484 ++++++++++++++++++
.../configureOrganizationsCols.tsx | 74 +++
frontend/src/components/Roles/CardRoles.tsx | 11 +
frontend/src/components/Roles/ListRoles.tsx | 9 +
.../components/Roles/configureRolesCols.tsx | 14 +
frontend/src/components/Users/CardUsers.tsx | 13 +
frontend/src/components/Users/ListUsers.tsx | 11 +
.../components/Users/configureUsersCols.tsx | 20 +
frontend/src/helpers/dataFormatter.js | 19 +
frontend/src/menuAside.ts | 8 +
frontend/src/pages/dashboard.tsx | 35 ++
.../pages/field_visits/[field_visitsId].tsx | 13 +
.../pages/field_visits/field_visits-edit.tsx | 13 +
.../pages/field_visits/field_visits-new.tsx | 27 +-
.../pages/field_visits/field_visits-table.tsx | 2 +-
.../pages/field_visits/field_visits-view.tsx | 6 +
.../pages/organizations/[organizationsId].tsx | 128 +++++
.../organizations/organizations-edit.tsx | 126 +++++
.../organizations/organizations-list.tsx | 165 ++++++
.../pages/organizations/organizations-new.tsx | 100 ++++
.../organizations/organizations-table.tsx | 164 ++++++
.../organizations/organizations-view.tsx | 355 +++++++++++++
frontend/src/pages/payments/[paymentsId].tsx | 13 +
frontend/src/pages/payments/payments-edit.tsx | 13 +
frontend/src/pages/payments/payments-new.tsx | 12 +
frontend/src/pages/payments/payments-view.tsx | 6 +
frontend/src/pages/projects/[projectsId].tsx | 23 +
frontend/src/pages/projects/projects-edit.tsx | 23 +
frontend/src/pages/projects/projects-new.tsx | 22 +
frontend/src/pages/projects/projects-view.tsx | 11 +
frontend/src/pages/roles/[rolesId].tsx | 10 +
frontend/src/pages/roles/roles-edit.tsx | 10 +
frontend/src/pages/roles/roles-new.tsx | 10 +
frontend/src/pages/roles/roles-view.tsx | 8 +
frontend/src/pages/users/[usersId].tsx | 13 +
frontend/src/pages/users/users-edit.tsx | 13 +
frontend/src/pages/users/users-list.tsx | 2 +
frontend/src/pages/users/users-new.tsx | 12 +
frontend/src/pages/users/users-table.tsx | 2 +
frontend/src/pages/users/users-view.tsx | 6 +
.../organizations/organizationsSlice.ts | 250 +++++++++
frontend/src/stores/store.ts | 2 +
68 files changed, 4241 insertions(+), 81 deletions(-)
create mode 100644 backend/src/db/api/organizations.js
create mode 100644 backend/src/db/migrations/1751832486825.js
create mode 100644 backend/src/db/models/organizations.js
create mode 100644 backend/src/db/seeders/20250706200806.js
create mode 100644 backend/src/routes/organizations.js
create mode 100644 backend/src/services/organizations.js
create mode 100644 frontend/json/runtimeError.json
create mode 100644 frontend/src/components/Organizations/CardOrganizations.tsx
create mode 100644 frontend/src/components/Organizations/ListOrganizations.tsx
create mode 100644 frontend/src/components/Organizations/TableOrganizations.tsx
create mode 100644 frontend/src/components/Organizations/configureOrganizationsCols.tsx
create mode 100644 frontend/src/pages/organizations/[organizationsId].tsx
create mode 100644 frontend/src/pages/organizations/organizations-edit.tsx
create mode 100644 frontend/src/pages/organizations/organizations-list.tsx
create mode 100644 frontend/src/pages/organizations/organizations-new.tsx
create mode 100644 frontend/src/pages/organizations/organizations-table.tsx
create mode 100644 frontend/src/pages/organizations/organizations-view.tsx
create mode 100644 frontend/src/stores/organizations/organizationsSlice.ts
diff --git a/.gitignore b/.gitignore
index e427ff3..d0eb167 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,8 @@
node_modules/
*/node_modules/
*/build/
+
+**/node_modules/
+**/build/
+.DS_Store
+.env
\ No newline at end of file
diff --git a/app-shell/src/_schema.json b/app-shell/src/_schema.json
index 190d4c4..a4c312f 100644
--- a/app-shell/src/_schema.json
+++ b/app-shell/src/_schema.json
@@ -1,5 +1,4 @@
-
-
{
- "Initial version": "{\"iv\":\"CuXJ4t6uAEv30knG\",\"encryptedData\":\"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\"}"
-}
+ "Initial version": "{\"iv\":\"CuXJ4t6uAEv30knG\",\"encryptedData\":\"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\"}",
+ "Updated via schema editor on 2025-07-06 20:08": "{\"iv\":\"f/Hw10tsWAdjXQSq\",\"encryptedData\":\"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\"}"
+}
\ No newline at end of file
diff --git a/backend/src/db/api/field_visits.js b/backend/src/db/api/field_visits.js
index 147af88..48562fb 100644
--- a/backend/src/db/api/field_visits.js
+++ b/backend/src/db/api/field_visits.js
@@ -34,6 +34,10 @@ module.exports = class Field_visitsDBApi {
transaction,
});
+ await field_visits.setOrganizations(data.organizations || null, {
+ transaction,
+ });
+
return field_visits;
}
@@ -114,6 +118,14 @@ module.exports = class Field_visitsDBApi {
);
}
+ if (data.organizations !== undefined) {
+ await field_visits.setOrganizations(
+ data.organizations,
+
+ { transaction },
+ );
+ }
+
return field_visits;
}
@@ -182,6 +194,10 @@ module.exports = class Field_visitsDBApi {
transaction,
});
+ output.organizations = await field_visits.getOrganizations({
+ transaction,
+ });
+
return output;
}
@@ -223,6 +239,32 @@ module.exports = class Field_visitsDBApi {
}
: {},
},
+
+ {
+ model: db.organizations,
+ as: 'organizations',
+
+ where: filter.organizations
+ ? {
+ [Op.or]: [
+ {
+ id: {
+ [Op.in]: filter.organizations
+ .split('|')
+ .map((term) => Utils.uuid(term)),
+ },
+ },
+ {
+ id: {
+ [Op.or]: filter.organizations
+ .split('|')
+ .map((term) => ({ [Op.iLike]: `%${term}%` })),
+ },
+ },
+ ],
+ }
+ : {},
+ },
];
if (filter) {
@@ -258,24 +300,6 @@ module.exports = class Field_visitsDBApi {
};
}
- if (filter.calendarStart && filter.calendarEnd) {
- where = {
- ...where,
- [Op.or]: [
- {
- start_date: {
- [Op.between]: [filter.calendarStart, filter.calendarEnd],
- },
- },
- {
- return_date: {
- [Op.between]: [filter.calendarStart, filter.calendarEnd],
- },
- },
- ],
- };
- }
-
if (filter.start_dateRange) {
const [start, end] = filter.start_dateRange;
diff --git a/backend/src/db/api/organizations.js b/backend/src/db/api/organizations.js
new file mode 100644
index 0000000..aad7ccf
--- /dev/null
+++ b/backend/src/db/api/organizations.js
@@ -0,0 +1,276 @@
+const db = require('../models');
+const FileDBApi = require('./file');
+const crypto = require('crypto');
+const Utils = require('../utils');
+
+const Sequelize = db.Sequelize;
+const Op = Sequelize.Op;
+
+module.exports = class OrganizationsDBApi {
+ static async create(data, options) {
+ const currentUser = (options && options.currentUser) || { id: null };
+ const transaction = (options && options.transaction) || undefined;
+
+ const organizations = await db.organizations.create(
+ {
+ id: data.id || undefined,
+
+ name: data.name || null,
+ importHash: data.importHash || null,
+ createdById: currentUser.id,
+ updatedById: currentUser.id,
+ },
+ { transaction },
+ );
+
+ return organizations;
+ }
+
+ static async bulkImport(data, options) {
+ const currentUser = (options && options.currentUser) || { id: null };
+ const transaction = (options && options.transaction) || undefined;
+
+ // Prepare data - wrapping individual data transformations in a map() method
+ const organizationsData = data.map((item, index) => ({
+ id: item.id || undefined,
+
+ name: item.name || null,
+ importHash: item.importHash || null,
+ createdById: currentUser.id,
+ updatedById: currentUser.id,
+ createdAt: new Date(Date.now() + index * 1000),
+ }));
+
+ // Bulk create items
+ const organizations = await db.organizations.bulkCreate(organizationsData, {
+ transaction,
+ });
+
+ // For each item created, replace relation files
+
+ return organizations;
+ }
+
+ static async update(id, data, options) {
+ const currentUser = (options && options.currentUser) || { id: null };
+ const transaction = (options && options.transaction) || undefined;
+
+ const organizations = await db.organizations.findByPk(
+ id,
+ {},
+ { transaction },
+ );
+
+ const updatePayload = {};
+
+ if (data.name !== undefined) updatePayload.name = data.name;
+
+ updatePayload.updatedById = currentUser.id;
+
+ await organizations.update(updatePayload, { transaction });
+
+ return organizations;
+ }
+
+ static async deleteByIds(ids, options) {
+ const currentUser = (options && options.currentUser) || { id: null };
+ const transaction = (options && options.transaction) || undefined;
+
+ const organizations = await db.organizations.findAll({
+ where: {
+ id: {
+ [Op.in]: ids,
+ },
+ },
+ transaction,
+ });
+
+ await db.sequelize.transaction(async (transaction) => {
+ for (const record of organizations) {
+ await record.update({ deletedBy: currentUser.id }, { transaction });
+ }
+ for (const record of organizations) {
+ await record.destroy({ transaction });
+ }
+ });
+
+ return organizations;
+ }
+
+ static async remove(id, options) {
+ const currentUser = (options && options.currentUser) || { id: null };
+ const transaction = (options && options.transaction) || undefined;
+
+ const organizations = await db.organizations.findByPk(id, options);
+
+ await organizations.update(
+ {
+ deletedBy: currentUser.id,
+ },
+ {
+ transaction,
+ },
+ );
+
+ await organizations.destroy({
+ transaction,
+ });
+
+ return organizations;
+ }
+
+ static async findBy(where, options) {
+ const transaction = (options && options.transaction) || undefined;
+
+ const organizations = await db.organizations.findOne(
+ { where },
+ { transaction },
+ );
+
+ if (!organizations) {
+ return organizations;
+ }
+
+ const output = organizations.get({ plain: true });
+
+ output.users_organizations = await organizations.getUsers_organizations({
+ transaction,
+ });
+
+ output.field_visits_organizations =
+ await organizations.getField_visits_organizations({
+ transaction,
+ });
+
+ output.payments_organizations =
+ await organizations.getPayments_organizations({
+ transaction,
+ });
+
+ output.projects_organizations =
+ await organizations.getProjects_organizations({
+ transaction,
+ });
+
+ return output;
+ }
+
+ static async findAll(filter, options) {
+ const limit = filter.limit || 0;
+ let offset = 0;
+ let where = {};
+ const currentPage = +filter.page;
+
+ offset = currentPage * limit;
+
+ const orderBy = null;
+
+ const transaction = (options && options.transaction) || undefined;
+
+ let include = [];
+
+ if (filter) {
+ if (filter.id) {
+ where = {
+ ...where,
+ ['id']: Utils.uuid(filter.id),
+ };
+ }
+
+ if (filter.name) {
+ where = {
+ ...where,
+ [Op.and]: Utils.ilike('organizations', 'name', filter.name),
+ };
+ }
+
+ if (filter.active !== undefined) {
+ where = {
+ ...where,
+ active: filter.active === true || filter.active === 'true',
+ };
+ }
+
+ if (filter.createdAtRange) {
+ const [start, end] = filter.createdAtRange;
+
+ if (start !== undefined && start !== null && start !== '') {
+ where = {
+ ...where,
+ ['createdAt']: {
+ ...where.createdAt,
+ [Op.gte]: start,
+ },
+ };
+ }
+
+ if (end !== undefined && end !== null && end !== '') {
+ where = {
+ ...where,
+ ['createdAt']: {
+ ...where.createdAt,
+ [Op.lte]: end,
+ },
+ };
+ }
+ }
+ }
+
+ const queryOptions = {
+ where,
+ include,
+ distinct: true,
+ order:
+ filter.field && filter.sort
+ ? [[filter.field, filter.sort]]
+ : [['createdAt', 'desc']],
+ transaction: options?.transaction,
+ logging: console.log,
+ };
+
+ if (!options?.countOnly) {
+ queryOptions.limit = limit ? Number(limit) : undefined;
+ queryOptions.offset = offset ? Number(offset) : undefined;
+ }
+
+ try {
+ const { rows, count } = await db.organizations.findAndCountAll(
+ queryOptions,
+ );
+
+ return {
+ rows: options?.countOnly ? [] : rows,
+ count: count,
+ };
+ } catch (error) {
+ console.error('Error executing query:', error);
+ throw error;
+ }
+ }
+
+ static async findAllAutocomplete(query, limit, offset) {
+ let where = {};
+
+ if (query) {
+ where = {
+ [Op.or]: [
+ { ['id']: Utils.uuid(query) },
+ Utils.ilike('organizations', 'id', query),
+ ],
+ };
+ }
+
+ const records = await db.organizations.findAll({
+ attributes: ['id', 'id'],
+ where,
+ limit: limit ? Number(limit) : undefined,
+ offset: offset ? Number(offset) : undefined,
+ orderBy: [['id', 'ASC']],
+ });
+
+ return records.map((record) => ({
+ id: record.id,
+ label: record.id,
+ }));
+ }
+};
diff --git a/backend/src/db/api/payments.js b/backend/src/db/api/payments.js
index 15d096d..5aa8e72 100644
--- a/backend/src/db/api/payments.js
+++ b/backend/src/db/api/payments.js
@@ -34,6 +34,10 @@ module.exports = class PaymentsDBApi {
transaction,
});
+ await payments.setOrganizations(data.organizations || null, {
+ transaction,
+ });
+
return payments;
}
@@ -109,6 +113,14 @@ module.exports = class PaymentsDBApi {
);
}
+ if (data.organizations !== undefined) {
+ await payments.setOrganizations(
+ data.organizations,
+
+ { transaction },
+ );
+ }
+
return payments;
}
@@ -174,6 +186,10 @@ module.exports = class PaymentsDBApi {
transaction,
});
+ output.organizations = await payments.getOrganizations({
+ transaction,
+ });
+
return output;
}
@@ -215,6 +231,32 @@ module.exports = class PaymentsDBApi {
}
: {},
},
+
+ {
+ model: db.organizations,
+ as: 'organizations',
+
+ where: filter.organizations
+ ? {
+ [Op.or]: [
+ {
+ id: {
+ [Op.in]: filter.organizations
+ .split('|')
+ .map((term) => Utils.uuid(term)),
+ },
+ },
+ {
+ id: {
+ [Op.or]: filter.organizations
+ .split('|')
+ .map((term) => ({ [Op.iLike]: `%${term}%` })),
+ },
+ },
+ ],
+ }
+ : {},
+ },
];
if (filter) {
diff --git a/backend/src/db/api/projects.js b/backend/src/db/api/projects.js
index 61dc291..b8b293c 100644
--- a/backend/src/db/api/projects.js
+++ b/backend/src/db/api/projects.js
@@ -21,6 +21,7 @@ module.exports = class ProjectsDBApi {
start_date: data.start_date || null,
end_date: data.end_date || null,
status: data.status || null,
+ cash_at_bank: data.cash_at_bank || null,
importHash: data.importHash || null,
createdById: currentUser.id,
updatedById: currentUser.id,
@@ -28,6 +29,10 @@ module.exports = class ProjectsDBApi {
{ transaction },
);
+ await projects.setOrganizations(data.organizations || null, {
+ transaction,
+ });
+
return projects;
}
@@ -45,6 +50,7 @@ module.exports = class ProjectsDBApi {
start_date: item.start_date || null,
end_date: item.end_date || null,
status: item.status || null,
+ cash_at_bank: item.cash_at_bank || null,
importHash: item.importHash || null,
createdById: currentUser.id,
updatedById: currentUser.id,
@@ -83,10 +89,21 @@ module.exports = class ProjectsDBApi {
if (data.status !== undefined) updatePayload.status = data.status;
+ if (data.cash_at_bank !== undefined)
+ updatePayload.cash_at_bank = data.cash_at_bank;
+
updatePayload.updatedById = currentUser.id;
await projects.update(updatePayload, { transaction });
+ if (data.organizations !== undefined) {
+ await projects.setOrganizations(
+ data.organizations,
+
+ { transaction },
+ );
+ }
+
return projects;
}
@@ -156,6 +173,10 @@ module.exports = class ProjectsDBApi {
transaction,
});
+ output.organizations = await projects.getOrganizations({
+ transaction,
+ });
+
return output;
}
@@ -171,7 +192,33 @@ module.exports = class ProjectsDBApi {
const transaction = (options && options.transaction) || undefined;
- let include = [];
+ let include = [
+ {
+ model: db.organizations,
+ as: 'organizations',
+
+ where: filter.organizations
+ ? {
+ [Op.or]: [
+ {
+ id: {
+ [Op.in]: filter.organizations
+ .split('|')
+ .map((term) => Utils.uuid(term)),
+ },
+ },
+ {
+ id: {
+ [Op.or]: filter.organizations
+ .split('|')
+ .map((term) => ({ [Op.iLike]: `%${term}%` })),
+ },
+ },
+ ],
+ }
+ : {},
+ },
+ ];
if (filter) {
if (filter.id) {
@@ -272,6 +319,30 @@ module.exports = class ProjectsDBApi {
}
}
+ if (filter.cash_at_bankRange) {
+ const [start, end] = filter.cash_at_bankRange;
+
+ if (start !== undefined && start !== null && start !== '') {
+ where = {
+ ...where,
+ cash_at_bank: {
+ ...where.cash_at_bank,
+ [Op.gte]: start,
+ },
+ };
+ }
+
+ if (end !== undefined && end !== null && end !== '') {
+ where = {
+ ...where,
+ cash_at_bank: {
+ ...where.cash_at_bank,
+ [Op.lte]: end,
+ },
+ };
+ }
+ }
+
if (filter.active !== undefined) {
where = {
...where,
diff --git a/backend/src/db/api/roles.js b/backend/src/db/api/roles.js
index 7a6596d..3324dae 100644
--- a/backend/src/db/api/roles.js
+++ b/backend/src/db/api/roles.js
@@ -17,6 +17,8 @@ module.exports = class RolesDBApi {
name: data.name || null,
role_customization: data.role_customization || null,
+ globalAccess: data.globalAccess || false,
+
importHash: data.importHash || null,
createdById: currentUser.id,
updatedById: currentUser.id,
@@ -41,6 +43,8 @@ module.exports = class RolesDBApi {
name: item.name || null,
role_customization: item.role_customization || null,
+ globalAccess: item.globalAccess || false,
+
importHash: item.importHash || null,
createdById: currentUser.id,
updatedById: currentUser.id,
@@ -68,6 +72,9 @@ module.exports = class RolesDBApi {
if (data.role_customization !== undefined)
updatePayload.role_customization = data.role_customization;
+ if (data.globalAccess !== undefined)
+ updatePayload.globalAccess = data.globalAccess;
+
updatePayload.updatedById = currentUser.id;
await roles.update(updatePayload, { transaction });
@@ -201,6 +208,13 @@ module.exports = class RolesDBApi {
};
}
+ if (filter.globalAccess) {
+ where = {
+ ...where,
+ globalAccess: filter.globalAccess,
+ };
+ }
+
if (filter.permissions) {
const searchTerms = filter.permissions.split('|');
diff --git a/backend/src/db/api/users.js b/backend/src/db/api/users.js
index 3a11e69..1dc4652 100644
--- a/backend/src/db/api/users.js
+++ b/backend/src/db/api/users.js
@@ -56,6 +56,10 @@ module.exports = class UsersDBApi {
});
}
+ await users.setOrganizations(data.data.organizations || null, {
+ transaction,
+ });
+
await users.setCustom_permissions(data.data.custom_permissions || [], {
transaction,
});
@@ -190,6 +194,14 @@ module.exports = class UsersDBApi {
);
}
+ if (data.organizations !== undefined) {
+ await users.setOrganizations(
+ data.organizations,
+
+ { transaction },
+ );
+ }
+
if (data.custom_permissions !== undefined) {
await users.setCustom_permissions(data.custom_permissions, {
transaction,
@@ -285,6 +297,10 @@ module.exports = class UsersDBApi {
transaction,
});
+ output.organizations = await users.getOrganizations({
+ transaction,
+ });
+
return output;
}
@@ -327,6 +343,32 @@ module.exports = class UsersDBApi {
: {},
},
+ {
+ model: db.organizations,
+ as: 'organizations',
+
+ where: filter.organizations
+ ? {
+ [Op.or]: [
+ {
+ id: {
+ [Op.in]: filter.organizations
+ .split('|')
+ .map((term) => Utils.uuid(term)),
+ },
+ },
+ {
+ id: {
+ [Op.or]: filter.organizations
+ .split('|')
+ .map((term) => ({ [Op.iLike]: `%${term}%` })),
+ },
+ },
+ ],
+ }
+ : {},
+ },
+
{
model: db.permissions,
as: 'custom_permissions',
diff --git a/backend/src/db/migrations/1751832486825.js b/backend/src/db/migrations/1751832486825.js
new file mode 100644
index 0000000..6c35854
--- /dev/null
+++ b/backend/src/db/migrations/1751832486825.js
@@ -0,0 +1,173 @@
+module.exports = {
+ /**
+ * @param {QueryInterface} queryInterface
+ * @param {Sequelize} Sequelize
+ * @returns {Promise}
+ */
+ async up(queryInterface, Sequelize) {
+ /**
+ * @type {Transaction}
+ */
+ const transaction = await queryInterface.sequelize.transaction();
+ try {
+ await queryInterface.createTable(
+ 'organizations',
+ {
+ id: {
+ type: Sequelize.DataTypes.UUID,
+ defaultValue: Sequelize.DataTypes.UUIDV4,
+ primaryKey: true,
+ },
+ createdById: {
+ type: Sequelize.DataTypes.UUID,
+ references: {
+ key: 'id',
+ model: 'users',
+ },
+ },
+ updatedById: {
+ type: Sequelize.DataTypes.UUID,
+ references: {
+ key: 'id',
+ model: 'users',
+ },
+ },
+ createdAt: { type: Sequelize.DataTypes.DATE },
+ updatedAt: { type: Sequelize.DataTypes.DATE },
+ deletedAt: { type: Sequelize.DataTypes.DATE },
+ importHash: {
+ type: Sequelize.DataTypes.STRING(255),
+ allowNull: true,
+ unique: true,
+ },
+ },
+ { transaction },
+ );
+
+ await queryInterface.addColumn(
+ 'users',
+ 'organizationsId',
+ {
+ type: Sequelize.DataTypes.UUID,
+
+ references: {
+ model: 'organizations',
+ key: 'id',
+ },
+ },
+ { transaction },
+ );
+
+ await queryInterface.addColumn(
+ 'field_visits',
+ 'organizationsId',
+ {
+ type: Sequelize.DataTypes.UUID,
+
+ references: {
+ model: 'organizations',
+ key: 'id',
+ },
+ },
+ { transaction },
+ );
+
+ await queryInterface.addColumn(
+ 'payments',
+ 'organizationsId',
+ {
+ type: Sequelize.DataTypes.UUID,
+
+ references: {
+ model: 'organizations',
+ key: 'id',
+ },
+ },
+ { transaction },
+ );
+
+ await queryInterface.addColumn(
+ 'projects',
+ 'organizationsId',
+ {
+ type: Sequelize.DataTypes.UUID,
+
+ references: {
+ model: 'organizations',
+ key: 'id',
+ },
+ },
+ { transaction },
+ );
+
+ await queryInterface.addColumn(
+ 'organizations',
+ 'name',
+ {
+ type: Sequelize.DataTypes.TEXT,
+ },
+ { transaction },
+ );
+
+ await queryInterface.addColumn(
+ 'roles',
+ 'globalAccess',
+ {
+ type: Sequelize.DataTypes.BOOLEAN,
+
+ defaultValue: false,
+ allowNull: false,
+ },
+ { transaction },
+ );
+
+ await transaction.commit();
+ } catch (err) {
+ await transaction.rollback();
+ throw err;
+ }
+ },
+ /**
+ * @param {QueryInterface} queryInterface
+ * @param {Sequelize} Sequelize
+ * @returns {Promise}
+ */
+ async down(queryInterface, Sequelize) {
+ /**
+ * @type {Transaction}
+ */
+ const transaction = await queryInterface.sequelize.transaction();
+ try {
+ await queryInterface.removeColumn('roles', 'globalAccess', {
+ transaction,
+ });
+
+ await queryInterface.removeColumn('organizations', 'name', {
+ transaction,
+ });
+
+ await queryInterface.removeColumn('projects', 'organizationsId', {
+ transaction,
+ });
+
+ await queryInterface.removeColumn('payments', 'organizationsId', {
+ transaction,
+ });
+
+ await queryInterface.removeColumn('field_visits', 'organizationsId', {
+ transaction,
+ });
+
+ await queryInterface.removeColumn('users', 'organizationsId', {
+ transaction,
+ });
+
+ await queryInterface.dropTable('organizations', { transaction });
+
+ await transaction.commit();
+ } catch (err) {
+ await transaction.rollback();
+ throw err;
+ }
+ },
+};
diff --git a/backend/src/db/models/field_visits.js b/backend/src/db/models/field_visits.js
index 86a1d57..8b00c6f 100644
--- a/backend/src/db/models/field_visits.js
+++ b/backend/src/db/models/field_visits.js
@@ -74,6 +74,14 @@ module.exports = function (sequelize, DataTypes) {
constraints: false,
});
+ db.field_visits.belongsTo(db.organizations, {
+ as: 'organizations',
+ foreignKey: {
+ name: 'organizationsId',
+ },
+ constraints: false,
+ });
+
db.field_visits.belongsTo(db.users, {
as: 'createdBy',
});
diff --git a/backend/src/db/models/organizations.js b/backend/src/db/models/organizations.js
new file mode 100644
index 0000000..2970d80
--- /dev/null
+++ b/backend/src/db/models/organizations.js
@@ -0,0 +1,81 @@
+const config = require('../../config');
+const providers = config.providers;
+const crypto = require('crypto');
+const bcrypt = require('bcrypt');
+const moment = require('moment');
+
+module.exports = function (sequelize, DataTypes) {
+ const organizations = sequelize.define(
+ 'organizations',
+ {
+ id: {
+ type: DataTypes.UUID,
+ defaultValue: DataTypes.UUIDV4,
+ primaryKey: true,
+ },
+
+ name: {
+ type: DataTypes.TEXT,
+ },
+
+ importHash: {
+ type: DataTypes.STRING(255),
+ allowNull: true,
+ unique: true,
+ },
+ },
+ {
+ timestamps: true,
+ paranoid: true,
+ freezeTableName: true,
+ },
+ );
+
+ organizations.associate = (db) => {
+ /// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity
+
+ db.organizations.hasMany(db.users, {
+ as: 'users_organizations',
+ foreignKey: {
+ name: 'organizationsId',
+ },
+ constraints: false,
+ });
+
+ db.organizations.hasMany(db.field_visits, {
+ as: 'field_visits_organizations',
+ foreignKey: {
+ name: 'organizationsId',
+ },
+ constraints: false,
+ });
+
+ db.organizations.hasMany(db.payments, {
+ as: 'payments_organizations',
+ foreignKey: {
+ name: 'organizationsId',
+ },
+ constraints: false,
+ });
+
+ db.organizations.hasMany(db.projects, {
+ as: 'projects_organizations',
+ foreignKey: {
+ name: 'organizationsId',
+ },
+ constraints: false,
+ });
+
+ //end loop
+
+ db.organizations.belongsTo(db.users, {
+ as: 'createdBy',
+ });
+
+ db.organizations.belongsTo(db.users, {
+ as: 'updatedBy',
+ });
+ };
+
+ return organizations;
+};
diff --git a/backend/src/db/models/payments.js b/backend/src/db/models/payments.js
index 4546331..4b0ce8e 100644
--- a/backend/src/db/models/payments.js
+++ b/backend/src/db/models/payments.js
@@ -76,6 +76,14 @@ module.exports = function (sequelize, DataTypes) {
constraints: false,
});
+ db.payments.belongsTo(db.organizations, {
+ as: 'organizations',
+ foreignKey: {
+ name: 'organizationsId',
+ },
+ constraints: false,
+ });
+
db.payments.belongsTo(db.users, {
as: 'createdBy',
});
diff --git a/backend/src/db/models/projects.js b/backend/src/db/models/projects.js
index 3013393..9bc1ae3 100644
--- a/backend/src/db/models/projects.js
+++ b/backend/src/db/models/projects.js
@@ -40,6 +40,10 @@ module.exports = function (sequelize, DataTypes) {
values: ['InProgress', 'Completed'],
},
+ cash_at_bank: {
+ type: DataTypes.DECIMAL,
+ },
+
importHash: {
type: DataTypes.STRING(255),
allowNull: true,
@@ -74,6 +78,14 @@ module.exports = function (sequelize, DataTypes) {
//end loop
+ db.projects.belongsTo(db.organizations, {
+ as: 'organizations',
+ foreignKey: {
+ name: 'organizationsId',
+ },
+ constraints: false,
+ });
+
db.projects.belongsTo(db.users, {
as: 'createdBy',
});
diff --git a/backend/src/db/models/roles.js b/backend/src/db/models/roles.js
index 0ff5736..0f144d5 100644
--- a/backend/src/db/models/roles.js
+++ b/backend/src/db/models/roles.js
@@ -22,6 +22,13 @@ module.exports = function (sequelize, DataTypes) {
type: DataTypes.TEXT,
},
+ globalAccess: {
+ type: DataTypes.BOOLEAN,
+
+ allowNull: false,
+ defaultValue: false,
+ },
+
importHash: {
type: DataTypes.STRING(255),
allowNull: true,
diff --git a/backend/src/db/models/users.js b/backend/src/db/models/users.js
index 74e549e..8c52d13 100644
--- a/backend/src/db/models/users.js
+++ b/backend/src/db/models/users.js
@@ -112,6 +112,14 @@ module.exports = function (sequelize, DataTypes) {
constraints: false,
});
+ db.users.belongsTo(db.organizations, {
+ as: 'organizations',
+ foreignKey: {
+ name: 'organizationsId',
+ },
+ constraints: false,
+ });
+
db.users.hasMany(db.file, {
as: 'avatar',
foreignKey: 'belongsToId',
diff --git a/backend/src/db/seeders/20200430130760-user-roles.js b/backend/src/db/seeders/20200430130760-user-roles.js
index 8336a2c..07a85d3 100644
--- a/backend/src/db/seeders/20200430130760-user-roles.js
+++ b/backend/src/db/seeders/20200430130760-user-roles.js
@@ -95,6 +95,7 @@ module.exports = {
'projects',
'roles',
'permissions',
+ 'organizations',
,
];
await queryInterface.bulkInsert(
@@ -678,6 +679,31 @@ primary key ("roles_permissionsId", "permissionId")
permissionId: getId('DELETE_PERMISSIONS'),
},
+ {
+ createdAt,
+ updatedAt,
+ roles_permissionsId: getId('Administrator'),
+ permissionId: getId('CREATE_ORGANIZATIONS'),
+ },
+ {
+ createdAt,
+ updatedAt,
+ roles_permissionsId: getId('Administrator'),
+ permissionId: getId('READ_ORGANIZATIONS'),
+ },
+ {
+ createdAt,
+ updatedAt,
+ roles_permissionsId: getId('Administrator'),
+ permissionId: getId('UPDATE_ORGANIZATIONS'),
+ },
+ {
+ createdAt,
+ updatedAt,
+ roles_permissionsId: getId('Administrator'),
+ permissionId: getId('DELETE_ORGANIZATIONS'),
+ },
+
{
createdAt,
updatedAt,
diff --git a/backend/src/db/seeders/20231127130745-sample-data.js b/backend/src/db/seeders/20231127130745-sample-data.js
index 2145e66..5b32640 100644
--- a/backend/src/db/seeders/20231127130745-sample-data.js
+++ b/backend/src/db/seeders/20231127130745-sample-data.js
@@ -7,6 +7,8 @@ const Payments = db.payments;
const Projects = db.projects;
+const Organizations = db.organizations;
+
const FieldVisitsData = [
{
employee_name: 'Alice Johnson',
@@ -26,6 +28,8 @@ const FieldVisitsData = [
amount: 200,
purpose: 'Site Inspection',
+
+ // type code here for "relation_one" field
},
{
@@ -41,11 +45,13 @@ const FieldVisitsData = [
return_date: new Date('2023-06-22T00:00:00Z'),
- payment_type: 'Transport',
+ payment_type: 'Fuel',
amount: 150,
purpose: 'Equipment Setup',
+
+ // type code here for "relation_one" field
},
{
@@ -61,11 +67,13 @@ const FieldVisitsData = [
return_date: new Date('2023-07-07T00:00:00Z'),
- payment_type: 'PerDiem',
+ payment_type: 'Fuel',
amount: 100,
purpose: 'Nutritional Workshop',
+
+ // type code here for "relation_one" field
},
{
@@ -81,11 +89,13 @@ const FieldVisitsData = [
return_date: new Date('2023-07-18T00:00:00Z'),
- payment_type: 'Fuel',
+ payment_type: 'Transport',
amount: 250,
purpose: 'Community Meeting',
+
+ // type code here for "relation_one" field
},
{
@@ -106,6 +116,8 @@ const FieldVisitsData = [
amount: 180,
purpose: 'Educational Seminar',
+
+ // type code here for "relation_one" field
},
];
@@ -128,6 +140,8 @@ const PaymentsData = [
status: 'Filled',
shelf_no: 'A1',
+
+ // type code here for "relation_one" field
},
{
@@ -141,13 +155,15 @@ const PaymentsData = [
amount: 12000,
- document_type: 'RV',
+ document_type: 'JV',
auto_generated_document_no: 'CDC002-JV-002',
- status: 'InProgress',
+ status: 'Filled',
shelf_no: 'B2',
+
+ // type code here for "relation_one" field
},
{
@@ -161,13 +177,15 @@ const PaymentsData = [
amount: 8000,
- document_type: 'JV',
+ document_type: 'BPV',
auto_generated_document_no: 'NNP003-RV-003',
status: 'InProgress',
shelf_no: 'C3',
+
+ // type code here for "relation_one" field
},
{
@@ -181,13 +199,15 @@ const PaymentsData = [
amount: 15000,
- document_type: 'RV',
+ document_type: 'JV',
auto_generated_document_no: 'ACDC004-BPV-004',
- status: 'Filled',
+ status: 'InProgress',
shelf_no: 'D4',
+
+ // type code here for "relation_one" field
},
{
@@ -201,13 +221,15 @@ const PaymentsData = [
amount: 3000,
- document_type: 'BPV',
+ document_type: 'JV',
auto_generated_document_no: 'EDU005-JV-005',
- status: 'Filled',
+ status: 'InProgress',
shelf_no: 'E5',
+
+ // type code here for "relation_one" field
},
];
@@ -223,7 +245,11 @@ const ProjectsData = [
end_date: new Date('2023-12-31T00:00:00Z'),
- status: 'Completed',
+ status: 'InProgress',
+
+ cash_at_bank: 96.92,
+
+ // type code here for "relation_one" field
},
{
@@ -238,6 +264,10 @@ const ProjectsData = [
end_date: new Date('2023-11-30T00:00:00Z'),
status: 'Completed',
+
+ cash_at_bank: 85.37,
+
+ // type code here for "relation_one" field
},
{
@@ -251,7 +281,11 @@ const ProjectsData = [
end_date: new Date('2023-10-15T00:00:00Z'),
- status: 'Completed',
+ status: 'InProgress',
+
+ cash_at_bank: 62.46,
+
+ // type code here for "relation_one" field
},
{
@@ -265,7 +299,11 @@ const ProjectsData = [
end_date: new Date('2023-09-20T00:00:00Z'),
- status: 'Completed',
+ status: 'InProgress',
+
+ cash_at_bank: 52.87,
+
+ // type code here for "relation_one" field
},
{
@@ -279,12 +317,95 @@ const ProjectsData = [
end_date: new Date('2023-12-01T00:00:00Z'),
- status: 'Completed',
+ status: 'InProgress',
+
+ cash_at_bank: 52.87,
+
+ // type code here for "relation_one" field
+ },
+];
+
+const OrganizationsData = [
+ {
+ name: 'Albrecht von Haller',
+ },
+
+ {
+ name: 'Louis Pasteur',
+ },
+
+ {
+ name: 'Ernst Haeckel',
+ },
+
+ {
+ name: 'John von Neumann',
+ },
+
+ {
+ name: 'Comte de Buffon',
},
];
// Similar logic for "relation_many"
+async function associateUserWithOrganization() {
+ const relatedOrganization0 = await Organizations.findOne({
+ offset: Math.floor(Math.random() * (await Organizations.count())),
+ });
+ const User0 = await Users.findOne({
+ order: [['id', 'ASC']],
+ offset: 0,
+ });
+ if (User0?.setOrganization) {
+ await User0.setOrganization(relatedOrganization0);
+ }
+
+ const relatedOrganization1 = await Organizations.findOne({
+ offset: Math.floor(Math.random() * (await Organizations.count())),
+ });
+ const User1 = await Users.findOne({
+ order: [['id', 'ASC']],
+ offset: 1,
+ });
+ if (User1?.setOrganization) {
+ await User1.setOrganization(relatedOrganization1);
+ }
+
+ const relatedOrganization2 = await Organizations.findOne({
+ offset: Math.floor(Math.random() * (await Organizations.count())),
+ });
+ const User2 = await Users.findOne({
+ order: [['id', 'ASC']],
+ offset: 2,
+ });
+ if (User2?.setOrganization) {
+ await User2.setOrganization(relatedOrganization2);
+ }
+
+ const relatedOrganization3 = await Organizations.findOne({
+ offset: Math.floor(Math.random() * (await Organizations.count())),
+ });
+ const User3 = await Users.findOne({
+ order: [['id', 'ASC']],
+ offset: 3,
+ });
+ if (User3?.setOrganization) {
+ await User3.setOrganization(relatedOrganization3);
+ }
+
+ const relatedOrganization4 = await Organizations.findOne({
+ offset: Math.floor(Math.random() * (await Organizations.count())),
+ });
+ const User4 = await Users.findOne({
+ order: [['id', 'ASC']],
+ offset: 4,
+ });
+ if (User4?.setOrganization) {
+ await User4.setOrganization(relatedOrganization4);
+ }
+}
+
async function associateFieldVisitWithProject() {
const relatedProject0 = await Projects.findOne({
offset: Math.floor(Math.random() * (await Projects.count())),
@@ -342,6 +463,63 @@ async function associateFieldVisitWithProject() {
}
}
+async function associateFieldVisitWithOrganization() {
+ const relatedOrganization0 = await Organizations.findOne({
+ offset: Math.floor(Math.random() * (await Organizations.count())),
+ });
+ const FieldVisit0 = await FieldVisits.findOne({
+ order: [['id', 'ASC']],
+ offset: 0,
+ });
+ if (FieldVisit0?.setOrganization) {
+ await FieldVisit0.setOrganization(relatedOrganization0);
+ }
+
+ const relatedOrganization1 = await Organizations.findOne({
+ offset: Math.floor(Math.random() * (await Organizations.count())),
+ });
+ const FieldVisit1 = await FieldVisits.findOne({
+ order: [['id', 'ASC']],
+ offset: 1,
+ });
+ if (FieldVisit1?.setOrganization) {
+ await FieldVisit1.setOrganization(relatedOrganization1);
+ }
+
+ const relatedOrganization2 = await Organizations.findOne({
+ offset: Math.floor(Math.random() * (await Organizations.count())),
+ });
+ const FieldVisit2 = await FieldVisits.findOne({
+ order: [['id', 'ASC']],
+ offset: 2,
+ });
+ if (FieldVisit2?.setOrganization) {
+ await FieldVisit2.setOrganization(relatedOrganization2);
+ }
+
+ const relatedOrganization3 = await Organizations.findOne({
+ offset: Math.floor(Math.random() * (await Organizations.count())),
+ });
+ const FieldVisit3 = await FieldVisits.findOne({
+ order: [['id', 'ASC']],
+ offset: 3,
+ });
+ if (FieldVisit3?.setOrganization) {
+ await FieldVisit3.setOrganization(relatedOrganization3);
+ }
+
+ const relatedOrganization4 = await Organizations.findOne({
+ offset: Math.floor(Math.random() * (await Organizations.count())),
+ });
+ const FieldVisit4 = await FieldVisits.findOne({
+ order: [['id', 'ASC']],
+ offset: 4,
+ });
+ if (FieldVisit4?.setOrganization) {
+ await FieldVisit4.setOrganization(relatedOrganization4);
+ }
+}
+
async function associatePaymentWithProject() {
const relatedProject0 = await Projects.findOne({
offset: Math.floor(Math.random() * (await Projects.count())),
@@ -399,6 +577,120 @@ async function associatePaymentWithProject() {
}
}
+async function associatePaymentWithOrganization() {
+ const relatedOrganization0 = await Organizations.findOne({
+ offset: Math.floor(Math.random() * (await Organizations.count())),
+ });
+ const Payment0 = await Payments.findOne({
+ order: [['id', 'ASC']],
+ offset: 0,
+ });
+ if (Payment0?.setOrganization) {
+ await Payment0.setOrganization(relatedOrganization0);
+ }
+
+ const relatedOrganization1 = await Organizations.findOne({
+ offset: Math.floor(Math.random() * (await Organizations.count())),
+ });
+ const Payment1 = await Payments.findOne({
+ order: [['id', 'ASC']],
+ offset: 1,
+ });
+ if (Payment1?.setOrganization) {
+ await Payment1.setOrganization(relatedOrganization1);
+ }
+
+ const relatedOrganization2 = await Organizations.findOne({
+ offset: Math.floor(Math.random() * (await Organizations.count())),
+ });
+ const Payment2 = await Payments.findOne({
+ order: [['id', 'ASC']],
+ offset: 2,
+ });
+ if (Payment2?.setOrganization) {
+ await Payment2.setOrganization(relatedOrganization2);
+ }
+
+ const relatedOrganization3 = await Organizations.findOne({
+ offset: Math.floor(Math.random() * (await Organizations.count())),
+ });
+ const Payment3 = await Payments.findOne({
+ order: [['id', 'ASC']],
+ offset: 3,
+ });
+ if (Payment3?.setOrganization) {
+ await Payment3.setOrganization(relatedOrganization3);
+ }
+
+ const relatedOrganization4 = await Organizations.findOne({
+ offset: Math.floor(Math.random() * (await Organizations.count())),
+ });
+ const Payment4 = await Payments.findOne({
+ order: [['id', 'ASC']],
+ offset: 4,
+ });
+ if (Payment4?.setOrganization) {
+ await Payment4.setOrganization(relatedOrganization4);
+ }
+}
+
+async function associateProjectWithOrganization() {
+ const relatedOrganization0 = await Organizations.findOne({
+ offset: Math.floor(Math.random() * (await Organizations.count())),
+ });
+ const Project0 = await Projects.findOne({
+ order: [['id', 'ASC']],
+ offset: 0,
+ });
+ if (Project0?.setOrganization) {
+ await Project0.setOrganization(relatedOrganization0);
+ }
+
+ const relatedOrganization1 = await Organizations.findOne({
+ offset: Math.floor(Math.random() * (await Organizations.count())),
+ });
+ const Project1 = await Projects.findOne({
+ order: [['id', 'ASC']],
+ offset: 1,
+ });
+ if (Project1?.setOrganization) {
+ await Project1.setOrganization(relatedOrganization1);
+ }
+
+ const relatedOrganization2 = await Organizations.findOne({
+ offset: Math.floor(Math.random() * (await Organizations.count())),
+ });
+ const Project2 = await Projects.findOne({
+ order: [['id', 'ASC']],
+ offset: 2,
+ });
+ if (Project2?.setOrganization) {
+ await Project2.setOrganization(relatedOrganization2);
+ }
+
+ const relatedOrganization3 = await Organizations.findOne({
+ offset: Math.floor(Math.random() * (await Organizations.count())),
+ });
+ const Project3 = await Projects.findOne({
+ order: [['id', 'ASC']],
+ offset: 3,
+ });
+ if (Project3?.setOrganization) {
+ await Project3.setOrganization(relatedOrganization3);
+ }
+
+ const relatedOrganization4 = await Organizations.findOne({
+ offset: Math.floor(Math.random() * (await Organizations.count())),
+ });
+ const Project4 = await Projects.findOne({
+ order: [['id', 'ASC']],
+ offset: 4,
+ });
+ if (Project4?.setOrganization) {
+ await Project4.setOrganization(relatedOrganization4);
+ }
+}
+
module.exports = {
up: async (queryInterface, Sequelize) => {
await FieldVisits.bulkCreate(FieldVisitsData);
@@ -407,12 +699,22 @@ module.exports = {
await Projects.bulkCreate(ProjectsData);
+ await Organizations.bulkCreate(OrganizationsData);
+
await Promise.all([
// Similar logic for "relation_many"
+ await associateUserWithOrganization(),
+
await associateFieldVisitWithProject(),
+ await associateFieldVisitWithOrganization(),
+
await associatePaymentWithProject(),
+
+ await associatePaymentWithOrganization(),
+
+ await associateProjectWithOrganization(),
]);
},
@@ -422,5 +724,7 @@ module.exports = {
await queryInterface.bulkDelete('payments', null, {});
await queryInterface.bulkDelete('projects', null, {});
+
+ await queryInterface.bulkDelete('organizations', null, {});
},
};
diff --git a/backend/src/db/seeders/20250706200806.js b/backend/src/db/seeders/20250706200806.js
new file mode 100644
index 0000000..a0d9408
--- /dev/null
+++ b/backend/src/db/seeders/20250706200806.js
@@ -0,0 +1,87 @@
+const { v4: uuid } = require('uuid');
+const db = require('../models');
+const Sequelize = require('sequelize');
+const config = require('../../config');
+
+module.exports = {
+ /**
+ * @param{import("sequelize").QueryInterface} queryInterface
+ * @return {Promise}
+ */
+ async up(queryInterface) {
+ const createdAt = new Date();
+ const updatedAt = new Date();
+
+ /** @type {Map} */
+ const idMap = new Map();
+
+ /**
+ * @param {string} key
+ * @return {string}
+ */
+ function getId(key) {
+ if (idMap.has(key)) {
+ return idMap.get(key);
+ }
+ const id = uuid();
+ idMap.set(key, id);
+ return id;
+ }
+
+ /**
+ * @param {string} name
+ */
+ function createPermissions(name) {
+ return [
+ {
+ id: getId(`CREATE_${name.toUpperCase()}`),
+ createdAt,
+ updatedAt,
+ name: `CREATE_${name.toUpperCase()}`,
+ },
+ {
+ id: getId(`READ_${name.toUpperCase()}`),
+ createdAt,
+ updatedAt,
+ name: `READ_${name.toUpperCase()}`,
+ },
+ {
+ id: getId(`UPDATE_${name.toUpperCase()}`),
+ createdAt,
+ updatedAt,
+ name: `UPDATE_${name.toUpperCase()}`,
+ },
+ {
+ id: getId(`DELETE_${name.toUpperCase()}`),
+ createdAt,
+ updatedAt,
+ name: `DELETE_${name.toUpperCase()}`,
+ },
+ ];
+ }
+
+ const entities = ['organizations'];
+
+ const createdPermissions = entities.flatMap(createPermissions);
+
+ // Add permissions to database
+ await queryInterface.bulkInsert('permissions', createdPermissions);
+ // Get permissions ids
+ const permissionsIds = createdPermissions.map((p) => p.id);
+ // Get admin role
+ const adminRole = await db.roles.findOne({
+ where: { name: config.roles.admin },
+ });
+
+ if (adminRole) {
+ // Add permissions to admin role if it exists
+ await adminRole.addPermissions(permissionsIds);
+ }
+ },
+ down: async (queryInterface, Sequelize) => {
+ await queryInterface.bulkDelete(
+ 'permissions',
+ entities.flatMap(createPermissions),
+ );
+ },
+};
diff --git a/backend/src/index.js b/backend/src/index.js
index 84b4542..3b939ae 100644
--- a/backend/src/index.js
+++ b/backend/src/index.js
@@ -29,6 +29,8 @@ const rolesRoutes = require('./routes/roles');
const permissionsRoutes = require('./routes/permissions');
+const organizationsRoutes = require('./routes/organizations');
+
const getBaseUrl = (url) => {
if (!url) return '';
return url.endsWith('/api') ? url.slice(0, -4) : url;
@@ -130,6 +132,12 @@ app.use(
permissionsRoutes,
);
+app.use(
+ '/api/organizations',
+ passport.authenticate('jwt', { session: false }),
+ organizationsRoutes,
+);
+
app.use(
'/api/openai',
passport.authenticate('jwt', { session: false }),
diff --git a/backend/src/routes/organizations.js b/backend/src/routes/organizations.js
new file mode 100644
index 0000000..a0db1d9
--- /dev/null
+++ b/backend/src/routes/organizations.js
@@ -0,0 +1,444 @@
+const express = require('express');
+
+const OrganizationsService = require('../services/organizations');
+const OrganizationsDBApi = require('../db/api/organizations');
+const wrapAsync = require('../helpers').wrapAsync;
+
+const router = express.Router();
+
+const { parse } = require('json2csv');
+
+const { checkCrudPermissions } = require('../middlewares/check-permissions');
+
+router.use(checkCrudPermissions('organizations'));
+
+/**
+ * @swagger
+ * components:
+ * schemas:
+ * Organizations:
+ * type: object
+ * properties:
+
+ * name:
+ * type: string
+ * default: name
+
+ */
+
+/**
+ * @swagger
+ * tags:
+ * name: Organizations
+ * description: The Organizations managing API
+ */
+
+/**
+ * @swagger
+ * /api/organizations:
+ * post:
+ * security:
+ * - bearerAuth: []
+ * tags: [Organizations]
+ * summary: Add new item
+ * description: Add new item
+ * requestBody:
+ * required: true
+ * content:
+ * application/json:
+ * schema:
+ * properties:
+ * data:
+ * description: Data of the updated item
+ * type: object
+ * $ref: "#/components/schemas/Organizations"
+ * responses:
+ * 200:
+ * description: The item was successfully added
+ * content:
+ * application/json:
+ * schema:
+ * $ref: "#/components/schemas/Organizations"
+ * 401:
+ * $ref: "#/components/responses/UnauthorizedError"
+ * 405:
+ * description: Invalid input data
+ * 500:
+ * description: Some server error
+ */
+router.post(
+ '/',
+ wrapAsync(async (req, res) => {
+ const referer =
+ req.headers.referer ||
+ `${req.protocol}://${req.hostname}${req.originalUrl}`;
+ const link = new URL(referer);
+ await OrganizationsService.create(
+ req.body.data,
+ req.currentUser,
+ true,
+ link.host,
+ );
+ const payload = true;
+ res.status(200).send(payload);
+ }),
+);
+
+/**
+ * @swagger
+ * /api/budgets/bulk-import:
+ * post:
+ * security:
+ * - bearerAuth: []
+ * tags: [Organizations]
+ * summary: Bulk import items
+ * description: Bulk import items
+ * requestBody:
+ * required: true
+ * content:
+ * application/json:
+ * schema:
+ * properties:
+ * data:
+ * description: Data of the updated items
+ * type: array
+ * items:
+ * $ref: "#/components/schemas/Organizations"
+ * responses:
+ * 200:
+ * description: The items were successfully imported
+ * content:
+ * application/json:
+ * schema:
+ * $ref: "#/components/schemas/Organizations"
+ * 401:
+ * $ref: "#/components/responses/UnauthorizedError"
+ * 405:
+ * description: Invalid input data
+ * 500:
+ * description: Some server error
+ *
+ */
+router.post(
+ '/bulk-import',
+ wrapAsync(async (req, res) => {
+ const referer =
+ req.headers.referer ||
+ `${req.protocol}://${req.hostname}${req.originalUrl}`;
+ const link = new URL(referer);
+ await OrganizationsService.bulkImport(req, res, true, link.host);
+ const payload = true;
+ res.status(200).send(payload);
+ }),
+);
+
+/**
+ * @swagger
+ * /api/organizations/{id}:
+ * put:
+ * security:
+ * - bearerAuth: []
+ * tags: [Organizations]
+ * summary: Update the data of the selected item
+ * description: Update the data of the selected item
+ * parameters:
+ * - in: path
+ * name: id
+ * description: Item ID to update
+ * required: true
+ * schema:
+ * type: string
+ * requestBody:
+ * description: Set new item data
+ * required: true
+ * content:
+ * application/json:
+ * schema:
+ * properties:
+ * id:
+ * description: ID of the updated item
+ * type: string
+ * data:
+ * description: Data of the updated item
+ * type: object
+ * $ref: "#/components/schemas/Organizations"
+ * required:
+ * - id
+ * responses:
+ * 200:
+ * description: The item data was successfully updated
+ * content:
+ * application/json:
+ * schema:
+ * $ref: "#/components/schemas/Organizations"
+ * 400:
+ * description: Invalid ID supplied
+ * 401:
+ * $ref: "#/components/responses/UnauthorizedError"
+ * 404:
+ * description: Item not found
+ * 500:
+ * description: Some server error
+ */
+router.put(
+ '/:id',
+ wrapAsync(async (req, res) => {
+ await OrganizationsService.update(
+ req.body.data,
+ req.body.id,
+ req.currentUser,
+ );
+ const payload = true;
+ res.status(200).send(payload);
+ }),
+);
+
+/**
+ * @swagger
+ * /api/organizations/{id}:
+ * delete:
+ * security:
+ * - bearerAuth: []
+ * tags: [Organizations]
+ * summary: Delete the selected item
+ * description: Delete the selected item
+ * parameters:
+ * - in: path
+ * name: id
+ * description: Item ID to delete
+ * required: true
+ * schema:
+ * type: string
+ * responses:
+ * 200:
+ * description: The item was successfully deleted
+ * content:
+ * application/json:
+ * schema:
+ * $ref: "#/components/schemas/Organizations"
+ * 400:
+ * description: Invalid ID supplied
+ * 401:
+ * $ref: "#/components/responses/UnauthorizedError"
+ * 404:
+ * description: Item not found
+ * 500:
+ * description: Some server error
+ */
+router.delete(
+ '/:id',
+ wrapAsync(async (req, res) => {
+ await OrganizationsService.remove(req.params.id, req.currentUser);
+ const payload = true;
+ res.status(200).send(payload);
+ }),
+);
+
+/**
+ * @swagger
+ * /api/organizations/deleteByIds:
+ * post:
+ * security:
+ * - bearerAuth: []
+ * tags: [Organizations]
+ * summary: Delete the selected item list
+ * description: Delete the selected item list
+ * requestBody:
+ * required: true
+ * content:
+ * application/json:
+ * schema:
+ * properties:
+ * ids:
+ * description: IDs of the updated items
+ * type: array
+ * responses:
+ * 200:
+ * description: The items was successfully deleted
+ * content:
+ * application/json:
+ * schema:
+ * $ref: "#/components/schemas/Organizations"
+ * 401:
+ * $ref: "#/components/responses/UnauthorizedError"
+ * 404:
+ * description: Items not found
+ * 500:
+ * description: Some server error
+ */
+router.post(
+ '/deleteByIds',
+ wrapAsync(async (req, res) => {
+ await OrganizationsService.deleteByIds(req.body.data, req.currentUser);
+ const payload = true;
+ res.status(200).send(payload);
+ }),
+);
+
+/**
+ * @swagger
+ * /api/organizations:
+ * get:
+ * security:
+ * - bearerAuth: []
+ * tags: [Organizations]
+ * summary: Get all organizations
+ * description: Get all organizations
+ * responses:
+ * 200:
+ * description: Organizations list successfully received
+ * content:
+ * application/json:
+ * schema:
+ * type: array
+ * items:
+ * $ref: "#/components/schemas/Organizations"
+ * 401:
+ * $ref: "#/components/responses/UnauthorizedError"
+ * 404:
+ * description: Data not found
+ * 500:
+ * description: Some server error
+ */
+router.get(
+ '/',
+ wrapAsync(async (req, res) => {
+ const filetype = req.query.filetype;
+
+ const currentUser = req.currentUser;
+ const payload = await OrganizationsDBApi.findAll(req.query, {
+ currentUser,
+ });
+ if (filetype && filetype === 'csv') {
+ const fields = ['id', 'name'];
+ const opts = { fields };
+ try {
+ const csv = parse(payload.rows, opts);
+ res.status(200).attachment(csv);
+ res.send(csv);
+ } catch (err) {
+ console.error(err);
+ }
+ } else {
+ res.status(200).send(payload);
+ }
+ }),
+);
+
+/**
+ * @swagger
+ * /api/organizations/count:
+ * get:
+ * security:
+ * - bearerAuth: []
+ * tags: [Organizations]
+ * summary: Count all organizations
+ * description: Count all organizations
+ * responses:
+ * 200:
+ * description: Organizations count successfully received
+ * content:
+ * application/json:
+ * schema:
+ * type: array
+ * items:
+ * $ref: "#/components/schemas/Organizations"
+ * 401:
+ * $ref: "#/components/responses/UnauthorizedError"
+ * 404:
+ * description: Data not found
+ * 500:
+ * description: Some server error
+ */
+router.get(
+ '/count',
+ wrapAsync(async (req, res) => {
+ const currentUser = req.currentUser;
+ const payload = await OrganizationsDBApi.findAll(req.query, null, {
+ countOnly: true,
+ currentUser,
+ });
+
+ res.status(200).send(payload);
+ }),
+);
+
+/**
+ * @swagger
+ * /api/organizations/autocomplete:
+ * get:
+ * security:
+ * - bearerAuth: []
+ * tags: [Organizations]
+ * summary: Find all organizations that match search criteria
+ * description: Find all organizations that match search criteria
+ * responses:
+ * 200:
+ * description: Organizations list successfully received
+ * content:
+ * application/json:
+ * schema:
+ * type: array
+ * items:
+ * $ref: "#/components/schemas/Organizations"
+ * 401:
+ * $ref: "#/components/responses/UnauthorizedError"
+ * 404:
+ * description: Data not found
+ * 500:
+ * description: Some server error
+ */
+router.get('/autocomplete', async (req, res) => {
+ const payload = await OrganizationsDBApi.findAllAutocomplete(
+ req.query.query,
+ req.query.limit,
+ req.query.offset,
+ );
+
+ res.status(200).send(payload);
+});
+
+/**
+ * @swagger
+ * /api/organizations/{id}:
+ * get:
+ * security:
+ * - bearerAuth: []
+ * tags: [Organizations]
+ * summary: Get selected item
+ * description: Get selected item
+ * parameters:
+ * - in: path
+ * name: id
+ * description: ID of item to get
+ * required: true
+ * schema:
+ * type: string
+ * responses:
+ * 200:
+ * description: Selected item successfully received
+ * content:
+ * application/json:
+ * schema:
+ * $ref: "#/components/schemas/Organizations"
+ * 400:
+ * description: Invalid ID supplied
+ * 401:
+ * $ref: "#/components/responses/UnauthorizedError"
+ * 404:
+ * description: Item not found
+ * 500:
+ * description: Some server error
+ */
+router.get(
+ '/:id',
+ wrapAsync(async (req, res) => {
+ const payload = await OrganizationsDBApi.findBy({ id: req.params.id });
+
+ res.status(200).send(payload);
+ }),
+);
+
+router.use('/', require('../helpers').commonErrorHandler);
+
+module.exports = router;
diff --git a/backend/src/services/organizations.js b/backend/src/services/organizations.js
new file mode 100644
index 0000000..453aaf3
--- /dev/null
+++ b/backend/src/services/organizations.js
@@ -0,0 +1,117 @@
+const db = require('../db/models');
+const OrganizationsDBApi = require('../db/api/organizations');
+const processFile = require('../middlewares/upload');
+const ValidationError = require('./notifications/errors/validation');
+const csv = require('csv-parser');
+const axios = require('axios');
+const config = require('../config');
+const stream = require('stream');
+
+module.exports = class OrganizationsService {
+ static async create(data, currentUser) {
+ const transaction = await db.sequelize.transaction();
+ try {
+ await OrganizationsDBApi.create(data, {
+ currentUser,
+ transaction,
+ });
+
+ await transaction.commit();
+ } catch (error) {
+ await transaction.rollback();
+ throw error;
+ }
+ }
+
+ static async bulkImport(req, res, sendInvitationEmails = true, host) {
+ const transaction = await db.sequelize.transaction();
+
+ try {
+ await processFile(req, res);
+ const bufferStream = new stream.PassThrough();
+ const results = [];
+
+ await bufferStream.end(Buffer.from(req.file.buffer, 'utf-8')); // convert Buffer to Stream
+
+ await new Promise((resolve, reject) => {
+ bufferStream
+ .pipe(csv())
+ .on('data', (data) => results.push(data))
+ .on('end', async () => {
+ console.log('CSV results', results);
+ resolve();
+ })
+ .on('error', (error) => reject(error));
+ });
+
+ await OrganizationsDBApi.bulkImport(results, {
+ transaction,
+ ignoreDuplicates: true,
+ validate: true,
+ currentUser: req.currentUser,
+ });
+
+ await transaction.commit();
+ } catch (error) {
+ await transaction.rollback();
+ throw error;
+ }
+ }
+
+ static async update(data, id, currentUser) {
+ const transaction = await db.sequelize.transaction();
+ try {
+ let organizations = await OrganizationsDBApi.findBy(
+ { id },
+ { transaction },
+ );
+
+ if (!organizations) {
+ throw new ValidationError('organizationsNotFound');
+ }
+
+ const updatedOrganizations = await OrganizationsDBApi.update(id, data, {
+ currentUser,
+ transaction,
+ });
+
+ await transaction.commit();
+ return updatedOrganizations;
+ } catch (error) {
+ await transaction.rollback();
+ throw error;
+ }
+ }
+
+ static async deleteByIds(ids, currentUser) {
+ const transaction = await db.sequelize.transaction();
+
+ try {
+ await OrganizationsDBApi.deleteByIds(ids, {
+ currentUser,
+ transaction,
+ });
+
+ await transaction.commit();
+ } catch (error) {
+ await transaction.rollback();
+ throw error;
+ }
+ }
+
+ static async remove(id, currentUser) {
+ const transaction = await db.sequelize.transaction();
+
+ try {
+ await OrganizationsDBApi.remove(id, {
+ currentUser,
+ transaction,
+ });
+
+ await transaction.commit();
+ } catch (error) {
+ await transaction.rollback();
+ throw error;
+ }
+ }
+};
diff --git a/backend/src/services/search.js b/backend/src/services/search.js
index 5fae432..7093533 100644
--- a/backend/src/services/search.js
+++ b/backend/src/services/search.js
@@ -56,11 +56,15 @@ module.exports = class SearchService {
],
projects: ['name', 'code', 'source_of_funds'],
+
+ organizations: ['name'],
};
const columnsInt = {
field_visits: ['amount'],
payments: ['amount'],
+
+ projects: ['cash_at_bank'],
};
let allFoundRecords = [];
diff --git a/frontend/json/runtimeError.json b/frontend/json/runtimeError.json
new file mode 100644
index 0000000..9e26dfe
--- /dev/null
+++ b/frontend/json/runtimeError.json
@@ -0,0 +1 @@
+{}
\ No newline at end of file
diff --git a/frontend/src/components/Field_visits/TableField_visits.tsx b/frontend/src/components/Field_visits/TableField_visits.tsx
index 0a10a4b..ad6ffa8 100644
--- a/frontend/src/components/Field_visits/TableField_visits.tsx
+++ b/frontend/src/components/Field_visits/TableField_visits.tsx
@@ -20,10 +20,7 @@ import _ from 'lodash';
import dataFormatter from '../../helpers/dataFormatter';
import { dataGridStyles } from '../../styles';
-import BigCalendar from '../BigCalendar';
-import { SlotInfo } from 'react-big-calendar';
-
-const perPage = 100;
+const perPage = 10;
const TableSampleField_visits = ({
filterItems,
@@ -104,12 +101,6 @@ const TableSampleField_visits = ({
setIsModalTrashActive(false);
};
- const handleCreateEventAction = ({ start, end }: SlotInfo) => {
- router.push(
- `/field_visits/field_visits-new?dateRangeStart=${start.toISOString()}&dateRangeEnd=${end.toISOString()}`,
- );
- };
-
const handleDeleteModalAction = (id: string) => {
setId(id);
setIsModalTrashActive(true);
@@ -473,25 +464,7 @@ const TableSampleField_visits = ({
Are you sure you want to delete this item?
- {!showGrid && (
- {
- loadData(
- 0,
- `&calendarStart=${range.start}&calendarEnd=${range.end}`,
- );
- }}
- entityName={'field_visits'}
- />
- )}
+ {dataGrid}
{showGrid && dataGrid}
diff --git a/frontend/src/components/Organizations/CardOrganizations.tsx b/frontend/src/components/Organizations/CardOrganizations.tsx
new file mode 100644
index 0000000..cc0a7cc
--- /dev/null
+++ b/frontend/src/components/Organizations/CardOrganizations.tsx
@@ -0,0 +1,108 @@
+import React from 'react';
+import ImageField from '../ImageField';
+import ListActionsPopover from '../ListActionsPopover';
+import { useAppSelector } from '../../stores/hooks';
+import dataFormatter from '../../helpers/dataFormatter';
+import { Pagination } from '../Pagination';
+import { saveFile } from '../../helpers/fileSaver';
+import LoadingSpinner from '../LoadingSpinner';
+import Link from 'next/link';
+
+import { hasPermission } from '../../helpers/userPermissions';
+
+type Props = {
+ organizations: any[];
+ loading: boolean;
+ onDelete: (id: string) => void;
+ currentPage: number;
+ numPages: number;
+ onPageChange: (page: number) => void;
+};
+
+const CardOrganizations = ({
+ organizations,
+ loading,
+ onDelete,
+ currentPage,
+ numPages,
+ onPageChange,
+}: Props) => {
+ const asideScrollbarsStyle = useAppSelector(
+ (state) => state.style.asideScrollbarsStyle,
+ );
+ const bgColor = useAppSelector((state) => state.style.cardsColor);
+ const darkMode = useAppSelector((state) => state.style.darkMode);
+ const corners = useAppSelector((state) => state.style.corners);
+ const focusRing = useAppSelector((state) => state.style.focusRingColor);
+
+ const currentUser = useAppSelector((state) => state.auth.currentUser);
+ const hasUpdatePermission = hasPermission(
+ currentUser,
+ 'UPDATE_ORGANIZATIONS',
+ );
+
+ return (
+
+ {loading &&
}
+
+ {!loading &&
+ organizations.map((item, index) => (
+ -
+
+
+ {item.id}
+
+
+
+
+
+
+
+
+
- Name
+
-
+
{item.name}
+
+
+
+
+ ))}
+ {!loading && organizations.length === 0 && (
+
+ )}
+
+
+
+ );
+};
+
+export default CardOrganizations;
diff --git a/frontend/src/components/Organizations/ListOrganizations.tsx b/frontend/src/components/Organizations/ListOrganizations.tsx
new file mode 100644
index 0000000..a383510
--- /dev/null
+++ b/frontend/src/components/Organizations/ListOrganizations.tsx
@@ -0,0 +1,90 @@
+import React from 'react';
+import CardBox from '../CardBox';
+import ImageField from '../ImageField';
+import dataFormatter from '../../helpers/dataFormatter';
+import { saveFile } from '../../helpers/fileSaver';
+import ListActionsPopover from '../ListActionsPopover';
+import { useAppSelector } from '../../stores/hooks';
+import { Pagination } from '../Pagination';
+import LoadingSpinner from '../LoadingSpinner';
+import Link from 'next/link';
+
+import { hasPermission } from '../../helpers/userPermissions';
+
+type Props = {
+ organizations: any[];
+ loading: boolean;
+ onDelete: (id: string) => void;
+ currentPage: number;
+ numPages: number;
+ onPageChange: (page: number) => void;
+};
+
+const ListOrganizations = ({
+ organizations,
+ loading,
+ onDelete,
+ currentPage,
+ numPages,
+ onPageChange,
+}: Props) => {
+ const currentUser = useAppSelector((state) => state.auth.currentUser);
+ const hasUpdatePermission = hasPermission(
+ currentUser,
+ 'UPDATE_ORGANIZATIONS',
+ );
+
+ const corners = useAppSelector((state) => state.style.corners);
+ const bgColor = useAppSelector((state) => state.style.cardsColor);
+
+ return (
+ <>
+
+ {loading &&
}
+ {!loading &&
+ organizations.map((item) => (
+
+
+
+
dark:divide-dark-700 overflow-x-auto'
+ }
+ >
+
+
+
+
+
+
+ ))}
+ {!loading && organizations.length === 0 && (
+
+ )}
+
+
+ >
+ );
+};
+
+export default ListOrganizations;
diff --git a/frontend/src/components/Organizations/TableOrganizations.tsx b/frontend/src/components/Organizations/TableOrganizations.tsx
new file mode 100644
index 0000000..5175c54
--- /dev/null
+++ b/frontend/src/components/Organizations/TableOrganizations.tsx
@@ -0,0 +1,484 @@
+import React, { useEffect, useState, useMemo } from 'react';
+import { createPortal } from 'react-dom';
+import { ToastContainer, toast } from 'react-toastify';
+import BaseButton from '../BaseButton';
+import CardBoxModal from '../CardBoxModal';
+import CardBox from '../CardBox';
+import {
+ fetch,
+ update,
+ deleteItem,
+ setRefetch,
+ deleteItemsByIds,
+} from '../../stores/organizations/organizationsSlice';
+import { useAppDispatch, useAppSelector } from '../../stores/hooks';
+import { useRouter } from 'next/router';
+import { Field, Form, Formik } from 'formik';
+import { DataGrid, GridColDef } from '@mui/x-data-grid';
+import { loadColumns } from './configureOrganizationsCols';
+import _ from 'lodash';
+import dataFormatter from '../../helpers/dataFormatter';
+import { dataGridStyles } from '../../styles';
+
+const perPage = 10;
+
+const TableSampleOrganizations = ({
+ filterItems,
+ setFilterItems,
+ filters,
+ showGrid,
+}) => {
+ const notify = (type, msg) => toast(msg, { type, position: 'bottom-center' });
+
+ const dispatch = useAppDispatch();
+ const router = useRouter();
+
+ const pagesList = [];
+ const [id, setId] = useState(null);
+ const [currentPage, setCurrentPage] = useState(0);
+ const [filterRequest, setFilterRequest] = React.useState('');
+ const [columns, setColumns] = useState([]);
+ const [selectedRows, setSelectedRows] = useState([]);
+ const [sortModel, setSortModel] = useState([
+ {
+ field: '',
+ sort: 'desc',
+ },
+ ]);
+
+ const {
+ organizations,
+ loading,
+ count,
+ notify: organizationsNotify,
+ refetch,
+ } = useAppSelector((state) => state.organizations);
+ const { currentUser } = useAppSelector((state) => state.auth);
+ const focusRing = useAppSelector((state) => state.style.focusRingColor);
+ const bgColor = useAppSelector((state) => state.style.bgLayoutColor);
+ const corners = useAppSelector((state) => state.style.corners);
+ const numPages =
+ Math.floor(count / perPage) === 0 ? 1 : Math.ceil(count / perPage);
+ for (let i = 0; i < numPages; i++) {
+ pagesList.push(i);
+ }
+
+ const loadData = async (page = currentPage, request = filterRequest) => {
+ if (page !== currentPage) setCurrentPage(page);
+ if (request !== filterRequest) setFilterRequest(request);
+ const { sort, field } = sortModel[0];
+
+ const query = `?page=${page}&limit=${perPage}${request}&sort=${sort}&field=${field}`;
+ dispatch(fetch({ limit: perPage, page, query }));
+ };
+
+ useEffect(() => {
+ if (organizationsNotify.showNotification) {
+ notify(
+ organizationsNotify.typeNotification,
+ organizationsNotify.textNotification,
+ );
+ }
+ }, [organizationsNotify.showNotification]);
+
+ useEffect(() => {
+ if (!currentUser) return;
+ loadData();
+ }, [sortModel, currentUser]);
+
+ useEffect(() => {
+ if (refetch) {
+ loadData(0);
+ dispatch(setRefetch(false));
+ }
+ }, [refetch, dispatch]);
+
+ const [isModalInfoActive, setIsModalInfoActive] = useState(false);
+ const [isModalTrashActive, setIsModalTrashActive] = useState(false);
+
+ const handleModalAction = () => {
+ setIsModalInfoActive(false);
+ setIsModalTrashActive(false);
+ };
+
+ const handleDeleteModalAction = (id: string) => {
+ setId(id);
+ setIsModalTrashActive(true);
+ };
+ const handleDeleteAction = async () => {
+ if (id) {
+ await dispatch(deleteItem(id));
+ await loadData(0);
+ setIsModalTrashActive(false);
+ }
+ };
+
+ const generateFilterRequests = useMemo(() => {
+ let request = '&';
+ filterItems.forEach((item) => {
+ const isRangeFilter = filters.find(
+ (filter) =>
+ filter.title === item.fields.selectedField &&
+ (filter.number || filter.date),
+ );
+
+ if (isRangeFilter) {
+ const from = item.fields.filterValueFrom;
+ const to = item.fields.filterValueTo;
+ if (from) {
+ request += `${item.fields.selectedField}Range=${from}&`;
+ }
+ if (to) {
+ request += `${item.fields.selectedField}Range=${to}&`;
+ }
+ } else {
+ const value = item.fields.filterValue;
+ if (value) {
+ request += `${item.fields.selectedField}=${value}&`;
+ }
+ }
+ });
+ return request;
+ }, [filterItems, filters]);
+
+ const deleteFilter = (value) => {
+ const newItems = filterItems.filter((item) => item.id !== value);
+
+ if (newItems.length) {
+ setFilterItems(newItems);
+ } else {
+ loadData(0, '');
+
+ setFilterItems(newItems);
+ }
+ };
+
+ const handleSubmit = () => {
+ loadData(0, generateFilterRequests);
+ };
+
+ const handleChange = (id) => (e) => {
+ const value = e.target.value;
+ const name = e.target.name;
+
+ setFilterItems(
+ filterItems.map((item) => {
+ if (item.id !== id) return item;
+ if (name === 'selectedField') return { id, fields: { [name]: value } };
+
+ return { id, fields: { ...item.fields, [name]: value } };
+ }),
+ );
+ };
+
+ const handleReset = () => {
+ setFilterItems([]);
+ loadData(0, '');
+ };
+
+ const onPageChange = (page: number) => {
+ loadData(page);
+ setCurrentPage(page);
+ };
+
+ useEffect(() => {
+ if (!currentUser) return;
+
+ loadColumns(handleDeleteModalAction, `organizations`, currentUser).then(
+ (newCols) => setColumns(newCols),
+ );
+ }, [currentUser]);
+
+ const handleTableSubmit = async (id: string, data) => {
+ if (!_.isEmpty(data)) {
+ await dispatch(update({ id, data }))
+ .unwrap()
+ .then((res) => res)
+ .catch((err) => {
+ throw new Error(err);
+ });
+ }
+ };
+
+ const onDeleteRows = async (selectedRows) => {
+ await dispatch(deleteItemsByIds(selectedRows));
+ await loadData(0);
+ };
+
+ const controlClasses =
+ 'w-full py-2 px-2 my-2 rounded dark:placeholder-gray-400 ' +
+ ` ${bgColor} ${focusRing} ${corners} ` +
+ 'dark:bg-slate-800 border';
+
+ const dataGrid = (
+
+ `datagrid--row`}
+ rows={organizations ?? []}
+ columns={columns}
+ initialState={{
+ pagination: {
+ paginationModel: {
+ pageSize: 10,
+ },
+ },
+ }}
+ disableRowSelectionOnClick
+ onProcessRowUpdateError={(params) => {
+ console.log('Error', params);
+ }}
+ processRowUpdate={async (newRow, oldRow) => {
+ const data = dataFormatter.dataGridEditFormatter(newRow);
+
+ try {
+ await handleTableSubmit(newRow.id, data);
+ return newRow;
+ } catch {
+ return oldRow;
+ }
+ }}
+ sortingMode={'server'}
+ checkboxSelection
+ onRowSelectionModelChange={(ids) => {
+ setSelectedRows(ids);
+ }}
+ onSortModelChange={(params) => {
+ params.length
+ ? setSortModel(params)
+ : setSortModel([{ field: '', sort: 'desc' }]);
+ }}
+ rowCount={count}
+ pageSizeOptions={[10]}
+ paginationMode={'server'}
+ loading={loading}
+ onPaginationModelChange={(params) => {
+ onPageChange(params.page);
+ }}
+ />
+
+ );
+
+ return (
+ <>
+ {filterItems && Array.isArray(filterItems) && filterItems.length ? (
+
+ null}
+ >
+
+
+
+ ) : null}
+
+ Are you sure you want to delete this item?
+
+
+ {dataGrid}
+
+ {selectedRows.length > 0 &&
+ createPortal(
+ onDeleteRows(selectedRows)}
+ />,
+ document.getElementById('delete-rows-button'),
+ )}
+
+ >
+ );
+};
+
+export default TableSampleOrganizations;
diff --git a/frontend/src/components/Organizations/configureOrganizationsCols.tsx b/frontend/src/components/Organizations/configureOrganizationsCols.tsx
new file mode 100644
index 0000000..b8b52e0
--- /dev/null
+++ b/frontend/src/components/Organizations/configureOrganizationsCols.tsx
@@ -0,0 +1,74 @@
+import React from 'react';
+import BaseIcon from '../BaseIcon';
+import { mdiEye, mdiTrashCan, mdiPencilOutline } from '@mdi/js';
+import axios from 'axios';
+import {
+ GridActionsCellItem,
+ GridRowParams,
+ GridValueGetterParams,
+} from '@mui/x-data-grid';
+import ImageField from '../ImageField';
+import { saveFile } from '../../helpers/fileSaver';
+import dataFormatter from '../../helpers/dataFormatter';
+import DataGridMultiSelect from '../DataGridMultiSelect';
+import ListActionsPopover from '../ListActionsPopover';
+
+import { hasPermission } from '../../helpers/userPermissions';
+
+type Params = (id: string) => void;
+
+export const loadColumns = async (
+ onDelete: Params,
+ entityName: string,
+
+ user,
+) => {
+ async function callOptionsApi(entityName: string) {
+ if (!hasPermission(user, 'READ_' + entityName.toUpperCase())) return [];
+
+ try {
+ const data = await axios(`/${entityName}/autocomplete?limit=100`);
+ return data.data;
+ } catch (error) {
+ console.log(error);
+ return [];
+ }
+ }
+
+ const hasUpdatePermission = hasPermission(user, 'UPDATE_ORGANIZATIONS');
+
+ return [
+ {
+ field: 'name',
+ headerName: 'Name',
+ flex: 1,
+ minWidth: 120,
+ filterable: false,
+ headerClassName: 'datagrid--header',
+ cellClassName: 'datagrid--cell',
+
+ editable: hasUpdatePermission,
+ },
+
+ {
+ field: 'actions',
+ type: 'actions',
+ minWidth: 30,
+ headerClassName: 'datagrid--header',
+ cellClassName: 'datagrid--cell',
+ getActions: (params: GridRowParams) => {
+ return [
+
+
+
,
+ ];
+ },
+ },
+ ];
+};
diff --git a/frontend/src/components/Roles/CardRoles.tsx b/frontend/src/components/Roles/CardRoles.tsx
index 4daa835..86d10a4 100644
--- a/frontend/src/components/Roles/CardRoles.tsx
+++ b/frontend/src/components/Roles/CardRoles.tsx
@@ -95,6 +95,17 @@ const CardRoles = ({
+
+
+
+ Global Access
+
+
+
+ {dataFormatter.booleanFormatter(item.globalAccess)}
+
+
+
))}
diff --git a/frontend/src/components/Roles/ListRoles.tsx b/frontend/src/components/Roles/ListRoles.tsx
index e4299d9..ef2de05 100644
--- a/frontend/src/components/Roles/ListRoles.tsx
+++ b/frontend/src/components/Roles/ListRoles.tsx
@@ -64,6 +64,15 @@ const ListRoles = ({
.join(', ')}
+
+
+
+ Global Access
+
+
+ {dataFormatter.booleanFormatter(item.globalAccess)}
+
+
+
+
+
+ Organizations
+
+
+
+ {dataFormatter.organizationsOneListFormatter(
+ item.organizations,
+ )}
+
+
+
))}
diff --git a/frontend/src/components/Users/ListUsers.tsx b/frontend/src/components/Users/ListUsers.tsx
index 61fe76d..e735124 100644
--- a/frontend/src/components/Users/ListUsers.tsx
+++ b/frontend/src/components/Users/ListUsers.tsx
@@ -113,6 +113,17 @@ const ListUsers = ({
.join(', ')}
+
+
+
+ Organizations
+
+
+ {dataFormatter.organizationsOneListFormatter(
+ item.organizations,
+ )}
+
+
value?.id,
+ getOptionLabel: (value: any) => value?.label,
+ valueOptions: await callOptionsApi('organizations'),
+ valueGetter: (params: GridValueGetterParams) =>
+ params?.value?.id ?? params?.value,
+ },
+
{
field: 'actions',
type: 'actions',
diff --git a/frontend/src/helpers/dataFormatter.js b/frontend/src/helpers/dataFormatter.js
index 13d5685..7111a2e 100644
--- a/frontend/src/helpers/dataFormatter.js
+++ b/frontend/src/helpers/dataFormatter.js
@@ -95,4 +95,23 @@ export default {
if (!val) return '';
return { label: val.name, id: val.id };
},
+
+ organizationsManyListFormatter(val) {
+ if (!val || !val.length) return [];
+ return val.map((item) => item.id);
+ },
+ organizationsOneListFormatter(val) {
+ if (!val) return '';
+ return val.id;
+ },
+ organizationsManyListFormatterEdit(val) {
+ if (!val || !val.length) return [];
+ return val.map((item) => {
+ return { id: item.id, label: item.id };
+ });
+ },
+ organizationsOneListFormatterEdit(val) {
+ if (!val) return '';
+ return { label: val.id, id: val.id };
+ },
};
diff --git a/frontend/src/menuAside.ts b/frontend/src/menuAside.ts
index 7352781..b498cf5 100644
--- a/frontend/src/menuAside.ts
+++ b/frontend/src/menuAside.ts
@@ -65,6 +65,14 @@ const menuAside: MenuAsideItem[] = [
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: '/profile',
label: 'Profile',
diff --git a/frontend/src/pages/dashboard.tsx b/frontend/src/pages/dashboard.tsx
index 6723809..eeacb50 100644
--- a/frontend/src/pages/dashboard.tsx
+++ b/frontend/src/pages/dashboard.tsx
@@ -34,6 +34,7 @@ const Dashboard = () => {
const [projects, setProjects] = React.useState(loadingMessage);
const [roles, setRoles] = React.useState(loadingMessage);
const [permissions, setPermissions] = React.useState(loadingMessage);
+ const [organizations, setOrganizations] = React.useState(loadingMessage);
const [widgetsRole, setWidgetsRole] = React.useState({
role: { value: '', label: '' },
@@ -51,6 +52,7 @@ const Dashboard = () => {
'projects',
'roles',
'permissions',
+ 'organizations',
];
const fns = [
setUsers,
@@ -59,6 +61,7 @@ const Dashboard = () => {
setProjects,
setRoles,
setPermissions,
+ setOrganizations,
];
const requests = entities.map((entity, index) => {
@@ -376,6 +379,38 @@ const Dashboard = () => {
)}
+
+ {hasPermission(currentUser, 'READ_ORGANIZATIONS') && (
+
+
+
+
+
+ Organizations
+
+
+ {organizations}
+
+
+
+
+
+
+
+
+ )}
>
diff --git a/frontend/src/pages/field_visits/[field_visitsId].tsx b/frontend/src/pages/field_visits/[field_visitsId].tsx
index cc1ad63..376a274 100644
--- a/frontend/src/pages/field_visits/[field_visitsId].tsx
+++ b/frontend/src/pages/field_visits/[field_visitsId].tsx
@@ -53,6 +53,8 @@ const EditField_visits = () => {
amount: '',
purpose: '',
+
+ organizations: null,
};
const [initialValues, setInitialValues] = useState(initVals);
@@ -201,6 +203,17 @@ const EditField_visits = () => {
+
+
+
+
diff --git a/frontend/src/pages/field_visits/field_visits-edit.tsx b/frontend/src/pages/field_visits/field_visits-edit.tsx
index 246f89d..0461eaf 100644
--- a/frontend/src/pages/field_visits/field_visits-edit.tsx
+++ b/frontend/src/pages/field_visits/field_visits-edit.tsx
@@ -53,6 +53,8 @@ const EditField_visitsPage = () => {
amount: '',
purpose: '',
+
+ organizations: null,
};
const [initialValues, setInitialValues] = useState(initVals);
@@ -199,6 +201,17 @@ const EditField_visitsPage = () => {
+
+
+
+
diff --git a/frontend/src/pages/field_visits/field_visits-new.tsx b/frontend/src/pages/field_visits/field_visits-new.tsx
index 5a5cfff..474a0e7 100644
--- a/frontend/src/pages/field_visits/field_visits-new.tsx
+++ b/frontend/src/pages/field_visits/field_visits-new.tsx
@@ -50,15 +50,14 @@ const initialValues = {
amount: '',
purpose: '',
+
+ organizations: '',
};
const Field_visitsNew = () => {
const router = useRouter();
const dispatch = useAppDispatch();
- // get from url params
- const { dateRangeStart, dateRangeEnd } = router.query;
-
const handleSubmit = async (data) => {
await dispatch(create(data));
await router.push('/field_visits/field_visits-list');
@@ -78,17 +77,7 @@ const Field_visitsNew = () => {
handleSubmit(values)}
>
diff --git a/frontend/src/pages/field_visits/field_visits-view.tsx b/frontend/src/pages/field_visits/field_visits-view.tsx
index ade8696..5abbb76 100644
--- a/frontend/src/pages/field_visits/field_visits-view.tsx
+++ b/frontend/src/pages/field_visits/field_visits-view.tsx
@@ -148,6 +148,12 @@ const Field_visitsView = () => {
{field_visits?.purpose}
+
+
organizations
+
+
{field_visits?.organizations?.id ?? 'No data'}
+
+
{
+ const router = useRouter();
+ const dispatch = useAppDispatch();
+ const initVals = {
+ name: '',
+ };
+ const [initialValues, setInitialValues] = useState(initVals);
+
+ const { organizations } = useAppSelector((state) => state.organizations);
+
+ const { organizationsId } = router.query;
+
+ useEffect(() => {
+ dispatch(fetch({ id: organizationsId }));
+ }, [organizationsId]);
+
+ useEffect(() => {
+ if (typeof organizations === 'object') {
+ setInitialValues(organizations);
+ }
+ }, [organizations]);
+
+ useEffect(() => {
+ if (typeof organizations === 'object') {
+ const newInitialVal = { ...initVals };
+
+ Object.keys(initVals).forEach(
+ (el) => (newInitialVal[el] = organizations[el]),
+ );
+
+ setInitialValues(newInitialVal);
+ }
+ }, [organizations]);
+
+ const handleSubmit = async (data) => {
+ await dispatch(update({ id: organizationsId, data }));
+ await router.push('/organizations/organizations-list');
+ };
+
+ return (
+ <>
+
+ {getPageTitle('Edit organizations')}
+
+
+
+ {''}
+
+
+ handleSubmit(values)}
+ >
+
+
+
+
+ >
+ );
+};
+
+EditOrganizations.getLayout = function getLayout(page: ReactElement) {
+ return (
+
+ {page}
+
+ );
+};
+
+export default EditOrganizations;
diff --git a/frontend/src/pages/organizations/organizations-edit.tsx b/frontend/src/pages/organizations/organizations-edit.tsx
new file mode 100644
index 0000000..5b37f27
--- /dev/null
+++ b/frontend/src/pages/organizations/organizations-edit.tsx
@@ -0,0 +1,126 @@
+import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js';
+import Head from 'next/head';
+import React, { ReactElement, useEffect, useState } from 'react';
+import DatePicker from 'react-datepicker';
+import 'react-datepicker/dist/react-datepicker.css';
+import dayjs from 'dayjs';
+
+import CardBox from '../../components/CardBox';
+import LayoutAuthenticated from '../../layouts/Authenticated';
+import SectionMain from '../../components/SectionMain';
+import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton';
+import { getPageTitle } from '../../config';
+
+import { Field, Form, Formik } from 'formik';
+import FormField from '../../components/FormField';
+import BaseDivider from '../../components/BaseDivider';
+import BaseButtons from '../../components/BaseButtons';
+import BaseButton from '../../components/BaseButton';
+import FormCheckRadio from '../../components/FormCheckRadio';
+import FormCheckRadioGroup from '../../components/FormCheckRadioGroup';
+import FormFilePicker from '../../components/FormFilePicker';
+import FormImagePicker from '../../components/FormImagePicker';
+import { SelectField } from '../../components/SelectField';
+import { SelectFieldMany } from '../../components/SelectFieldMany';
+import { SwitchField } from '../../components/SwitchField';
+import { RichTextField } from '../../components/RichTextField';
+
+import { update, fetch } from '../../stores/organizations/organizationsSlice';
+import { useAppDispatch, useAppSelector } from '../../stores/hooks';
+import { useRouter } from 'next/router';
+import { saveFile } from '../../helpers/fileSaver';
+import dataFormatter from '../../helpers/dataFormatter';
+import ImageField from '../../components/ImageField';
+
+const EditOrganizationsPage = () => {
+ const router = useRouter();
+ const dispatch = useAppDispatch();
+ const initVals = {
+ name: '',
+ };
+ const [initialValues, setInitialValues] = useState(initVals);
+
+ const { organizations } = useAppSelector((state) => state.organizations);
+
+ const { id } = router.query;
+
+ useEffect(() => {
+ dispatch(fetch({ id: id }));
+ }, [id]);
+
+ useEffect(() => {
+ if (typeof organizations === 'object') {
+ setInitialValues(organizations);
+ }
+ }, [organizations]);
+
+ useEffect(() => {
+ if (typeof organizations === 'object') {
+ const newInitialVal = { ...initVals };
+ Object.keys(initVals).forEach(
+ (el) => (newInitialVal[el] = organizations[el]),
+ );
+ setInitialValues(newInitialVal);
+ }
+ }, [organizations]);
+
+ const handleSubmit = async (data) => {
+ await dispatch(update({ id: id, data }));
+ await router.push('/organizations/organizations-list');
+ };
+
+ return (
+ <>
+
+ {getPageTitle('Edit organizations')}
+
+
+
+ {''}
+
+
+ handleSubmit(values)}
+ >
+
+
+
+
+ >
+ );
+};
+
+EditOrganizationsPage.getLayout = function getLayout(page: ReactElement) {
+ return (
+
+ {page}
+
+ );
+};
+
+export default EditOrganizationsPage;
diff --git a/frontend/src/pages/organizations/organizations-list.tsx b/frontend/src/pages/organizations/organizations-list.tsx
new file mode 100644
index 0000000..9e0a09b
--- /dev/null
+++ b/frontend/src/pages/organizations/organizations-list.tsx
@@ -0,0 +1,165 @@
+import { mdiChartTimelineVariant } from '@mdi/js';
+import Head from 'next/head';
+import { uniqueId } from 'lodash';
+import React, { ReactElement, useState } from 'react';
+import CardBox from '../../components/CardBox';
+import LayoutAuthenticated from '../../layouts/Authenticated';
+import SectionMain from '../../components/SectionMain';
+import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton';
+import { getPageTitle } from '../../config';
+import TableOrganizations from '../../components/Organizations/TableOrganizations';
+import BaseButton from '../../components/BaseButton';
+import axios from 'axios';
+import Link from 'next/link';
+import { useAppDispatch, useAppSelector } from '../../stores/hooks';
+import CardBoxModal from '../../components/CardBoxModal';
+import DragDropFilePicker from '../../components/DragDropFilePicker';
+import {
+ setRefetch,
+ uploadCsv,
+} from '../../stores/organizations/organizationsSlice';
+
+import { hasPermission } from '../../helpers/userPermissions';
+
+const OrganizationsTablesPage = () => {
+ const [filterItems, setFilterItems] = useState([]);
+ const [csvFile, setCsvFile] = useState(null);
+ const [isModalActive, setIsModalActive] = useState(false);
+ const [showTableView, setShowTableView] = useState(false);
+
+ const { currentUser } = useAppSelector((state) => state.auth);
+
+ const dispatch = useAppDispatch();
+
+ const [filters] = useState([{ label: 'Name', title: 'name' }]);
+
+ const hasCreatePermission =
+ currentUser && hasPermission(currentUser, 'CREATE_ORGANIZATIONS');
+
+ const addFilter = () => {
+ const newItem = {
+ id: uniqueId(),
+ fields: {
+ filterValue: '',
+ filterValueFrom: '',
+ filterValueTo: '',
+ selectedField: '',
+ },
+ };
+ newItem.fields.selectedField = filters[0].title;
+ setFilterItems([...filterItems, newItem]);
+ };
+
+ const getOrganizationsCSV = async () => {
+ const response = await axios({
+ url: '/organizations?filetype=csv',
+ method: 'GET',
+ responseType: 'blob',
+ });
+ const type = response.headers['content-type'];
+ const blob = new Blob([response.data], { type: type });
+ const link = document.createElement('a');
+ link.href = window.URL.createObjectURL(blob);
+ link.download = 'organizationsCSV.csv';
+ link.click();
+ };
+
+ const onModalConfirm = async () => {
+ if (!csvFile) return;
+ await dispatch(uploadCsv(csvFile));
+ dispatch(setRefetch(true));
+ setCsvFile(null);
+ setIsModalActive(false);
+ };
+
+ const onModalCancel = () => {
+ setCsvFile(null);
+ setIsModalActive(false);
+ };
+
+ return (
+ <>
+
+ {getPageTitle('Organizations')}
+
+
+
+ {''}
+
+
+ {hasCreatePermission && (
+
+ )}
+
+
+
+
+ {hasCreatePermission && (
+ setIsModalActive(true)}
+ />
+ )}
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+};
+
+OrganizationsTablesPage.getLayout = function getLayout(page: ReactElement) {
+ return (
+
+ {page}
+
+ );
+};
+
+export default OrganizationsTablesPage;
diff --git a/frontend/src/pages/organizations/organizations-new.tsx b/frontend/src/pages/organizations/organizations-new.tsx
new file mode 100644
index 0000000..2787572
--- /dev/null
+++ b/frontend/src/pages/organizations/organizations-new.tsx
@@ -0,0 +1,100 @@
+import {
+ mdiAccount,
+ mdiChartTimelineVariant,
+ mdiMail,
+ mdiUpload,
+} from '@mdi/js';
+import Head from 'next/head';
+import React, { ReactElement } from 'react';
+import CardBox from '../../components/CardBox';
+import LayoutAuthenticated from '../../layouts/Authenticated';
+import SectionMain from '../../components/SectionMain';
+import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton';
+import { getPageTitle } from '../../config';
+
+import { Field, Form, Formik } from 'formik';
+import FormField from '../../components/FormField';
+import BaseDivider from '../../components/BaseDivider';
+import BaseButtons from '../../components/BaseButtons';
+import BaseButton from '../../components/BaseButton';
+import FormCheckRadio from '../../components/FormCheckRadio';
+import FormCheckRadioGroup from '../../components/FormCheckRadioGroup';
+import FormFilePicker from '../../components/FormFilePicker';
+import FormImagePicker from '../../components/FormImagePicker';
+import { SwitchField } from '../../components/SwitchField';
+
+import { SelectField } from '../../components/SelectField';
+import { SelectFieldMany } from '../../components/SelectFieldMany';
+import { RichTextField } from '../../components/RichTextField';
+
+import { create } from '../../stores/organizations/organizationsSlice';
+import { useAppDispatch } from '../../stores/hooks';
+import { useRouter } from 'next/router';
+import moment from 'moment';
+
+const initialValues = {
+ name: '',
+};
+
+const OrganizationsNew = () => {
+ const router = useRouter();
+ const dispatch = useAppDispatch();
+
+ const handleSubmit = async (data) => {
+ await dispatch(create(data));
+ await router.push('/organizations/organizations-list');
+ };
+ return (
+ <>
+
+ {getPageTitle('New Item')}
+
+
+
+ {''}
+
+
+ handleSubmit(values)}
+ >
+
+
+
+
+ >
+ );
+};
+
+OrganizationsNew.getLayout = function getLayout(page: ReactElement) {
+ return (
+
+ {page}
+
+ );
+};
+
+export default OrganizationsNew;
diff --git a/frontend/src/pages/organizations/organizations-table.tsx b/frontend/src/pages/organizations/organizations-table.tsx
new file mode 100644
index 0000000..489c393
--- /dev/null
+++ b/frontend/src/pages/organizations/organizations-table.tsx
@@ -0,0 +1,164 @@
+import { mdiChartTimelineVariant } from '@mdi/js';
+import Head from 'next/head';
+import { uniqueId } from 'lodash';
+import React, { ReactElement, useState } from 'react';
+import CardBox from '../../components/CardBox';
+import LayoutAuthenticated from '../../layouts/Authenticated';
+import SectionMain from '../../components/SectionMain';
+import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton';
+import { getPageTitle } from '../../config';
+import TableOrganizations from '../../components/Organizations/TableOrganizations';
+import BaseButton from '../../components/BaseButton';
+import axios from 'axios';
+import Link from 'next/link';
+import { useAppDispatch, useAppSelector } from '../../stores/hooks';
+import CardBoxModal from '../../components/CardBoxModal';
+import DragDropFilePicker from '../../components/DragDropFilePicker';
+import {
+ setRefetch,
+ uploadCsv,
+} from '../../stores/organizations/organizationsSlice';
+
+import { hasPermission } from '../../helpers/userPermissions';
+
+const OrganizationsTablesPage = () => {
+ const [filterItems, setFilterItems] = useState([]);
+ const [csvFile, setCsvFile] = useState(null);
+ const [isModalActive, setIsModalActive] = useState(false);
+ const [showTableView, setShowTableView] = useState(false);
+
+ const { currentUser } = useAppSelector((state) => state.auth);
+
+ const dispatch = useAppDispatch();
+
+ const [filters] = useState([{ label: 'Name', title: 'name' }]);
+
+ const hasCreatePermission =
+ currentUser && hasPermission(currentUser, 'CREATE_ORGANIZATIONS');
+
+ const addFilter = () => {
+ const newItem = {
+ id: uniqueId(),
+ fields: {
+ filterValue: '',
+ filterValueFrom: '',
+ filterValueTo: '',
+ selectedField: '',
+ },
+ };
+ newItem.fields.selectedField = filters[0].title;
+ setFilterItems([...filterItems, newItem]);
+ };
+
+ const getOrganizationsCSV = async () => {
+ const response = await axios({
+ url: '/organizations?filetype=csv',
+ method: 'GET',
+ responseType: 'blob',
+ });
+ const type = response.headers['content-type'];
+ const blob = new Blob([response.data], { type: type });
+ const link = document.createElement('a');
+ link.href = window.URL.createObjectURL(blob);
+ link.download = 'organizationsCSV.csv';
+ link.click();
+ };
+
+ const onModalConfirm = async () => {
+ if (!csvFile) return;
+ await dispatch(uploadCsv(csvFile));
+ dispatch(setRefetch(true));
+ setCsvFile(null);
+ setIsModalActive(false);
+ };
+
+ const onModalCancel = () => {
+ setCsvFile(null);
+ setIsModalActive(false);
+ };
+
+ return (
+ <>
+
+ {getPageTitle('Organizations')}
+
+
+
+ {''}
+
+
+ {hasCreatePermission && (
+
+ )}
+
+
+
+
+ {hasCreatePermission && (
+ setIsModalActive(true)}
+ />
+ )}
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+};
+
+OrganizationsTablesPage.getLayout = function getLayout(page: ReactElement) {
+ return (
+
+ {page}
+
+ );
+};
+
+export default OrganizationsTablesPage;
diff --git a/frontend/src/pages/organizations/organizations-view.tsx b/frontend/src/pages/organizations/organizations-view.tsx
new file mode 100644
index 0000000..93c58b5
--- /dev/null
+++ b/frontend/src/pages/organizations/organizations-view.tsx
@@ -0,0 +1,355 @@
+import React, { ReactElement, useEffect } from 'react';
+import Head from 'next/head';
+import DatePicker from 'react-datepicker';
+import 'react-datepicker/dist/react-datepicker.css';
+import dayjs from 'dayjs';
+import { useAppDispatch, useAppSelector } from '../../stores/hooks';
+import { useRouter } from 'next/router';
+import { fetch } from '../../stores/organizations/organizationsSlice';
+import { saveFile } from '../../helpers/fileSaver';
+import dataFormatter from '../../helpers/dataFormatter';
+import ImageField from '../../components/ImageField';
+import LayoutAuthenticated from '../../layouts/Authenticated';
+import { getPageTitle } from '../../config';
+import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton';
+import SectionMain from '../../components/SectionMain';
+import CardBox from '../../components/CardBox';
+import BaseButton from '../../components/BaseButton';
+import BaseDivider from '../../components/BaseDivider';
+import { mdiChartTimelineVariant } from '@mdi/js';
+import { SwitchField } from '../../components/SwitchField';
+import FormField from '../../components/FormField';
+
+const OrganizationsView = () => {
+ const router = useRouter();
+ const dispatch = useAppDispatch();
+ const { organizations } = useAppSelector((state) => state.organizations);
+
+ const { id } = router.query;
+
+ function removeLastCharacter(str) {
+ console.log(str, `str`);
+ return str.slice(0, -1);
+ }
+
+ useEffect(() => {
+ dispatch(fetch({ id }));
+ }, [dispatch, id]);
+
+ return (
+ <>
+
+ {getPageTitle('View organizations')}
+
+
+
+
+
+
+
+
Name
+
{organizations?.name}
+
+
+ <>
+ Users Organizations
+
+
+
+
+
+ | First Name |
+
+ Last Name |
+
+ Phone Number |
+
+ E-Mail |
+
+ Disabled |
+
+
+
+ {organizations.users_organizations &&
+ Array.isArray(organizations.users_organizations) &&
+ organizations.users_organizations.map((item: any) => (
+
+ router.push(`/users/users-view/?id=${item.id}`)
+ }
+ >
+ | {item.firstName} |
+
+ {item.lastName} |
+
+ {item.phoneNumber} |
+
+ {item.email} |
+
+
+ {dataFormatter.booleanFormatter(item.disabled)}
+ |
+
+ ))}
+
+
+
+ {!organizations?.users_organizations?.length && (
+ No data
+ )}
+
+ >
+
+ <>
+ Field_visits organizations
+
+
+
+
+
+ | EmployeeName |
+
+ Site/PlaceName |
+
+ StartDate |
+
+ DepartureDate |
+
+ ReturnDate |
+
+ PaymentType |
+
+ Amount |
+
+ Purpose |
+
+
+
+ {organizations.field_visits_organizations &&
+ Array.isArray(organizations.field_visits_organizations) &&
+ organizations.field_visits_organizations.map(
+ (item: any) => (
+
+ router.push(
+ `/field_visits/field_visits-view/?id=${item.id}`,
+ )
+ }
+ >
+ |
+ {item.employee_name}
+ |
+
+ {item.site_name} |
+
+
+ {dataFormatter.dateTimeFormatter(item.start_date)}
+ |
+
+
+ {dataFormatter.dateTimeFormatter(
+ item.departure_date,
+ )}
+ |
+
+
+ {dataFormatter.dateTimeFormatter(
+ item.return_date,
+ )}
+ |
+
+
+ {item.payment_type}
+ |
+
+ {item.amount} |
+
+ {item.purpose} |
+
+ ),
+ )}
+
+
+
+ {!organizations?.field_visits_organizations?.length && (
+ No data
+ )}
+
+ >
+
+ <>
+ Payments organizations
+
+
+
+
+
+ | Payee |
+
+ PaymentDate |
+
+ Purpose |
+
+ Amount |
+
+ DocumentType |
+
+ Auto-generatedDocumentNo. |
+
+ Status |
+
+ ShelfNo. |
+
+
+
+ {organizations.payments_organizations &&
+ Array.isArray(organizations.payments_organizations) &&
+ organizations.payments_organizations.map((item: any) => (
+
+ router.push(
+ `/payments/payments-view/?id=${item.id}`,
+ )
+ }
+ >
+ | {item.payee} |
+
+
+ {dataFormatter.dateTimeFormatter(item.payment_date)}
+ |
+
+ {item.purpose} |
+
+ {item.amount} |
+
+
+ {item.document_type}
+ |
+
+
+ {item.auto_generated_document_no}
+ |
+
+ {item.status} |
+
+ {item.shelf_no} |
+
+ ))}
+
+
+
+ {!organizations?.payments_organizations?.length && (
+ No data
+ )}
+
+ >
+
+ <>
+ Projects organizations
+
+
+
+
+
+ | ProjectName |
+
+ ProjectCode |
+
+ SourceofFunds |
+
+ StartDate |
+
+ EndDate |
+
+ Status |
+
+ Cash at bank |
+
+
+
+ {organizations.projects_organizations &&
+ Array.isArray(organizations.projects_organizations) &&
+ organizations.projects_organizations.map((item: any) => (
+
+ router.push(
+ `/projects/projects-view/?id=${item.id}`,
+ )
+ }
+ >
+ | {item.name} |
+
+ {item.code} |
+
+
+ {item.source_of_funds}
+ |
+
+
+ {dataFormatter.dateTimeFormatter(item.start_date)}
+ |
+
+
+ {dataFormatter.dateTimeFormatter(item.end_date)}
+ |
+
+ {item.status} |
+
+ {item.cash_at_bank} |
+
+ ))}
+
+
+
+ {!organizations?.projects_organizations?.length && (
+ No data
+ )}
+
+ >
+
+
+
+ router.push('/organizations/organizations-list')}
+ />
+
+
+ >
+ );
+};
+
+OrganizationsView.getLayout = function getLayout(page: ReactElement) {
+ return (
+
+ {page}
+
+ );
+};
+
+export default OrganizationsView;
diff --git a/frontend/src/pages/payments/[paymentsId].tsx b/frontend/src/pages/payments/[paymentsId].tsx
index 00674e8..3f7eec0 100644
--- a/frontend/src/pages/payments/[paymentsId].tsx
+++ b/frontend/src/pages/payments/[paymentsId].tsx
@@ -53,6 +53,8 @@ const EditPayments = () => {
status: '',
shelf_no: '',
+
+ organizations: null,
};
const [initialValues, setInitialValues] = useState(initVals);
@@ -180,6 +182,17 @@ const EditPayments = () => {
+
+
+
+
diff --git a/frontend/src/pages/payments/payments-edit.tsx b/frontend/src/pages/payments/payments-edit.tsx
index 3b14988..da1d619 100644
--- a/frontend/src/pages/payments/payments-edit.tsx
+++ b/frontend/src/pages/payments/payments-edit.tsx
@@ -53,6 +53,8 @@ const EditPaymentsPage = () => {
status: '',
shelf_no: '',
+
+ organizations: null,
};
const [initialValues, setInitialValues] = useState(initVals);
@@ -178,6 +180,17 @@ const EditPaymentsPage = () => {
+
+
+
+
diff --git a/frontend/src/pages/payments/payments-new.tsx b/frontend/src/pages/payments/payments-new.tsx
index 21a71bc..816c65a 100644
--- a/frontend/src/pages/payments/payments-new.tsx
+++ b/frontend/src/pages/payments/payments-new.tsx
@@ -50,6 +50,8 @@ const initialValues = {
status: 'InProgress',
shelf_no: '',
+
+ organizations: '',
};
const PaymentsNew = () => {
@@ -142,6 +144,16 @@ const PaymentsNew = () => {
+
+
+
+
diff --git a/frontend/src/pages/payments/payments-view.tsx b/frontend/src/pages/payments/payments-view.tsx
index 8683987..921e769 100644
--- a/frontend/src/pages/payments/payments-view.tsx
+++ b/frontend/src/pages/payments/payments-view.tsx
@@ -114,6 +114,12 @@ const PaymentsView = () => {
{payments?.shelf_no}
+
+
organizations
+
+
{payments?.organizations?.id ?? 'No data'}
+
+
{
end_date: new Date(),
status: '',
+
+ cash_at_bank: '',
+
+ organizations: null,
};
const [initialValues, setInitialValues] = useState(initVals);
@@ -157,6 +161,25 @@ const EditProjects = () => {
+
+
+
+
+
+
+
+
diff --git a/frontend/src/pages/projects/projects-edit.tsx b/frontend/src/pages/projects/projects-edit.tsx
index d8bae62..3a25f71 100644
--- a/frontend/src/pages/projects/projects-edit.tsx
+++ b/frontend/src/pages/projects/projects-edit.tsx
@@ -47,6 +47,10 @@ const EditProjectsPage = () => {
end_date: new Date(),
status: '',
+
+ cash_at_bank: '',
+
+ organizations: null,
};
const [initialValues, setInitialValues] = useState(initVals);
@@ -155,6 +159,25 @@ const EditProjectsPage = () => {
+
+
+
+
+
+
+
+
diff --git a/frontend/src/pages/projects/projects-new.tsx b/frontend/src/pages/projects/projects-new.tsx
index a28a764..e0e0a94 100644
--- a/frontend/src/pages/projects/projects-new.tsx
+++ b/frontend/src/pages/projects/projects-new.tsx
@@ -44,6 +44,10 @@ const initialValues = {
end_date: '',
status: 'InProgress',
+
+ cash_at_bank: '',
+
+ organizations: '',
};
const ProjectsNew = () => {
@@ -121,6 +125,24 @@ const ProjectsNew = () => {
+
+
+
+
+
+
+
+
diff --git a/frontend/src/pages/projects/projects-view.tsx b/frontend/src/pages/projects/projects-view.tsx
index dcc96d7..b1d7d23 100644
--- a/frontend/src/pages/projects/projects-view.tsx
+++ b/frontend/src/pages/projects/projects-view.tsx
@@ -112,6 +112,17 @@ const ProjectsView = () => {
{projects?.status ?? 'No data'}
+
+
Cash at bank
+
{projects?.cash_at_bank || 'No data'}
+
+
+
+
organizations
+
+
{projects?.organizations?.id ?? 'No data'}
+
+
<>
Field_visits Project
{
name: '',
permissions: [],
+
+ globalAccess: false,
};
const [initialValues, setInitialValues] = useState(initVals);
@@ -106,6 +108,14 @@ const EditRoles = () => {
>
+
+
+
+
diff --git a/frontend/src/pages/roles/roles-edit.tsx b/frontend/src/pages/roles/roles-edit.tsx
index 88500cd..0059e70 100644
--- a/frontend/src/pages/roles/roles-edit.tsx
+++ b/frontend/src/pages/roles/roles-edit.tsx
@@ -39,6 +39,8 @@ const EditRolesPage = () => {
name: '',
permissions: [],
+
+ globalAccess: false,
};
const [initialValues, setInitialValues] = useState(initVals);
@@ -104,6 +106,14 @@ const EditRolesPage = () => {
>
+
+
+
+
diff --git a/frontend/src/pages/roles/roles-new.tsx b/frontend/src/pages/roles/roles-new.tsx
index 1c068c1..18f111f 100644
--- a/frontend/src/pages/roles/roles-new.tsx
+++ b/frontend/src/pages/roles/roles-new.tsx
@@ -36,6 +36,8 @@ const initialValues = {
name: '',
permissions: [],
+
+ globalAccess: false,
};
const RolesNew = () => {
@@ -79,6 +81,14 @@ const RolesNew = () => {
>
+
+
+
+
diff --git a/frontend/src/pages/roles/roles-view.tsx b/frontend/src/pages/roles/roles-view.tsx
index b7368ff..9cf5d43 100644
--- a/frontend/src/pages/roles/roles-view.tsx
+++ b/frontend/src/pages/roles/roles-view.tsx
@@ -96,6 +96,14 @@ const RolesView = () => {
>
+
+ null }}
+ disabled
+ />
+
+
<>
Users App Role
{
custom_permissions: [],
+ organizations: null,
+
password: '',
};
const [initialValues, setInitialValues] = useState(initVals);
@@ -170,6 +172,17 @@ const EditUsers = () => {
>
+
+
+
+
diff --git a/frontend/src/pages/users/users-edit.tsx b/frontend/src/pages/users/users-edit.tsx
index 87d7090..67b3a95 100644
--- a/frontend/src/pages/users/users-edit.tsx
+++ b/frontend/src/pages/users/users-edit.tsx
@@ -52,6 +52,8 @@ const EditUsersPage = () => {
custom_permissions: [],
+ organizations: null,
+
password: '',
};
const [initialValues, setInitialValues] = useState(initVals);
@@ -168,6 +170,17 @@ const EditUsersPage = () => {
>
+
+
+
+
diff --git a/frontend/src/pages/users/users-list.tsx b/frontend/src/pages/users/users-list.tsx
index 6d03ebc..8a5de89 100644
--- a/frontend/src/pages/users/users-list.tsx
+++ b/frontend/src/pages/users/users-list.tsx
@@ -36,6 +36,8 @@ const UsersTablesPage = () => {
{ label: 'App Role', title: 'app_role' },
+ { label: 'Organizations', title: 'organizations' },
+
{ label: 'Custom Permissions', title: 'custom_permissions' },
]);
diff --git a/frontend/src/pages/users/users-new.tsx b/frontend/src/pages/users/users-new.tsx
index 4f3fa45..8c25d83 100644
--- a/frontend/src/pages/users/users-new.tsx
+++ b/frontend/src/pages/users/users-new.tsx
@@ -48,6 +48,8 @@ const initialValues = {
app_role: '',
custom_permissions: [],
+
+ organizations: '',
};
const UsersNew = () => {
@@ -140,6 +142,16 @@ const UsersNew = () => {
>
+
+
+
+
diff --git a/frontend/src/pages/users/users-table.tsx b/frontend/src/pages/users/users-table.tsx
index a408cd6..de28081 100644
--- a/frontend/src/pages/users/users-table.tsx
+++ b/frontend/src/pages/users/users-table.tsx
@@ -36,6 +36,8 @@ const UsersTablesPage = () => {
{ label: 'App Role', title: 'app_role' },
+ { label: 'Organizations', title: 'organizations' },
+
{ label: 'Custom Permissions', title: 'custom_permissions' },
]);
diff --git a/frontend/src/pages/users/users-view.tsx b/frontend/src/pages/users/users-view.tsx
index ce0851d..6af29e8 100644
--- a/frontend/src/pages/users/users-view.tsx
+++ b/frontend/src/pages/users/users-view.tsx
@@ -138,6 +138,12 @@ const UsersView = () => {
>
+
+
Organizations
+
+
{users?.organizations?.id ?? 'No data'}
+
+
{
+ const { id, query } = data;
+ const result = await axios.get(
+ `organizations${query || (id ? `/${id}` : '')}`,
+ );
+ return id
+ ? result.data
+ : { rows: result.data.rows, count: result.data.count };
+ },
+);
+
+export const deleteItemsByIds = createAsyncThunk(
+ 'organizations/deleteByIds',
+ async (data: any, { rejectWithValue }) => {
+ try {
+ await axios.post('organizations/deleteByIds', { data });
+ } catch (error) {
+ if (!error.response) {
+ throw error;
+ }
+
+ return rejectWithValue(error.response.data);
+ }
+ },
+);
+
+export const deleteItem = createAsyncThunk(
+ 'organizations/deleteOrganizations',
+ async (id: string, { rejectWithValue }) => {
+ try {
+ await axios.delete(`organizations/${id}`);
+ } catch (error) {
+ if (!error.response) {
+ throw error;
+ }
+
+ return rejectWithValue(error.response.data);
+ }
+ },
+);
+
+export const create = createAsyncThunk(
+ 'organizations/createOrganizations',
+ async (data: any, { rejectWithValue }) => {
+ try {
+ const result = await axios.post('organizations', { data });
+ return result.data;
+ } catch (error) {
+ if (!error.response) {
+ throw error;
+ }
+
+ return rejectWithValue(error.response.data);
+ }
+ },
+);
+
+export const uploadCsv = createAsyncThunk(
+ 'organizations/uploadCsv',
+ async (file: File, { rejectWithValue }) => {
+ try {
+ const data = new FormData();
+ data.append('file', file);
+ data.append('filename', file.name);
+
+ const result = await axios.post('organizations/bulk-import', data, {
+ headers: {
+ 'Content-Type': 'multipart/form-data',
+ },
+ });
+
+ return result.data;
+ } catch (error) {
+ if (!error.response) {
+ throw error;
+ }
+
+ return rejectWithValue(error.response.data);
+ }
+ },
+);
+
+export const update = createAsyncThunk(
+ 'organizations/updateOrganizations',
+ async (payload: any, { rejectWithValue }) => {
+ try {
+ const result = await axios.put(`organizations/${payload.id}`, {
+ id: payload.id,
+ data: payload.data,
+ });
+ return result.data;
+ } catch (error) {
+ if (!error.response) {
+ throw error;
+ }
+
+ return rejectWithValue(error.response.data);
+ }
+ },
+);
+
+export const organizationsSlice = createSlice({
+ name: 'organizations',
+ initialState,
+ reducers: {
+ setRefetch: (state, action: PayloadAction) => {
+ state.refetch = action.payload;
+ },
+ },
+ extraReducers: (builder) => {
+ builder.addCase(fetch.pending, (state) => {
+ state.loading = true;
+ resetNotify(state);
+ });
+ builder.addCase(fetch.rejected, (state, action) => {
+ state.loading = false;
+ rejectNotify(state, action);
+ });
+
+ builder.addCase(fetch.fulfilled, (state, action) => {
+ if (action.payload.rows && action.payload.count >= 0) {
+ state.organizations = action.payload.rows;
+ state.count = action.payload.count;
+ } else {
+ state.organizations = action.payload;
+ }
+ state.loading = false;
+ });
+
+ builder.addCase(deleteItemsByIds.pending, (state) => {
+ state.loading = true;
+ resetNotify(state);
+ });
+
+ builder.addCase(deleteItemsByIds.fulfilled, (state) => {
+ state.loading = false;
+ fulfilledNotify(state, 'Organizations has been deleted');
+ });
+
+ builder.addCase(deleteItemsByIds.rejected, (state, action) => {
+ state.loading = false;
+ rejectNotify(state, action);
+ });
+
+ builder.addCase(deleteItem.pending, (state) => {
+ state.loading = true;
+ resetNotify(state);
+ });
+
+ builder.addCase(deleteItem.fulfilled, (state) => {
+ state.loading = false;
+ fulfilledNotify(
+ state,
+ `${'Organizations'.slice(0, -1)} has been deleted`,
+ );
+ });
+
+ builder.addCase(deleteItem.rejected, (state, action) => {
+ state.loading = false;
+ rejectNotify(state, action);
+ });
+
+ builder.addCase(create.pending, (state) => {
+ state.loading = true;
+ resetNotify(state);
+ });
+ builder.addCase(create.rejected, (state, action) => {
+ state.loading = false;
+ rejectNotify(state, action);
+ });
+
+ builder.addCase(create.fulfilled, (state) => {
+ state.loading = false;
+ fulfilledNotify(
+ state,
+ `${'Organizations'.slice(0, -1)} has been created`,
+ );
+ });
+
+ builder.addCase(update.pending, (state) => {
+ state.loading = true;
+ resetNotify(state);
+ });
+ builder.addCase(update.fulfilled, (state) => {
+ state.loading = false;
+ fulfilledNotify(
+ state,
+ `${'Organizations'.slice(0, -1)} has been updated`,
+ );
+ });
+ builder.addCase(update.rejected, (state, action) => {
+ state.loading = false;
+ rejectNotify(state, action);
+ });
+
+ builder.addCase(uploadCsv.pending, (state) => {
+ state.loading = true;
+ resetNotify(state);
+ });
+ builder.addCase(uploadCsv.fulfilled, (state) => {
+ state.loading = false;
+ fulfilledNotify(state, 'Organizations has been uploaded');
+ });
+ builder.addCase(uploadCsv.rejected, (state, action) => {
+ state.loading = false;
+ rejectNotify(state, action);
+ });
+ },
+});
+
+// Action creators are generated for each case reducer function
+export const { setRefetch } = organizationsSlice.actions;
+
+export default organizationsSlice.reducer;
diff --git a/frontend/src/stores/store.ts b/frontend/src/stores/store.ts
index 97f8a4e..63b6a70 100644
--- a/frontend/src/stores/store.ts
+++ b/frontend/src/stores/store.ts
@@ -10,6 +10,7 @@ import paymentsSlice from './payments/paymentsSlice';
import projectsSlice from './projects/projectsSlice';
import rolesSlice from './roles/rolesSlice';
import permissionsSlice from './permissions/permissionsSlice';
+import organizationsSlice from './organizations/organizationsSlice';
export const store = configureStore({
reducer: {
@@ -24,6 +25,7 @@ export const store = configureStore({
projects: projectsSlice,
roles: rolesSlice,
permissions: permissionsSlice,
+ organizations: organizationsSlice,
},
});