From 7f865aad3b741b8d33a263ac9fb833f6e0624525 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Sat, 24 May 2025 09:29:17 +0000 Subject: [PATCH] Auto commit: 2025-05-24T09:29:16.982Z --- app-shell/src/_schema.json | 2 +- backend/src/db/api/loyaltytier.js | 253 +++++++++ backend/src/db/migrations/1748078864723.js | 72 +++ backend/src/db/migrations/1748078906634.js | 47 ++ backend/src/db/models/loyaltytier.js | 49 ++ .../db/seeders/20200430130760-user-roles.js | 26 + .../db/seeders/20231127130745-sample-data.js | 124 ++++- backend/src/db/seeders/20250524092744.js | 87 ++++ backend/src/index.js | 8 + backend/src/routes/loyaltytier.js | 442 ++++++++++++++++ backend/src/services/loyaltytier.js | 114 ++++ backend/src/services/search.js | 2 + frontend/json/runtimeError.json | 1 + .../Loyaltytier/CardLoyaltytier.tsx | 105 ++++ .../Loyaltytier/ListLoyaltytier.tsx | 89 ++++ .../Loyaltytier/TableLoyaltytier.tsx | 487 ++++++++++++++++++ .../Loyaltytier/configureLoyaltytierCols.tsx | 74 +++ .../components/WebPageComponents/Footer.tsx | 2 +- .../components/WebPageComponents/Header.tsx | 2 +- frontend/src/menuAside.ts | 8 + frontend/src/pages/dashboard.tsx | 35 ++ frontend/src/pages/index.tsx | 2 +- .../src/pages/loyaltytier/[loyaltytierId].tsx | 126 +++++ .../pages/loyaltytier/loyaltytier-edit.tsx | 124 +++++ .../pages/loyaltytier/loyaltytier-list.tsx | 165 ++++++ .../src/pages/loyaltytier/loyaltytier-new.tsx | 98 ++++ .../pages/loyaltytier/loyaltytier-table.tsx | 164 ++++++ .../pages/loyaltytier/loyaltytier-view.tsx | 83 +++ .../stores/loyaltytier/loyaltytierSlice.ts | 241 +++++++++ frontend/src/stores/store.ts | 2 + 30 files changed, 3026 insertions(+), 8 deletions(-) create mode 100644 backend/src/db/api/loyaltytier.js create mode 100644 backend/src/db/migrations/1748078864723.js create mode 100644 backend/src/db/migrations/1748078906634.js create mode 100644 backend/src/db/models/loyaltytier.js create mode 100644 backend/src/db/seeders/20250524092744.js create mode 100644 backend/src/routes/loyaltytier.js create mode 100644 backend/src/services/loyaltytier.js create mode 100644 frontend/json/runtimeError.json create mode 100644 frontend/src/components/Loyaltytier/CardLoyaltytier.tsx create mode 100644 frontend/src/components/Loyaltytier/ListLoyaltytier.tsx create mode 100644 frontend/src/components/Loyaltytier/TableLoyaltytier.tsx create mode 100644 frontend/src/components/Loyaltytier/configureLoyaltytierCols.tsx create mode 100644 frontend/src/pages/loyaltytier/[loyaltytierId].tsx create mode 100644 frontend/src/pages/loyaltytier/loyaltytier-edit.tsx create mode 100644 frontend/src/pages/loyaltytier/loyaltytier-list.tsx create mode 100644 frontend/src/pages/loyaltytier/loyaltytier-new.tsx create mode 100644 frontend/src/pages/loyaltytier/loyaltytier-table.tsx create mode 100644 frontend/src/pages/loyaltytier/loyaltytier-view.tsx create mode 100644 frontend/src/stores/loyaltytier/loyaltytierSlice.ts diff --git a/app-shell/src/_schema.json b/app-shell/src/_schema.json index 88f6126..367e410 100644 --- a/app-shell/src/_schema.json +++ b/app-shell/src/_schema.json @@ -1,4 +1,4 @@ { "Initial version": "{\"iv\":\"AHKOiwdeOuoeiNVz\",\"encryptedData\":\"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\"}", - "": "{\"iv\":\"6rpS/qNQl3y2mmUT\",\"encryptedData\":\"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\"}" + "": "{\"iv\":\"3O3M4d6w4OMgVGnH\",\"encryptedData\":\"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\"}" } \ No newline at end of file diff --git a/backend/src/db/api/loyaltytier.js b/backend/src/db/api/loyaltytier.js new file mode 100644 index 0000000..c9067ae --- /dev/null +++ b/backend/src/db/api/loyaltytier.js @@ -0,0 +1,253 @@ +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 LoyaltytierDBApi { + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const loyaltytier = await db.loyaltytier.create( + { + id: data.id || undefined, + + name: data.name || null, + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + return loyaltytier; + } + + 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 loyaltytierData = data.map((item, index) => ({ + id: item.id || undefined, + + name: item.name || null, + importHash: item.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + // Bulk create items + const loyaltytier = await db.loyaltytier.bulkCreate(loyaltytierData, { + transaction, + }); + + // For each item created, replace relation files + + return loyaltytier; + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const loyaltytier = await db.loyaltytier.findByPk(id, {}, { transaction }); + + const updatePayload = {}; + + if (data.name !== undefined) updatePayload.name = data.name; + + updatePayload.updatedById = currentUser.id; + + await loyaltytier.update(updatePayload, { transaction }); + + return loyaltytier; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const loyaltytier = await db.loyaltytier.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of loyaltytier) { + await record.update({ deletedBy: currentUser.id }, { transaction }); + } + for (const record of loyaltytier) { + await record.destroy({ transaction }); + } + }); + + return loyaltytier; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const loyaltytier = await db.loyaltytier.findByPk(id, options); + + await loyaltytier.update( + { + deletedBy: currentUser.id, + }, + { + transaction, + }, + ); + + await loyaltytier.destroy({ + transaction, + }); + + return loyaltytier; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const loyaltytier = await db.loyaltytier.findOne( + { where }, + { transaction }, + ); + + if (!loyaltytier) { + return loyaltytier; + } + + const output = loyaltytier.get({ plain: true }); + + return output; + } + + static async findAll(filter, options) { + const limit = filter.limit || 0; + let offset = 0; + let where = {}; + const currentPage = +filter.page; + + offset = currentPage * limit; + + const orderBy = null; + + const transaction = (options && options.transaction) || undefined; + + let include = []; + + if (filter) { + if (filter.id) { + where = { + ...where, + ['id']: Utils.uuid(filter.id), + }; + } + + if (filter.name) { + where = { + ...where, + [Op.and]: Utils.ilike('loyaltytier', 'name', filter.name), + }; + } + + if (filter.active !== undefined) { + where = { + ...where, + active: filter.active === true || filter.active === 'true', + }; + } + + if (filter.createdAtRange) { + const [start, end] = filter.createdAtRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.lte]: end, + }, + }; + } + } + } + + const queryOptions = { + where, + include, + distinct: true, + order: + filter.field && filter.sort + ? [[filter.field, filter.sort]] + : [['createdAt', 'desc']], + transaction: options?.transaction, + logging: console.log, + }; + + if (!options?.countOnly) { + queryOptions.limit = limit ? Number(limit) : undefined; + queryOptions.offset = offset ? Number(offset) : undefined; + } + + try { + const { rows, count } = await db.loyaltytier.findAndCountAll( + queryOptions, + ); + + return { + rows: options?.countOnly ? [] : rows, + count: count, + }; + } catch (error) { + console.error('Error executing query:', error); + throw error; + } + } + + static async findAllAutocomplete(query, limit, offset) { + let where = {}; + + if (query) { + where = { + [Op.or]: [ + { ['id']: Utils.uuid(query) }, + Utils.ilike('loyaltytier', 'name', query), + ], + }; + } + + const records = await db.loyaltytier.findAll({ + attributes: ['id', 'name'], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + orderBy: [['name', 'ASC']], + }); + + return records.map((record) => ({ + id: record.id, + label: record.name, + })); + } +}; diff --git a/backend/src/db/migrations/1748078864723.js b/backend/src/db/migrations/1748078864723.js new file mode 100644 index 0000000..8d15a81 --- /dev/null +++ b/backend/src/db/migrations/1748078864723.js @@ -0,0 +1,72 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.createTable( + 'loyaltytier', + { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.DataTypes.UUIDV4, + primaryKey: true, + }, + createdById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + updatedById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + createdAt: { type: Sequelize.DataTypes.DATE }, + updatedAt: { type: Sequelize.DataTypes.DATE }, + deletedAt: { type: Sequelize.DataTypes.DATE }, + importHash: { + type: Sequelize.DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { transaction }, + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async down(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.dropTable('loyaltytier', { transaction }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1748078906634.js b/backend/src/db/migrations/1748078906634.js new file mode 100644 index 0000000..513f6a5 --- /dev/null +++ b/backend/src/db/migrations/1748078906634.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( + 'loyaltytier', + 'name', + { + 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('loyaltytier', 'name', { transaction }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/models/loyaltytier.js b/backend/src/db/models/loyaltytier.js new file mode 100644 index 0000000..8773280 --- /dev/null +++ b/backend/src/db/models/loyaltytier.js @@ -0,0 +1,49 @@ +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 loyaltytier = sequelize.define( + 'loyaltytier', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + + name: { + type: DataTypes.TEXT, + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + loyaltytier.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.loyaltytier.belongsTo(db.users, { + as: 'createdBy', + }); + + db.loyaltytier.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return loyaltytier; +}; diff --git a/backend/src/db/seeders/20200430130760-user-roles.js b/backend/src/db/seeders/20200430130760-user-roles.js index 48c0117..0d48f1e 100644 --- a/backend/src/db/seeders/20200430130760-user-roles.js +++ b/backend/src/db/seeders/20200430130760-user-roles.js @@ -102,6 +102,7 @@ module.exports = { 'vouchers', 'roles', 'permissions', + 'loyaltytier', , ]; await queryInterface.bulkInsert( @@ -686,6 +687,31 @@ primary key ("roles_permissionsId", "permissionId") permissionId: getId('DELETE_PERMISSIONS'), }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('CREATE_LOYALTYTIER'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('READ_LOYALTYTIER'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('UPDATE_LOYALTYTIER'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('DELETE_LOYALTYTIER'), + }, + { createdAt, updatedAt, diff --git a/backend/src/db/seeders/20231127130745-sample-data.js b/backend/src/db/seeders/20231127130745-sample-data.js index 5f91fc3..df80374 100644 --- a/backend/src/db/seeders/20231127130745-sample-data.js +++ b/backend/src/db/seeders/20231127130745-sample-data.js @@ -11,6 +11,8 @@ const Payments = db.payments; const Vouchers = db.vouchers; +const Loyaltytier = db.loyaltytier; + const AgentsData = [ { name: 'Agent A', @@ -35,6 +37,14 @@ const AgentsData = [ phone_number: '3453453456', }, + + { + name: 'Agent D', + + email: 'agent.d@example.com', + + phone_number: '4564564567', + }, ]; const BookingsData = [ @@ -53,7 +63,7 @@ const BookingsData = [ { // type code here for "relation_one" field - service_type: 'Flight', + service_type: 'Package', booking_date: new Date('2023-11-02T12:00:00Z'), @@ -65,7 +75,7 @@ const BookingsData = [ { // type code here for "relation_one" field - service_type: 'Tour', + service_type: 'Hotel', booking_date: new Date('2023-11-03T14:00:00Z'), @@ -73,6 +83,18 @@ const BookingsData = [ // type code here for "relation_one" field }, + + { + // type code here for "relation_one" field + + service_type: 'Hotel', + + booking_date: new Date('2023-11-04T16:00:00Z'), + + total_amount: 200, + + // type code here for "relation_one" field + }, ]; const CustomersData = [ @@ -105,6 +127,16 @@ const CustomersData = [ phone_number: '1122334455', }, + + { + first_name: 'Bob', + + last_name: 'Brown', + + email: 'bob.brown@example.com', + + phone_number: '5566778899', + }, ]; const PaymentsData = [ @@ -125,7 +157,7 @@ const PaymentsData = [ amount: 300, - status: 'Failed', + status: 'Pending', }, { @@ -135,7 +167,17 @@ const PaymentsData = [ amount: 800, - status: 'Completed', + status: 'Pending', + }, + + { + // type code here for "relation_one" field + + payment_date: new Date('2023-11-04T17:00:00Z'), + + amount: 200, + + status: 'Pending', }, ]; @@ -163,6 +205,32 @@ const VouchersData = [ issue_date: new Date('2023-11-03T16:00:00Z'), }, + + { + // type code here for "relation_one" field + + voucher_code: 'VCH45678', + + issue_date: new Date('2023-11-04T18:00:00Z'), + }, +]; + +const LoyaltytierData = [ + { + name: 'B. F. Skinner', + }, + + { + name: 'Carl Gauss (Karl Friedrich Gauss)', + }, + + { + name: 'August Kekule', + }, + + { + name: 'James Watson', + }, ]; // Similar logic for "relation_many" @@ -200,6 +268,17 @@ async function associateBookingWithCustomer() { if (Booking2?.setCustomer) { await Booking2.setCustomer(relatedCustomer2); } + + const relatedCustomer3 = await Customers.findOne({ + offset: Math.floor(Math.random() * (await Customers.count())), + }); + const Booking3 = await Bookings.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (Booking3?.setCustomer) { + await Booking3.setCustomer(relatedCustomer3); + } } async function associateBookingWithAgent() { @@ -235,6 +314,17 @@ async function associateBookingWithAgent() { if (Booking2?.setAgent) { await Booking2.setAgent(relatedAgent2); } + + const relatedAgent3 = await Agents.findOne({ + offset: Math.floor(Math.random() * (await Agents.count())), + }); + const Booking3 = await Bookings.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (Booking3?.setAgent) { + await Booking3.setAgent(relatedAgent3); + } } async function associatePaymentWithBooking() { @@ -270,6 +360,17 @@ async function associatePaymentWithBooking() { if (Payment2?.setBooking) { await Payment2.setBooking(relatedBooking2); } + + const relatedBooking3 = await Bookings.findOne({ + offset: Math.floor(Math.random() * (await Bookings.count())), + }); + const Payment3 = await Payments.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (Payment3?.setBooking) { + await Payment3.setBooking(relatedBooking3); + } } async function associateVoucherWithBooking() { @@ -305,6 +406,17 @@ async function associateVoucherWithBooking() { if (Voucher2?.setBooking) { await Voucher2.setBooking(relatedBooking2); } + + const relatedBooking3 = await Bookings.findOne({ + offset: Math.floor(Math.random() * (await Bookings.count())), + }); + const Voucher3 = await Vouchers.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (Voucher3?.setBooking) { + await Voucher3.setBooking(relatedBooking3); + } } module.exports = { @@ -319,6 +431,8 @@ module.exports = { await Vouchers.bulkCreate(VouchersData); + await Loyaltytier.bulkCreate(LoyaltytierData); + await Promise.all([ // Similar logic for "relation_many" @@ -342,5 +456,7 @@ module.exports = { await queryInterface.bulkDelete('payments', null, {}); await queryInterface.bulkDelete('vouchers', null, {}); + + await queryInterface.bulkDelete('loyaltytier', null, {}); }, }; diff --git a/backend/src/db/seeders/20250524092744.js b/backend/src/db/seeders/20250524092744.js new file mode 100644 index 0000000..7d7ab98 --- /dev/null +++ b/backend/src/db/seeders/20250524092744.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 = ['loyaltytier']; + + const createdPermissions = entities.flatMap(createPermissions); + + // Add permissions to database + await queryInterface.bulkInsert('permissions', createdPermissions); + // Get permissions ids + const permissionsIds = createdPermissions.map((p) => p.id); + // Get admin role + const adminRole = await db.roles.findOne({ + where: { name: config.roles.admin }, + }); + + if (adminRole) { + // Add permissions to admin role if it exists + await adminRole.addPermissions(permissionsIds); + } + }, + down: async (queryInterface, Sequelize) => { + await queryInterface.bulkDelete( + 'permissions', + entities.flatMap(createPermissions), + ); + }, +}; diff --git a/backend/src/index.js b/backend/src/index.js index 237eb50..53f1aa1 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -35,6 +35,8 @@ const rolesRoutes = require('./routes/roles'); const permissionsRoutes = require('./routes/permissions'); +const loyaltytierRoutes = require('./routes/loyaltytier'); + const getBaseUrl = (url) => { if (!url) return ''; return url.endsWith('/api') ? url.slice(0, -4) : url; @@ -148,6 +150,12 @@ app.use( permissionsRoutes, ); +app.use( + '/api/loyaltytier', + passport.authenticate('jwt', { session: false }), + loyaltytierRoutes, +); + app.use( '/api/openai', passport.authenticate('jwt', { session: false }), diff --git a/backend/src/routes/loyaltytier.js b/backend/src/routes/loyaltytier.js new file mode 100644 index 0000000..b44680f --- /dev/null +++ b/backend/src/routes/loyaltytier.js @@ -0,0 +1,442 @@ +const express = require('express'); + +const LoyaltytierService = require('../services/loyaltytier'); +const LoyaltytierDBApi = require('../db/api/loyaltytier'); +const wrapAsync = require('../helpers').wrapAsync; + +const router = express.Router(); + +const { parse } = require('json2csv'); + +const { checkCrudPermissions } = require('../middlewares/check-permissions'); + +router.use(checkCrudPermissions('loyaltytier')); + +/** + * @swagger + * components: + * schemas: + * Loyaltytier: + * type: object + * properties: + + * name: + * type: string + * default: name + + */ + +/** + * @swagger + * tags: + * name: Loyaltytier + * description: The Loyaltytier managing API + */ + +/** + * @swagger + * /api/loyaltytier: + * post: + * security: + * - bearerAuth: [] + * tags: [Loyaltytier] + * 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/Loyaltytier" + * responses: + * 200: + * description: The item was successfully added + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Loyaltytier" + * 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 LoyaltytierService.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: [Loyaltytier] + * 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/Loyaltytier" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Loyaltytier" + * 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 LoyaltytierService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/loyaltytier/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Loyaltytier] + * 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/Loyaltytier" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Loyaltytier" + * 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 LoyaltytierService.update( + req.body.data, + req.body.id, + req.currentUser, + ); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/loyaltytier/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Loyaltytier] + * 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/Loyaltytier" + * 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 LoyaltytierService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/loyaltytier/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Loyaltytier] + * 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/Loyaltytier" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post( + '/deleteByIds', + wrapAsync(async (req, res) => { + await LoyaltytierService.deleteByIds(req.body.data, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/loyaltytier: + * get: + * security: + * - bearerAuth: [] + * tags: [Loyaltytier] + * summary: Get all loyaltytier + * description: Get all loyaltytier + * responses: + * 200: + * description: Loyaltytier list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Loyaltytier" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get( + '/', + wrapAsync(async (req, res) => { + const filetype = req.query.filetype; + + const currentUser = req.currentUser; + const payload = await LoyaltytierDBApi.findAll(req.query, { currentUser }); + if (filetype && filetype === 'csv') { + const fields = ['id', 'name']; + const opts = { fields }; + try { + const csv = parse(payload.rows, opts); + res.status(200).attachment(csv); + res.send(csv); + } catch (err) { + console.error(err); + } + } else { + res.status(200).send(payload); + } + }), +); + +/** + * @swagger + * /api/loyaltytier/count: + * get: + * security: + * - bearerAuth: [] + * tags: [Loyaltytier] + * summary: Count all loyaltytier + * description: Count all loyaltytier + * responses: + * 200: + * description: Loyaltytier count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Loyaltytier" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get( + '/count', + wrapAsync(async (req, res) => { + const currentUser = req.currentUser; + const payload = await LoyaltytierDBApi.findAll(req.query, null, { + countOnly: true, + currentUser, + }); + + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/loyaltytier/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [Loyaltytier] + * summary: Find all loyaltytier that match search criteria + * description: Find all loyaltytier that match search criteria + * responses: + * 200: + * description: Loyaltytier list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Loyaltytier" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/autocomplete', async (req, res) => { + const payload = await LoyaltytierDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/loyaltytier/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Loyaltytier] + * 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/Loyaltytier" + * 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 LoyaltytierDBApi.findBy({ id: req.params.id }); + + res.status(200).send(payload); + }), +); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/services/loyaltytier.js b/backend/src/services/loyaltytier.js new file mode 100644 index 0000000..413a30a --- /dev/null +++ b/backend/src/services/loyaltytier.js @@ -0,0 +1,114 @@ +const db = require('../db/models'); +const LoyaltytierDBApi = require('../db/api/loyaltytier'); +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 LoyaltytierService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await LoyaltytierDBApi.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 LoyaltytierDBApi.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 loyaltytier = await LoyaltytierDBApi.findBy({ id }, { transaction }); + + if (!loyaltytier) { + throw new ValidationError('loyaltytierNotFound'); + } + + const updatedLoyaltytier = await LoyaltytierDBApi.update(id, data, { + currentUser, + transaction, + }); + + await transaction.commit(); + return updatedLoyaltytier; + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await LoyaltytierDBApi.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 LoyaltytierDBApi.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 4e419ec..5493e43 100644 --- a/backend/src/services/search.js +++ b/backend/src/services/search.js @@ -48,6 +48,8 @@ module.exports = class SearchService { customers: ['first_name', 'last_name', 'email', 'phone_number'], vouchers: ['voucher_code'], + + loyaltytier: ['name'], }; const columnsInt = { bookings: ['total_amount'], diff --git a/frontend/json/runtimeError.json b/frontend/json/runtimeError.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/frontend/json/runtimeError.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/frontend/src/components/Loyaltytier/CardLoyaltytier.tsx b/frontend/src/components/Loyaltytier/CardLoyaltytier.tsx new file mode 100644 index 0000000..1a82b54 --- /dev/null +++ b/frontend/src/components/Loyaltytier/CardLoyaltytier.tsx @@ -0,0 +1,105 @@ +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 = { + loyaltytier: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const CardLoyaltytier = ({ + loyaltytier, + 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_LOYALTYTIER'); + + return ( +
+ {loading && } +
    + {!loading && + loyaltytier.map((item, index) => ( +
  • +
    + + {item.name} + + +
    + +
    +
    +
    +
    +
    Name
    +
    +
    {item.name}
    +
    +
    +
    +
  • + ))} + {!loading && loyaltytier.length === 0 && ( +
    +

    No data to display

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

Name

+

{item.name}

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

No data to display

+
+ )} +
+
+ +
+ + ); +}; + +export default ListLoyaltytier; diff --git a/frontend/src/components/Loyaltytier/TableLoyaltytier.tsx b/frontend/src/components/Loyaltytier/TableLoyaltytier.tsx new file mode 100644 index 0000000..95972d9 --- /dev/null +++ b/frontend/src/components/Loyaltytier/TableLoyaltytier.tsx @@ -0,0 +1,487 @@ +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/loyaltytier/loyaltytierSlice'; +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 './configureLoyaltytierCols'; +import _ from 'lodash'; +import dataFormatter from '../../helpers/dataFormatter'; +import { dataGridStyles } from '../../styles'; + +const perPage = 10; + +const TableSampleLoyaltytier = ({ + 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 { + loyaltytier, + loading, + count, + notify: loyaltytierNotify, + refetch, + } = useAppSelector((state) => state.loyaltytier); + 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 (loyaltytierNotify.showNotification) { + notify( + loyaltytierNotify.typeNotification, + loyaltytierNotify.textNotification, + ); + } + }, [loyaltytierNotify.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, `loyaltytier`, 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={loyaltytier ?? []} + 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 TableSampleLoyaltytier; diff --git a/frontend/src/components/Loyaltytier/configureLoyaltytierCols.tsx b/frontend/src/components/Loyaltytier/configureLoyaltytierCols.tsx new file mode 100644 index 0000000..50a6693 --- /dev/null +++ b/frontend/src/components/Loyaltytier/configureLoyaltytierCols.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_LOYALTYTIER'); + + return [ + { + field: 'name', + headerName: 'Name', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + + { + field: 'actions', + type: 'actions', + minWidth: 30, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + getActions: (params: GridRowParams) => { + return [ +
+ +
, + ]; + }, + }, + ]; +}; diff --git a/frontend/src/components/WebPageComponents/Footer.tsx b/frontend/src/components/WebPageComponents/Footer.tsx index bd9c5a1..8a84dff 100644 --- a/frontend/src/components/WebPageComponents/Footer.tsx +++ b/frontend/src/components/WebPageComponents/Footer.tsx @@ -17,7 +17,7 @@ export default function WebSiteFooter({ projectName }: WebSiteFooterProps) { const borders = useAppSelector((state) => state.style.borders); const websiteHeder = useAppSelector((state) => state.style.websiteHeder); - const style = FooterStyle.WITH_PROJECT_NAME; + const style = FooterStyle.WITH_PAGES; const design = FooterDesigns.DEFAULT_DESIGN; diff --git a/frontend/src/components/WebPageComponents/Header.tsx b/frontend/src/components/WebPageComponents/Header.tsx index 7feed61..ebfb323 100644 --- a/frontend/src/components/WebPageComponents/Header.tsx +++ b/frontend/src/components/WebPageComponents/Header.tsx @@ -19,7 +19,7 @@ export default function WebSiteHeader({ projectName }: WebSiteHeaderProps) { const style = HeaderStyle.PAGES_LEFT; - const design = HeaderDesigns.DEFAULT_DESIGN; + const design = HeaderDesigns.DESIGN_DIVERSITY; return (
{ const [vouchers, setVouchers] = React.useState(loadingMessage); const [roles, setRoles] = React.useState(loadingMessage); const [permissions, setPermissions] = React.useState(loadingMessage); + const [loyaltytier, setLoyaltytier] = React.useState(loadingMessage); const [widgetsRole, setWidgetsRole] = React.useState({ role: { value: '', label: '' }, @@ -55,6 +56,7 @@ const Dashboard = () => { 'vouchers', 'roles', 'permissions', + 'loyaltytier', ]; const fns = [ setUsers, @@ -65,6 +67,7 @@ const Dashboard = () => { setVouchers, setRoles, setPermissions, + setLoyaltytier, ]; const requests = entities.map((entity, index) => { @@ -456,6 +459,38 @@ const Dashboard = () => {
)} + + {hasPermission(currentUser, 'READ_LOYALTYTIER') && ( + +
+
+
+
+ Loyaltytier +
+
+ {loyaltytier} +
+
+
+ +
+
+
+ + )} diff --git a/frontend/src/pages/index.tsx b/frontend/src/pages/index.tsx index 4803e62..b0e28bd 100644 --- a/frontend/src/pages/index.tsx +++ b/frontend/src/pages/index.tsx @@ -139,7 +139,7 @@ export default function WebSite() { { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + name: '', + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { loyaltytier } = useAppSelector((state) => state.loyaltytier); + + const { loyaltytierId } = router.query; + + useEffect(() => { + dispatch(fetch({ id: loyaltytierId })); + }, [loyaltytierId]); + + useEffect(() => { + if (typeof loyaltytier === 'object') { + setInitialValues(loyaltytier); + } + }, [loyaltytier]); + + useEffect(() => { + if (typeof loyaltytier === 'object') { + const newInitialVal = { ...initVals }; + + Object.keys(initVals).forEach( + (el) => (newInitialVal[el] = loyaltytier[el]), + ); + + setInitialValues(newInitialVal); + } + }, [loyaltytier]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: loyaltytierId, data })); + await router.push('/loyaltytier/loyaltytier-list'); + }; + + return ( + <> + + {getPageTitle('Edit loyaltytier')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + + + + + router.push('/loyaltytier/loyaltytier-list')} + /> + + +
+
+
+ + ); +}; + +EditLoyaltytier.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditLoyaltytier; diff --git a/frontend/src/pages/loyaltytier/loyaltytier-edit.tsx b/frontend/src/pages/loyaltytier/loyaltytier-edit.tsx new file mode 100644 index 0000000..cfd1213 --- /dev/null +++ b/frontend/src/pages/loyaltytier/loyaltytier-edit.tsx @@ -0,0 +1,124 @@ +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/loyaltytier/loyaltytierSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; + +const EditLoyaltytierPage = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + name: '', + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { loyaltytier } = useAppSelector((state) => state.loyaltytier); + + const { id } = router.query; + + useEffect(() => { + dispatch(fetch({ id: id })); + }, [id]); + + useEffect(() => { + if (typeof loyaltytier === 'object') { + setInitialValues(loyaltytier); + } + }, [loyaltytier]); + + useEffect(() => { + if (typeof loyaltytier === 'object') { + const newInitialVal = { ...initVals }; + Object.keys(initVals).forEach( + (el) => (newInitialVal[el] = loyaltytier[el]), + ); + setInitialValues(newInitialVal); + } + }, [loyaltytier]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: id, data })); + await router.push('/loyaltytier/loyaltytier-list'); + }; + + return ( + <> + + {getPageTitle('Edit loyaltytier')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + + + + + router.push('/loyaltytier/loyaltytier-list')} + /> + + +
+
+
+ + ); +}; + +EditLoyaltytierPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditLoyaltytierPage; diff --git a/frontend/src/pages/loyaltytier/loyaltytier-list.tsx b/frontend/src/pages/loyaltytier/loyaltytier-list.tsx new file mode 100644 index 0000000..72689be --- /dev/null +++ b/frontend/src/pages/loyaltytier/loyaltytier-list.tsx @@ -0,0 +1,165 @@ +import { mdiChartTimelineVariant } from '@mdi/js'; +import Head from 'next/head'; +import { uniqueId } from 'lodash'; +import React, { ReactElement, useState } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; +import TableLoyaltytier from '../../components/Loyaltytier/TableLoyaltytier'; +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/loyaltytier/loyaltytierSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const LoyaltytierTablesPage = () => { + const [filterItems, setFilterItems] = useState([]); + const [csvFile, setCsvFile] = useState(null); + const [isModalActive, setIsModalActive] = useState(false); + const [showTableView, setShowTableView] = useState(false); + + const { currentUser } = useAppSelector((state) => state.auth); + + const dispatch = useAppDispatch(); + + const [filters] = useState([{ label: 'Name', title: 'name' }]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_LOYALTYTIER'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getLoyaltytierCSV = async () => { + const response = await axios({ + url: '/loyaltytier?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 = 'loyaltytierCSV.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('Loyaltytier')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
+
+
+
+ + + + +
+ + + + + ); +}; + +LoyaltytierTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default LoyaltytierTablesPage; diff --git a/frontend/src/pages/loyaltytier/loyaltytier-new.tsx b/frontend/src/pages/loyaltytier/loyaltytier-new.tsx new file mode 100644 index 0000000..91458f3 --- /dev/null +++ b/frontend/src/pages/loyaltytier/loyaltytier-new.tsx @@ -0,0 +1,98 @@ +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/loyaltytier/loyaltytierSlice'; +import { useAppDispatch } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import moment from 'moment'; + +const initialValues = { + name: '', +}; + +const LoyaltytierNew = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + + const handleSubmit = async (data) => { + await dispatch(create(data)); + await router.push('/loyaltytier/loyaltytier-list'); + }; + return ( + <> + + {getPageTitle('New Item')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + + + + + router.push('/loyaltytier/loyaltytier-list')} + /> + + +
+
+
+ + ); +}; + +LoyaltytierNew.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default LoyaltytierNew; diff --git a/frontend/src/pages/loyaltytier/loyaltytier-table.tsx b/frontend/src/pages/loyaltytier/loyaltytier-table.tsx new file mode 100644 index 0000000..dbfabab --- /dev/null +++ b/frontend/src/pages/loyaltytier/loyaltytier-table.tsx @@ -0,0 +1,164 @@ +import { mdiChartTimelineVariant } from '@mdi/js'; +import Head from 'next/head'; +import { uniqueId } from 'lodash'; +import React, { ReactElement, useState } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; +import TableLoyaltytier from '../../components/Loyaltytier/TableLoyaltytier'; +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/loyaltytier/loyaltytierSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const LoyaltytierTablesPage = () => { + const [filterItems, setFilterItems] = useState([]); + const [csvFile, setCsvFile] = useState(null); + const [isModalActive, setIsModalActive] = useState(false); + const [showTableView, setShowTableView] = useState(false); + + const { currentUser } = useAppSelector((state) => state.auth); + + const dispatch = useAppDispatch(); + + const [filters] = useState([{ label: 'Name', title: 'name' }]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_LOYALTYTIER'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getLoyaltytierCSV = async () => { + const response = await axios({ + url: '/loyaltytier?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 = 'loyaltytierCSV.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('Loyaltytier')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
+
+
+
+ + + +
+ + + + + ); +}; + +LoyaltytierTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default LoyaltytierTablesPage; diff --git a/frontend/src/pages/loyaltytier/loyaltytier-view.tsx b/frontend/src/pages/loyaltytier/loyaltytier-view.tsx new file mode 100644 index 0000000..136711e --- /dev/null +++ b/frontend/src/pages/loyaltytier/loyaltytier-view.tsx @@ -0,0 +1,83 @@ +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/loyaltytier/loyaltytierSlice'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import { getPageTitle } from '../../config'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import SectionMain from '../../components/SectionMain'; +import CardBox from '../../components/CardBox'; +import BaseButton from '../../components/BaseButton'; +import BaseDivider from '../../components/BaseDivider'; +import { mdiChartTimelineVariant } from '@mdi/js'; +import { SwitchField } from '../../components/SwitchField'; +import FormField from '../../components/FormField'; + +const LoyaltytierView = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const { loyaltytier } = useAppSelector((state) => state.loyaltytier); + + 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 loyaltytier')} + + + + + + +
+

Name

+

{loyaltytier?.name}

+
+ + + + router.push('/loyaltytier/loyaltytier-list')} + /> +
+
+ + ); +}; + +LoyaltytierView.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default LoyaltytierView; diff --git a/frontend/src/stores/loyaltytier/loyaltytierSlice.ts b/frontend/src/stores/loyaltytier/loyaltytierSlice.ts new file mode 100644 index 0000000..493a911 --- /dev/null +++ b/frontend/src/stores/loyaltytier/loyaltytierSlice.ts @@ -0,0 +1,241 @@ +import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit'; +import axios from 'axios'; +import { + fulfilledNotify, + rejectNotify, + resetNotify, +} from '../../helpers/notifyStateHandler'; + +interface MainState { + loyaltytier: any; + loading: boolean; + count: number; + refetch: boolean; + rolesWidgets: any[]; + notify: { + showNotification: boolean; + textNotification: string; + typeNotification: string; + }; +} + +const initialState: MainState = { + loyaltytier: [], + loading: false, + count: 0, + refetch: false, + rolesWidgets: [], + notify: { + showNotification: false, + textNotification: '', + typeNotification: 'warn', + }, +}; + +export const fetch = createAsyncThunk( + 'loyaltytier/fetch', + async (data: any) => { + const { id, query } = data; + const result = await axios.get( + `loyaltytier${query || (id ? `/${id}` : '')}`, + ); + return id + ? result.data + : { rows: result.data.rows, count: result.data.count }; + }, +); + +export const deleteItemsByIds = createAsyncThunk( + 'loyaltytier/deleteByIds', + async (data: any, { rejectWithValue }) => { + try { + await axios.post('loyaltytier/deleteByIds', { data }); + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const deleteItem = createAsyncThunk( + 'loyaltytier/deleteLoyaltytier', + async (id: string, { rejectWithValue }) => { + try { + await axios.delete(`loyaltytier/${id}`); + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const create = createAsyncThunk( + 'loyaltytier/createLoyaltytier', + async (data: any, { rejectWithValue }) => { + try { + const result = await axios.post('loyaltytier', { data }); + return result.data; + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const uploadCsv = createAsyncThunk( + 'loyaltytier/uploadCsv', + async (file: File, { rejectWithValue }) => { + try { + const data = new FormData(); + data.append('file', file); + data.append('filename', file.name); + + const result = await axios.post('loyaltytier/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( + 'loyaltytier/updateLoyaltytier', + async (payload: any, { rejectWithValue }) => { + try { + const result = await axios.put(`loyaltytier/${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 loyaltytierSlice = createSlice({ + name: 'loyaltytier', + 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.loyaltytier = action.payload.rows; + state.count = action.payload.count; + } else { + state.loyaltytier = 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, 'Loyaltytier 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, `${'Loyaltytier'.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, `${'Loyaltytier'.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, `${'Loyaltytier'.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, 'Loyaltytier 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 } = loyaltytierSlice.actions; + +export default loyaltytierSlice.reducer; diff --git a/frontend/src/stores/store.ts b/frontend/src/stores/store.ts index addd439..44cf2b8 100644 --- a/frontend/src/stores/store.ts +++ b/frontend/src/stores/store.ts @@ -12,6 +12,7 @@ import paymentsSlice from './payments/paymentsSlice'; import vouchersSlice from './vouchers/vouchersSlice'; import rolesSlice from './roles/rolesSlice'; import permissionsSlice from './permissions/permissionsSlice'; +import loyaltytierSlice from './loyaltytier/loyaltytierSlice'; export const store = configureStore({ reducer: { @@ -28,6 +29,7 @@ export const store = configureStore({ vouchers: vouchersSlice, roles: rolesSlice, permissions: permissionsSlice, + loyaltytier: loyaltytierSlice, }, });