From 175e75ff0507c019e785f40eb46b17f870b4f997 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Thu, 17 Jul 2025 09:48:56 +0000 Subject: [PATCH] Updated via schema editor on 2025-07-17 09:48 --- app-shell/src/_schema.json | 3 +- backend/src/db/api/assets.js | 18 - backend/src/db/api/compliance_certificates.js | 18 - backend/src/db/api/reports.js | 235 +++++++++ backend/src/db/migrations/1752745679986.js | 72 +++ backend/src/db/models/reports.js | 45 ++ .../db/seeders/20200430130760-user-roles.js | 26 + .../db/seeders/20231127130745-sample-data.js | 230 +-------- backend/src/db/seeders/20250717094759.js | 87 ++++ backend/src/index.js | 8 + backend/src/routes/reports.js | 434 ++++++++++++++++ backend/src/services/reports.js | 114 +++++ .../src/components/Assets/TableAssets.tsx | 33 +- .../TableCompliance_certificates.tsx | 33 +- .../src/components/Reports/CardReports.tsx | 98 ++++ .../src/components/Reports/ListReports.tsx | 82 +++ .../src/components/Reports/TableReports.tsx | 481 ++++++++++++++++++ .../Reports/configureReportsCols.tsx | 62 +++ .../components/WebPageComponents/Footer.tsx | 2 +- .../components/WebPageComponents/Header.tsx | 4 +- frontend/src/menuAside.ts | 8 + frontend/src/pages/assets/assets-list.tsx | 4 - frontend/src/pages/assets/assets-new.tsx | 15 +- frontend/src/pages/assets/assets-table.tsx | 2 +- .../compliance_certificates-list.tsx | 8 - .../compliance_certificates-new.tsx | 15 +- .../compliance_certificates-table.tsx | 2 +- frontend/src/pages/dashboard.tsx | 35 ++ frontend/src/pages/index.tsx | 2 +- frontend/src/pages/reports/[reportsId].tsx | 118 +++++ frontend/src/pages/reports/reports-edit.tsx | 116 +++++ frontend/src/pages/reports/reports-list.tsx | 162 ++++++ frontend/src/pages/reports/reports-new.tsx | 92 ++++ frontend/src/pages/reports/reports-table.tsx | 161 ++++++ frontend/src/pages/reports/reports-view.tsx | 78 +++ frontend/src/pages/web_pages/home.tsx | 2 +- frontend/src/stores/reports/reportsSlice.ts | 236 +++++++++ frontend/src/stores/store.ts | 2 + 38 files changed, 2786 insertions(+), 357 deletions(-) create mode 100644 backend/src/db/api/reports.js create mode 100644 backend/src/db/migrations/1752745679986.js create mode 100644 backend/src/db/models/reports.js create mode 100644 backend/src/db/seeders/20250717094759.js create mode 100644 backend/src/routes/reports.js create mode 100644 backend/src/services/reports.js create mode 100644 frontend/src/components/Reports/CardReports.tsx create mode 100644 frontend/src/components/Reports/ListReports.tsx create mode 100644 frontend/src/components/Reports/TableReports.tsx create mode 100644 frontend/src/components/Reports/configureReportsCols.tsx create mode 100644 frontend/src/pages/reports/[reportsId].tsx create mode 100644 frontend/src/pages/reports/reports-edit.tsx create mode 100644 frontend/src/pages/reports/reports-list.tsx create mode 100644 frontend/src/pages/reports/reports-new.tsx create mode 100644 frontend/src/pages/reports/reports-table.tsx create mode 100644 frontend/src/pages/reports/reports-view.tsx create mode 100644 frontend/src/stores/reports/reportsSlice.ts diff --git a/app-shell/src/_schema.json b/app-shell/src/_schema.json index 23c1ee6..d8f9845 100644 --- a/app-shell/src/_schema.json +++ b/app-shell/src/_schema.json @@ -1,4 +1,5 @@ { "Initial version": "{\"iv\":\"l1iiytIQiH8jYglg\",\"encryptedData\":\"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\"}", - "Updated via schema editor on 2025-07-17 09:30": "{\"iv\":\"9PYE7S3nKi3IOBLa\",\"encryptedData\":\"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\"}" + "Updated via schema editor on 2025-07-17 09:30": "{\"iv\":\"9PYE7S3nKi3IOBLa\",\"encryptedData\":\"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\"}", + "Updated via schema editor on 2025-07-17 09:48": "{\"iv\":\"eOtd41SAbhusvr67\",\"encryptedData\":\"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\"}" } \ No newline at end of file diff --git a/backend/src/db/api/assets.js b/backend/src/db/api/assets.js index 5f46215..aa0ffee 100644 --- a/backend/src/db/api/assets.js +++ b/backend/src/db/api/assets.js @@ -245,24 +245,6 @@ module.exports = class AssetsDBApi { }; } - if (filter.calendarStart && filter.calendarEnd) { - where = { - ...where, - [Op.or]: [ - { - purchase_date: { - [Op.between]: [filter.calendarStart, filter.calendarEnd], - }, - }, - { - maintenance_due_date: { - [Op.between]: [filter.calendarStart, filter.calendarEnd], - }, - }, - ], - }; - } - if (filter.purchase_dateRange) { const [start, end] = filter.purchase_dateRange; diff --git a/backend/src/db/api/compliance_certificates.js b/backend/src/db/api/compliance_certificates.js index 19483a1..9b67a73 100644 --- a/backend/src/db/api/compliance_certificates.js +++ b/backend/src/db/api/compliance_certificates.js @@ -226,24 +226,6 @@ module.exports = class Compliance_certificatesDBApi { }; } - if (filter.calendarStart && filter.calendarEnd) { - where = { - ...where, - [Op.or]: [ - { - issue_date: { - [Op.between]: [filter.calendarStart, filter.calendarEnd], - }, - }, - { - expiry_date: { - [Op.between]: [filter.calendarStart, filter.calendarEnd], - }, - }, - ], - }; - } - if (filter.issue_dateRange) { const [start, end] = filter.issue_dateRange; diff --git a/backend/src/db/api/reports.js b/backend/src/db/api/reports.js new file mode 100644 index 0000000..5128cfe --- /dev/null +++ b/backend/src/db/api/reports.js @@ -0,0 +1,235 @@ +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 ReportsDBApi { + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const reports = await db.reports.create( + { + id: data.id || undefined, + + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + return reports; + } + + 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 reportsData = data.map((item, index) => ({ + id: item.id || undefined, + + importHash: item.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + // Bulk create items + const reports = await db.reports.bulkCreate(reportsData, { transaction }); + + // For each item created, replace relation files + + return reports; + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const reports = await db.reports.findByPk(id, {}, { transaction }); + + const updatePayload = {}; + + updatePayload.updatedById = currentUser.id; + + await reports.update(updatePayload, { transaction }); + + return reports; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const reports = await db.reports.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of reports) { + await record.update({ deletedBy: currentUser.id }, { transaction }); + } + for (const record of reports) { + await record.destroy({ transaction }); + } + }); + + return reports; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const reports = await db.reports.findByPk(id, options); + + await reports.update( + { + deletedBy: currentUser.id, + }, + { + transaction, + }, + ); + + await reports.destroy({ + transaction, + }); + + return reports; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const reports = await db.reports.findOne({ where }, { transaction }); + + if (!reports) { + return reports; + } + + const output = reports.get({ plain: true }); + + 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.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.reports.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('reports', 'id', query), + ], + }; + } + + const records = await db.reports.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/migrations/1752745679986.js b/backend/src/db/migrations/1752745679986.js new file mode 100644 index 0000000..2aac93e --- /dev/null +++ b/backend/src/db/migrations/1752745679986.js @@ -0,0 +1,72 @@ +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( + 'reports', + { + 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 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.dropTable('reports', { transaction }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/models/reports.js b/backend/src/db/models/reports.js new file mode 100644 index 0000000..367e6f2 --- /dev/null +++ b/backend/src/db/models/reports.js @@ -0,0 +1,45 @@ +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 reports = sequelize.define( + 'reports', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + reports.associate = (db) => { + /// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity + + //end loop + + db.reports.belongsTo(db.users, { + as: 'createdBy', + }); + + db.reports.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return reports; +}; diff --git a/backend/src/db/seeders/20200430130760-user-roles.js b/backend/src/db/seeders/20200430130760-user-roles.js index 8577d2e..703820f 100644 --- a/backend/src/db/seeders/20200430130760-user-roles.js +++ b/backend/src/db/seeders/20200430130760-user-roles.js @@ -102,6 +102,7 @@ module.exports = { 'software_licenses', 'roles', 'permissions', + 'reports', , ]; await queryInterface.bulkInsert( @@ -861,6 +862,31 @@ primary key ("roles_permissionsId", "permissionId") permissionId: getId('DELETE_PERMISSIONS'), }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('CREATE_REPORTS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('READ_REPORTS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('UPDATE_REPORTS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('DELETE_REPORTS'), + }, + { createdAt, updatedAt, diff --git a/backend/src/db/seeders/20231127130745-sample-data.js b/backend/src/db/seeders/20231127130745-sample-data.js index 2e385a3..3064820 100644 --- a/backend/src/db/seeders/20231127130745-sample-data.js +++ b/backend/src/db/seeders/20231127130745-sample-data.js @@ -11,6 +11,8 @@ const Employees = db.employees; const SoftwareLicenses = db.software_licenses; +const Reports = db.reports; + const AssetsData = [ { asset_name: 'Dell Laptop', @@ -23,17 +25,17 @@ const AssetsData = [ maintenance_due_date: new Date('2023-01-15T00:00:00Z'), - asset_po: 'Alexander Fleming', + asset_po: 'Stephen Hawking', asset_eol: new Date(Date.now()), - asset_purchase_price: 'Francis Crick', + asset_purchase_price: 'Galileo Galilei', }, { asset_name: 'HP Printer', - asset_type: 'Software', + asset_type: 'Hardware', // type code here for "relation_one" field @@ -41,11 +43,11 @@ const AssetsData = [ maintenance_due_date: new Date('2022-06-10T00:00:00Z'), - asset_po: 'Sigmund Freud', + asset_po: 'James Clerk Maxwell', asset_eol: new Date(Date.now()), - asset_purchase_price: 'Ernest Rutherford', + asset_purchase_price: 'Leonard Euler', }, { @@ -59,47 +61,11 @@ const AssetsData = [ maintenance_due_date: new Date('2023-03-01T00:00:00Z'), - asset_po: 'Christiaan Huygens', + asset_po: 'Ernst Haeckel', asset_eol: new Date(Date.now()), - asset_purchase_price: 'Charles Sherrington', - }, - - { - asset_name: 'Salesforce License', - - asset_type: 'Software', - - // type code here for "relation_one" field - - purchase_date: new Date('2021-11-20T00:00:00Z'), - - maintenance_due_date: new Date('2022-11-20T00:00:00Z'), - - asset_po: 'George Gaylord Simpson', - - asset_eol: new Date(Date.now()), - - asset_purchase_price: 'Pierre Simon de Laplace', - }, - - { - asset_name: 'MacBook Pro', - - asset_type: 'Software', - - // type code here for "relation_one" field - - purchase_date: new Date('2022-05-05T00:00:00Z'), - - maintenance_due_date: new Date('2023-05-05T00:00:00Z'), - - asset_po: 'Alfred Binet', - - asset_eol: new Date(Date.now()), - - asset_purchase_price: 'Enrico Fermi', + asset_purchase_price: 'Christiaan Huygens', }, ]; @@ -133,26 +99,6 @@ const ComplianceCertificatesData = [ // type code here for "relation_one" field }, - - { - certificate_name: 'HIPAA Compliance', - - issue_date: new Date('2020-09-10T00:00:00Z'), - - expiry_date: new Date('2022-09-10T00:00:00Z'), - - // type code here for "relation_one" field - }, - - { - certificate_name: 'PCI DSS', - - issue_date: new Date('2021-11-01T00:00:00Z'), - - expiry_date: new Date('2023-11-01T00:00:00Z'), - - // type code here for "relation_one" field - }, ]; const DepartmentsData = [ @@ -173,18 +119,6 @@ const DepartmentsData = [ // type code here for "relation_many" field }, - - { - name: 'Arthur Eddington', - - // type code here for "relation_many" field - }, - - { - name: 'Hans Bethe', - - // type code here for "relation_many" field - }, ]; const EmployeesData = [ @@ -211,7 +145,7 @@ const EmployeesData = [ // type code here for "relation_one" field - status: 'Inactive', + status: 'Active', // type code here for "relation_one" field }, @@ -225,45 +159,17 @@ const EmployeesData = [ // type code here for "relation_one" field - status: 'Inactive', - - // type code here for "relation_one" field - }, - - { - first_name: 'Bob', - - last_name: 'Brown', - - email: 'bob.brown@example.com', - - // type code here for "relation_one" field - status: 'Active', // type code here for "relation_one" field }, - - { - first_name: 'Charlie', - - last_name: 'Davis', - - email: 'charlie.davis@example.com', - - // type code here for "relation_one" field - - status: 'Inactive', - - // type code here for "relation_one" field - }, ]; const SoftwareLicensesData = [ { software_name: 'Microsoft 365', - license_type: 'Salesforce', + license_type: 'Microsoft365', expiry_date: new Date('2023-12-31T00:00:00Z'), }, @@ -271,7 +177,7 @@ const SoftwareLicensesData = [ { software_name: 'Salesforce', - license_type: 'Microsoft365', + license_type: 'Salesforce', expiry_date: new Date('2023-06-30T00:00:00Z'), }, @@ -279,28 +185,14 @@ const SoftwareLicensesData = [ { software_name: 'Adobe Creative Cloud', - license_type: 'Microsoft365', + license_type: 'Salesforce', expiry_date: new Date('2023-09-15T00:00:00Z'), }, - - { - software_name: 'Slack', - - license_type: 'Microsoft365', - - expiry_date: new Date('2023-11-20T00:00:00Z'), - }, - - { - software_name: 'Zoom', - - license_type: 'Salesforce', - - expiry_date: new Date('2023-08-25T00:00:00Z'), - }, ]; +const ReportsData = [{}, {}, {}]; + // Similar logic for "relation_many" async function associateAssetWithAssigned_to() { @@ -336,28 +228,6 @@ async function associateAssetWithAssigned_to() { if (Asset2?.setAssigned_to) { await Asset2.setAssigned_to(relatedAssigned_to2); } - - const relatedAssigned_to3 = await Employees.findOne({ - offset: Math.floor(Math.random() * (await Employees.count())), - }); - const Asset3 = await Assets.findOne({ - order: [['id', 'ASC']], - offset: 3, - }); - if (Asset3?.setAssigned_to) { - await Asset3.setAssigned_to(relatedAssigned_to3); - } - - const relatedAssigned_to4 = await Employees.findOne({ - offset: Math.floor(Math.random() * (await Employees.count())), - }); - const Asset4 = await Assets.findOne({ - order: [['id', 'ASC']], - offset: 4, - }); - if (Asset4?.setAssigned_to) { - await Asset4.setAssigned_to(relatedAssigned_to4); - } } async function associateComplianceCertificateWithAssigned_to() { @@ -393,28 +263,6 @@ async function associateComplianceCertificateWithAssigned_to() { if (ComplianceCertificate2?.setAssigned_to) { await ComplianceCertificate2.setAssigned_to(relatedAssigned_to2); } - - const relatedAssigned_to3 = await Employees.findOne({ - offset: Math.floor(Math.random() * (await Employees.count())), - }); - const ComplianceCertificate3 = await ComplianceCertificates.findOne({ - order: [['id', 'ASC']], - offset: 3, - }); - if (ComplianceCertificate3?.setAssigned_to) { - await ComplianceCertificate3.setAssigned_to(relatedAssigned_to3); - } - - const relatedAssigned_to4 = await Employees.findOne({ - offset: Math.floor(Math.random() * (await Employees.count())), - }); - const ComplianceCertificate4 = await ComplianceCertificates.findOne({ - order: [['id', 'ASC']], - offset: 4, - }); - if (ComplianceCertificate4?.setAssigned_to) { - await ComplianceCertificate4.setAssigned_to(relatedAssigned_to4); - } } // Similar logic for "relation_many" @@ -452,28 +300,6 @@ async function associateEmployeeWithManager() { if (Employee2?.setManager) { await Employee2.setManager(relatedManager2); } - - const relatedManager3 = await Employees.findOne({ - offset: Math.floor(Math.random() * (await Employees.count())), - }); - const Employee3 = await Employees.findOne({ - order: [['id', 'ASC']], - offset: 3, - }); - if (Employee3?.setManager) { - await Employee3.setManager(relatedManager3); - } - - const relatedManager4 = await Employees.findOne({ - offset: Math.floor(Math.random() * (await Employees.count())), - }); - const Employee4 = await Employees.findOne({ - order: [['id', 'ASC']], - offset: 4, - }); - if (Employee4?.setManager) { - await Employee4.setManager(relatedManager4); - } } async function associateEmployeeWithDepartment() { @@ -509,28 +335,6 @@ async function associateEmployeeWithDepartment() { if (Employee2?.setDepartment) { await Employee2.setDepartment(relatedDepartment2); } - - const relatedDepartment3 = await Departments.findOne({ - offset: Math.floor(Math.random() * (await Departments.count())), - }); - const Employee3 = await Employees.findOne({ - order: [['id', 'ASC']], - offset: 3, - }); - if (Employee3?.setDepartment) { - await Employee3.setDepartment(relatedDepartment3); - } - - const relatedDepartment4 = await Departments.findOne({ - offset: Math.floor(Math.random() * (await Departments.count())), - }); - const Employee4 = await Employees.findOne({ - order: [['id', 'ASC']], - offset: 4, - }); - if (Employee4?.setDepartment) { - await Employee4.setDepartment(relatedDepartment4); - } } module.exports = { @@ -545,6 +349,8 @@ module.exports = { await SoftwareLicenses.bulkCreate(SoftwareLicensesData); + await Reports.bulkCreate(ReportsData); + await Promise.all([ // Similar logic for "relation_many" @@ -570,5 +376,7 @@ module.exports = { await queryInterface.bulkDelete('employees', null, {}); await queryInterface.bulkDelete('software_licenses', null, {}); + + await queryInterface.bulkDelete('reports', null, {}); }, }; diff --git a/backend/src/db/seeders/20250717094759.js b/backend/src/db/seeders/20250717094759.js new file mode 100644 index 0000000..d2b8537 --- /dev/null +++ b/backend/src/db/seeders/20250717094759.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 = ['reports']; + + 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 702853e..80d7c22 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -35,6 +35,8 @@ const rolesRoutes = require('./routes/roles'); const permissionsRoutes = require('./routes/permissions'); +const reportsRoutes = require('./routes/reports'); + const getBaseUrl = (url) => { if (!url) return ''; return url.endsWith('/api') ? url.slice(0, -4) : url; @@ -148,6 +150,12 @@ app.use( permissionsRoutes, ); +app.use( + '/api/reports', + passport.authenticate('jwt', { session: false }), + reportsRoutes, +); + app.use( '/api/openai', passport.authenticate('jwt', { session: false }), diff --git a/backend/src/routes/reports.js b/backend/src/routes/reports.js new file mode 100644 index 0000000..e4e748e --- /dev/null +++ b/backend/src/routes/reports.js @@ -0,0 +1,434 @@ +const express = require('express'); + +const ReportsService = require('../services/reports'); +const ReportsDBApi = require('../db/api/reports'); +const wrapAsync = require('../helpers').wrapAsync; + +const router = express.Router(); + +const { parse } = require('json2csv'); + +const { checkCrudPermissions } = require('../middlewares/check-permissions'); + +router.use(checkCrudPermissions('reports')); + +/** + * @swagger + * components: + * schemas: + * Reports: + * type: object + * properties: + + */ + +/** + * @swagger + * tags: + * name: Reports + * description: The Reports managing API + */ + +/** + * @swagger + * /api/reports: + * post: + * security: + * - bearerAuth: [] + * tags: [Reports] + * 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/Reports" + * responses: + * 200: + * description: The item was successfully added + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Reports" + * 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 ReportsService.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: [Reports] + * 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/Reports" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Reports" + * 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 ReportsService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/reports/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Reports] + * 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/Reports" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Reports" + * 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 ReportsService.update(req.body.data, req.body.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/reports/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Reports] + * 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/Reports" + * 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 ReportsService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/reports/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Reports] + * 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/Reports" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post( + '/deleteByIds', + wrapAsync(async (req, res) => { + await ReportsService.deleteByIds(req.body.data, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/reports: + * get: + * security: + * - bearerAuth: [] + * tags: [Reports] + * summary: Get all reports + * description: Get all reports + * responses: + * 200: + * description: Reports list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Reports" + * 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 ReportsDBApi.findAll(req.query, { currentUser }); + if (filetype && filetype === 'csv') { + const fields = ['id']; + 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/reports/count: + * get: + * security: + * - bearerAuth: [] + * tags: [Reports] + * summary: Count all reports + * description: Count all reports + * responses: + * 200: + * description: Reports count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Reports" + * 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 ReportsDBApi.findAll(req.query, null, { + countOnly: true, + currentUser, + }); + + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/reports/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [Reports] + * summary: Find all reports that match search criteria + * description: Find all reports that match search criteria + * responses: + * 200: + * description: Reports list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Reports" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/autocomplete', async (req, res) => { + const payload = await ReportsDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/reports/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Reports] + * 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/Reports" + * 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 ReportsDBApi.findBy({ id: req.params.id }); + + res.status(200).send(payload); + }), +); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/services/reports.js b/backend/src/services/reports.js new file mode 100644 index 0000000..0213823 --- /dev/null +++ b/backend/src/services/reports.js @@ -0,0 +1,114 @@ +const db = require('../db/models'); +const ReportsDBApi = require('../db/api/reports'); +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 ReportsService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await ReportsDBApi.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 ReportsDBApi.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 reports = await ReportsDBApi.findBy({ id }, { transaction }); + + if (!reports) { + throw new ValidationError('reportsNotFound'); + } + + const updatedReports = await ReportsDBApi.update(id, data, { + currentUser, + transaction, + }); + + await transaction.commit(); + return updatedReports; + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await ReportsDBApi.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 ReportsDBApi.remove(id, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; diff --git a/frontend/src/components/Assets/TableAssets.tsx b/frontend/src/components/Assets/TableAssets.tsx index b2119b4..59134be 100644 --- a/frontend/src/components/Assets/TableAssets.tsx +++ b/frontend/src/components/Assets/TableAssets.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 TableSampleAssets = ({ filterItems, @@ -101,12 +98,6 @@ const TableSampleAssets = ({ setIsModalTrashActive(false); }; - const handleCreateEventAction = ({ start, end }: SlotInfo) => { - router.push( - `/assets/assets-new?dateRangeStart=${start.toISOString()}&dateRangeEnd=${end.toISOString()}`, - ); - }; - const handleDeleteModalAction = (id: string) => { setId(id); setIsModalTrashActive(true); @@ -470,27 +461,7 @@ const TableSampleAssets = ({

Are you sure you want to delete this item?

- {!showGrid && ( - { - loadData( - 0, - `&calendarStart=${range.start}&calendarEnd=${range.end}`, - ); - }} - entityName={'assets'} - /> - )} - - {showGrid && dataGrid} + {dataGrid} {selectedRows.length > 0 && createPortal( diff --git a/frontend/src/components/Compliance_certificates/TableCompliance_certificates.tsx b/frontend/src/components/Compliance_certificates/TableCompliance_certificates.tsx index b1a101d..9c984d4 100644 --- a/frontend/src/components/Compliance_certificates/TableCompliance_certificates.tsx +++ b/frontend/src/components/Compliance_certificates/TableCompliance_certificates.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 TableSampleCompliance_certificates = ({ filterItems, @@ -104,12 +101,6 @@ const TableSampleCompliance_certificates = ({ setIsModalTrashActive(false); }; - const handleCreateEventAction = ({ start, end }: SlotInfo) => { - router.push( - `/compliance_certificates/compliance_certificates-new?dateRangeStart=${start.toISOString()}&dateRangeEnd=${end.toISOString()}`, - ); - }; - const handleDeleteModalAction = (id: string) => { setId(id); setIsModalTrashActive(true); @@ -475,27 +466,7 @@ const TableSampleCompliance_certificates = ({

Are you sure you want to delete this item?

- {!showGrid && ( - { - loadData( - 0, - `&calendarStart=${range.start}&calendarEnd=${range.end}`, - ); - }} - entityName={'compliance_certificates'} - /> - )} - - {showGrid && dataGrid} + {dataGrid} {selectedRows.length > 0 && createPortal( diff --git a/frontend/src/components/Reports/CardReports.tsx b/frontend/src/components/Reports/CardReports.tsx new file mode 100644 index 0000000..885eb7c --- /dev/null +++ b/frontend/src/components/Reports/CardReports.tsx @@ -0,0 +1,98 @@ +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 = { + reports: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const CardReports = ({ + reports, + 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_REPORTS'); + + return ( +
+ {loading && } +
    + {!loading && + reports.map((item, index) => ( +
  • +
    + + {item.id} + + +
    + +
    +
    +
    +
  • + ))} + {!loading && reports.length === 0 && ( +
    +

    No data to display

    +
    + )} +
+
+ +
+
+ ); +}; + +export default CardReports; diff --git a/frontend/src/components/Reports/ListReports.tsx b/frontend/src/components/Reports/ListReports.tsx new file mode 100644 index 0000000..015ff25 --- /dev/null +++ b/frontend/src/components/Reports/ListReports.tsx @@ -0,0 +1,82 @@ +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 = { + reports: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const ListReports = ({ + reports, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const currentUser = useAppSelector((state) => state.auth.currentUser); + const hasUpdatePermission = hasPermission(currentUser, 'UPDATE_REPORTS'); + + const corners = useAppSelector((state) => state.style.corners); + const bgColor = useAppSelector((state) => state.style.cardsColor); + + return ( + <> +
+ {loading && } + {!loading && + reports.map((item) => ( +
+ +
+ dark:divide-dark-700 overflow-x-auto' + } + > + +
+
+
+ ))} + {!loading && reports.length === 0 && ( +
+

No data to display

+
+ )} +
+
+ +
+ + ); +}; + +export default ListReports; diff --git a/frontend/src/components/Reports/TableReports.tsx b/frontend/src/components/Reports/TableReports.tsx new file mode 100644 index 0000000..120bc77 --- /dev/null +++ b/frontend/src/components/Reports/TableReports.tsx @@ -0,0 +1,481 @@ +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/reports/reportsSlice'; +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 './configureReportsCols'; +import _ from 'lodash'; +import dataFormatter from '../../helpers/dataFormatter'; +import { dataGridStyles } from '../../styles'; + +const perPage = 10; + +const TableSampleReports = ({ + 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 { + reports, + loading, + count, + notify: reportsNotify, + refetch, + } = useAppSelector((state) => state.reports); + 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 (reportsNotify.showNotification) { + notify(reportsNotify.typeNotification, reportsNotify.textNotification); + } + }, [reportsNotify.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, `reports`, 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={reports ?? []} + 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} + > +
+ <> + {filterItems && + filterItems.map((filterItem) => { + return ( +
+
+
+ Filter +
+ + {filters.map((selectOption) => ( + + ))} + +
+ {filters.find( + (filter) => + filter.title === filterItem?.fields?.selectedField, + )?.type === 'enum' ? ( +
+
Value
+ + + {filters + .find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + ) + ?.options?.map((option) => ( + + ))} + +
+ ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + )?.number ? ( +
+
+
+ From +
+ +
+
+
+ To +
+ +
+
+ ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + )?.date ? ( +
+
+
+ From +
+ +
+
+
+ To +
+ +
+
+ ) : ( +
+
+ Contains +
+ +
+ )} +
+
+ Action +
+ { + deleteFilter(filterItem.id); + }} + /> +
+
+ ); + })} +
+ + +
+ +
+
+
+ ) : null} + +

Are you sure you want to delete this item?

+
+ + {dataGrid} + + {selectedRows.length > 0 && + createPortal( + onDeleteRows(selectedRows)} + />, + document.getElementById('delete-rows-button'), + )} + + + ); +}; + +export default TableSampleReports; diff --git a/frontend/src/components/Reports/configureReportsCols.tsx b/frontend/src/components/Reports/configureReportsCols.tsx new file mode 100644 index 0000000..a73fc43 --- /dev/null +++ b/frontend/src/components/Reports/configureReportsCols.tsx @@ -0,0 +1,62 @@ +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_REPORTS'); + + return [ + { + field: 'actions', + type: 'actions', + minWidth: 30, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + getActions: (params: GridRowParams) => { + return [ +
+ +
, + ]; + }, + }, + ]; +}; diff --git a/frontend/src/components/WebPageComponents/Footer.tsx b/frontend/src/components/WebPageComponents/Footer.tsx index b9818da..07d5aa8 100644 --- a/frontend/src/components/WebPageComponents/Footer.tsx +++ b/frontend/src/components/WebPageComponents/Footer.tsx @@ -17,7 +17,7 @@ export default function WebSiteFooter({ projectName }: WebSiteFooterProps) { const borders = useAppSelector((state) => state.style.borders); const websiteHeder = useAppSelector((state) => state.style.websiteHeder); - const style = FooterStyle.WITH_PROJECT_NAME; + const style = FooterStyle.WITH_PAGES; const design = FooterDesigns.DESIGN_DIVERSITY; diff --git a/frontend/src/components/WebPageComponents/Header.tsx b/frontend/src/components/WebPageComponents/Header.tsx index 6fe6a0f..c35aef0 100644 --- a/frontend/src/components/WebPageComponents/Header.tsx +++ b/frontend/src/components/WebPageComponents/Header.tsx @@ -17,9 +17,9 @@ export default function WebSiteHeader({ projectName }: WebSiteHeaderProps) { const websiteHeder = useAppSelector((state) => state.style.websiteHeder); const borders = useAppSelector((state) => state.style.borders); - const style = HeaderStyle.PAGES_RIGHT; + const style = HeaderStyle.PAGES_LEFT; - const design = HeaderDesigns.DEFAULT_DESIGN; + const design = HeaderDesigns.DESIGN_DIVERSITY; return (
{
- -
- Switch to Table -
diff --git a/frontend/src/pages/assets/assets-new.tsx b/frontend/src/pages/assets/assets-new.tsx index fdfdef0..2931a3b 100644 --- a/frontend/src/pages/assets/assets-new.tsx +++ b/frontend/src/pages/assets/assets-new.tsx @@ -54,9 +54,6 @@ const AssetsNew = () => { 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('/assets/assets-list'); @@ -76,17 +73,7 @@ const AssetsNew = () => { handleSubmit(values)} >
diff --git a/frontend/src/pages/assets/assets-table.tsx b/frontend/src/pages/assets/assets-table.tsx index 532ae5a..bbe5404 100644 --- a/frontend/src/pages/assets/assets-table.tsx +++ b/frontend/src/pages/assets/assets-table.tsx @@ -143,7 +143,7 @@ const AssetsTablesPage = () => {
- Back to calendar + Back to table
diff --git a/frontend/src/pages/compliance_certificates/compliance_certificates-list.tsx b/frontend/src/pages/compliance_certificates/compliance_certificates-list.tsx index 38e9678..2a911e3 100644 --- a/frontend/src/pages/compliance_certificates/compliance_certificates-list.tsx +++ b/frontend/src/pages/compliance_certificates/compliance_certificates-list.tsx @@ -131,14 +131,6 @@ const Compliance_certificatesTablesPage = () => {
- -
- - Switch to Table - -
diff --git a/frontend/src/pages/compliance_certificates/compliance_certificates-new.tsx b/frontend/src/pages/compliance_certificates/compliance_certificates-new.tsx index 4a01cec..3ed0452 100644 --- a/frontend/src/pages/compliance_certificates/compliance_certificates-new.tsx +++ b/frontend/src/pages/compliance_certificates/compliance_certificates-new.tsx @@ -46,9 +46,6 @@ const Compliance_certificatesNew = () => { 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('/compliance_certificates/compliance_certificates-list'); @@ -68,17 +65,7 @@ const Compliance_certificatesNew = () => { handleSubmit(values)} > diff --git a/frontend/src/pages/compliance_certificates/compliance_certificates-table.tsx b/frontend/src/pages/compliance_certificates/compliance_certificates-table.tsx index 246daca..2c640e5 100644 --- a/frontend/src/pages/compliance_certificates/compliance_certificates-table.tsx +++ b/frontend/src/pages/compliance_certificates/compliance_certificates-table.tsx @@ -134,7 +134,7 @@ const Compliance_certificatesTablesPage = () => { - Back to calendar + Back to table diff --git a/frontend/src/pages/dashboard.tsx b/frontend/src/pages/dashboard.tsx index c8aff98..88a7425 100644 --- a/frontend/src/pages/dashboard.tsx +++ b/frontend/src/pages/dashboard.tsx @@ -38,6 +38,7 @@ const Dashboard = () => { React.useState(loadingMessage); const [roles, setRoles] = React.useState(loadingMessage); const [permissions, setPermissions] = React.useState(loadingMessage); + const [reports, setReports] = React.useState(loadingMessage); const [widgetsRole, setWidgetsRole] = React.useState({ role: { value: '', label: '' }, @@ -57,6 +58,7 @@ const Dashboard = () => { 'software_licenses', 'roles', 'permissions', + 'reports', ]; const fns = [ setUsers, @@ -67,6 +69,7 @@ const Dashboard = () => { setSoftware_licenses, setRoles, setPermissions, + setReports, ]; const requests = entities.map((entity, index) => { @@ -458,6 +461,38 @@ const Dashboard = () => { )} + + {hasPermission(currentUser, 'READ_REPORTS') && ( + +
+
+
+
+ Reports +
+
+ {reports} +
+
+
+ +
+
+
+ + )} diff --git a/frontend/src/pages/index.tsx b/frontend/src/pages/index.tsx index 900779c..c7ad061 100644 --- a/frontend/src/pages/index.tsx +++ b/frontend/src/pages/index.tsx @@ -93,7 +93,7 @@ export default function WebSite() { { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = {}; + const [initialValues, setInitialValues] = useState(initVals); + + const { reports } = useAppSelector((state) => state.reports); + + const { reportsId } = router.query; + + useEffect(() => { + dispatch(fetch({ id: reportsId })); + }, [reportsId]); + + useEffect(() => { + if (typeof reports === 'object') { + setInitialValues(reports); + } + }, [reports]); + + useEffect(() => { + if (typeof reports === 'object') { + const newInitialVal = { ...initVals }; + + Object.keys(initVals).forEach((el) => (newInitialVal[el] = reports[el])); + + setInitialValues(newInitialVal); + } + }, [reports]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: reportsId, data })); + await router.push('/reports/reports-list'); + }; + + return ( + <> + + {getPageTitle('Edit reports')} + + + + {''} + + + handleSubmit(values)} + > + + + + + + router.push('/reports/reports-list')} + /> + + + + + + + ); +}; + +EditReports.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditReports; diff --git a/frontend/src/pages/reports/reports-edit.tsx b/frontend/src/pages/reports/reports-edit.tsx new file mode 100644 index 0000000..4f0298f --- /dev/null +++ b/frontend/src/pages/reports/reports-edit.tsx @@ -0,0 +1,116 @@ +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/reports/reportsSlice'; +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 EditReportsPage = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = {}; + const [initialValues, setInitialValues] = useState(initVals); + + const { reports } = useAppSelector((state) => state.reports); + + const { id } = router.query; + + useEffect(() => { + dispatch(fetch({ id: id })); + }, [id]); + + useEffect(() => { + if (typeof reports === 'object') { + setInitialValues(reports); + } + }, [reports]); + + useEffect(() => { + if (typeof reports === 'object') { + const newInitialVal = { ...initVals }; + Object.keys(initVals).forEach((el) => (newInitialVal[el] = reports[el])); + setInitialValues(newInitialVal); + } + }, [reports]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: id, data })); + await router.push('/reports/reports-list'); + }; + + return ( + <> + + {getPageTitle('Edit reports')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + router.push('/reports/reports-list')} + /> + + +
+
+
+ + ); +}; + +EditReportsPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditReportsPage; diff --git a/frontend/src/pages/reports/reports-list.tsx b/frontend/src/pages/reports/reports-list.tsx new file mode 100644 index 0000000..aaa30d0 --- /dev/null +++ b/frontend/src/pages/reports/reports-list.tsx @@ -0,0 +1,162 @@ +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 TableReports from '../../components/Reports/TableReports'; +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/reports/reportsSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const ReportsTablesPage = () => { + 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([]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_REPORTS'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getReportsCSV = async () => { + const response = await axios({ + url: '/reports?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 = 'reportsCSV.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('Reports')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
+
+
+
+ + + + +
+ + + + + ); +}; + +ReportsTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default ReportsTablesPage; diff --git a/frontend/src/pages/reports/reports-new.tsx b/frontend/src/pages/reports/reports-new.tsx new file mode 100644 index 0000000..0348396 --- /dev/null +++ b/frontend/src/pages/reports/reports-new.tsx @@ -0,0 +1,92 @@ +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/reports/reportsSlice'; +import { useAppDispatch } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import moment from 'moment'; + +const initialValues = {}; + +const ReportsNew = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + + const handleSubmit = async (data) => { + await dispatch(create(data)); + await router.push('/reports/reports-list'); + }; + return ( + <> + + {getPageTitle('New Item')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + router.push('/reports/reports-list')} + /> + + +
+
+
+ + ); +}; + +ReportsNew.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default ReportsNew; diff --git a/frontend/src/pages/reports/reports-table.tsx b/frontend/src/pages/reports/reports-table.tsx new file mode 100644 index 0000000..924339d --- /dev/null +++ b/frontend/src/pages/reports/reports-table.tsx @@ -0,0 +1,161 @@ +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 TableReports from '../../components/Reports/TableReports'; +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/reports/reportsSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const ReportsTablesPage = () => { + 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([]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_REPORTS'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getReportsCSV = async () => { + const response = await axios({ + url: '/reports?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 = 'reportsCSV.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('Reports')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
+
+
+
+ + + +
+ + + + + ); +}; + +ReportsTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default ReportsTablesPage; diff --git a/frontend/src/pages/reports/reports-view.tsx b/frontend/src/pages/reports/reports-view.tsx new file mode 100644 index 0000000..b53c55c --- /dev/null +++ b/frontend/src/pages/reports/reports-view.tsx @@ -0,0 +1,78 @@ +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/reports/reportsSlice'; +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 ReportsView = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const { reports } = useAppSelector((state) => state.reports); + + 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 reports')} + + + + + + + + + router.push('/reports/reports-list')} + /> + + + + ); +}; + +ReportsView.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default ReportsView; diff --git a/frontend/src/pages/web_pages/home.tsx b/frontend/src/pages/web_pages/home.tsx index bf03145..468d019 100644 --- a/frontend/src/pages/web_pages/home.tsx +++ b/frontend/src/pages/web_pages/home.tsx @@ -77,7 +77,7 @@ export default function WebSite() { { + const { id, query } = data; + const result = await axios.get(`reports${query || (id ? `/${id}` : '')}`); + return id + ? result.data + : { rows: result.data.rows, count: result.data.count }; +}); + +export const deleteItemsByIds = createAsyncThunk( + 'reports/deleteByIds', + async (data: any, { rejectWithValue }) => { + try { + await axios.post('reports/deleteByIds', { data }); + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const deleteItem = createAsyncThunk( + 'reports/deleteReports', + async (id: string, { rejectWithValue }) => { + try { + await axios.delete(`reports/${id}`); + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const create = createAsyncThunk( + 'reports/createReports', + async (data: any, { rejectWithValue }) => { + try { + const result = await axios.post('reports', { data }); + return result.data; + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const uploadCsv = createAsyncThunk( + 'reports/uploadCsv', + async (file: File, { rejectWithValue }) => { + try { + const data = new FormData(); + data.append('file', file); + data.append('filename', file.name); + + const result = await axios.post('reports/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( + 'reports/updateReports', + async (payload: any, { rejectWithValue }) => { + try { + const result = await axios.put(`reports/${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 reportsSlice = createSlice({ + name: 'reports', + 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.reports = action.payload.rows; + state.count = action.payload.count; + } else { + state.reports = 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, 'Reports 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, `${'Reports'.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, `${'Reports'.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, `${'Reports'.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, 'Reports 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 } = reportsSlice.actions; + +export default reportsSlice.reducer; diff --git a/frontend/src/stores/store.ts b/frontend/src/stores/store.ts index dad2765..3a6dc31 100644 --- a/frontend/src/stores/store.ts +++ b/frontend/src/stores/store.ts @@ -12,6 +12,7 @@ import employeesSlice from './employees/employeesSlice'; import software_licensesSlice from './software_licenses/software_licensesSlice'; import rolesSlice from './roles/rolesSlice'; import permissionsSlice from './permissions/permissionsSlice'; +import reportsSlice from './reports/reportsSlice'; export const store = configureStore({ reducer: { @@ -28,6 +29,7 @@ export const store = configureStore({ software_licenses: software_licensesSlice, roles: rolesSlice, permissions: permissionsSlice, + reports: reportsSlice, }, });