diff --git a/.gitignore b/.gitignore index e427ff3..d0eb167 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,8 @@ node_modules/ */node_modules/ */build/ + +**/node_modules/ +**/build/ +.DS_Store +.env \ No newline at end of file diff --git a/app-shell/src/_schema.json b/app-shell/src/_schema.json index d82ba08..2fc68a8 100644 --- a/app-shell/src/_schema.json +++ b/app-shell/src/_schema.json @@ -1,5 +1,4 @@ - - { - "Initial version": "{\"iv\":\"D/N4UFWNVW2pO99H\",\"encryptedData\":\"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\"}" -} + "Initial version": "{\"iv\":\"D/N4UFWNVW2pO99H\",\"encryptedData\":\"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\"}", + "": "{\"iv\":\"CjmOeaE5WXPbi+8S\",\"encryptedData\":\"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\"}" +} \ No newline at end of file diff --git a/backend/src/db/api/agencies.js b/backend/src/db/api/agencies.js index cfbc14d..d6e907a 100644 --- a/backend/src/db/api/agencies.js +++ b/backend/src/db/api/agencies.js @@ -151,6 +151,10 @@ module.exports = class AgenciesDBApi { transaction, }); + output.cart_agencies = await agencies.getCart_agencies({ + transaction, + }); + return output; } diff --git a/backend/src/db/api/cart.js b/backend/src/db/api/cart.js new file mode 100644 index 0000000..1972ded --- /dev/null +++ b/backend/src/db/api/cart.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 CartDBApi { + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const cart = await db.cart.create( + { + id: data.id || undefined, + + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + await cart.setAgencies(data.agencies || null, { + transaction, + }); + + await cart.setUser(data.user || null, { + transaction, + }); + + return cart; + } + + 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 cartData = data.map((item, index) => ({ + id: item.id || undefined, + + importHash: item.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + // Bulk create items + const cart = await db.cart.bulkCreate(cartData, { transaction }); + + // For each item created, replace relation files + + return cart; + } + + 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 cart = await db.cart.findByPk(id, {}, { transaction }); + + const updatePayload = {}; + + updatePayload.updatedById = currentUser.id; + + await cart.update(updatePayload, { transaction }); + + if (data.agencies !== undefined) { + await cart.setAgencies( + data.agencies, + + { transaction }, + ); + } + + if (data.user !== undefined) { + await cart.setUser( + data.user, + + { transaction }, + ); + } + + return cart; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const cart = await db.cart.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of cart) { + await record.update({ deletedBy: currentUser.id }, { transaction }); + } + for (const record of cart) { + await record.destroy({ transaction }); + } + }); + + return cart; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const cart = await db.cart.findByPk(id, options); + + await cart.update( + { + deletedBy: currentUser.id, + }, + { + transaction, + }, + ); + + await cart.destroy({ + transaction, + }); + + return cart; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const cart = await db.cart.findOne({ where }, { transaction }); + + if (!cart) { + return cart; + } + + const output = cart.get({ plain: true }); + + output.agencies = await cart.getAgencies({ + transaction, + }); + + output.user = await cart.getUser({ + 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 userAgencies = (user && user.agencies?.id) || null; + + if (userAgencies) { + if (options?.currentUser?.agenciesId) { + where.agenciesId = options.currentUser.agenciesId; + } + } + + offset = currentPage * limit; + + const orderBy = null; + + const transaction = (options && options.transaction) || undefined; + + let include = [ + { + model: db.agencies, + as: 'agencies', + }, + + { + model: db.users, + as: 'user', + + where: filter.user + ? { + [Op.or]: [ + { + id: { + [Op.in]: filter.user + .split('|') + .map((term) => Utils.uuid(term)), + }, + }, + { + firstName: { + [Op.or]: filter.user + .split('|') + .map((term) => ({ [Op.iLike]: `%${term}%` })), + }, + }, + ], + } + : {}, + }, + ]; + + if (filter) { + if (filter.id) { + where = { + ...where, + ['id']: Utils.uuid(filter.id), + }; + } + + if (filter.active !== undefined) { + where = { + ...where, + active: filter.active === true || filter.active === 'true', + }; + } + + if (filter.agencies) { + const listItems = filter.agencies.split('|').map((item) => { + return Utils.uuid(item); + }); + + where = { + ...where, + agenciesId: { [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.agenciesId; + } + + 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.cart.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('cart', 'id', query), + ], + }; + } + + const records = await db.cart.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/users.js b/backend/src/db/api/users.js index 16614b9..1ed5739 100644 --- a/backend/src/db/api/users.js +++ b/backend/src/db/api/users.js @@ -283,6 +283,10 @@ module.exports = class UsersDBApi { transaction, }); + output.cart_user = await users.getCart_user({ + transaction, + }); + output.avatar = await users.getAvatar({ transaction, }); diff --git a/backend/src/db/migrations/1748063329115.js b/backend/src/db/migrations/1748063329115.js new file mode 100644 index 0000000..131460d --- /dev/null +++ b/backend/src/db/migrations/1748063329115.js @@ -0,0 +1,88 @@ +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( + 'cart', + { + 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( + 'cart', + 'agenciesId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'agencies', + 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('cart', 'agenciesId', { transaction }); + + await queryInterface.dropTable('cart', { transaction }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1748063369260.js b/backend/src/db/migrations/1748063369260.js new file mode 100644 index 0000000..481e528 --- /dev/null +++ b/backend/src/db/migrations/1748063369260.js @@ -0,0 +1,52 @@ +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( + 'cart', + 'userId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'users', + 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('cart', 'userId', { transaction }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/models/agencies.js b/backend/src/db/models/agencies.js index dfd519c..c3ff344 100644 --- a/backend/src/db/models/agencies.js +++ b/backend/src/db/models/agencies.js @@ -82,6 +82,14 @@ module.exports = function (sequelize, DataTypes) { constraints: false, }); + db.agencies.hasMany(db.cart, { + as: 'cart_agencies', + foreignKey: { + name: 'agenciesId', + }, + constraints: false, + }); + //end loop db.agencies.belongsTo(db.users, { diff --git a/backend/src/db/models/cart.js b/backend/src/db/models/cart.js new file mode 100644 index 0000000..a8ae2e7 --- /dev/null +++ b/backend/src/db/models/cart.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 cart = sequelize.define( + 'cart', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + cart.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.cart.belongsTo(db.agencies, { + as: 'agencies', + foreignKey: { + name: 'agenciesId', + }, + constraints: false, + }); + + db.cart.belongsTo(db.users, { + as: 'user', + foreignKey: { + name: 'userId', + }, + constraints: false, + }); + + db.cart.belongsTo(db.users, { + as: 'createdBy', + }); + + db.cart.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return cart; +}; diff --git a/backend/src/db/models/users.js b/backend/src/db/models/users.js index c43477d..b16d548 100644 --- a/backend/src/db/models/users.js +++ b/backend/src/db/models/users.js @@ -110,6 +110,14 @@ module.exports = function (sequelize, DataTypes) { constraints: false, }); + db.users.hasMany(db.cart, { + as: 'cart_user', + foreignKey: { + name: 'userId', + }, + constraints: false, + }); + //end loop db.users.belongsTo(db.roles, { diff --git a/backend/src/db/seeders/20200430130760-user-roles.js b/backend/src/db/seeders/20200430130760-user-roles.js index fb25704..aeb9e9b 100644 --- a/backend/src/db/seeders/20200430130760-user-roles.js +++ b/backend/src/db/seeders/20200430130760-user-roles.js @@ -108,6 +108,7 @@ module.exports = { 'roles', 'permissions', 'agencies', + 'cart', , ]; await queryInterface.bulkInsert( @@ -577,6 +578,31 @@ primary key ("roles_permissionsId", "permissionId") permissionId: getId('DELETE_TOURS'), }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('CREATE_CART'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('READ_CART'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('UPDATE_CART'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('DELETE_CART'), + }, + { createdAt, updatedAt, @@ -752,6 +778,31 @@ primary key ("roles_permissionsId", "permissionId") permissionId: getId('DELETE_AGENCIES'), }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('CREATE_CART'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('READ_CART'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('UPDATE_CART'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('DELETE_CART'), + }, + { createdAt, updatedAt, diff --git a/backend/src/db/seeders/20231127130745-sample-data.js b/backend/src/db/seeders/20231127130745-sample-data.js index a06bbbc..21293ab 100644 --- a/backend/src/db/seeders/20231127130745-sample-data.js +++ b/backend/src/db/seeders/20231127130745-sample-data.js @@ -9,6 +9,8 @@ const Tours = db.tours; const Agencies = db.agencies; +const Cart = db.cart; + const AgentsData = [ { name: 'Alice Green', @@ -49,16 +51,6 @@ const AgentsData = [ // type code here for "relation_one" field }, - - { - name: 'Eve Red', - - commission_rate: 0.11, - - // type code here for "relation_one" field - - // type code here for "relation_one" field - }, ]; const BookingsData = [ @@ -81,7 +73,7 @@ const BookingsData = [ booking_date: new Date('2023-12-05T11:00:00Z'), - status: 'cancelled', + status: 'confirmed', // type code here for "relation_one" field }, @@ -93,7 +85,7 @@ const BookingsData = [ booking_date: new Date('2023-12-10T12:00:00Z'), - status: 'confirmed', + status: 'pending', // type code here for "relation_one" field }, @@ -105,19 +97,7 @@ const BookingsData = [ booking_date: new Date('2023-12-15T13:00:00Z'), - status: 'cancelled', - - // type code here for "relation_one" field - }, - - { - // type code here for "relation_one" field - - // type code here for "relation_one" field - - booking_date: new Date('2023-12-20T14:00:00Z'), - - status: 'confirmed', + status: 'pending', // type code here for "relation_one" field }, @@ -187,22 +167,6 @@ const ToursData = [ // type code here for "relation_one" field }, - - { - title: 'Alexandria Cultural Journey', - - description: 'Immerse yourself in the rich history of Alexandria.', - - price: 900, - - start_date: new Date('2024-05-15T09:00:00Z'), - - end_date: new Date('2024-05-20T18:00:00Z'), - - // type code here for "relation_one" field - - // type code here for "relation_one" field - }, ]; const AgenciesData = [ @@ -221,9 +185,27 @@ const AgenciesData = [ { name: 'Cultural Journeys', }, +]; + +const CartData = [ + { + // type code here for "relation_one" field + // type code here for "relation_one" field + }, { - name: 'Luxury Escapes', + // type code here for "relation_one" field + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + // type code here for "relation_one" field }, ]; @@ -273,17 +255,6 @@ async function associateUserWithAgency() { if (User3?.setAgency) { await User3.setAgency(relatedAgency3); } - - const relatedAgency4 = await Agencies.findOne({ - offset: Math.floor(Math.random() * (await Agencies.count())), - }); - const User4 = await Users.findOne({ - order: [['id', 'ASC']], - offset: 4, - }); - if (User4?.setAgency) { - await User4.setAgency(relatedAgency4); - } } async function associateAgentWithAgency() { @@ -330,17 +301,6 @@ async function associateAgentWithAgency() { if (Agent3?.setAgency) { await Agent3.setAgency(relatedAgency3); } - - const relatedAgency4 = await Agencies.findOne({ - offset: Math.floor(Math.random() * (await Agencies.count())), - }); - const Agent4 = await Agents.findOne({ - order: [['id', 'ASC']], - offset: 4, - }); - if (Agent4?.setAgency) { - await Agent4.setAgency(relatedAgency4); - } } async function associateAgentWithAgency() { @@ -387,17 +347,6 @@ async function associateAgentWithAgency() { if (Agent3?.setAgency) { await Agent3.setAgency(relatedAgency3); } - - const relatedAgency4 = await Agencies.findOne({ - offset: Math.floor(Math.random() * (await Agencies.count())), - }); - const Agent4 = await Agents.findOne({ - order: [['id', 'ASC']], - offset: 4, - }); - if (Agent4?.setAgency) { - await Agent4.setAgency(relatedAgency4); - } } async function associateBookingWithTour() { @@ -444,17 +393,6 @@ async function associateBookingWithTour() { if (Booking3?.setTour) { await Booking3.setTour(relatedTour3); } - - const relatedTour4 = await Tours.findOne({ - offset: Math.floor(Math.random() * (await Tours.count())), - }); - const Booking4 = await Bookings.findOne({ - order: [['id', 'ASC']], - offset: 4, - }); - if (Booking4?.setTour) { - await Booking4.setTour(relatedTour4); - } } async function associateBookingWithCustomer() { @@ -501,17 +439,6 @@ async function associateBookingWithCustomer() { if (Booking3?.setCustomer) { await Booking3.setCustomer(relatedCustomer3); } - - const relatedCustomer4 = await Users.findOne({ - offset: Math.floor(Math.random() * (await Users.count())), - }); - const Booking4 = await Bookings.findOne({ - order: [['id', 'ASC']], - offset: 4, - }); - if (Booking4?.setCustomer) { - await Booking4.setCustomer(relatedCustomer4); - } } async function associateBookingWithAgency() { @@ -558,16 +485,51 @@ async function associateBookingWithAgency() { if (Booking3?.setAgency) { await Booking3.setAgency(relatedAgency3); } +} - const relatedAgency4 = await Agencies.findOne({ +async function associateTourWithAgency() { + const relatedAgency0 = await Agencies.findOne({ offset: Math.floor(Math.random() * (await Agencies.count())), }); - const Booking4 = await Bookings.findOne({ + const Tour0 = await Tours.findOne({ order: [['id', 'ASC']], - offset: 4, + offset: 0, }); - if (Booking4?.setAgency) { - await Booking4.setAgency(relatedAgency4); + if (Tour0?.setAgency) { + await Tour0.setAgency(relatedAgency0); + } + + const relatedAgency1 = await Agencies.findOne({ + offset: Math.floor(Math.random() * (await Agencies.count())), + }); + const Tour1 = await Tours.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (Tour1?.setAgency) { + await Tour1.setAgency(relatedAgency1); + } + + const relatedAgency2 = await Agencies.findOne({ + offset: Math.floor(Math.random() * (await Agencies.count())), + }); + const Tour2 = await Tours.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (Tour2?.setAgency) { + await Tour2.setAgency(relatedAgency2); + } + + const relatedAgency3 = await Agencies.findOne({ + offset: Math.floor(Math.random() * (await Agencies.count())), + }); + const Tour3 = await Tours.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (Tour3?.setAgency) { + await Tour3.setAgency(relatedAgency3); } } @@ -615,73 +577,97 @@ async function associateTourWithAgency() { if (Tour3?.setAgency) { await Tour3.setAgency(relatedAgency3); } - - const relatedAgency4 = await Agencies.findOne({ - offset: Math.floor(Math.random() * (await Agencies.count())), - }); - const Tour4 = await Tours.findOne({ - order: [['id', 'ASC']], - offset: 4, - }); - if (Tour4?.setAgency) { - await Tour4.setAgency(relatedAgency4); - } } -async function associateTourWithAgency() { +async function associateCartWithAgency() { const relatedAgency0 = await Agencies.findOne({ offset: Math.floor(Math.random() * (await Agencies.count())), }); - const Tour0 = await Tours.findOne({ + const Cart0 = await Cart.findOne({ order: [['id', 'ASC']], offset: 0, }); - if (Tour0?.setAgency) { - await Tour0.setAgency(relatedAgency0); + if (Cart0?.setAgency) { + await Cart0.setAgency(relatedAgency0); } const relatedAgency1 = await Agencies.findOne({ offset: Math.floor(Math.random() * (await Agencies.count())), }); - const Tour1 = await Tours.findOne({ + const Cart1 = await Cart.findOne({ order: [['id', 'ASC']], offset: 1, }); - if (Tour1?.setAgency) { - await Tour1.setAgency(relatedAgency1); + if (Cart1?.setAgency) { + await Cart1.setAgency(relatedAgency1); } const relatedAgency2 = await Agencies.findOne({ offset: Math.floor(Math.random() * (await Agencies.count())), }); - const Tour2 = await Tours.findOne({ + const Cart2 = await Cart.findOne({ order: [['id', 'ASC']], offset: 2, }); - if (Tour2?.setAgency) { - await Tour2.setAgency(relatedAgency2); + if (Cart2?.setAgency) { + await Cart2.setAgency(relatedAgency2); } const relatedAgency3 = await Agencies.findOne({ offset: Math.floor(Math.random() * (await Agencies.count())), }); - const Tour3 = await Tours.findOne({ + const Cart3 = await Cart.findOne({ order: [['id', 'ASC']], offset: 3, }); - if (Tour3?.setAgency) { - await Tour3.setAgency(relatedAgency3); + if (Cart3?.setAgency) { + await Cart3.setAgency(relatedAgency3); + } +} + +async function associateCartWithUser() { + const relatedUser0 = await Users.findOne({ + offset: Math.floor(Math.random() * (await Users.count())), + }); + const Cart0 = await Cart.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (Cart0?.setUser) { + await Cart0.setUser(relatedUser0); } - const relatedAgency4 = await Agencies.findOne({ - offset: Math.floor(Math.random() * (await Agencies.count())), + const relatedUser1 = await Users.findOne({ + offset: Math.floor(Math.random() * (await Users.count())), }); - const Tour4 = await Tours.findOne({ + const Cart1 = await Cart.findOne({ order: [['id', 'ASC']], - offset: 4, + offset: 1, }); - if (Tour4?.setAgency) { - await Tour4.setAgency(relatedAgency4); + if (Cart1?.setUser) { + await Cart1.setUser(relatedUser1); + } + + const relatedUser2 = await Users.findOne({ + offset: Math.floor(Math.random() * (await Users.count())), + }); + const Cart2 = await Cart.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (Cart2?.setUser) { + await Cart2.setUser(relatedUser2); + } + + const relatedUser3 = await Users.findOne({ + offset: Math.floor(Math.random() * (await Users.count())), + }); + const Cart3 = await Cart.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (Cart3?.setUser) { + await Cart3.setUser(relatedUser3); } } @@ -695,6 +681,8 @@ module.exports = { await Agencies.bulkCreate(AgenciesData); + await Cart.bulkCreate(CartData); + await Promise.all([ // Similar logic for "relation_many" @@ -713,6 +701,10 @@ module.exports = { await associateTourWithAgency(), await associateTourWithAgency(), + + await associateCartWithAgency(), + + await associateCartWithUser(), ]); }, @@ -724,5 +716,7 @@ module.exports = { await queryInterface.bulkDelete('tours', null, {}); await queryInterface.bulkDelete('agencies', null, {}); + + await queryInterface.bulkDelete('cart', null, {}); }, }; diff --git a/backend/src/db/seeders/20250524050849.js b/backend/src/db/seeders/20250524050849.js new file mode 100644 index 0000000..b77443c --- /dev/null +++ b/backend/src/db/seeders/20250524050849.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 = ['cart']; + + 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 30c7f60..31fdb65 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -35,6 +35,8 @@ const permissionsRoutes = require('./routes/permissions'); const agenciesRoutes = require('./routes/agencies'); +const cartRoutes = require('./routes/cart'); + const getBaseUrl = (url) => { if (!url) return ''; return url.endsWith('/api') ? url.slice(0, -4) : url; @@ -142,6 +144,12 @@ app.use( agenciesRoutes, ); +app.use( + '/api/cart', + passport.authenticate('jwt', { session: false }), + cartRoutes, +); + app.use( '/api/openai', passport.authenticate('jwt', { session: false }), diff --git a/backend/src/routes/cart.js b/backend/src/routes/cart.js new file mode 100644 index 0000000..e4cd25a --- /dev/null +++ b/backend/src/routes/cart.js @@ -0,0 +1,443 @@ +const express = require('express'); + +const CartService = require('../services/cart'); +const CartDBApi = require('../db/api/cart'); +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('cart')); + +/** + * @swagger + * components: + * schemas: + * Cart: + * type: object + * properties: + + */ + +/** + * @swagger + * tags: + * name: Cart + * description: The Cart managing API + */ + +/** + * @swagger + * /api/cart: + * post: + * security: + * - bearerAuth: [] + * tags: [Cart] + * 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/Cart" + * responses: + * 200: + * description: The item was successfully added + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Cart" + * 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 CartService.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: [Cart] + * 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/Cart" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Cart" + * 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 CartService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/cart/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Cart] + * 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/Cart" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Cart" + * 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 CartService.update(req.body.data, req.body.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/cart/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Cart] + * 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/Cart" + * 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 CartService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/cart/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Cart] + * 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/Cart" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post( + '/deleteByIds', + wrapAsync(async (req, res) => { + await CartService.deleteByIds(req.body.data, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/cart: + * get: + * security: + * - bearerAuth: [] + * tags: [Cart] + * summary: Get all cart + * description: Get all cart + * responses: + * 200: + * description: Cart list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Cart" + * 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 CartDBApi.findAll(req.query, globalAccess, { + currentUser, + }); + if (filetype && filetype === 'csv') { + const fields = ['id']; + const opts = { fields }; + try { + const csv = parse(payload.rows, opts); + res.status(200).attachment(csv); + res.send(csv); + } catch (err) { + console.error(err); + } + } else { + res.status(200).send(payload); + } + }), +); + +/** + * @swagger + * /api/cart/count: + * get: + * security: + * - bearerAuth: [] + * tags: [Cart] + * summary: Count all cart + * description: Count all cart + * responses: + * 200: + * description: Cart count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Cart" + * 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 CartDBApi.findAll(req.query, globalAccess, { + countOnly: true, + currentUser, + }); + + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/cart/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [Cart] + * summary: Find all cart that match search criteria + * description: Find all cart that match search criteria + * responses: + * 200: + * description: Cart list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Cart" + * 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 CartDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + globalAccess, + organizationId, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/cart/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Cart] + * 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/Cart" + * 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 CartDBApi.findBy({ id: req.params.id }); + + res.status(200).send(payload); + }), +); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/services/cart.js b/backend/src/services/cart.js new file mode 100644 index 0000000..d7bc7a4 --- /dev/null +++ b/backend/src/services/cart.js @@ -0,0 +1,114 @@ +const db = require('../db/models'); +const CartDBApi = require('../db/api/cart'); +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 CartService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await CartDBApi.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 CartDBApi.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 cart = await CartDBApi.findBy({ id }, { transaction }); + + if (!cart) { + throw new ValidationError('cartNotFound'); + } + + const updatedCart = await CartDBApi.update(id, data, { + currentUser, + transaction, + }); + + await transaction.commit(); + return updatedCart; + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await CartDBApi.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 CartDBApi.remove(id, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; diff --git a/frontend/src/components/Cart/CardCart.tsx b/frontend/src/components/Cart/CardCart.tsx new file mode 100644 index 0000000..be8e2fd --- /dev/null +++ b/frontend/src/components/Cart/CardCart.tsx @@ -0,0 +1,107 @@ +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 = { + cart: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const CardCart = ({ + cart, + 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_CART'); + + return ( +
+ {loading && } +
    + {!loading && + cart.map((item, index) => ( +
  • +
    + + {item.id} + + +
    + +
    +
    +
    +
    +
    User
    +
    +
    + {dataFormatter.usersOneListFormatter(item.user)} +
    +
    +
    +
    +
  • + ))} + {!loading && cart.length === 0 && ( +
    +

    No data to display

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

User

+

+ {dataFormatter.usersOneListFormatter(item.user)} +

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

No data to display

+
+ )} +
+
+ +
+ + ); +}; + +export default ListCart; diff --git a/frontend/src/components/Cart/TableCart.tsx b/frontend/src/components/Cart/TableCart.tsx new file mode 100644 index 0000000..e75f246 --- /dev/null +++ b/frontend/src/components/Cart/TableCart.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/cart/cartSlice'; +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 './configureCartCols'; +import _ from 'lodash'; +import dataFormatter from '../../helpers/dataFormatter'; +import { dataGridStyles } from '../../styles'; + +const perPage = 10; + +const TableSampleCart = ({ + 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 { + cart, + loading, + count, + notify: cartNotify, + refetch, + } = useAppSelector((state) => state.cart); + 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 (cartNotify.showNotification) { + notify(cartNotify.typeNotification, cartNotify.textNotification); + } + }, [cartNotify.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, `cart`, 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={cart ?? []} + 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 TableSampleCart; diff --git a/frontend/src/components/Cart/configureCartCols.tsx b/frontend/src/components/Cart/configureCartCols.tsx new file mode 100644 index 0000000..7547541 --- /dev/null +++ b/frontend/src/components/Cart/configureCartCols.tsx @@ -0,0 +1,82 @@ +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_CART'); + + return [ + { + field: 'user', + headerName: 'User', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + sortable: false, + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('users'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + }, + + { + field: '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 b1ac8c2..8a84dff 100644 --- a/frontend/src/components/WebPageComponents/Footer.tsx +++ b/frontend/src/components/WebPageComponents/Footer.tsx @@ -19,7 +19,7 @@ export default function WebSiteFooter({ projectName }: WebSiteFooterProps) { const style = FooterStyle.WITH_PAGES; - const design = FooterDesigns.DESIGN_DIVERSITY; + const design = FooterDesigns.DEFAULT_DESIGN; return (
{ + <> +

Cart agencies

+ +
+ + + + + + {agencies.cart_agencies && + Array.isArray(agencies.cart_agencies) && + agencies.cart_agencies.map((item: any) => ( + + router.push(`/cart/cart-view/?id=${item.id}`) + } + > + ))} + +
+
+ {!agencies?.cart_agencies?.length && ( +
No data
+ )} +
+ + { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + agencies: null, + + user: null, + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { cart } = useAppSelector((state) => state.cart); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { cartId } = router.query; + + useEffect(() => { + dispatch(fetch({ id: cartId })); + }, [cartId]); + + useEffect(() => { + if (typeof cart === 'object') { + setInitialValues(cart); + } + }, [cart]); + + useEffect(() => { + if (typeof cart === 'object') { + const newInitialVal = { ...initVals }; + + Object.keys(initVals).forEach((el) => (newInitialVal[el] = cart[el])); + + setInitialValues(newInitialVal); + } + }, [cart]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: cartId, data })); + await router.push('/cart/cart-list'); + }; + + return ( + <> + + {getPageTitle('Edit cart')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + + + + + + + + + router.push('/cart/cart-list')} + /> + + +
+
+
+ + ); +}; + +EditCart.getLayout = function getLayout(page: ReactElement) { + return ( + {page} + ); +}; + +export default EditCart; diff --git a/frontend/src/pages/cart/cart-edit.tsx b/frontend/src/pages/cart/cart-edit.tsx new file mode 100644 index 0000000..84d2e6e --- /dev/null +++ b/frontend/src/pages/cart/cart-edit.tsx @@ -0,0 +1,144 @@ +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/cart/cartSlice'; +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 EditCartPage = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + agencies: null, + + user: null, + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { cart } = useAppSelector((state) => state.cart); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { id } = router.query; + + useEffect(() => { + dispatch(fetch({ id: id })); + }, [id]); + + useEffect(() => { + if (typeof cart === 'object') { + setInitialValues(cart); + } + }, [cart]); + + useEffect(() => { + if (typeof cart === 'object') { + const newInitialVal = { ...initVals }; + Object.keys(initVals).forEach((el) => (newInitialVal[el] = cart[el])); + setInitialValues(newInitialVal); + } + }, [cart]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: id, data })); + await router.push('/cart/cart-list'); + }; + + return ( + <> + + {getPageTitle('Edit cart')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + + + + + + + + + router.push('/cart/cart-list')} + /> + + +
+
+
+ + ); +}; + +EditCartPage.getLayout = function getLayout(page: ReactElement) { + return ( + {page} + ); +}; + +export default EditCartPage; diff --git a/frontend/src/pages/cart/cart-list.tsx b/frontend/src/pages/cart/cart-list.tsx new file mode 100644 index 0000000..6a8e9c9 --- /dev/null +++ b/frontend/src/pages/cart/cart-list.tsx @@ -0,0 +1,160 @@ +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 TableCart from '../../components/Cart/TableCart'; +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/cart/cartSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const CartTablesPage = () => { + 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: 'User', title: 'user' }]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_CART'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getCartCSV = async () => { + const response = await axios({ + url: '/cart?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 = 'cartCSV.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('Cart')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
+
+
+
+ + + + +
+ + + + + ); +}; + +CartTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + {page} + ); +}; + +export default CartTablesPage; diff --git a/frontend/src/pages/cart/cart-new.tsx b/frontend/src/pages/cart/cart-new.tsx new file mode 100644 index 0000000..1904619 --- /dev/null +++ b/frontend/src/pages/cart/cart-new.tsx @@ -0,0 +1,114 @@ +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/cart/cartSlice'; +import { useAppDispatch } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import moment from 'moment'; + +const initialValues = { + agencies: '', + + user: '', +}; + +const CartNew = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + + const handleSubmit = async (data) => { + await dispatch(create(data)); + await router.push('/cart/cart-list'); + }; + return ( + <> + + {getPageTitle('New Item')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + + + + + + + + + router.push('/cart/cart-list')} + /> + + +
+
+
+ + ); +}; + +CartNew.getLayout = function getLayout(page: ReactElement) { + return ( + {page} + ); +}; + +export default CartNew; diff --git a/frontend/src/pages/cart/cart-table.tsx b/frontend/src/pages/cart/cart-table.tsx new file mode 100644 index 0000000..bcd094f --- /dev/null +++ b/frontend/src/pages/cart/cart-table.tsx @@ -0,0 +1,159 @@ +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 TableCart from '../../components/Cart/TableCart'; +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/cart/cartSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const CartTablesPage = () => { + 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: 'User', title: 'user' }]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_CART'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getCartCSV = async () => { + const response = await axios({ + url: '/cart?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 = 'cartCSV.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('Cart')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
+
+
+
+ + + +
+ + + + + ); +}; + +CartTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + {page} + ); +}; + +export default CartTablesPage; diff --git a/frontend/src/pages/cart/cart-view.tsx b/frontend/src/pages/cart/cart-view.tsx new file mode 100644 index 0000000..35b9369 --- /dev/null +++ b/frontend/src/pages/cart/cart-view.tsx @@ -0,0 +1,92 @@ +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/cart/cartSlice'; +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 CartView = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const { cart } = useAppSelector((state) => state.cart); + + 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 cart')} + + + + + + +
+

agencies

+ +

{cart?.agencies?.name ?? 'No data'}

+
+ +
+

User

+ +

{cart?.user?.firstName ?? 'No data'}

+
+ + + + router.push('/cart/cart-list')} + /> +
+
+ + ); +}; + +CartView.getLayout = function getLayout(page: ReactElement) { + return ( + {page} + ); +}; + +export default CartView; diff --git a/frontend/src/pages/dashboard.tsx b/frontend/src/pages/dashboard.tsx index f2650f7..a20e233 100644 --- a/frontend/src/pages/dashboard.tsx +++ b/frontend/src/pages/dashboard.tsx @@ -35,6 +35,7 @@ const Dashboard = () => { const [roles, setRoles] = React.useState(loadingMessage); const [permissions, setPermissions] = React.useState(loadingMessage); const [agencies, setAgencies] = React.useState(loadingMessage); + const [cart, setCart] = React.useState(loadingMessage); const [widgetsRole, setWidgetsRole] = React.useState({ role: { value: '', label: '' }, @@ -55,6 +56,7 @@ const Dashboard = () => { 'roles', 'permissions', 'agencies', + 'cart', ]; const fns = [ setUsers, @@ -64,6 +66,7 @@ const Dashboard = () => { setRoles, setPermissions, setAgencies, + setCart, ]; const requests = entities.map((entity, index) => { @@ -415,6 +418,38 @@ const Dashboard = () => {
)} + + {hasPermission(currentUser, 'READ_CART') && ( + +
+
+
+
+ Cart +
+
+ {cart} +
+
+
+ +
+
+
+ + )} diff --git a/frontend/src/pages/index.tsx b/frontend/src/pages/index.tsx index 6b7cd74..dfd3fb3 100644 --- a/frontend/src/pages/index.tsx +++ b/frontend/src/pages/index.tsx @@ -171,7 +171,7 @@ export default function WebSite() { { + <> +

Cart User

+ +
+ + + + + + {users.cart_user && + Array.isArray(users.cart_user) && + users.cart_user.map((item: any) => ( + + router.push(`/cart/cart-view/?id=${item.id}`) + } + > + ))} + +
+
+ {!users?.cart_user?.length && ( +
No data
+ )} +
+ + { + const { id, query } = data; + const result = await axios.get(`cart${query || (id ? `/${id}` : '')}`); + return id + ? result.data + : { rows: result.data.rows, count: result.data.count }; +}); + +export const deleteItemsByIds = createAsyncThunk( + 'cart/deleteByIds', + async (data: any, { rejectWithValue }) => { + try { + await axios.post('cart/deleteByIds', { data }); + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const deleteItem = createAsyncThunk( + 'cart/deleteCart', + async (id: string, { rejectWithValue }) => { + try { + await axios.delete(`cart/${id}`); + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const create = createAsyncThunk( + 'cart/createCart', + async (data: any, { rejectWithValue }) => { + try { + const result = await axios.post('cart', { data }); + return result.data; + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const uploadCsv = createAsyncThunk( + 'cart/uploadCsv', + async (file: File, { rejectWithValue }) => { + try { + const data = new FormData(); + data.append('file', file); + data.append('filename', file.name); + + const result = await axios.post('cart/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( + 'cart/updateCart', + async (payload: any, { rejectWithValue }) => { + try { + const result = await axios.put(`cart/${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 cartSlice = createSlice({ + name: 'cart', + 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.cart = action.payload.rows; + state.count = action.payload.count; + } else { + state.cart = 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, 'Cart 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, `${'Cart'.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, `${'Cart'.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, `${'Cart'.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, 'Cart 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 } = cartSlice.actions; + +export default cartSlice.reducer; diff --git a/frontend/src/stores/store.ts b/frontend/src/stores/store.ts index d09f666..9926cb5 100644 --- a/frontend/src/stores/store.ts +++ b/frontend/src/stores/store.ts @@ -11,6 +11,7 @@ import toursSlice from './tours/toursSlice'; import rolesSlice from './roles/rolesSlice'; import permissionsSlice from './permissions/permissionsSlice'; import agenciesSlice from './agencies/agenciesSlice'; +import cartSlice from './cart/cartSlice'; export const store = configureStore({ reducer: { @@ -26,6 +27,7 @@ export const store = configureStore({ roles: rolesSlice, permissions: permissionsSlice, agencies: agenciesSlice, + cart: cartSlice, }, });