From 0a75564dbee668ffcfba76af523c43a78f327c83 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Fri, 12 Sep 2025 08:44:01 +0000 Subject: [PATCH] Updated via schema editor on 2025-09-12 08:43 --- app-shell/src/_schema.json | 3 +- backend/src/db/api/companies.js | 9 + backend/src/db/api/invoice_line_items.js | 322 ++++++++++++ backend/src/db/api/invoices.js | 333 ++++++++++++ backend/src/db/migrations/1757666397035.js | 90 ++++ backend/src/db/migrations/1757666435481.js | 49 ++ backend/src/db/migrations/1757666476417.js | 47 ++ backend/src/db/migrations/1757666503525.js | 90 ++++ backend/src/db/migrations/1757666528218.js | 49 ++ backend/src/db/models/companies.js | 16 + backend/src/db/models/invoice_line_items.js | 57 ++ backend/src/db/models/invoices.js | 61 +++ .../db/seeders/20200430130760-user-roles.js | 102 ++++ .../db/seeders/20231127130745-sample-data.js | 212 +++++++- backend/src/db/seeders/20250912083957.js | 87 ++++ backend/src/db/seeders/20250912084143.js | 87 ++++ backend/src/index.js | 16 + backend/src/routes/invoice_line_items.js | 459 ++++++++++++++++ backend/src/routes/invoices.js | 456 ++++++++++++++++ backend/src/services/invoice_line_items.js | 121 +++++ backend/src/services/invoices.js | 114 ++++ backend/src/services/search.js | 6 + .../CardInvoice_line_items.tsx | 112 ++++ .../ListInvoice_line_items.tsx | 92 ++++ .../TableInvoice_line_items.tsx | 489 ++++++++++++++++++ .../configureInvoice_line_itemsCols.tsx | 74 +++ .../src/components/Invoices/CardInvoices.tsx | 120 +++++ .../src/components/Invoices/ListInvoices.tsx | 94 ++++ .../src/components/Invoices/TableInvoices.tsx | 484 +++++++++++++++++ .../Invoices/configureInvoicesCols.tsx | 88 ++++ .../components/WebPageComponents/Footer.tsx | 4 +- .../components/WebPageComponents/Header.tsx | 4 +- frontend/src/menuAside.ts | 16 + .../src/pages/companies/companies-view.tsx | 82 +++ frontend/src/pages/dashboard.tsx | 71 +++ .../[invoice_line_itemsId].tsx | 150 ++++++ .../invoice_line_items-edit.tsx | 148 ++++++ .../invoice_line_items-list.tsx | 167 ++++++ .../invoice_line_items-new.tsx | 112 ++++ .../invoice_line_items-table.tsx | 166 ++++++ .../invoice_line_items-view.tsx | 97 ++++ frontend/src/pages/invoices/[invoicesId].tsx | 151 ++++++ frontend/src/pages/invoices/invoices-edit.tsx | 149 ++++++ frontend/src/pages/invoices/invoices-list.tsx | 166 ++++++ frontend/src/pages/invoices/invoices-new.tsx | 120 +++++ .../src/pages/invoices/invoices-table.tsx | 165 ++++++ frontend/src/pages/invoices/invoices-view.tsx | 98 ++++ frontend/src/pages/web_pages/services.tsx | 2 +- .../invoice_line_itemsSlice.ts | 250 +++++++++ frontend/src/stores/invoices/invoicesSlice.ts | 236 +++++++++ frontend/src/stores/store.ts | 4 + 51 files changed, 6687 insertions(+), 10 deletions(-) create mode 100644 backend/src/db/api/invoice_line_items.js create mode 100644 backend/src/db/api/invoices.js create mode 100644 backend/src/db/migrations/1757666397035.js create mode 100644 backend/src/db/migrations/1757666435481.js create mode 100644 backend/src/db/migrations/1757666476417.js create mode 100644 backend/src/db/migrations/1757666503525.js create mode 100644 backend/src/db/migrations/1757666528218.js create mode 100644 backend/src/db/models/invoice_line_items.js create mode 100644 backend/src/db/models/invoices.js create mode 100644 backend/src/db/seeders/20250912083957.js create mode 100644 backend/src/db/seeders/20250912084143.js create mode 100644 backend/src/routes/invoice_line_items.js create mode 100644 backend/src/routes/invoices.js create mode 100644 backend/src/services/invoice_line_items.js create mode 100644 backend/src/services/invoices.js create mode 100644 frontend/src/components/Invoice_line_items/CardInvoice_line_items.tsx create mode 100644 frontend/src/components/Invoice_line_items/ListInvoice_line_items.tsx create mode 100644 frontend/src/components/Invoice_line_items/TableInvoice_line_items.tsx create mode 100644 frontend/src/components/Invoice_line_items/configureInvoice_line_itemsCols.tsx create mode 100644 frontend/src/components/Invoices/CardInvoices.tsx create mode 100644 frontend/src/components/Invoices/ListInvoices.tsx create mode 100644 frontend/src/components/Invoices/TableInvoices.tsx create mode 100644 frontend/src/components/Invoices/configureInvoicesCols.tsx create mode 100644 frontend/src/pages/invoice_line_items/[invoice_line_itemsId].tsx create mode 100644 frontend/src/pages/invoice_line_items/invoice_line_items-edit.tsx create mode 100644 frontend/src/pages/invoice_line_items/invoice_line_items-list.tsx create mode 100644 frontend/src/pages/invoice_line_items/invoice_line_items-new.tsx create mode 100644 frontend/src/pages/invoice_line_items/invoice_line_items-table.tsx create mode 100644 frontend/src/pages/invoice_line_items/invoice_line_items-view.tsx create mode 100644 frontend/src/pages/invoices/[invoicesId].tsx create mode 100644 frontend/src/pages/invoices/invoices-edit.tsx create mode 100644 frontend/src/pages/invoices/invoices-list.tsx create mode 100644 frontend/src/pages/invoices/invoices-new.tsx create mode 100644 frontend/src/pages/invoices/invoices-table.tsx create mode 100644 frontend/src/pages/invoices/invoices-view.tsx create mode 100644 frontend/src/stores/invoice_line_items/invoice_line_itemsSlice.ts create mode 100644 frontend/src/stores/invoices/invoicesSlice.ts diff --git a/app-shell/src/_schema.json b/app-shell/src/_schema.json index 6e6b519..c172957 100644 --- a/app-shell/src/_schema.json +++ b/app-shell/src/_schema.json @@ -1,5 +1,6 @@ { "1": "{\"iv\":\"bFesGkedo+skv7zq\",\"encryptedData\":\"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\"}", "3": "{\"iv\":\"eHq1+XlKprO4YPsO\",\"encryptedData\":\"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\"}", - "Initial version": "{\"iv\":\"0qUDGuKmsqCzF1T6\",\"encryptedData\":\"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\"}" + "Initial version": "{\"iv\":\"0qUDGuKmsqCzF1T6\",\"encryptedData\":\"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\"}", + "Updated via schema editor on 2025-09-12 08:43": "{\"iv\":\"BhZIJ62BJ7o+FZ6p\",\"encryptedData\":\"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\"}" } \ No newline at end of file diff --git a/backend/src/db/api/companies.js b/backend/src/db/api/companies.js index 832742c..cf8f98b 100644 --- a/backend/src/db/api/companies.js +++ b/backend/src/db/api/companies.js @@ -155,6 +155,15 @@ module.exports = class CompaniesDBApi { transaction, }); + output.invoices_companies = await companies.getInvoices_companies({ + transaction, + }); + + output.invoice_line_items_companies = + await companies.getInvoice_line_items_companies({ + transaction, + }); + return output; } diff --git a/backend/src/db/api/invoice_line_items.js b/backend/src/db/api/invoice_line_items.js new file mode 100644 index 0000000..0cda776 --- /dev/null +++ b/backend/src/db/api/invoice_line_items.js @@ -0,0 +1,322 @@ +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 Invoice_line_itemsDBApi { + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const invoice_line_items = await db.invoice_line_items.create( + { + id: data.id || undefined, + + description: data.description || null, + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + await invoice_line_items.setCompanies(data.companies || null, { + transaction, + }); + + return invoice_line_items; + } + + 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 invoice_line_itemsData = data.map((item, index) => ({ + id: item.id || undefined, + + description: item.description || null, + importHash: item.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + // Bulk create items + const invoice_line_items = await db.invoice_line_items.bulkCreate( + invoice_line_itemsData, + { transaction }, + ); + + // For each item created, replace relation files + + return invoice_line_items; + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + const globalAccess = currentUser.app_role?.globalAccess; + + const invoice_line_items = await db.invoice_line_items.findByPk( + id, + {}, + { transaction }, + ); + + const updatePayload = {}; + + if (data.description !== undefined) + updatePayload.description = data.description; + + updatePayload.updatedById = currentUser.id; + + await invoice_line_items.update(updatePayload, { transaction }); + + if (data.companies !== undefined) { + await invoice_line_items.setCompanies( + data.companies, + + { transaction }, + ); + } + + return invoice_line_items; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const invoice_line_items = await db.invoice_line_items.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of invoice_line_items) { + await record.update({ deletedBy: currentUser.id }, { transaction }); + } + for (const record of invoice_line_items) { + await record.destroy({ transaction }); + } + }); + + return invoice_line_items; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const invoice_line_items = await db.invoice_line_items.findByPk( + id, + options, + ); + + await invoice_line_items.update( + { + deletedBy: currentUser.id, + }, + { + transaction, + }, + ); + + await invoice_line_items.destroy({ + transaction, + }); + + return invoice_line_items; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const invoice_line_items = await db.invoice_line_items.findOne( + { where }, + { transaction }, + ); + + if (!invoice_line_items) { + return invoice_line_items; + } + + const output = invoice_line_items.get({ plain: true }); + + output.companies = await invoice_line_items.getCompanies({ + transaction, + }); + + return output; + } + + static async findAll(filter, globalAccess, options) { + const limit = filter.limit || 0; + let offset = 0; + let where = {}; + const currentPage = +filter.page; + + const user = (options && options.currentUser) || null; + const userCompanies = (user && user.companies?.id) || null; + + if (userCompanies) { + if (options?.currentUser?.companiesId) { + where.companiesId = options.currentUser.companiesId; + } + } + + offset = currentPage * limit; + + const orderBy = null; + + const transaction = (options && options.transaction) || undefined; + + let include = [ + { + model: db.companies, + as: 'companies', + }, + ]; + + if (filter) { + if (filter.id) { + where = { + ...where, + ['id']: Utils.uuid(filter.id), + }; + } + + if (filter.description) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'invoice_line_items', + 'description', + filter.description, + ), + }; + } + + if (filter.active !== undefined) { + where = { + ...where, + active: filter.active === true || filter.active === 'true', + }; + } + + if (filter.companies) { + const listItems = filter.companies.split('|').map((item) => { + return Utils.uuid(item); + }); + + where = { + ...where, + companiesId: { [Op.or]: listItems }, + }; + } + + 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, + }, + }; + } + } + } + + if (globalAccess) { + delete where.companiesId; + } + + 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.invoice_line_items.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, + globalAccess, + organizationId, + ) { + let where = {}; + + if (!globalAccess && organizationId) { + where.organizationId = organizationId; + } + + if (query) { + where = { + [Op.or]: [ + { ['id']: Utils.uuid(query) }, + Utils.ilike('invoice_line_items', 'id', query), + ], + }; + } + + const records = await db.invoice_line_items.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/invoices.js b/backend/src/db/api/invoices.js new file mode 100644 index 0000000..d8e3b2e --- /dev/null +++ b/backend/src/db/api/invoices.js @@ -0,0 +1,333 @@ +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 InvoicesDBApi { + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const invoices = await db.invoices.create( + { + id: data.id || undefined, + + total_amount: data.total_amount || null, + status: data.status || null, + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + await invoices.setCompanies(data.companies || null, { + transaction, + }); + + return invoices; + } + + 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 invoicesData = data.map((item, index) => ({ + id: item.id || undefined, + + total_amount: item.total_amount || null, + status: item.status || null, + importHash: item.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + // Bulk create items + const invoices = await db.invoices.bulkCreate(invoicesData, { + transaction, + }); + + // For each item created, replace relation files + + return invoices; + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + const globalAccess = currentUser.app_role?.globalAccess; + + const invoices = await db.invoices.findByPk(id, {}, { transaction }); + + const updatePayload = {}; + + if (data.total_amount !== undefined) + updatePayload.total_amount = data.total_amount; + + if (data.status !== undefined) updatePayload.status = data.status; + + updatePayload.updatedById = currentUser.id; + + await invoices.update(updatePayload, { transaction }); + + if (data.companies !== undefined) { + await invoices.setCompanies( + data.companies, + + { transaction }, + ); + } + + return invoices; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const invoices = await db.invoices.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of invoices) { + await record.update({ deletedBy: currentUser.id }, { transaction }); + } + for (const record of invoices) { + await record.destroy({ transaction }); + } + }); + + return invoices; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const invoices = await db.invoices.findByPk(id, options); + + await invoices.update( + { + deletedBy: currentUser.id, + }, + { + transaction, + }, + ); + + await invoices.destroy({ + transaction, + }); + + return invoices; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const invoices = await db.invoices.findOne({ where }, { transaction }); + + if (!invoices) { + return invoices; + } + + const output = invoices.get({ plain: true }); + + output.companies = await invoices.getCompanies({ + transaction, + }); + + return output; + } + + static async findAll(filter, globalAccess, options) { + const limit = filter.limit || 0; + let offset = 0; + let where = {}; + const currentPage = +filter.page; + + const user = (options && options.currentUser) || null; + const userCompanies = (user && user.companies?.id) || null; + + if (userCompanies) { + if (options?.currentUser?.companiesId) { + where.companiesId = options.currentUser.companiesId; + } + } + + offset = currentPage * limit; + + const orderBy = null; + + const transaction = (options && options.transaction) || undefined; + + let include = [ + { + model: db.companies, + as: 'companies', + }, + ]; + + if (filter) { + if (filter.id) { + where = { + ...where, + ['id']: Utils.uuid(filter.id), + }; + } + + if (filter.status) { + where = { + ...where, + [Op.and]: Utils.ilike('invoices', 'status', filter.status), + }; + } + + if (filter.total_amountRange) { + const [start, end] = filter.total_amountRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + total_amount: { + ...where.total_amount, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + total_amount: { + ...where.total_amount, + [Op.lte]: end, + }, + }; + } + } + + if (filter.active !== undefined) { + where = { + ...where, + active: filter.active === true || filter.active === 'true', + }; + } + + if (filter.companies) { + const listItems = filter.companies.split('|').map((item) => { + return Utils.uuid(item); + }); + + where = { + ...where, + companiesId: { [Op.or]: listItems }, + }; + } + + 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, + }, + }; + } + } + } + + if (globalAccess) { + delete where.companiesId; + } + + 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.invoices.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, + globalAccess, + organizationId, + ) { + let where = {}; + + if (!globalAccess && organizationId) { + where.organizationId = organizationId; + } + + if (query) { + where = { + [Op.or]: [ + { ['id']: Utils.uuid(query) }, + Utils.ilike('invoices', 'id', query), + ], + }; + } + + const records = await db.invoices.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/1757666397035.js b/backend/src/db/migrations/1757666397035.js new file mode 100644 index 0000000..2893837 --- /dev/null +++ b/backend/src/db/migrations/1757666397035.js @@ -0,0 +1,90 @@ +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( + 'invoices', + { + 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( + 'invoices', + 'companiesId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'companies', + key: 'id', + }, + }, + { 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('invoices', 'companiesId', { + transaction, + }); + + await queryInterface.dropTable('invoices', { transaction }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1757666435481.js b/backend/src/db/migrations/1757666435481.js new file mode 100644 index 0000000..209c78c --- /dev/null +++ b/backend/src/db/migrations/1757666435481.js @@ -0,0 +1,49 @@ +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.addColumn( + 'invoices', + 'total_amount', + { + type: Sequelize.DataTypes.DECIMAL, + }, + { 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('invoices', 'total_amount', { + transaction, + }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1757666476417.js b/backend/src/db/migrations/1757666476417.js new file mode 100644 index 0000000..91fe044 --- /dev/null +++ b/backend/src/db/migrations/1757666476417.js @@ -0,0 +1,47 @@ +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.addColumn( + 'invoices', + 'status', + { + type: Sequelize.DataTypes.TEXT, + }, + { 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('invoices', 'status', { transaction }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1757666503525.js b/backend/src/db/migrations/1757666503525.js new file mode 100644 index 0000000..78608c3 --- /dev/null +++ b/backend/src/db/migrations/1757666503525.js @@ -0,0 +1,90 @@ +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( + 'invoice_line_items', + { + 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( + 'invoice_line_items', + 'companiesId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'companies', + key: 'id', + }, + }, + { 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('invoice_line_items', 'companiesId', { + transaction, + }); + + await queryInterface.dropTable('invoice_line_items', { transaction }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1757666528218.js b/backend/src/db/migrations/1757666528218.js new file mode 100644 index 0000000..4697df2 --- /dev/null +++ b/backend/src/db/migrations/1757666528218.js @@ -0,0 +1,49 @@ +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.addColumn( + 'invoice_line_items', + 'description', + { + type: Sequelize.DataTypes.TEXT, + }, + { 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('invoice_line_items', 'description', { + transaction, + }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/models/companies.js b/backend/src/db/models/companies.js index 7eed693..60f2160 100644 --- a/backend/src/db/models/companies.js +++ b/backend/src/db/models/companies.js @@ -82,6 +82,22 @@ module.exports = function (sequelize, DataTypes) { constraints: false, }); + db.companies.hasMany(db.invoices, { + as: 'invoices_companies', + foreignKey: { + name: 'companiesId', + }, + constraints: false, + }); + + db.companies.hasMany(db.invoice_line_items, { + as: 'invoice_line_items_companies', + foreignKey: { + name: 'companiesId', + }, + constraints: false, + }); + //end loop db.companies.belongsTo(db.users, { diff --git a/backend/src/db/models/invoice_line_items.js b/backend/src/db/models/invoice_line_items.js new file mode 100644 index 0000000..ab8deba --- /dev/null +++ b/backend/src/db/models/invoice_line_items.js @@ -0,0 +1,57 @@ +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 invoice_line_items = sequelize.define( + 'invoice_line_items', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + + description: { + type: DataTypes.TEXT, + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + invoice_line_items.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.invoice_line_items.belongsTo(db.companies, { + as: 'companies', + foreignKey: { + name: 'companiesId', + }, + constraints: false, + }); + + db.invoice_line_items.belongsTo(db.users, { + as: 'createdBy', + }); + + db.invoice_line_items.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return invoice_line_items; +}; diff --git a/backend/src/db/models/invoices.js b/backend/src/db/models/invoices.js new file mode 100644 index 0000000..b301c19 --- /dev/null +++ b/backend/src/db/models/invoices.js @@ -0,0 +1,61 @@ +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 invoices = sequelize.define( + 'invoices', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + + total_amount: { + type: DataTypes.DECIMAL, + }, + + status: { + type: DataTypes.TEXT, + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + invoices.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.invoices.belongsTo(db.companies, { + as: 'companies', + foreignKey: { + name: 'companiesId', + }, + constraints: false, + }); + + db.invoices.belongsTo(db.users, { + as: 'createdBy', + }); + + db.invoices.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return invoices; +}; diff --git a/backend/src/db/seeders/20200430130760-user-roles.js b/backend/src/db/seeders/20200430130760-user-roles.js index 49de179..e56e49e 100644 --- a/backend/src/db/seeders/20200430130760-user-roles.js +++ b/backend/src/db/seeders/20200430130760-user-roles.js @@ -114,6 +114,8 @@ module.exports = { 'roles', 'permissions', 'companies', + 'invoices', + 'invoice_line_items', , ]; await queryInterface.bulkInsert( @@ -727,6 +729,56 @@ primary key ("roles_permissionsId", "permissionId") permissionId: getId('DELETE_TRUCKS'), }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('CREATE_INVOICES'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('READ_INVOICES'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('UPDATE_INVOICES'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('DELETE_INVOICES'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('CREATE_INVOICE_LINE_ITEMS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('READ_INVOICE_LINE_ITEMS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('UPDATE_INVOICE_LINE_ITEMS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('DELETE_INVOICE_LINE_ITEMS'), + }, + { createdAt, updatedAt, @@ -927,6 +979,56 @@ primary key ("roles_permissionsId", "permissionId") permissionId: getId('DELETE_COMPANIES'), }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('CREATE_INVOICES'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('READ_INVOICES'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('UPDATE_INVOICES'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('DELETE_INVOICES'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('CREATE_INVOICE_LINE_ITEMS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('READ_INVOICE_LINE_ITEMS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('UPDATE_INVOICE_LINE_ITEMS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('DELETE_INVOICE_LINE_ITEMS'), + }, + { createdAt, updatedAt, diff --git a/backend/src/db/seeders/20231127130745-sample-data.js b/backend/src/db/seeders/20231127130745-sample-data.js index 61a5554..a8ebfde 100644 --- a/backend/src/db/seeders/20231127130745-sample-data.js +++ b/backend/src/db/seeders/20231127130745-sample-data.js @@ -11,11 +11,15 @@ const Trucks = db.trucks; const Companies = db.companies; +const Invoices = db.invoices; + +const InvoiceLineItems = db.invoice_line_items; + const HelpRequestsData = [ { request_date: new Date('2023-10-01T08:30:00Z'), - status: 'InProgress', + status: 'Pending', // type code here for "relation_one" field @@ -27,7 +31,7 @@ const HelpRequestsData = [ { request_date: new Date('2023-10-02T09:00:00Z'), - status: 'Pending', + status: 'Completed', // type code here for "relation_one" field @@ -39,7 +43,7 @@ const HelpRequestsData = [ { request_date: new Date('2023-10-03T10:15:00Z'), - status: 'Pending', + status: 'Completed', // type code here for "relation_one" field @@ -63,7 +67,7 @@ const HelpRequestsData = [ { request_date: new Date('2023-10-05T12:30:00Z'), - status: 'InProgress', + status: 'Completed', // type code here for "relation_one" field @@ -261,6 +265,80 @@ const CompaniesData = [ }, ]; +const InvoicesData = [ + { + // type code here for "relation_one" field + + total_amount: 60.94, + + status: 'Gertrude Belle Elion', + }, + + { + // type code here for "relation_one" field + + total_amount: 59.51, + + status: 'Michael Faraday', + }, + + { + // type code here for "relation_one" field + + total_amount: 92.67, + + status: 'Antoine Laurent Lavoisier', + }, + + { + // type code here for "relation_one" field + + total_amount: 28.14, + + status: 'Leonard Euler', + }, + + { + // type code here for "relation_one" field + + total_amount: 54.97, + + status: 'August Kekule', + }, +]; + +const InvoiceLineItemsData = [ + { + // type code here for "relation_one" field + + description: 'Hans Selye', + }, + + { + // type code here for "relation_one" field + + description: 'William Harvey', + }, + + { + // type code here for "relation_one" field + + description: 'Alfred Kinsey', + }, + + { + // type code here for "relation_one" field + + description: 'William Herschel', + }, + + { + // type code here for "relation_one" field + + description: 'Antoine Laurent Lavoisier', + }, +]; + // Similar logic for "relation_many" async function associateUserWithCompany() { @@ -778,6 +856,120 @@ async function associateTruckWithCompany() { } } +async function associateInvoiceWithCompany() { + const relatedCompany0 = await Companies.findOne({ + offset: Math.floor(Math.random() * (await Companies.count())), + }); + const Invoice0 = await Invoices.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (Invoice0?.setCompany) { + await Invoice0.setCompany(relatedCompany0); + } + + const relatedCompany1 = await Companies.findOne({ + offset: Math.floor(Math.random() * (await Companies.count())), + }); + const Invoice1 = await Invoices.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (Invoice1?.setCompany) { + await Invoice1.setCompany(relatedCompany1); + } + + const relatedCompany2 = await Companies.findOne({ + offset: Math.floor(Math.random() * (await Companies.count())), + }); + const Invoice2 = await Invoices.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (Invoice2?.setCompany) { + await Invoice2.setCompany(relatedCompany2); + } + + const relatedCompany3 = await Companies.findOne({ + offset: Math.floor(Math.random() * (await Companies.count())), + }); + const Invoice3 = await Invoices.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (Invoice3?.setCompany) { + await Invoice3.setCompany(relatedCompany3); + } + + const relatedCompany4 = await Companies.findOne({ + offset: Math.floor(Math.random() * (await Companies.count())), + }); + const Invoice4 = await Invoices.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (Invoice4?.setCompany) { + await Invoice4.setCompany(relatedCompany4); + } +} + +async function associateInvoiceLineItemWithCompany() { + const relatedCompany0 = await Companies.findOne({ + offset: Math.floor(Math.random() * (await Companies.count())), + }); + const InvoiceLineItem0 = await InvoiceLineItems.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (InvoiceLineItem0?.setCompany) { + await InvoiceLineItem0.setCompany(relatedCompany0); + } + + const relatedCompany1 = await Companies.findOne({ + offset: Math.floor(Math.random() * (await Companies.count())), + }); + const InvoiceLineItem1 = await InvoiceLineItems.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (InvoiceLineItem1?.setCompany) { + await InvoiceLineItem1.setCompany(relatedCompany1); + } + + const relatedCompany2 = await Companies.findOne({ + offset: Math.floor(Math.random() * (await Companies.count())), + }); + const InvoiceLineItem2 = await InvoiceLineItems.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (InvoiceLineItem2?.setCompany) { + await InvoiceLineItem2.setCompany(relatedCompany2); + } + + const relatedCompany3 = await Companies.findOne({ + offset: Math.floor(Math.random() * (await Companies.count())), + }); + const InvoiceLineItem3 = await InvoiceLineItems.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (InvoiceLineItem3?.setCompany) { + await InvoiceLineItem3.setCompany(relatedCompany3); + } + + const relatedCompany4 = await Companies.findOne({ + offset: Math.floor(Math.random() * (await Companies.count())), + }); + const InvoiceLineItem4 = await InvoiceLineItems.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (InvoiceLineItem4?.setCompany) { + await InvoiceLineItem4.setCompany(relatedCompany4); + } +} + module.exports = { up: async (queryInterface, Sequelize) => { await HelpRequests.bulkCreate(HelpRequestsData); @@ -790,6 +982,10 @@ module.exports = { await Companies.bulkCreate(CompaniesData); + await Invoices.bulkCreate(InvoicesData); + + await InvoiceLineItems.bulkCreate(InvoiceLineItemsData); + await Promise.all([ // Similar logic for "relation_many" @@ -812,6 +1008,10 @@ module.exports = { // Similar logic for "relation_many" await associateTruckWithCompany(), + + await associateInvoiceWithCompany(), + + await associateInvoiceLineItemWithCompany(), ]); }, @@ -825,5 +1025,9 @@ module.exports = { await queryInterface.bulkDelete('trucks', null, {}); await queryInterface.bulkDelete('companies', null, {}); + + await queryInterface.bulkDelete('invoices', null, {}); + + await queryInterface.bulkDelete('invoice_line_items', null, {}); }, }; diff --git a/backend/src/db/seeders/20250912083957.js b/backend/src/db/seeders/20250912083957.js new file mode 100644 index 0000000..4582dbb --- /dev/null +++ b/backend/src/db/seeders/20250912083957.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 = ['invoices']; + + 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.super_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/db/seeders/20250912084143.js b/backend/src/db/seeders/20250912084143.js new file mode 100644 index 0000000..8ac700a --- /dev/null +++ b/backend/src/db/seeders/20250912084143.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 = ['invoice_line_items']; + + 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.super_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 1dcf943..d69d25e 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -37,6 +37,10 @@ const permissionsRoutes = require('./routes/permissions'); const companiesRoutes = require('./routes/companies'); +const invoicesRoutes = require('./routes/invoices'); + +const invoice_line_itemsRoutes = require('./routes/invoice_line_items'); + const getBaseUrl = (url) => { if (!url) return ''; return url.endsWith('/api') ? url.slice(0, -4) : url; @@ -150,6 +154,18 @@ app.use( companiesRoutes, ); +app.use( + '/api/invoices', + passport.authenticate('jwt', { session: false }), + invoicesRoutes, +); + +app.use( + '/api/invoice_line_items', + passport.authenticate('jwt', { session: false }), + invoice_line_itemsRoutes, +); + app.use( '/api/openai', passport.authenticate('jwt', { session: false }), diff --git a/backend/src/routes/invoice_line_items.js b/backend/src/routes/invoice_line_items.js new file mode 100644 index 0000000..9351937 --- /dev/null +++ b/backend/src/routes/invoice_line_items.js @@ -0,0 +1,459 @@ +const express = require('express'); + +const Invoice_line_itemsService = require('../services/invoice_line_items'); +const Invoice_line_itemsDBApi = require('../db/api/invoice_line_items'); +const wrapAsync = require('../helpers').wrapAsync; + +const config = require('../config'); + +const router = express.Router(); + +const { parse } = require('json2csv'); + +const { checkCrudPermissions } = require('../middlewares/check-permissions'); + +router.use(checkCrudPermissions('invoice_line_items')); + +/** + * @swagger + * components: + * schemas: + * Invoice_line_items: + * type: object + * properties: + + * description: + * type: string + * default: description + + */ + +/** + * @swagger + * tags: + * name: Invoice_line_items + * description: The Invoice_line_items managing API + */ + +/** + * @swagger + * /api/invoice_line_items: + * post: + * security: + * - bearerAuth: [] + * tags: [Invoice_line_items] + * 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/Invoice_line_items" + * responses: + * 200: + * description: The item was successfully added + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Invoice_line_items" + * 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 Invoice_line_itemsService.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: [Invoice_line_items] + * 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/Invoice_line_items" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Invoice_line_items" + * 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 Invoice_line_itemsService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/invoice_line_items/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Invoice_line_items] + * 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/Invoice_line_items" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Invoice_line_items" + * 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 Invoice_line_itemsService.update( + req.body.data, + req.body.id, + req.currentUser, + ); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/invoice_line_items/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Invoice_line_items] + * 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/Invoice_line_items" + * 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 Invoice_line_itemsService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/invoice_line_items/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Invoice_line_items] + * 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/Invoice_line_items" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post( + '/deleteByIds', + wrapAsync(async (req, res) => { + await Invoice_line_itemsService.deleteByIds(req.body.data, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/invoice_line_items: + * get: + * security: + * - bearerAuth: [] + * tags: [Invoice_line_items] + * summary: Get all invoice_line_items + * description: Get all invoice_line_items + * responses: + * 200: + * description: Invoice_line_items list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Invoice_line_items" + * 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 globalAccess = req.currentUser.app_role.globalAccess; + + const currentUser = req.currentUser; + const payload = await Invoice_line_itemsDBApi.findAll( + req.query, + globalAccess, + { currentUser }, + ); + if (filetype && filetype === 'csv') { + const fields = ['id', 'description']; + 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/invoice_line_items/count: + * get: + * security: + * - bearerAuth: [] + * tags: [Invoice_line_items] + * summary: Count all invoice_line_items + * description: Count all invoice_line_items + * responses: + * 200: + * description: Invoice_line_items count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Invoice_line_items" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get( + '/count', + wrapAsync(async (req, res) => { + const globalAccess = req.currentUser.app_role.globalAccess; + + const currentUser = req.currentUser; + const payload = await Invoice_line_itemsDBApi.findAll( + req.query, + globalAccess, + { countOnly: true, currentUser }, + ); + + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/invoice_line_items/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [Invoice_line_items] + * summary: Find all invoice_line_items that match search criteria + * description: Find all invoice_line_items that match search criteria + * responses: + * 200: + * description: Invoice_line_items list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Invoice_line_items" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/autocomplete', async (req, res) => { + const globalAccess = req.currentUser.app_role.globalAccess; + + const organizationId = req.currentUser.organization?.id; + + const payload = await Invoice_line_itemsDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + globalAccess, + organizationId, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/invoice_line_items/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Invoice_line_items] + * 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/Invoice_line_items" + * 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 Invoice_line_itemsDBApi.findBy({ id: req.params.id }); + + res.status(200).send(payload); + }), +); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/routes/invoices.js b/backend/src/routes/invoices.js new file mode 100644 index 0000000..1a41c5d --- /dev/null +++ b/backend/src/routes/invoices.js @@ -0,0 +1,456 @@ +const express = require('express'); + +const InvoicesService = require('../services/invoices'); +const InvoicesDBApi = require('../db/api/invoices'); +const wrapAsync = require('../helpers').wrapAsync; + +const config = require('../config'); + +const router = express.Router(); + +const { parse } = require('json2csv'); + +const { checkCrudPermissions } = require('../middlewares/check-permissions'); + +router.use(checkCrudPermissions('invoices')); + +/** + * @swagger + * components: + * schemas: + * Invoices: + * type: object + * properties: + + * status: + * type: string + * default: status + + * total_amount: + * type: integer + * format: int64 + + */ + +/** + * @swagger + * tags: + * name: Invoices + * description: The Invoices managing API + */ + +/** + * @swagger + * /api/invoices: + * post: + * security: + * - bearerAuth: [] + * tags: [Invoices] + * 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/Invoices" + * responses: + * 200: + * description: The item was successfully added + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Invoices" + * 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 InvoicesService.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: [Invoices] + * 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/Invoices" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Invoices" + * 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 InvoicesService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/invoices/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Invoices] + * 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/Invoices" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Invoices" + * 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 InvoicesService.update(req.body.data, req.body.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/invoices/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Invoices] + * 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/Invoices" + * 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 InvoicesService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/invoices/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Invoices] + * 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/Invoices" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post( + '/deleteByIds', + wrapAsync(async (req, res) => { + await InvoicesService.deleteByIds(req.body.data, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/invoices: + * get: + * security: + * - bearerAuth: [] + * tags: [Invoices] + * summary: Get all invoices + * description: Get all invoices + * responses: + * 200: + * description: Invoices list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Invoices" + * 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 globalAccess = req.currentUser.app_role.globalAccess; + + const currentUser = req.currentUser; + const payload = await InvoicesDBApi.findAll(req.query, globalAccess, { + currentUser, + }); + if (filetype && filetype === 'csv') { + const fields = ['id', 'status', 'total_amount']; + 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/invoices/count: + * get: + * security: + * - bearerAuth: [] + * tags: [Invoices] + * summary: Count all invoices + * description: Count all invoices + * responses: + * 200: + * description: Invoices count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Invoices" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get( + '/count', + wrapAsync(async (req, res) => { + const globalAccess = req.currentUser.app_role.globalAccess; + + const currentUser = req.currentUser; + const payload = await InvoicesDBApi.findAll(req.query, globalAccess, { + countOnly: true, + currentUser, + }); + + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/invoices/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [Invoices] + * summary: Find all invoices that match search criteria + * description: Find all invoices that match search criteria + * responses: + * 200: + * description: Invoices list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Invoices" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/autocomplete', async (req, res) => { + const globalAccess = req.currentUser.app_role.globalAccess; + + const organizationId = req.currentUser.organization?.id; + + const payload = await InvoicesDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + globalAccess, + organizationId, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/invoices/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Invoices] + * 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/Invoices" + * 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 InvoicesDBApi.findBy({ id: req.params.id }); + + res.status(200).send(payload); + }), +); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/services/invoice_line_items.js b/backend/src/services/invoice_line_items.js new file mode 100644 index 0000000..9e2ddf4 --- /dev/null +++ b/backend/src/services/invoice_line_items.js @@ -0,0 +1,121 @@ +const db = require('../db/models'); +const Invoice_line_itemsDBApi = require('../db/api/invoice_line_items'); +const processFile = require('../middlewares/upload'); +const ValidationError = require('./notifications/errors/validation'); +const csv = require('csv-parser'); +const axios = require('axios'); +const config = require('../config'); +const stream = require('stream'); + +module.exports = class Invoice_line_itemsService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await Invoice_line_itemsDBApi.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 Invoice_line_itemsDBApi.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 invoice_line_items = await Invoice_line_itemsDBApi.findBy( + { id }, + { transaction }, + ); + + if (!invoice_line_items) { + throw new ValidationError('invoice_line_itemsNotFound'); + } + + const updatedInvoice_line_items = await Invoice_line_itemsDBApi.update( + id, + data, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + return updatedInvoice_line_items; + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await Invoice_line_itemsDBApi.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 Invoice_line_itemsDBApi.remove(id, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; diff --git a/backend/src/services/invoices.js b/backend/src/services/invoices.js new file mode 100644 index 0000000..e21c6a6 --- /dev/null +++ b/backend/src/services/invoices.js @@ -0,0 +1,114 @@ +const db = require('../db/models'); +const InvoicesDBApi = require('../db/api/invoices'); +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 InvoicesService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await InvoicesDBApi.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 InvoicesDBApi.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 invoices = await InvoicesDBApi.findBy({ id }, { transaction }); + + if (!invoices) { + throw new ValidationError('invoicesNotFound'); + } + + const updatedInvoices = await InvoicesDBApi.update(id, data, { + currentUser, + transaction, + }); + + await transaction.commit(); + return updatedInvoices; + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await InvoicesDBApi.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 InvoicesDBApi.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 b3fd9d7..ca04152 100644 --- a/backend/src/services/search.js +++ b/backend/src/services/search.js @@ -50,9 +50,15 @@ module.exports = class SearchService { trucks: ['license_plate', 'model'], companies: ['name'], + + invoices: ['status'], + + invoice_line_items: ['description'], }; const columnsInt = { services: ['price'], + + invoices: ['total_amount'], }; let allFoundRecords = []; diff --git a/frontend/src/components/Invoice_line_items/CardInvoice_line_items.tsx b/frontend/src/components/Invoice_line_items/CardInvoice_line_items.tsx new file mode 100644 index 0000000..4cb4569 --- /dev/null +++ b/frontend/src/components/Invoice_line_items/CardInvoice_line_items.tsx @@ -0,0 +1,112 @@ +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 = { + invoice_line_items: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const CardInvoice_line_items = ({ + invoice_line_items, + 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_INVOICE_LINE_ITEMS', + ); + + return ( +
+ {loading && } +
    + {!loading && + invoice_line_items.map((item, index) => ( +
  • +
    + + {item.id} + + +
    + +
    +
    +
    +
    +
    + Description +
    +
    +
    + {item.description} +
    +
    +
    +
    +
  • + ))} + {!loading && invoice_line_items.length === 0 && ( +
    +

    No data to display

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

Description

+

{item.description}

+
+ + +
+
+
+ ))} + {!loading && invoice_line_items.length === 0 && ( +
+

No data to display

+
+ )} +
+
+ +
+ + ); +}; + +export default ListInvoice_line_items; diff --git a/frontend/src/components/Invoice_line_items/TableInvoice_line_items.tsx b/frontend/src/components/Invoice_line_items/TableInvoice_line_items.tsx new file mode 100644 index 0000000..a7ddcf8 --- /dev/null +++ b/frontend/src/components/Invoice_line_items/TableInvoice_line_items.tsx @@ -0,0 +1,489 @@ +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/invoice_line_items/invoice_line_itemsSlice'; +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 './configureInvoice_line_itemsCols'; +import _ from 'lodash'; +import dataFormatter from '../../helpers/dataFormatter'; +import { dataGridStyles } from '../../styles'; + +const perPage = 10; + +const TableSampleInvoice_line_items = ({ + 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 { + invoice_line_items, + loading, + count, + notify: invoice_line_itemsNotify, + refetch, + } = useAppSelector((state) => state.invoice_line_items); + 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 (invoice_line_itemsNotify.showNotification) { + notify( + invoice_line_itemsNotify.typeNotification, + invoice_line_itemsNotify.textNotification, + ); + } + }, [invoice_line_itemsNotify.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, + `invoice_line_items`, + 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={invoice_line_items ?? []} + 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 TableSampleInvoice_line_items; diff --git a/frontend/src/components/Invoice_line_items/configureInvoice_line_itemsCols.tsx b/frontend/src/components/Invoice_line_items/configureInvoice_line_itemsCols.tsx new file mode 100644 index 0000000..f575fc9 --- /dev/null +++ b/frontend/src/components/Invoice_line_items/configureInvoice_line_itemsCols.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_INVOICE_LINE_ITEMS'); + + return [ + { + field: 'description', + headerName: 'Description', + 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/Invoices/CardInvoices.tsx b/frontend/src/components/Invoices/CardInvoices.tsx new file mode 100644 index 0000000..e67d8db --- /dev/null +++ b/frontend/src/components/Invoices/CardInvoices.tsx @@ -0,0 +1,120 @@ +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 = { + invoices: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const CardInvoices = ({ + invoices, + 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_INVOICES'); + + return ( +
+ {loading && } +
    + {!loading && + invoices.map((item, index) => ( +
  • +
    + + {item.id} + + +
    + +
    +
    +
    +
    +
    + Total amount +
    +
    +
    + {item.total_amount} +
    +
    +
    + +
    +
    + Status +
    +
    +
    + {item.status} +
    +
    +
    +
    +
  • + ))} + {!loading && invoices.length === 0 && ( +
    +

    No data to display

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

Total amount

+

{item.total_amount}

+
+ +
+

Status

+

{item.status}

+
+ + +
+
+
+ ))} + {!loading && invoices.length === 0 && ( +
+

No data to display

+
+ )} +
+
+ +
+ + ); +}; + +export default ListInvoices; diff --git a/frontend/src/components/Invoices/TableInvoices.tsx b/frontend/src/components/Invoices/TableInvoices.tsx new file mode 100644 index 0000000..a751ec4 --- /dev/null +++ b/frontend/src/components/Invoices/TableInvoices.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/invoices/invoicesSlice'; +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 './configureInvoicesCols'; +import _ from 'lodash'; +import dataFormatter from '../../helpers/dataFormatter'; +import { dataGridStyles } from '../../styles'; + +const perPage = 10; + +const TableSampleInvoices = ({ + 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 { + invoices, + loading, + count, + notify: invoicesNotify, + refetch, + } = useAppSelector((state) => state.invoices); + 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 (invoicesNotify.showNotification) { + notify(invoicesNotify.typeNotification, invoicesNotify.textNotification); + } + }, [invoicesNotify.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, `invoices`, 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={invoices ?? []} + 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 TableSampleInvoices; diff --git a/frontend/src/components/Invoices/configureInvoicesCols.tsx b/frontend/src/components/Invoices/configureInvoicesCols.tsx new file mode 100644 index 0000000..c41ad85 --- /dev/null +++ b/frontend/src/components/Invoices/configureInvoicesCols.tsx @@ -0,0 +1,88 @@ +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_INVOICES'); + + return [ + { + field: 'total_amount', + headerName: 'Total amount', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + type: 'number', + }, + + { + field: 'status', + headerName: 'Status', + 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/WebPageComponents/Footer.tsx b/frontend/src/components/WebPageComponents/Footer.tsx index bd9c5a1..b1ac8c2 100644 --- a/frontend/src/components/WebPageComponents/Footer.tsx +++ b/frontend/src/components/WebPageComponents/Footer.tsx @@ -17,9 +17,9 @@ 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.DEFAULT_DESIGN; + const design = FooterDesigns.DESIGN_DIVERSITY; return (
state.style.websiteHeder); const borders = useAppSelector((state) => state.style.borders); - const style = HeaderStyle.PAGES_LEFT; + const style = HeaderStyle.PAGES_RIGHT; - const design = HeaderDesigns.DESIGN_DIVERSITY; + const design = HeaderDesigns.DEFAULT_DESIGN; return (
{ + <> +

Invoices companies

+ +
+ + + + + + + + + + {companies.invoices_companies && + Array.isArray(companies.invoices_companies) && + companies.invoices_companies.map((item: any) => ( + + router.push( + `/invoices/invoices-view/?id=${item.id}`, + ) + } + > + + + + + ))} + +
Total amountStatus
{item.total_amount}{item.status}
+
+ {!companies?.invoices_companies?.length && ( +
No data
+ )} +
+ + + <> +

+ Invoice_line_items companies +

+ +
+ + + + + + + + {companies.invoice_line_items_companies && + Array.isArray(companies.invoice_line_items_companies) && + companies.invoice_line_items_companies.map( + (item: any) => ( + + router.push( + `/invoice_line_items/invoice_line_items-view/?id=${item.id}`, + ) + } + > + + + ), + )} + +
Description
{item.description}
+
+ {!companies?.invoice_line_items_companies?.length && ( +
No data
+ )} +
+ + { const [roles, setRoles] = React.useState(loadingMessage); const [permissions, setPermissions] = React.useState(loadingMessage); const [companies, setCompanies] = React.useState(loadingMessage); + const [invoices, setInvoices] = React.useState(loadingMessage); + const [invoice_line_items, setInvoice_line_items] = + React.useState(loadingMessage); const [widgetsRole, setWidgetsRole] = React.useState({ role: { value: '', label: '' }, @@ -57,6 +60,8 @@ const Dashboard = () => { 'roles', 'permissions', 'companies', + 'invoices', + 'invoice_line_items', ]; const fns = [ setUsers, @@ -67,6 +72,8 @@ const Dashboard = () => { setRoles, setPermissions, setCompanies, + setInvoices, + setInvoice_line_items, ]; const requests = entities.map((entity, index) => { @@ -454,6 +461,70 @@ const Dashboard = () => {
)} + + {hasPermission(currentUser, 'READ_INVOICES') && ( + +
+
+
+
+ Invoices +
+
+ {invoices} +
+
+
+ +
+
+
+ + )} + + {hasPermission(currentUser, 'READ_INVOICE_LINE_ITEMS') && ( + +
+
+
+
+ Invoice line items +
+
+ {invoice_line_items} +
+
+
+ +
+
+
+ + )}
diff --git a/frontend/src/pages/invoice_line_items/[invoice_line_itemsId].tsx b/frontend/src/pages/invoice_line_items/[invoice_line_itemsId].tsx new file mode 100644 index 0000000..ed6b09d --- /dev/null +++ b/frontend/src/pages/invoice_line_items/[invoice_line_itemsId].tsx @@ -0,0 +1,150 @@ +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/invoice_line_items/invoice_line_itemsSlice'; +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'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const EditInvoice_line_items = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + companies: null, + + description: '', + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { invoice_line_items } = useAppSelector( + (state) => state.invoice_line_items, + ); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { invoice_line_itemsId } = router.query; + + useEffect(() => { + dispatch(fetch({ id: invoice_line_itemsId })); + }, [invoice_line_itemsId]); + + useEffect(() => { + if (typeof invoice_line_items === 'object') { + setInitialValues(invoice_line_items); + } + }, [invoice_line_items]); + + useEffect(() => { + if (typeof invoice_line_items === 'object') { + const newInitialVal = { ...initVals }; + + Object.keys(initVals).forEach( + (el) => (newInitialVal[el] = invoice_line_items[el]), + ); + + setInitialValues(newInitialVal); + } + }, [invoice_line_items]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: invoice_line_itemsId, data })); + await router.push('/invoice_line_items/invoice_line_items-list'); + }; + + return ( + <> + + {getPageTitle('Edit invoice_line_items')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + + + + + + + + + + router.push('/invoice_line_items/invoice_line_items-list') + } + /> + + +
+
+
+ + ); +}; + +EditInvoice_line_items.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditInvoice_line_items; diff --git a/frontend/src/pages/invoice_line_items/invoice_line_items-edit.tsx b/frontend/src/pages/invoice_line_items/invoice_line_items-edit.tsx new file mode 100644 index 0000000..0660af9 --- /dev/null +++ b/frontend/src/pages/invoice_line_items/invoice_line_items-edit.tsx @@ -0,0 +1,148 @@ +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/invoice_line_items/invoice_line_itemsSlice'; +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'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const EditInvoice_line_itemsPage = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + companies: null, + + description: '', + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { invoice_line_items } = useAppSelector( + (state) => state.invoice_line_items, + ); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { id } = router.query; + + useEffect(() => { + dispatch(fetch({ id: id })); + }, [id]); + + useEffect(() => { + if (typeof invoice_line_items === 'object') { + setInitialValues(invoice_line_items); + } + }, [invoice_line_items]); + + useEffect(() => { + if (typeof invoice_line_items === 'object') { + const newInitialVal = { ...initVals }; + Object.keys(initVals).forEach( + (el) => (newInitialVal[el] = invoice_line_items[el]), + ); + setInitialValues(newInitialVal); + } + }, [invoice_line_items]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: id, data })); + await router.push('/invoice_line_items/invoice_line_items-list'); + }; + + return ( + <> + + {getPageTitle('Edit invoice_line_items')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + + + + + + + + + + router.push('/invoice_line_items/invoice_line_items-list') + } + /> + + +
+
+
+ + ); +}; + +EditInvoice_line_itemsPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditInvoice_line_itemsPage; diff --git a/frontend/src/pages/invoice_line_items/invoice_line_items-list.tsx b/frontend/src/pages/invoice_line_items/invoice_line_items-list.tsx new file mode 100644 index 0000000..6d2e966 --- /dev/null +++ b/frontend/src/pages/invoice_line_items/invoice_line_items-list.tsx @@ -0,0 +1,167 @@ +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 TableInvoice_line_items from '../../components/Invoice_line_items/TableInvoice_line_items'; +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/invoice_line_items/invoice_line_itemsSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const Invoice_line_itemsTablesPage = () => { + 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: 'Description', title: 'description' }]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_INVOICE_LINE_ITEMS'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getInvoice_line_itemsCSV = async () => { + const response = await axios({ + url: '/invoice_line_items?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 = 'invoice_line_itemsCSV.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('Invoice_line_items')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
+
+
+
+ + + + +
+ + + + + ); +}; + +Invoice_line_itemsTablesPage.getLayout = function getLayout( + page: ReactElement, +) { + return ( + + {page} + + ); +}; + +export default Invoice_line_itemsTablesPage; diff --git a/frontend/src/pages/invoice_line_items/invoice_line_items-new.tsx b/frontend/src/pages/invoice_line_items/invoice_line_items-new.tsx new file mode 100644 index 0000000..dcf5ed3 --- /dev/null +++ b/frontend/src/pages/invoice_line_items/invoice_line_items-new.tsx @@ -0,0 +1,112 @@ +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/invoice_line_items/invoice_line_itemsSlice'; +import { useAppDispatch } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import moment from 'moment'; + +const initialValues = { + companies: '', + + description: '', +}; + +const Invoice_line_itemsNew = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + + const handleSubmit = async (data) => { + await dispatch(create(data)); + await router.push('/invoice_line_items/invoice_line_items-list'); + }; + return ( + <> + + {getPageTitle('New Item')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + + + + + + + + + + router.push('/invoice_line_items/invoice_line_items-list') + } + /> + + +
+
+
+ + ); +}; + +Invoice_line_itemsNew.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Invoice_line_itemsNew; diff --git a/frontend/src/pages/invoice_line_items/invoice_line_items-table.tsx b/frontend/src/pages/invoice_line_items/invoice_line_items-table.tsx new file mode 100644 index 0000000..af6dcb8 --- /dev/null +++ b/frontend/src/pages/invoice_line_items/invoice_line_items-table.tsx @@ -0,0 +1,166 @@ +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 TableInvoice_line_items from '../../components/Invoice_line_items/TableInvoice_line_items'; +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/invoice_line_items/invoice_line_itemsSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const Invoice_line_itemsTablesPage = () => { + 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: 'Description', title: 'description' }]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_INVOICE_LINE_ITEMS'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getInvoice_line_itemsCSV = async () => { + const response = await axios({ + url: '/invoice_line_items?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 = 'invoice_line_itemsCSV.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('Invoice_line_items')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
+
+
+
+ + + +
+ + + + + ); +}; + +Invoice_line_itemsTablesPage.getLayout = function getLayout( + page: ReactElement, +) { + return ( + + {page} + + ); +}; + +export default Invoice_line_itemsTablesPage; diff --git a/frontend/src/pages/invoice_line_items/invoice_line_items-view.tsx b/frontend/src/pages/invoice_line_items/invoice_line_items-view.tsx new file mode 100644 index 0000000..9f387de --- /dev/null +++ b/frontend/src/pages/invoice_line_items/invoice_line_items-view.tsx @@ -0,0 +1,97 @@ +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/invoice_line_items/invoice_line_itemsSlice'; +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'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const Invoice_line_itemsView = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const { invoice_line_items } = useAppSelector( + (state) => state.invoice_line_items, + ); + + const { currentUser } = useAppSelector((state) => state.auth); + + 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 invoice_line_items')} + + + + + + +
+

companies

+ +

{invoice_line_items?.companies?.name ?? 'No data'}

+
+ +
+

Description

+

{invoice_line_items?.description}

+
+ + + + + router.push('/invoice_line_items/invoice_line_items-list') + } + /> +
+
+ + ); +}; + +Invoice_line_itemsView.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Invoice_line_itemsView; diff --git a/frontend/src/pages/invoices/[invoicesId].tsx b/frontend/src/pages/invoices/[invoicesId].tsx new file mode 100644 index 0000000..9f153b3 --- /dev/null +++ b/frontend/src/pages/invoices/[invoicesId].tsx @@ -0,0 +1,151 @@ +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/invoices/invoicesSlice'; +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'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const EditInvoices = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + companies: null, + + total_amount: '', + + status: '', + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { invoices } = useAppSelector((state) => state.invoices); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { invoicesId } = router.query; + + useEffect(() => { + dispatch(fetch({ id: invoicesId })); + }, [invoicesId]); + + useEffect(() => { + if (typeof invoices === 'object') { + setInitialValues(invoices); + } + }, [invoices]); + + useEffect(() => { + if (typeof invoices === 'object') { + const newInitialVal = { ...initVals }; + + Object.keys(initVals).forEach((el) => (newInitialVal[el] = invoices[el])); + + setInitialValues(newInitialVal); + } + }, [invoices]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: invoicesId, data })); + await router.push('/invoices/invoices-list'); + }; + + return ( + <> + + {getPageTitle('Edit invoices')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + + + + + + + + + + + + + router.push('/invoices/invoices-list')} + /> + + +
+
+
+ + ); +}; + +EditInvoices.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditInvoices; diff --git a/frontend/src/pages/invoices/invoices-edit.tsx b/frontend/src/pages/invoices/invoices-edit.tsx new file mode 100644 index 0000000..6b4224a --- /dev/null +++ b/frontend/src/pages/invoices/invoices-edit.tsx @@ -0,0 +1,149 @@ +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/invoices/invoicesSlice'; +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'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const EditInvoicesPage = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + companies: null, + + total_amount: '', + + status: '', + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { invoices } = useAppSelector((state) => state.invoices); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { id } = router.query; + + useEffect(() => { + dispatch(fetch({ id: id })); + }, [id]); + + useEffect(() => { + if (typeof invoices === 'object') { + setInitialValues(invoices); + } + }, [invoices]); + + useEffect(() => { + if (typeof invoices === 'object') { + const newInitialVal = { ...initVals }; + Object.keys(initVals).forEach((el) => (newInitialVal[el] = invoices[el])); + setInitialValues(newInitialVal); + } + }, [invoices]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: id, data })); + await router.push('/invoices/invoices-list'); + }; + + return ( + <> + + {getPageTitle('Edit invoices')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + + + + + + + + + + + + + router.push('/invoices/invoices-list')} + /> + + +
+
+
+ + ); +}; + +EditInvoicesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditInvoicesPage; diff --git a/frontend/src/pages/invoices/invoices-list.tsx b/frontend/src/pages/invoices/invoices-list.tsx new file mode 100644 index 0000000..6d720ea --- /dev/null +++ b/frontend/src/pages/invoices/invoices-list.tsx @@ -0,0 +1,166 @@ +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 TableInvoices from '../../components/Invoices/TableInvoices'; +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/invoices/invoicesSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const InvoicesTablesPage = () => { + 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: 'Status', title: 'status' }, + + { label: 'Total amount', title: 'total_amount', number: 'true' }, + ]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_INVOICES'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getInvoicesCSV = async () => { + const response = await axios({ + url: '/invoices?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 = 'invoicesCSV.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('Invoices')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
+
+
+
+ + + + +
+ + + + + ); +}; + +InvoicesTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default InvoicesTablesPage; diff --git a/frontend/src/pages/invoices/invoices-new.tsx b/frontend/src/pages/invoices/invoices-new.tsx new file mode 100644 index 0000000..7a3f9a4 --- /dev/null +++ b/frontend/src/pages/invoices/invoices-new.tsx @@ -0,0 +1,120 @@ +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/invoices/invoicesSlice'; +import { useAppDispatch } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import moment from 'moment'; + +const initialValues = { + companies: '', + + total_amount: '', + + status: '', +}; + +const InvoicesNew = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + + const handleSubmit = async (data) => { + await dispatch(create(data)); + await router.push('/invoices/invoices-list'); + }; + return ( + <> + + {getPageTitle('New Item')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + + + + + + + + + + + + + router.push('/invoices/invoices-list')} + /> + + +
+
+
+ + ); +}; + +InvoicesNew.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default InvoicesNew; diff --git a/frontend/src/pages/invoices/invoices-table.tsx b/frontend/src/pages/invoices/invoices-table.tsx new file mode 100644 index 0000000..8e113a3 --- /dev/null +++ b/frontend/src/pages/invoices/invoices-table.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 TableInvoices from '../../components/Invoices/TableInvoices'; +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/invoices/invoicesSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const InvoicesTablesPage = () => { + 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: 'Status', title: 'status' }, + + { label: 'Total amount', title: 'total_amount', number: 'true' }, + ]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_INVOICES'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getInvoicesCSV = async () => { + const response = await axios({ + url: '/invoices?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 = 'invoicesCSV.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('Invoices')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
+
+
+
+ + + +
+ + + + + ); +}; + +InvoicesTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default InvoicesTablesPage; diff --git a/frontend/src/pages/invoices/invoices-view.tsx b/frontend/src/pages/invoices/invoices-view.tsx new file mode 100644 index 0000000..66837a8 --- /dev/null +++ b/frontend/src/pages/invoices/invoices-view.tsx @@ -0,0 +1,98 @@ +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/invoices/invoicesSlice'; +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'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const InvoicesView = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const { invoices } = useAppSelector((state) => state.invoices); + + const { currentUser } = useAppSelector((state) => state.auth); + + 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 invoices')} + + + + + + +
+

companies

+ +

{invoices?.companies?.name ?? 'No data'}

+
+ +
+

Total amount

+

{invoices?.total_amount || 'No data'}

+
+ +
+

Status

+

{invoices?.status}

+
+ + + + router.push('/invoices/invoices-list')} + /> +
+
+ + ); +}; + +InvoicesView.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default InvoicesView; diff --git a/frontend/src/pages/web_pages/services.tsx b/frontend/src/pages/web_pages/services.tsx index f61921f..bef016c 100644 --- a/frontend/src/pages/web_pages/services.tsx +++ b/frontend/src/pages/web_pages/services.tsx @@ -115,7 +115,7 @@ export default function WebSite() { { + const { id, query } = data; + const result = await axios.get( + `invoice_line_items${query || (id ? `/${id}` : '')}`, + ); + return id + ? result.data + : { rows: result.data.rows, count: result.data.count }; + }, +); + +export const deleteItemsByIds = createAsyncThunk( + 'invoice_line_items/deleteByIds', + async (data: any, { rejectWithValue }) => { + try { + await axios.post('invoice_line_items/deleteByIds', { data }); + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const deleteItem = createAsyncThunk( + 'invoice_line_items/deleteInvoice_line_items', + async (id: string, { rejectWithValue }) => { + try { + await axios.delete(`invoice_line_items/${id}`); + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const create = createAsyncThunk( + 'invoice_line_items/createInvoice_line_items', + async (data: any, { rejectWithValue }) => { + try { + const result = await axios.post('invoice_line_items', { data }); + return result.data; + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const uploadCsv = createAsyncThunk( + 'invoice_line_items/uploadCsv', + async (file: File, { rejectWithValue }) => { + try { + const data = new FormData(); + data.append('file', file); + data.append('filename', file.name); + + const result = await axios.post('invoice_line_items/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( + 'invoice_line_items/updateInvoice_line_items', + async (payload: any, { rejectWithValue }) => { + try { + const result = await axios.put(`invoice_line_items/${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 invoice_line_itemsSlice = createSlice({ + name: 'invoice_line_items', + 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.invoice_line_items = action.payload.rows; + state.count = action.payload.count; + } else { + state.invoice_line_items = 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, 'Invoice_line_items 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, + `${'Invoice_line_items'.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, + `${'Invoice_line_items'.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, + `${'Invoice_line_items'.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, 'Invoice_line_items 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 } = invoice_line_itemsSlice.actions; + +export default invoice_line_itemsSlice.reducer; diff --git a/frontend/src/stores/invoices/invoicesSlice.ts b/frontend/src/stores/invoices/invoicesSlice.ts new file mode 100644 index 0000000..91afe65 --- /dev/null +++ b/frontend/src/stores/invoices/invoicesSlice.ts @@ -0,0 +1,236 @@ +import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit'; +import axios from 'axios'; +import { + fulfilledNotify, + rejectNotify, + resetNotify, +} from '../../helpers/notifyStateHandler'; + +interface MainState { + invoices: any; + loading: boolean; + count: number; + refetch: boolean; + rolesWidgets: any[]; + notify: { + showNotification: boolean; + textNotification: string; + typeNotification: string; + }; +} + +const initialState: MainState = { + invoices: [], + loading: false, + count: 0, + refetch: false, + rolesWidgets: [], + notify: { + showNotification: false, + textNotification: '', + typeNotification: 'warn', + }, +}; + +export const fetch = createAsyncThunk('invoices/fetch', async (data: any) => { + const { id, query } = data; + const result = await axios.get(`invoices${query || (id ? `/${id}` : '')}`); + return id + ? result.data + : { rows: result.data.rows, count: result.data.count }; +}); + +export const deleteItemsByIds = createAsyncThunk( + 'invoices/deleteByIds', + async (data: any, { rejectWithValue }) => { + try { + await axios.post('invoices/deleteByIds', { data }); + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const deleteItem = createAsyncThunk( + 'invoices/deleteInvoices', + async (id: string, { rejectWithValue }) => { + try { + await axios.delete(`invoices/${id}`); + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const create = createAsyncThunk( + 'invoices/createInvoices', + async (data: any, { rejectWithValue }) => { + try { + const result = await axios.post('invoices', { data }); + return result.data; + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const uploadCsv = createAsyncThunk( + 'invoices/uploadCsv', + async (file: File, { rejectWithValue }) => { + try { + const data = new FormData(); + data.append('file', file); + data.append('filename', file.name); + + const result = await axios.post('invoices/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( + 'invoices/updateInvoices', + async (payload: any, { rejectWithValue }) => { + try { + const result = await axios.put(`invoices/${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 invoicesSlice = createSlice({ + name: 'invoices', + 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.invoices = action.payload.rows; + state.count = action.payload.count; + } else { + state.invoices = 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, 'Invoices 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, `${'Invoices'.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, `${'Invoices'.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, `${'Invoices'.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, 'Invoices 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 } = invoicesSlice.actions; + +export default invoicesSlice.reducer; diff --git a/frontend/src/stores/store.ts b/frontend/src/stores/store.ts index 22829da..21d8efc 100644 --- a/frontend/src/stores/store.ts +++ b/frontend/src/stores/store.ts @@ -12,6 +12,8 @@ import trucksSlice from './trucks/trucksSlice'; import rolesSlice from './roles/rolesSlice'; import permissionsSlice from './permissions/permissionsSlice'; import companiesSlice from './companies/companiesSlice'; +import invoicesSlice from './invoices/invoicesSlice'; +import invoice_line_itemsSlice from './invoice_line_items/invoice_line_itemsSlice'; export const store = configureStore({ reducer: { @@ -28,6 +30,8 @@ export const store = configureStore({ roles: rolesSlice, permissions: permissionsSlice, companies: companiesSlice, + invoices: invoicesSlice, + invoice_line_items: invoice_line_itemsSlice, }, });