diff --git a/app-shell/src/_schema.json b/app-shell/src/_schema.json index 367e410..c2282f1 100644 --- a/app-shell/src/_schema.json +++ b/app-shell/src/_schema.json @@ -1,4 +1,5 @@ { "Initial version": "{\"iv\":\"AHKOiwdeOuoeiNVz\",\"encryptedData\":\"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\"}", - "": "{\"iv\":\"3O3M4d6w4OMgVGnH\",\"encryptedData\":\"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\"}" + "": "{\"iv\":\"3O3M4d6w4OMgVGnH\",\"encryptedData\":\"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\"}", + "n": "{\"iv\":\"gRg/zOOkFm2RAPnu\",\"encryptedData\":\"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\"}" } \ No newline at end of file diff --git a/backend/src/db/api/bookings.js b/backend/src/db/api/bookings.js index 9cb55a0..0cea0a1 100644 --- a/backend/src/db/api/bookings.js +++ b/backend/src/db/api/bookings.js @@ -169,6 +169,10 @@ module.exports = class BookingsDBApi { transaction, }); + output.redemption_booking = await bookings.getRedemption_booking({ + transaction, + }); + output.customer = await bookings.getCustomer({ transaction, }); diff --git a/backend/src/db/api/loyaltytier.js b/backend/src/db/api/loyaltytier.js index c9067ae..c0d966a 100644 --- a/backend/src/db/api/loyaltytier.js +++ b/backend/src/db/api/loyaltytier.js @@ -16,6 +16,9 @@ module.exports = class LoyaltytierDBApi { id: data.id || undefined, name: data.name || null, + annualfee: data.annualfee || null, + pointspersar: data.pointspersar || null, + description: data.description || null, importHash: data.importHash || null, createdById: currentUser.id, updatedById: currentUser.id, @@ -35,6 +38,9 @@ module.exports = class LoyaltytierDBApi { id: item.id || undefined, name: item.name || null, + annualfee: item.annualfee || null, + pointspersar: item.pointspersar || null, + description: item.description || null, importHash: item.importHash || null, createdById: currentUser.id, updatedById: currentUser.id, @@ -61,6 +67,14 @@ module.exports = class LoyaltytierDBApi { if (data.name !== undefined) updatePayload.name = data.name; + if (data.annualfee !== undefined) updatePayload.annualfee = data.annualfee; + + if (data.pointspersar !== undefined) + updatePayload.pointspersar = data.pointspersar; + + if (data.description !== undefined) + updatePayload.description = data.description; + updatePayload.updatedById = currentUser.id; await loyaltytier.update(updatePayload, { transaction }); @@ -129,6 +143,10 @@ module.exports = class LoyaltytierDBApi { const output = loyaltytier.get({ plain: true }); + output.users_loyaltytier = await loyaltytier.getUsers_loyaltytier({ + transaction, + }); + return output; } @@ -161,6 +179,65 @@ module.exports = class LoyaltytierDBApi { }; } + if (filter.description) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'loyaltytier', + 'description', + filter.description, + ), + }; + } + + if (filter.annualfeeRange) { + const [start, end] = filter.annualfeeRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + annualfee: { + ...where.annualfee, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + annualfee: { + ...where.annualfee, + [Op.lte]: end, + }, + }; + } + } + + if (filter.pointspersarRange) { + const [start, end] = filter.pointspersarRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + pointspersar: { + ...where.pointspersar, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + pointspersar: { + ...where.pointspersar, + [Op.lte]: end, + }, + }; + } + } + if (filter.active !== undefined) { where = { ...where, diff --git a/backend/src/db/api/redemption.js b/backend/src/db/api/redemption.js new file mode 100644 index 0000000..e23b84c --- /dev/null +++ b/backend/src/db/api/redemption.js @@ -0,0 +1,402 @@ +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 RedemptionDBApi { + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const redemption = await db.redemption.create( + { + id: data.id || undefined, + + pointsused: data.pointsused || null, + redemptiontype: data.redemptiontype || null, + discountvalue: data.discountvalue || null, + status: data.status || null, + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + await redemption.setUser(data.user || null, { + transaction, + }); + + await redemption.setBooking(data.booking || null, { + transaction, + }); + + return redemption; + } + + 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 redemptionData = data.map((item, index) => ({ + id: item.id || undefined, + + pointsused: item.pointsused || null, + redemptiontype: item.redemptiontype || null, + discountvalue: item.discountvalue || null, + status: item.status || null, + importHash: item.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + // Bulk create items + const redemption = await db.redemption.bulkCreate(redemptionData, { + transaction, + }); + + // For each item created, replace relation files + + return redemption; + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const redemption = await db.redemption.findByPk(id, {}, { transaction }); + + const updatePayload = {}; + + if (data.pointsused !== undefined) + updatePayload.pointsused = data.pointsused; + + if (data.redemptiontype !== undefined) + updatePayload.redemptiontype = data.redemptiontype; + + if (data.discountvalue !== undefined) + updatePayload.discountvalue = data.discountvalue; + + if (data.status !== undefined) updatePayload.status = data.status; + + updatePayload.updatedById = currentUser.id; + + await redemption.update(updatePayload, { transaction }); + + if (data.user !== undefined) { + await redemption.setUser( + data.user, + + { transaction }, + ); + } + + if (data.booking !== undefined) { + await redemption.setBooking( + data.booking, + + { transaction }, + ); + } + + return redemption; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const redemption = await db.redemption.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of redemption) { + await record.update({ deletedBy: currentUser.id }, { transaction }); + } + for (const record of redemption) { + await record.destroy({ transaction }); + } + }); + + return redemption; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const redemption = await db.redemption.findByPk(id, options); + + await redemption.update( + { + deletedBy: currentUser.id, + }, + { + transaction, + }, + ); + + await redemption.destroy({ + transaction, + }); + + return redemption; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const redemption = await db.redemption.findOne({ where }, { transaction }); + + if (!redemption) { + return redemption; + } + + const output = redemption.get({ plain: true }); + + output.user = await redemption.getUser({ + transaction, + }); + + output.booking = await redemption.getBooking({ + transaction, + }); + + return output; + } + + static async findAll(filter, options) { + const limit = filter.limit || 0; + let offset = 0; + let where = {}; + const currentPage = +filter.page; + + offset = currentPage * limit; + + const orderBy = null; + + const transaction = (options && options.transaction) || undefined; + + let include = [ + { + 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}%` })), + }, + }, + ], + } + : {}, + }, + + { + model: db.bookings, + as: 'booking', + + where: filter.booking + ? { + [Op.or]: [ + { + id: { + [Op.in]: filter.booking + .split('|') + .map((term) => Utils.uuid(term)), + }, + }, + { + service_type: { + [Op.or]: filter.booking + .split('|') + .map((term) => ({ [Op.iLike]: `%${term}%` })), + }, + }, + ], + } + : {}, + }, + ]; + + if (filter) { + if (filter.id) { + where = { + ...where, + ['id']: Utils.uuid(filter.id), + }; + } + + if (filter.pointsusedRange) { + const [start, end] = filter.pointsusedRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + pointsused: { + ...where.pointsused, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + pointsused: { + ...where.pointsused, + [Op.lte]: end, + }, + }; + } + } + + if (filter.discountvalueRange) { + const [start, end] = filter.discountvalueRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + discountvalue: { + ...where.discountvalue, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + discountvalue: { + ...where.discountvalue, + [Op.lte]: end, + }, + }; + } + } + + if (filter.active !== undefined) { + where = { + ...where, + active: filter.active === true || filter.active === 'true', + }; + } + + if (filter.redemptiontype) { + where = { + ...where, + redemptiontype: filter.redemptiontype, + }; + } + + if (filter.status) { + where = { + ...where, + status: filter.status, + }; + } + + 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.redemption.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('redemption', 'id', query), + ], + }; + } + + const records = await db.redemption.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/referral.js b/backend/src/db/api/referral.js new file mode 100644 index 0000000..cf5ac51 --- /dev/null +++ b/backend/src/db/api/referral.js @@ -0,0 +1,306 @@ +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 ReferralDBApi { + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const referral = await db.referral.create( + { + id: data.id || undefined, + + referredemail: data.referredemail || null, + status: data.status || null, + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + await referral.setReferrer(data.referrer || null, { + transaction, + }); + + return referral; + } + + 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 referralData = data.map((item, index) => ({ + id: item.id || undefined, + + referredemail: item.referredemail || null, + status: item.status || null, + importHash: item.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + // Bulk create items + const referral = await db.referral.bulkCreate(referralData, { + transaction, + }); + + // For each item created, replace relation files + + return referral; + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const referral = await db.referral.findByPk(id, {}, { transaction }); + + const updatePayload = {}; + + if (data.referredemail !== undefined) + updatePayload.referredemail = data.referredemail; + + if (data.status !== undefined) updatePayload.status = data.status; + + updatePayload.updatedById = currentUser.id; + + await referral.update(updatePayload, { transaction }); + + if (data.referrer !== undefined) { + await referral.setReferrer( + data.referrer, + + { transaction }, + ); + } + + return referral; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const referral = await db.referral.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of referral) { + await record.update({ deletedBy: currentUser.id }, { transaction }); + } + for (const record of referral) { + await record.destroy({ transaction }); + } + }); + + return referral; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const referral = await db.referral.findByPk(id, options); + + await referral.update( + { + deletedBy: currentUser.id, + }, + { + transaction, + }, + ); + + await referral.destroy({ + transaction, + }); + + return referral; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const referral = await db.referral.findOne({ where }, { transaction }); + + if (!referral) { + return referral; + } + + const output = referral.get({ plain: true }); + + output.referrer = await referral.getReferrer({ + transaction, + }); + + return output; + } + + static async findAll(filter, options) { + const limit = filter.limit || 0; + let offset = 0; + let where = {}; + const currentPage = +filter.page; + + offset = currentPage * limit; + + const orderBy = null; + + const transaction = (options && options.transaction) || undefined; + + let include = [ + { + model: db.users, + as: 'referrer', + + where: filter.referrer + ? { + [Op.or]: [ + { + id: { + [Op.in]: filter.referrer + .split('|') + .map((term) => Utils.uuid(term)), + }, + }, + { + firstName: { + [Op.or]: filter.referrer + .split('|') + .map((term) => ({ [Op.iLike]: `%${term}%` })), + }, + }, + ], + } + : {}, + }, + ]; + + if (filter) { + if (filter.id) { + where = { + ...where, + ['id']: Utils.uuid(filter.id), + }; + } + + if (filter.referredemail) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'referral', + 'referredemail', + filter.referredemail, + ), + }; + } + + if (filter.active !== undefined) { + where = { + ...where, + active: filter.active === true || filter.active === 'true', + }; + } + + if (filter.status) { + where = { + ...where, + status: filter.status, + }; + } + + 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.referral.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('referral', 'id', query), + ], + }; + } + + const records = await db.referral.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 3a11e69..d7e8580 100644 --- a/backend/src/db/api/users.js +++ b/backend/src/db/api/users.js @@ -34,6 +34,8 @@ module.exports = class UsersDBApi { passwordResetTokenExpiresAt: data.data.passwordResetTokenExpiresAt || null, provider: data.data.provider || null, + pointsbalance: data.data.pointsbalance || null, + referralcode: data.data.referralcode || null, importHash: data.data.importHash || null, createdById: currentUser.id, updatedById: currentUser.id, @@ -56,6 +58,10 @@ module.exports = class UsersDBApi { }); } + await users.setLoyaltytier(data.data.loyaltytier || null, { + transaction, + }); + await users.setCustom_permissions(data.data.custom_permissions || [], { transaction, }); @@ -96,6 +102,8 @@ module.exports = class UsersDBApi { passwordResetToken: item.passwordResetToken || null, passwordResetTokenExpiresAt: item.passwordResetTokenExpiresAt || null, provider: item.provider || null, + pointsbalance: item.pointsbalance || null, + referralcode: item.referralcode || null, importHash: item.importHash || null, createdById: currentUser.id, updatedById: currentUser.id, @@ -178,6 +186,12 @@ module.exports = class UsersDBApi { if (data.provider !== undefined) updatePayload.provider = data.provider; + if (data.pointsbalance !== undefined) + updatePayload.pointsbalance = data.pointsbalance; + + if (data.referralcode !== undefined) + updatePayload.referralcode = data.referralcode; + updatePayload.updatedById = currentUser.id; await users.update(updatePayload, { transaction }); @@ -190,6 +204,14 @@ module.exports = class UsersDBApi { ); } + if (data.loyaltytier !== undefined) { + await users.setLoyaltytier( + data.loyaltytier, + + { transaction }, + ); + } + if (data.custom_permissions !== undefined) { await users.setCustom_permissions(data.custom_permissions, { transaction, @@ -267,6 +289,14 @@ module.exports = class UsersDBApi { const output = users.get({ plain: true }); + output.referral_referrer = await users.getReferral_referrer({ + transaction, + }); + + output.redemption_user = await users.getRedemption_user({ + transaction, + }); + output.avatar = await users.getAvatar({ transaction, }); @@ -285,6 +315,10 @@ module.exports = class UsersDBApi { transaction, }); + output.loyaltytier = await users.getLoyaltytier({ + transaction, + }); + return output; } @@ -327,6 +361,32 @@ module.exports = class UsersDBApi { : {}, }, + { + model: db.loyaltytier, + as: 'loyaltytier', + + where: filter.loyaltytier + ? { + [Op.or]: [ + { + id: { + [Op.in]: filter.loyaltytier + .split('|') + .map((term) => Utils.uuid(term)), + }, + }, + { + name: { + [Op.or]: filter.loyaltytier + .split('|') + .map((term) => ({ [Op.iLike]: `%${term}%` })), + }, + }, + ], + } + : {}, + }, + { model: db.permissions, as: 'custom_permissions', @@ -411,6 +471,13 @@ module.exports = class UsersDBApi { }; } + if (filter.referralcode) { + where = { + ...where, + [Op.and]: Utils.ilike('users', 'referralcode', filter.referralcode), + }; + } + if (filter.emailVerificationTokenExpiresAtRange) { const [start, end] = filter.emailVerificationTokenExpiresAtRange; @@ -459,6 +526,30 @@ module.exports = class UsersDBApi { } } + if (filter.pointsbalanceRange) { + const [start, end] = filter.pointsbalanceRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + pointsbalance: { + ...where.pointsbalance, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + pointsbalance: { + ...where.pointsbalance, + [Op.lte]: end, + }, + }; + } + } + if (filter.active !== undefined) { where = { ...where, diff --git a/backend/src/db/migrations/1748078935638.js b/backend/src/db/migrations/1748078935638.js new file mode 100644 index 0000000..a84af4c --- /dev/null +++ b/backend/src/db/migrations/1748078935638.js @@ -0,0 +1,49 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.addColumn( + 'loyaltytier', + 'annualfee', + { + type: Sequelize.DataTypes.DECIMAL, + }, + { transaction }, + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async down(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.removeColumn('loyaltytier', 'annualfee', { + transaction, + }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1748078966955.js b/backend/src/db/migrations/1748078966955.js new file mode 100644 index 0000000..5241419 --- /dev/null +++ b/backend/src/db/migrations/1748078966955.js @@ -0,0 +1,49 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.addColumn( + 'loyaltytier', + 'pointspersar', + { + type: Sequelize.DataTypes.DECIMAL, + }, + { transaction }, + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async down(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.removeColumn('loyaltytier', 'pointspersar', { + transaction, + }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1748079001996.js b/backend/src/db/migrations/1748079001996.js new file mode 100644 index 0000000..0a09ff8 --- /dev/null +++ b/backend/src/db/migrations/1748079001996.js @@ -0,0 +1,49 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.addColumn( + 'loyaltytier', + 'description', + { + type: Sequelize.DataTypes.TEXT, + }, + { transaction }, + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async down(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.removeColumn('loyaltytier', 'description', { + transaction, + }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1748079044910.js b/backend/src/db/migrations/1748079044910.js new file mode 100644 index 0000000..72ff531 --- /dev/null +++ b/backend/src/db/migrations/1748079044910.js @@ -0,0 +1,54 @@ +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( + 'users', + 'loyaltytierId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'loyaltytier', + 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('users', 'loyaltytierId', { + transaction, + }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1748079122373.js b/backend/src/db/migrations/1748079122373.js new file mode 100644 index 0000000..ea9264b --- /dev/null +++ b/backend/src/db/migrations/1748079122373.js @@ -0,0 +1,49 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.addColumn( + 'users', + 'pointsbalance', + { + type: Sequelize.DataTypes.INTEGER, + }, + { 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('users', 'pointsbalance', { + transaction, + }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1748079157620.js b/backend/src/db/migrations/1748079157620.js new file mode 100644 index 0000000..9fc48dc --- /dev/null +++ b/backend/src/db/migrations/1748079157620.js @@ -0,0 +1,49 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.addColumn( + 'users', + 'referralcode', + { + 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('users', 'referralcode', { + transaction, + }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1748079188113.js b/backend/src/db/migrations/1748079188113.js new file mode 100644 index 0000000..74b0a70 --- /dev/null +++ b/backend/src/db/migrations/1748079188113.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( + 'referral', + { + 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('referral', { transaction }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1748079231733.js b/backend/src/db/migrations/1748079231733.js new file mode 100644 index 0000000..2759bff --- /dev/null +++ b/backend/src/db/migrations/1748079231733.js @@ -0,0 +1,54 @@ +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( + 'referral', + 'referrerId', + { + 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('referral', 'referrerId', { + transaction, + }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1748079291100.js b/backend/src/db/migrations/1748079291100.js new file mode 100644 index 0000000..811e528 --- /dev/null +++ b/backend/src/db/migrations/1748079291100.js @@ -0,0 +1,49 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.addColumn( + 'referral', + 'referredemail', + { + 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('referral', 'referredemail', { + transaction, + }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1748079326438.js b/backend/src/db/migrations/1748079326438.js new file mode 100644 index 0000000..a824747 --- /dev/null +++ b/backend/src/db/migrations/1748079326438.js @@ -0,0 +1,49 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.addColumn( + 'referral', + 'status', + { + type: Sequelize.DataTypes.ENUM, + + values: ['value'], + }, + { 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('referral', 'status', { transaction }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1748079355731.js b/backend/src/db/migrations/1748079355731.js new file mode 100644 index 0000000..7823ac4 --- /dev/null +++ b/backend/src/db/migrations/1748079355731.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( + 'redemption', + { + 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('redemption', { transaction }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1748079386360.js b/backend/src/db/migrations/1748079386360.js new file mode 100644 index 0000000..34e2297 --- /dev/null +++ b/backend/src/db/migrations/1748079386360.js @@ -0,0 +1,54 @@ +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( + 'redemption', + '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('redemption', 'userId', { + transaction, + }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1748079423627.js b/backend/src/db/migrations/1748079423627.js new file mode 100644 index 0000000..b9baad9 --- /dev/null +++ b/backend/src/db/migrations/1748079423627.js @@ -0,0 +1,49 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.addColumn( + 'redemption', + 'pointsused', + { + type: Sequelize.DataTypes.INTEGER, + }, + { 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('redemption', 'pointsused', { + transaction, + }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1748079463221.js b/backend/src/db/migrations/1748079463221.js new file mode 100644 index 0000000..39fa979 --- /dev/null +++ b/backend/src/db/migrations/1748079463221.js @@ -0,0 +1,51 @@ +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( + 'redemption', + 'redemptiontype', + { + type: Sequelize.DataTypes.ENUM, + + values: ['value'], + }, + { 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('redemption', 'redemptiontype', { + transaction, + }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1748079630221.js b/backend/src/db/migrations/1748079630221.js new file mode 100644 index 0000000..a01ad58 --- /dev/null +++ b/backend/src/db/migrations/1748079630221.js @@ -0,0 +1,49 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.addColumn( + 'redemption', + 'discountvalue', + { + type: Sequelize.DataTypes.DECIMAL, + }, + { transaction }, + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async down(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.removeColumn('redemption', 'discountvalue', { + transaction, + }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1748079688214.js b/backend/src/db/migrations/1748079688214.js new file mode 100644 index 0000000..e47c8f8 --- /dev/null +++ b/backend/src/db/migrations/1748079688214.js @@ -0,0 +1,51 @@ +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( + 'redemption', + 'status', + { + type: Sequelize.DataTypes.ENUM, + + values: ['value'], + }, + { 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('redemption', 'status', { + transaction, + }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1748079723177.js b/backend/src/db/migrations/1748079723177.js new file mode 100644 index 0000000..1ecb17b --- /dev/null +++ b/backend/src/db/migrations/1748079723177.js @@ -0,0 +1,54 @@ +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( + 'redemption', + 'bookingId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'bookings', + 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('redemption', 'bookingId', { + transaction, + }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/models/bookings.js b/backend/src/db/models/bookings.js index 1851ceb..247adf6 100644 --- a/backend/src/db/models/bookings.js +++ b/backend/src/db/models/bookings.js @@ -60,6 +60,14 @@ module.exports = function (sequelize, DataTypes) { constraints: false, }); + db.bookings.hasMany(db.redemption, { + as: 'redemption_booking', + foreignKey: { + name: 'bookingId', + }, + constraints: false, + }); + //end loop db.bookings.belongsTo(db.customers, { diff --git a/backend/src/db/models/loyaltytier.js b/backend/src/db/models/loyaltytier.js index 8773280..2087e9e 100644 --- a/backend/src/db/models/loyaltytier.js +++ b/backend/src/db/models/loyaltytier.js @@ -18,6 +18,18 @@ module.exports = function (sequelize, DataTypes) { type: DataTypes.TEXT, }, + annualfee: { + type: DataTypes.DECIMAL, + }, + + pointspersar: { + type: DataTypes.DECIMAL, + }, + + description: { + type: DataTypes.TEXT, + }, + importHash: { type: DataTypes.STRING(255), allowNull: true, @@ -34,6 +46,14 @@ module.exports = function (sequelize, DataTypes) { loyaltytier.associate = (db) => { /// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity + db.loyaltytier.hasMany(db.users, { + as: 'users_loyaltytier', + foreignKey: { + name: 'loyaltytierId', + }, + constraints: false, + }); + //end loop db.loyaltytier.belongsTo(db.users, { diff --git a/backend/src/db/models/redemption.js b/backend/src/db/models/redemption.js new file mode 100644 index 0000000..fb3c033 --- /dev/null +++ b/backend/src/db/models/redemption.js @@ -0,0 +1,81 @@ +const config = require('../../config'); +const providers = config.providers; +const crypto = require('crypto'); +const bcrypt = require('bcrypt'); +const moment = require('moment'); + +module.exports = function (sequelize, DataTypes) { + const redemption = sequelize.define( + 'redemption', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + + pointsused: { + type: DataTypes.INTEGER, + }, + + redemptiontype: { + type: DataTypes.ENUM, + + values: ['value'], + }, + + discountvalue: { + type: DataTypes.DECIMAL, + }, + + status: { + type: DataTypes.ENUM, + + values: ['value'], + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + redemption.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.redemption.belongsTo(db.users, { + as: 'user', + foreignKey: { + name: 'userId', + }, + constraints: false, + }); + + db.redemption.belongsTo(db.bookings, { + as: 'booking', + foreignKey: { + name: 'bookingId', + }, + constraints: false, + }); + + db.redemption.belongsTo(db.users, { + as: 'createdBy', + }); + + db.redemption.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return redemption; +}; diff --git a/backend/src/db/models/referral.js b/backend/src/db/models/referral.js new file mode 100644 index 0000000..3cfd722 --- /dev/null +++ b/backend/src/db/models/referral.js @@ -0,0 +1,63 @@ +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 referral = sequelize.define( + 'referral', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + + referredemail: { + type: DataTypes.TEXT, + }, + + status: { + type: DataTypes.ENUM, + + values: ['value'], + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + referral.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.referral.belongsTo(db.users, { + as: 'referrer', + foreignKey: { + name: 'referrerId', + }, + constraints: false, + }); + + db.referral.belongsTo(db.users, { + as: 'createdBy', + }); + + db.referral.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return referral; +}; diff --git a/backend/src/db/models/users.js b/backend/src/db/models/users.js index 74e549e..27d86c7 100644 --- a/backend/src/db/models/users.js +++ b/backend/src/db/models/users.js @@ -68,6 +68,14 @@ module.exports = function (sequelize, DataTypes) { type: DataTypes.TEXT, }, + pointsbalance: { + type: DataTypes.INTEGER, + }, + + referralcode: { + type: DataTypes.TEXT, + }, + importHash: { type: DataTypes.STRING(255), allowNull: true, @@ -102,6 +110,22 @@ module.exports = function (sequelize, DataTypes) { /// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity + db.users.hasMany(db.referral, { + as: 'referral_referrer', + foreignKey: { + name: 'referrerId', + }, + constraints: false, + }); + + db.users.hasMany(db.redemption, { + as: 'redemption_user', + foreignKey: { + name: 'userId', + }, + constraints: false, + }); + //end loop db.users.belongsTo(db.roles, { @@ -112,6 +136,14 @@ module.exports = function (sequelize, DataTypes) { constraints: false, }); + db.users.belongsTo(db.loyaltytier, { + as: 'loyaltytier', + foreignKey: { + name: 'loyaltytierId', + }, + constraints: false, + }); + db.users.hasMany(db.file, { as: 'avatar', foreignKey: 'belongsToId', diff --git a/backend/src/db/seeders/20200430130760-user-roles.js b/backend/src/db/seeders/20200430130760-user-roles.js index 0d48f1e..92e53b6 100644 --- a/backend/src/db/seeders/20200430130760-user-roles.js +++ b/backend/src/db/seeders/20200430130760-user-roles.js @@ -103,6 +103,8 @@ module.exports = { 'roles', 'permissions', 'loyaltytier', + 'referral', + 'redemption', , ]; await queryInterface.bulkInsert( @@ -712,6 +714,56 @@ primary key ("roles_permissionsId", "permissionId") permissionId: getId('DELETE_LOYALTYTIER'), }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('CREATE_REFERRAL'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('READ_REFERRAL'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('UPDATE_REFERRAL'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('DELETE_REFERRAL'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('CREATE_REDEMPTION'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('READ_REDEMPTION'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('UPDATE_REDEMPTION'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('DELETE_REDEMPTION'), + }, + { createdAt, updatedAt, diff --git a/backend/src/db/seeders/20231127130745-sample-data.js b/backend/src/db/seeders/20231127130745-sample-data.js index df80374..36435c6 100644 --- a/backend/src/db/seeders/20231127130745-sample-data.js +++ b/backend/src/db/seeders/20231127130745-sample-data.js @@ -13,6 +13,10 @@ const Vouchers = db.vouchers; const Loyaltytier = db.loyaltytier; +const Referral = db.referral; + +const Redemption = db.redemption; + const AgentsData = [ { name: 'Agent A', @@ -45,13 +49,21 @@ const AgentsData = [ phone_number: '4564564567', }, + + { + name: 'Agent E', + + email: 'agent.e@example.com', + + phone_number: '5675675678', + }, ]; const BookingsData = [ { // type code here for "relation_one" field - service_type: 'Hotel', + service_type: 'Package', booking_date: new Date('2023-11-01T10:00:00Z'), @@ -63,7 +75,7 @@ const BookingsData = [ { // type code here for "relation_one" field - service_type: 'Package', + service_type: 'Tour', booking_date: new Date('2023-11-02T12:00:00Z'), @@ -75,7 +87,7 @@ const BookingsData = [ { // type code here for "relation_one" field - service_type: 'Hotel', + service_type: 'Tour', booking_date: new Date('2023-11-03T14:00:00Z'), @@ -87,7 +99,7 @@ const BookingsData = [ { // type code here for "relation_one" field - service_type: 'Hotel', + service_type: 'Package', booking_date: new Date('2023-11-04T16:00:00Z'), @@ -95,6 +107,18 @@ const BookingsData = [ // type code here for "relation_one" field }, + + { + // type code here for "relation_one" field + + service_type: 'Tour', + + booking_date: new Date('2023-11-05T18:00:00Z'), + + total_amount: 450, + + // type code here for "relation_one" field + }, ]; const CustomersData = [ @@ -137,6 +161,16 @@ const CustomersData = [ phone_number: '5566778899', }, + + { + first_name: 'Charlie', + + last_name: 'Davis', + + email: 'charlie.davis@example.com', + + phone_number: '6677889900', + }, ]; const PaymentsData = [ @@ -147,7 +181,7 @@ const PaymentsData = [ amount: 500, - status: 'Failed', + status: 'Pending', }, { @@ -157,7 +191,7 @@ const PaymentsData = [ amount: 300, - status: 'Pending', + status: 'Failed', }, { @@ -167,7 +201,7 @@ const PaymentsData = [ amount: 800, - status: 'Pending', + status: 'Completed', }, { @@ -179,6 +213,16 @@ const PaymentsData = [ status: 'Pending', }, + + { + // type code here for "relation_one" field + + payment_date: new Date('2023-11-05T19:00:00Z'), + + amount: 450, + + status: 'Failed', + }, ]; const VouchersData = [ @@ -213,28 +257,241 @@ const VouchersData = [ issue_date: new Date('2023-11-04T18:00:00Z'), }, + + { + // type code here for "relation_one" field + + voucher_code: 'VCH56789', + + issue_date: new Date('2023-11-05T20:00:00Z'), + }, ]; const LoyaltytierData = [ + { + name: 'John Dalton', + + annualfee: 74.62, + + pointspersar: 51.91, + + description: 'Jean Baptiste Lamarck', + }, + { name: 'B. F. Skinner', + + annualfee: 10.09, + + pointspersar: 44.45, + + description: 'Werner Heisenberg', }, { - name: 'Carl Gauss (Karl Friedrich Gauss)', + name: 'Tycho Brahe', + + annualfee: 86.15, + + pointspersar: 10.74, + + description: 'Emil Fischer', }, { - name: 'August Kekule', + name: 'Francis Crick', + + annualfee: 20.76, + + pointspersar: 72.74, + + description: 'William Bayliss', }, { - name: 'James Watson', + name: 'Thomas Hunt Morgan', + + annualfee: 97.56, + + pointspersar: 39.08, + + description: 'Johannes Kepler', + }, +]; + +const ReferralData = [ + { + // type code here for "relation_one" field + + referredemail: 'Ernst Haeckel', + + status: 'value', + }, + + { + // type code here for "relation_one" field + + referredemail: 'Heike Kamerlingh Onnes', + + status: 'value', + }, + + { + // type code here for "relation_one" field + + referredemail: 'Robert Koch', + + status: 'value', + }, + + { + // type code here for "relation_one" field + + referredemail: 'Jean Piaget', + + status: 'value', + }, + + { + // type code here for "relation_one" field + + referredemail: 'Jean Baptiste Lamarck', + + status: 'value', + }, +]; + +const RedemptionData = [ + { + // type code here for "relation_one" field + + pointsused: 5, + + redemptiontype: 'value', + + discountvalue: 62.55, + + status: 'value', + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + pointsused: 5, + + redemptiontype: 'value', + + discountvalue: 99.45, + + status: 'value', + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + pointsused: 8, + + redemptiontype: 'value', + + discountvalue: 29.35, + + status: 'value', + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + pointsused: 5, + + redemptiontype: 'value', + + discountvalue: 91.62, + + status: 'value', + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + pointsused: 9, + + redemptiontype: 'value', + + discountvalue: 87.53, + + status: 'value', + + // type code here for "relation_one" field }, ]; // Similar logic for "relation_many" +async function associateUserWithLoyaltytier() { + const relatedLoyaltytier0 = await Loyaltytier.findOne({ + offset: Math.floor(Math.random() * (await Loyaltytier.count())), + }); + const User0 = await Users.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (User0?.setLoyaltytier) { + await User0.setLoyaltytier(relatedLoyaltytier0); + } + + const relatedLoyaltytier1 = await Loyaltytier.findOne({ + offset: Math.floor(Math.random() * (await Loyaltytier.count())), + }); + const User1 = await Users.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (User1?.setLoyaltytier) { + await User1.setLoyaltytier(relatedLoyaltytier1); + } + + const relatedLoyaltytier2 = await Loyaltytier.findOne({ + offset: Math.floor(Math.random() * (await Loyaltytier.count())), + }); + const User2 = await Users.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (User2?.setLoyaltytier) { + await User2.setLoyaltytier(relatedLoyaltytier2); + } + + const relatedLoyaltytier3 = await Loyaltytier.findOne({ + offset: Math.floor(Math.random() * (await Loyaltytier.count())), + }); + const User3 = await Users.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (User3?.setLoyaltytier) { + await User3.setLoyaltytier(relatedLoyaltytier3); + } + + const relatedLoyaltytier4 = await Loyaltytier.findOne({ + offset: Math.floor(Math.random() * (await Loyaltytier.count())), + }); + const User4 = await Users.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (User4?.setLoyaltytier) { + await User4.setLoyaltytier(relatedLoyaltytier4); + } +} + async function associateBookingWithCustomer() { const relatedCustomer0 = await Customers.findOne({ offset: Math.floor(Math.random() * (await Customers.count())), @@ -279,6 +536,17 @@ async function associateBookingWithCustomer() { if (Booking3?.setCustomer) { await Booking3.setCustomer(relatedCustomer3); } + + const relatedCustomer4 = await Customers.findOne({ + offset: Math.floor(Math.random() * (await Customers.count())), + }); + const Booking4 = await Bookings.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (Booking4?.setCustomer) { + await Booking4.setCustomer(relatedCustomer4); + } } async function associateBookingWithAgent() { @@ -325,6 +593,17 @@ async function associateBookingWithAgent() { if (Booking3?.setAgent) { await Booking3.setAgent(relatedAgent3); } + + const relatedAgent4 = await Agents.findOne({ + offset: Math.floor(Math.random() * (await Agents.count())), + }); + const Booking4 = await Bookings.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (Booking4?.setAgent) { + await Booking4.setAgent(relatedAgent4); + } } async function associatePaymentWithBooking() { @@ -371,6 +650,17 @@ async function associatePaymentWithBooking() { if (Payment3?.setBooking) { await Payment3.setBooking(relatedBooking3); } + + const relatedBooking4 = await Bookings.findOne({ + offset: Math.floor(Math.random() * (await Bookings.count())), + }); + const Payment4 = await Payments.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (Payment4?.setBooking) { + await Payment4.setBooking(relatedBooking4); + } } async function associateVoucherWithBooking() { @@ -417,6 +707,188 @@ async function associateVoucherWithBooking() { if (Voucher3?.setBooking) { await Voucher3.setBooking(relatedBooking3); } + + const relatedBooking4 = await Bookings.findOne({ + offset: Math.floor(Math.random() * (await Bookings.count())), + }); + const Voucher4 = await Vouchers.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (Voucher4?.setBooking) { + await Voucher4.setBooking(relatedBooking4); + } +} + +async function associateReferralWithReferrer() { + const relatedReferrer0 = await Users.findOne({ + offset: Math.floor(Math.random() * (await Users.count())), + }); + const Referral0 = await Referral.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (Referral0?.setReferrer) { + await Referral0.setReferrer(relatedReferrer0); + } + + const relatedReferrer1 = await Users.findOne({ + offset: Math.floor(Math.random() * (await Users.count())), + }); + const Referral1 = await Referral.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (Referral1?.setReferrer) { + await Referral1.setReferrer(relatedReferrer1); + } + + const relatedReferrer2 = await Users.findOne({ + offset: Math.floor(Math.random() * (await Users.count())), + }); + const Referral2 = await Referral.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (Referral2?.setReferrer) { + await Referral2.setReferrer(relatedReferrer2); + } + + const relatedReferrer3 = await Users.findOne({ + offset: Math.floor(Math.random() * (await Users.count())), + }); + const Referral3 = await Referral.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (Referral3?.setReferrer) { + await Referral3.setReferrer(relatedReferrer3); + } + + const relatedReferrer4 = await Users.findOne({ + offset: Math.floor(Math.random() * (await Users.count())), + }); + const Referral4 = await Referral.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (Referral4?.setReferrer) { + await Referral4.setReferrer(relatedReferrer4); + } +} + +async function associateRedemptionWithUser() { + const relatedUser0 = await Users.findOne({ + offset: Math.floor(Math.random() * (await Users.count())), + }); + const Redemption0 = await Redemption.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (Redemption0?.setUser) { + await Redemption0.setUser(relatedUser0); + } + + const relatedUser1 = await Users.findOne({ + offset: Math.floor(Math.random() * (await Users.count())), + }); + const Redemption1 = await Redemption.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (Redemption1?.setUser) { + await Redemption1.setUser(relatedUser1); + } + + const relatedUser2 = await Users.findOne({ + offset: Math.floor(Math.random() * (await Users.count())), + }); + const Redemption2 = await Redemption.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (Redemption2?.setUser) { + await Redemption2.setUser(relatedUser2); + } + + const relatedUser3 = await Users.findOne({ + offset: Math.floor(Math.random() * (await Users.count())), + }); + const Redemption3 = await Redemption.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (Redemption3?.setUser) { + await Redemption3.setUser(relatedUser3); + } + + const relatedUser4 = await Users.findOne({ + offset: Math.floor(Math.random() * (await Users.count())), + }); + const Redemption4 = await Redemption.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (Redemption4?.setUser) { + await Redemption4.setUser(relatedUser4); + } +} + +async function associateRedemptionWithBooking() { + const relatedBooking0 = await Bookings.findOne({ + offset: Math.floor(Math.random() * (await Bookings.count())), + }); + const Redemption0 = await Redemption.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (Redemption0?.setBooking) { + await Redemption0.setBooking(relatedBooking0); + } + + const relatedBooking1 = await Bookings.findOne({ + offset: Math.floor(Math.random() * (await Bookings.count())), + }); + const Redemption1 = await Redemption.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (Redemption1?.setBooking) { + await Redemption1.setBooking(relatedBooking1); + } + + const relatedBooking2 = await Bookings.findOne({ + offset: Math.floor(Math.random() * (await Bookings.count())), + }); + const Redemption2 = await Redemption.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (Redemption2?.setBooking) { + await Redemption2.setBooking(relatedBooking2); + } + + const relatedBooking3 = await Bookings.findOne({ + offset: Math.floor(Math.random() * (await Bookings.count())), + }); + const Redemption3 = await Redemption.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (Redemption3?.setBooking) { + await Redemption3.setBooking(relatedBooking3); + } + + const relatedBooking4 = await Bookings.findOne({ + offset: Math.floor(Math.random() * (await Bookings.count())), + }); + const Redemption4 = await Redemption.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (Redemption4?.setBooking) { + await Redemption4.setBooking(relatedBooking4); + } } module.exports = { @@ -433,9 +905,15 @@ module.exports = { await Loyaltytier.bulkCreate(LoyaltytierData); + await Referral.bulkCreate(ReferralData); + + await Redemption.bulkCreate(RedemptionData); + await Promise.all([ // Similar logic for "relation_many" + await associateUserWithLoyaltytier(), + await associateBookingWithCustomer(), await associateBookingWithAgent(), @@ -443,6 +921,12 @@ module.exports = { await associatePaymentWithBooking(), await associateVoucherWithBooking(), + + await associateReferralWithReferrer(), + + await associateRedemptionWithUser(), + + await associateRedemptionWithBooking(), ]); }, @@ -458,5 +942,9 @@ module.exports = { await queryInterface.bulkDelete('vouchers', null, {}); await queryInterface.bulkDelete('loyaltytier', null, {}); + + await queryInterface.bulkDelete('referral', null, {}); + + await queryInterface.bulkDelete('redemption', null, {}); }, }; diff --git a/backend/src/db/seeders/20250524093308.js b/backend/src/db/seeders/20250524093308.js new file mode 100644 index 0000000..d550eac --- /dev/null +++ b/backend/src/db/seeders/20250524093308.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 = ['referral']; + + 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/db/seeders/20250524093555.js b/backend/src/db/seeders/20250524093555.js new file mode 100644 index 0000000..fbd0aac --- /dev/null +++ b/backend/src/db/seeders/20250524093555.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 = ['redemption']; + + 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 53f1aa1..f6d1aab 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -37,6 +37,10 @@ const permissionsRoutes = require('./routes/permissions'); const loyaltytierRoutes = require('./routes/loyaltytier'); +const referralRoutes = require('./routes/referral'); + +const redemptionRoutes = require('./routes/redemption'); + const getBaseUrl = (url) => { if (!url) return ''; return url.endsWith('/api') ? url.slice(0, -4) : url; @@ -156,6 +160,18 @@ app.use( loyaltytierRoutes, ); +app.use( + '/api/referral', + passport.authenticate('jwt', { session: false }), + referralRoutes, +); + +app.use( + '/api/redemption', + passport.authenticate('jwt', { session: false }), + redemptionRoutes, +); + app.use( '/api/openai', passport.authenticate('jwt', { session: false }), diff --git a/backend/src/routes/loyaltytier.js b/backend/src/routes/loyaltytier.js index b44680f..4033e0f 100644 --- a/backend/src/routes/loyaltytier.js +++ b/backend/src/routes/loyaltytier.js @@ -23,6 +23,16 @@ router.use(checkCrudPermissions('loyaltytier')); * name: * type: string * default: name + * description: + * type: string + * default: description + + * annualfee: + * type: integer + * format: int64 + * pointspersar: + * type: integer + * format: int64 */ @@ -308,7 +318,7 @@ router.get( const currentUser = req.currentUser; const payload = await LoyaltytierDBApi.findAll(req.query, { currentUser }); if (filetype && filetype === 'csv') { - const fields = ['id', 'name']; + const fields = ['id', 'name', 'description', 'annualfee', 'pointspersar']; const opts = { fields }; try { const csv = parse(payload.rows, opts); diff --git a/backend/src/routes/redemption.js b/backend/src/routes/redemption.js new file mode 100644 index 0000000..fe1f63b --- /dev/null +++ b/backend/src/routes/redemption.js @@ -0,0 +1,444 @@ +const express = require('express'); + +const RedemptionService = require('../services/redemption'); +const RedemptionDBApi = require('../db/api/redemption'); +const wrapAsync = require('../helpers').wrapAsync; + +const router = express.Router(); + +const { parse } = require('json2csv'); + +const { checkCrudPermissions } = require('../middlewares/check-permissions'); + +router.use(checkCrudPermissions('redemption')); + +/** + * @swagger + * components: + * schemas: + * Redemption: + * type: object + * properties: + + * pointsused: + * type: integer + * format: int64 + + * discountvalue: + * type: integer + * format: int64 + + * + * + */ + +/** + * @swagger + * tags: + * name: Redemption + * description: The Redemption managing API + */ + +/** + * @swagger + * /api/redemption: + * post: + * security: + * - bearerAuth: [] + * tags: [Redemption] + * 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/Redemption" + * responses: + * 200: + * description: The item was successfully added + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Redemption" + * 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 RedemptionService.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: [Redemption] + * 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/Redemption" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Redemption" + * 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 RedemptionService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/redemption/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Redemption] + * 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/Redemption" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Redemption" + * 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 RedemptionService.update(req.body.data, req.body.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/redemption/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Redemption] + * 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/Redemption" + * 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 RedemptionService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/redemption/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Redemption] + * 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/Redemption" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post( + '/deleteByIds', + wrapAsync(async (req, res) => { + await RedemptionService.deleteByIds(req.body.data, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/redemption: + * get: + * security: + * - bearerAuth: [] + * tags: [Redemption] + * summary: Get all redemption + * description: Get all redemption + * responses: + * 200: + * description: Redemption list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Redemption" + * 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 RedemptionDBApi.findAll(req.query, { currentUser }); + if (filetype && filetype === 'csv') { + const fields = ['id', 'pointsused', 'discountvalue']; + 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/redemption/count: + * get: + * security: + * - bearerAuth: [] + * tags: [Redemption] + * summary: Count all redemption + * description: Count all redemption + * responses: + * 200: + * description: Redemption count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Redemption" + * 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 RedemptionDBApi.findAll(req.query, null, { + countOnly: true, + currentUser, + }); + + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/redemption/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [Redemption] + * summary: Find all redemption that match search criteria + * description: Find all redemption that match search criteria + * responses: + * 200: + * description: Redemption list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Redemption" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/autocomplete', async (req, res) => { + const payload = await RedemptionDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/redemption/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Redemption] + * 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/Redemption" + * 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 RedemptionDBApi.findBy({ id: req.params.id }); + + res.status(200).send(payload); + }), +); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/routes/referral.js b/backend/src/routes/referral.js new file mode 100644 index 0000000..97e008c --- /dev/null +++ b/backend/src/routes/referral.js @@ -0,0 +1,439 @@ +const express = require('express'); + +const ReferralService = require('../services/referral'); +const ReferralDBApi = require('../db/api/referral'); +const wrapAsync = require('../helpers').wrapAsync; + +const router = express.Router(); + +const { parse } = require('json2csv'); + +const { checkCrudPermissions } = require('../middlewares/check-permissions'); + +router.use(checkCrudPermissions('referral')); + +/** + * @swagger + * components: + * schemas: + * Referral: + * type: object + * properties: + + * referredemail: + * type: string + * default: referredemail + + * + */ + +/** + * @swagger + * tags: + * name: Referral + * description: The Referral managing API + */ + +/** + * @swagger + * /api/referral: + * post: + * security: + * - bearerAuth: [] + * tags: [Referral] + * 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/Referral" + * responses: + * 200: + * description: The item was successfully added + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Referral" + * 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 ReferralService.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: [Referral] + * 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/Referral" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Referral" + * 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 ReferralService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/referral/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Referral] + * 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/Referral" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Referral" + * 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 ReferralService.update(req.body.data, req.body.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/referral/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Referral] + * 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/Referral" + * 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 ReferralService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/referral/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Referral] + * 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/Referral" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post( + '/deleteByIds', + wrapAsync(async (req, res) => { + await ReferralService.deleteByIds(req.body.data, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/referral: + * get: + * security: + * - bearerAuth: [] + * tags: [Referral] + * summary: Get all referral + * description: Get all referral + * responses: + * 200: + * description: Referral list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Referral" + * 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 ReferralDBApi.findAll(req.query, { currentUser }); + if (filetype && filetype === 'csv') { + const fields = ['id', 'referredemail']; + 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/referral/count: + * get: + * security: + * - bearerAuth: [] + * tags: [Referral] + * summary: Count all referral + * description: Count all referral + * responses: + * 200: + * description: Referral count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Referral" + * 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 ReferralDBApi.findAll(req.query, null, { + countOnly: true, + currentUser, + }); + + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/referral/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [Referral] + * summary: Find all referral that match search criteria + * description: Find all referral that match search criteria + * responses: + * 200: + * description: Referral list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Referral" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/autocomplete', async (req, res) => { + const payload = await ReferralDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/referral/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Referral] + * 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/Referral" + * 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 ReferralDBApi.findBy({ id: req.params.id }); + + res.status(200).send(payload); + }), +); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/routes/users.js b/backend/src/routes/users.js index a6d214f..f1bc61f 100644 --- a/backend/src/routes/users.js +++ b/backend/src/routes/users.js @@ -32,6 +32,13 @@ router.use(checkCrudPermissions('users')); * email: * type: string * default: email + * referralcode: + * type: string + * default: referralcode + + * pointsbalance: + * type: integer + * format: int64 */ @@ -308,7 +315,15 @@ router.get( const currentUser = req.currentUser; const payload = await UsersDBApi.findAll(req.query, { currentUser }); if (filetype && filetype === 'csv') { - const fields = ['id', 'firstName', 'lastName', 'phoneNumber', 'email']; + const fields = [ + 'id', + 'firstName', + 'lastName', + 'phoneNumber', + 'email', + 'referralcode', + 'pointsbalance', + ]; const opts = { fields }; try { const csv = parse(payload.rows, opts); diff --git a/backend/src/services/redemption.js b/backend/src/services/redemption.js new file mode 100644 index 0000000..c18a1c6 --- /dev/null +++ b/backend/src/services/redemption.js @@ -0,0 +1,114 @@ +const db = require('../db/models'); +const RedemptionDBApi = require('../db/api/redemption'); +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 RedemptionService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await RedemptionDBApi.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 RedemptionDBApi.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 redemption = await RedemptionDBApi.findBy({ id }, { transaction }); + + if (!redemption) { + throw new ValidationError('redemptionNotFound'); + } + + const updatedRedemption = await RedemptionDBApi.update(id, data, { + currentUser, + transaction, + }); + + await transaction.commit(); + return updatedRedemption; + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await RedemptionDBApi.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 RedemptionDBApi.remove(id, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; diff --git a/backend/src/services/referral.js b/backend/src/services/referral.js new file mode 100644 index 0000000..09ecc3e --- /dev/null +++ b/backend/src/services/referral.js @@ -0,0 +1,114 @@ +const db = require('../db/models'); +const ReferralDBApi = require('../db/api/referral'); +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 ReferralService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await ReferralDBApi.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 ReferralDBApi.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 referral = await ReferralDBApi.findBy({ id }, { transaction }); + + if (!referral) { + throw new ValidationError('referralNotFound'); + } + + const updatedReferral = await ReferralDBApi.update(id, data, { + currentUser, + transaction, + }); + + await transaction.commit(); + return updatedReferral; + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await ReferralDBApi.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 ReferralDBApi.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 5493e43..595c552 100644 --- a/backend/src/services/search.js +++ b/backend/src/services/search.js @@ -41,7 +41,17 @@ module.exports = class SearchService { throw new ValidationError('iam.errors.searchQueryRequired'); } const tableColumns = { - users: ['firstName', 'lastName', 'phoneNumber', 'email'], + users: [ + 'firstName', + + 'lastName', + + 'phoneNumber', + + 'email', + + 'referralcode', + ], agents: ['name', 'email', 'phone_number'], @@ -49,12 +59,20 @@ module.exports = class SearchService { vouchers: ['voucher_code'], - loyaltytier: ['name'], + loyaltytier: ['name', 'description'], + + referral: ['referredemail'], }; const columnsInt = { + users: ['pointsbalance'], + bookings: ['total_amount'], payments: ['amount'], + + loyaltytier: ['annualfee', 'pointspersar'], + + redemption: ['pointsused', 'discountvalue'], }; let allFoundRecords = []; diff --git a/frontend/src/components/Loyaltytier/CardLoyaltytier.tsx b/frontend/src/components/Loyaltytier/CardLoyaltytier.tsx index 1a82b54..dfa34a1 100644 --- a/frontend/src/components/Loyaltytier/CardLoyaltytier.tsx +++ b/frontend/src/components/Loyaltytier/CardLoyaltytier.tsx @@ -82,6 +82,39 @@ const CardLoyaltytier = ({
{item.name}
+ +
+
+ Annualfee +
+
+
+ {item.annualfee} +
+
+
+ +
+
+ Pointspersar +
+
+
+ {item.pointspersar} +
+
+
+ +
+
+ Description +
+
+
+ {item.description} +
+
+
))} diff --git a/frontend/src/components/Loyaltytier/ListLoyaltytier.tsx b/frontend/src/components/Loyaltytier/ListLoyaltytier.tsx index 9974953..9598b39 100644 --- a/frontend/src/components/Loyaltytier/ListLoyaltytier.tsx +++ b/frontend/src/components/Loyaltytier/ListLoyaltytier.tsx @@ -57,6 +57,21 @@ const ListLoyaltytier = ({

Name

{item.name}

+ +
+

Annualfee

+

{item.annualfee}

+
+ +
+

Pointspersar

+

{item.pointspersar}

+
+ +
+

Description

+

{item.description}

+
void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const CardRedemption = ({ + redemption, + 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_REDEMPTION'); + + return ( +
+ {loading && } +
    + {!loading && + redemption.map((item, index) => ( +
  • +
    + + {item.id} + + +
    + +
    +
    +
    +
    +
    User
    +
    +
    + {dataFormatter.usersOneListFormatter(item.user)} +
    +
    +
    + +
    +
    + Pointsused +
    +
    +
    + {item.pointsused} +
    +
    +
    + +
    +
    + Redemptiontype +
    +
    +
    + {item.redemptiontype} +
    +
    +
    + +
    +
    + Discountvalue +
    +
    +
    + {item.discountvalue} +
    +
    +
    + +
    +
    + Status +
    +
    +
    + {item.status} +
    +
    +
    + +
    +
    + Booking +
    +
    +
    + {dataFormatter.bookingsOneListFormatter(item.booking)} +
    +
    +
    +
    +
  • + ))} + {!loading && redemption.length === 0 && ( +
    +

    No data to display

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

User

+

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

+
+ +
+

Pointsused

+

{item.pointsused}

+
+ +
+

+ Redemptiontype +

+

{item.redemptiontype}

+
+ +
+

+ Discountvalue +

+

{item.discountvalue}

+
+ +
+

Status

+

{item.status}

+
+ +
+

Booking

+

+ {dataFormatter.bookingsOneListFormatter(item.booking)} +

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

No data to display

+
+ )} +
+
+ +
+ + ); +}; + +export default ListRedemption; diff --git a/frontend/src/components/Redemption/TableRedemption.tsx b/frontend/src/components/Redemption/TableRedemption.tsx new file mode 100644 index 0000000..7fcc7f5 --- /dev/null +++ b/frontend/src/components/Redemption/TableRedemption.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/redemption/redemptionSlice'; +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 './configureRedemptionCols'; +import _ from 'lodash'; +import dataFormatter from '../../helpers/dataFormatter'; +import { dataGridStyles } from '../../styles'; + +const perPage = 10; + +const TableSampleRedemption = ({ + 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 { + redemption, + loading, + count, + notify: redemptionNotify, + refetch, + } = useAppSelector((state) => state.redemption); + 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 (redemptionNotify.showNotification) { + notify( + redemptionNotify.typeNotification, + redemptionNotify.textNotification, + ); + } + }, [redemptionNotify.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, `redemption`, 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={redemption ?? []} + 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 TableSampleRedemption; diff --git a/frontend/src/components/Redemption/configureRedemptionCols.tsx b/frontend/src/components/Redemption/configureRedemptionCols.tsx new file mode 100644 index 0000000..7bf49eb --- /dev/null +++ b/frontend/src/components/Redemption/configureRedemptionCols.tsx @@ -0,0 +1,160 @@ +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_REDEMPTION'); + + 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: 'pointsused', + headerName: 'Pointsused', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + type: 'number', + }, + + { + field: 'redemptiontype', + headerName: 'Redemptiontype', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + type: 'singleSelect', + valueOptions: ['value'], + }, + + { + field: 'discountvalue', + headerName: 'Discountvalue', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + type: 'number', + }, + + { + field: 'status', + headerName: 'Status', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + type: 'singleSelect', + valueOptions: ['value'], + }, + + { + field: 'booking', + headerName: 'Booking', + 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('bookings'), + 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/Referral/CardReferral.tsx b/frontend/src/components/Referral/CardReferral.tsx new file mode 100644 index 0000000..e71b95b --- /dev/null +++ b/frontend/src/components/Referral/CardReferral.tsx @@ -0,0 +1,131 @@ +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 = { + referral: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const CardReferral = ({ + referral, + 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_REFERRAL'); + + return ( +
+ {loading && } +
    + {!loading && + referral.map((item, index) => ( +
  • +
    + + {item.id} + + +
    + +
    +
    +
    +
    +
    + Referrer +
    +
    +
    + {dataFormatter.usersOneListFormatter(item.referrer)} +
    +
    +
    + +
    +
    + Referredemail +
    +
    +
    + {item.referredemail} +
    +
    +
    + +
    +
    + Status +
    +
    +
    + {item.status} +
    +
    +
    +
    +
  • + ))} + {!loading && referral.length === 0 && ( +
    +

    No data to display

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

Referrer

+

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

+
+ +
+

+ Referredemail +

+

{item.referredemail}

+
+ +
+

Status

+

{item.status}

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

No data to display

+
+ )} +
+
+ +
+ + ); +}; + +export default ListReferral; diff --git a/frontend/src/components/Referral/TableReferral.tsx b/frontend/src/components/Referral/TableReferral.tsx new file mode 100644 index 0000000..6eff4c8 --- /dev/null +++ b/frontend/src/components/Referral/TableReferral.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/referral/referralSlice'; +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 './configureReferralCols'; +import _ from 'lodash'; +import dataFormatter from '../../helpers/dataFormatter'; +import { dataGridStyles } from '../../styles'; + +const perPage = 10; + +const TableSampleReferral = ({ + 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 { + referral, + loading, + count, + notify: referralNotify, + refetch, + } = useAppSelector((state) => state.referral); + 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 (referralNotify.showNotification) { + notify(referralNotify.typeNotification, referralNotify.textNotification); + } + }, [referralNotify.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, `referral`, 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={referral ?? []} + 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 TableSampleReferral; diff --git a/frontend/src/components/Referral/configureReferralCols.tsx b/frontend/src/components/Referral/configureReferralCols.tsx new file mode 100644 index 0000000..ee2a9ec --- /dev/null +++ b/frontend/src/components/Referral/configureReferralCols.tsx @@ -0,0 +1,109 @@ +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_REFERRAL'); + + return [ + { + field: 'referrer', + headerName: 'Referrer', + 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: 'referredemail', + headerName: 'Referredemail', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + + { + field: 'status', + headerName: 'Status', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + type: 'singleSelect', + valueOptions: ['value'], + }, + + { + field: 'actions', + type: 'actions', + minWidth: 30, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + getActions: (params: GridRowParams) => { + return [ +
+ +
, + ]; + }, + }, + ]; +}; diff --git a/frontend/src/components/Users/CardUsers.tsx b/frontend/src/components/Users/CardUsers.tsx index 91f2dda..4d86d52 100644 --- a/frontend/src/components/Users/CardUsers.tsx +++ b/frontend/src/components/Users/CardUsers.tsx @@ -173,6 +173,41 @@ const CardUsers = ({ + +
+
+ Loyaltytier +
+
+
+ {dataFormatter.loyaltytierOneListFormatter( + item.loyaltytier, + )} +
+
+
+ +
+
+ Pointsbalance +
+
+
+ {item.pointsbalance} +
+
+
+ +
+
+ Referralcode +
+
+
+ {item.referralcode} +
+
+
))} diff --git a/frontend/src/components/Users/ListUsers.tsx b/frontend/src/components/Users/ListUsers.tsx index 2533b8a..d2c2bb8 100644 --- a/frontend/src/components/Users/ListUsers.tsx +++ b/frontend/src/components/Users/ListUsers.tsx @@ -115,6 +115,27 @@ const ListUsers = ({ .join(', ')}

+ +
+

Loyaltytier

+

+ {dataFormatter.loyaltytierOneListFormatter( + item.loyaltytier, + )} +

+
+ +
+

+ Pointsbalance +

+

{item.pointsbalance}

+
+ +
+

Referralcode

+

{item.referralcode}

+
value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('loyaltytier'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + }, + + { + field: 'pointsbalance', + headerName: 'Pointsbalance', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + type: 'number', + }, + + { + field: 'referralcode', + headerName: 'Referralcode', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + { field: 'actions', type: 'actions', diff --git a/frontend/src/components/WebPageComponents/Footer.tsx b/frontend/src/components/WebPageComponents/Footer.tsx index 8a84dff..bd9c5a1 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_PAGES; + const style = FooterStyle.WITH_PROJECT_NAME; const design = FooterDesigns.DEFAULT_DESIGN; diff --git a/frontend/src/components/WebPageComponents/Header.tsx b/frontend/src/components/WebPageComponents/Header.tsx index ebfb323..7feed61 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.DESIGN_DIVERSITY; + const design = HeaderDesigns.DEFAULT_DESIGN; return (
item.firstName); + }, + usersOneListFormatter(val) { + if (!val) return ''; + return val.firstName; + }, + usersManyListFormatterEdit(val) { + if (!val || !val.length) return []; + return val.map((item) => { + return { id: item.id, label: item.firstName }; + }); + }, + usersOneListFormatterEdit(val) { + if (!val) return ''; + return { label: val.firstName, id: val.id }; + }, + agentsManyListFormatter(val) { if (!val || !val.length) return []; return val.map((item) => item.name); @@ -133,4 +152,23 @@ export default { if (!val) return ''; return { label: val.name, id: val.id }; }, + + loyaltytierManyListFormatter(val) { + if (!val || !val.length) return []; + return val.map((item) => item.name); + }, + loyaltytierOneListFormatter(val) { + if (!val) return ''; + return val.name; + }, + loyaltytierManyListFormatterEdit(val) { + if (!val || !val.length) return []; + return val.map((item) => { + return { id: item.id, label: item.name }; + }); + }, + loyaltytierOneListFormatterEdit(val) { + if (!val) return ''; + return { label: val.name, id: val.id }; + }, }; diff --git a/frontend/src/menuAside.ts b/frontend/src/menuAside.ts index d8e286a..a58fa17 100644 --- a/frontend/src/menuAside.ts +++ b/frontend/src/menuAside.ts @@ -95,6 +95,22 @@ const menuAside: MenuAsideItem[] = [ icon: icon.mdiTable ?? icon.mdiTable, permissions: 'READ_LOYALTYTIER', }, + { + href: '/referral/referral-list', + label: 'Referral', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: icon.mdiTable ?? icon.mdiTable, + permissions: 'READ_REFERRAL', + }, + { + href: '/redemption/redemption-list', + label: 'Redemption', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: icon.mdiTable ?? icon.mdiTable, + permissions: 'READ_REDEMPTION', + }, { href: '/profile', label: 'Profile', diff --git a/frontend/src/pages/bookings/bookings-view.tsx b/frontend/src/pages/bookings/bookings-view.tsx index aca2f70..8e67b85 100644 --- a/frontend/src/pages/bookings/bookings-view.tsx +++ b/frontend/src/pages/bookings/bookings-view.tsx @@ -185,6 +185,59 @@ const BookingsView = () => { + <> +

Redemption Booking

+ +
+ + + + + + + + + + + + + + {bookings.redemption_booking && + Array.isArray(bookings.redemption_booking) && + bookings.redemption_booking.map((item: any) => ( + + router.push( + `/redemption/redemption-view/?id=${item.id}`, + ) + } + > + + + + + + + + + ))} + +
PointsusedRedemptiontypeDiscountvalueStatus
{item.pointsused} + {item.redemptiontype} + + {item.discountvalue} + {item.status}
+
+ {!bookings?.redemption_booking?.length && ( +
No data
+ )} +
+ + { const [roles, setRoles] = React.useState(loadingMessage); const [permissions, setPermissions] = React.useState(loadingMessage); const [loyaltytier, setLoyaltytier] = React.useState(loadingMessage); + const [referral, setReferral] = React.useState(loadingMessage); + const [redemption, setRedemption] = React.useState(loadingMessage); const [widgetsRole, setWidgetsRole] = React.useState({ role: { value: '', label: '' }, @@ -57,6 +59,8 @@ const Dashboard = () => { 'roles', 'permissions', 'loyaltytier', + 'referral', + 'redemption', ]; const fns = [ setUsers, @@ -68,6 +72,8 @@ const Dashboard = () => { setRoles, setPermissions, setLoyaltytier, + setReferral, + setRedemption, ]; const requests = entities.map((entity, index) => { @@ -491,6 +497,70 @@ const Dashboard = () => {
)} + + {hasPermission(currentUser, 'READ_REFERRAL') && ( + +
+
+
+
+ Referral +
+
+ {referral} +
+
+
+ +
+
+
+ + )} + + {hasPermission(currentUser, 'READ_REDEMPTION') && ( + +
+
+
+
+ Redemption +
+
+ {redemption} +
+
+
+ +
+
+
+ + )} diff --git a/frontend/src/pages/loyaltytier/[loyaltytierId].tsx b/frontend/src/pages/loyaltytier/[loyaltytierId].tsx index 51713f4..5702a7e 100644 --- a/frontend/src/pages/loyaltytier/[loyaltytierId].tsx +++ b/frontend/src/pages/loyaltytier/[loyaltytierId].tsx @@ -37,6 +37,12 @@ const EditLoyaltytier = () => { const dispatch = useAppDispatch(); const initVals = { name: '', + + annualfee: '', + + pointspersar: '', + + description: '', }; const [initialValues, setInitialValues] = useState(initVals); @@ -95,6 +101,22 @@ const EditLoyaltytier = () => { + + + + + + + + + + + + diff --git a/frontend/src/pages/loyaltytier/loyaltytier-edit.tsx b/frontend/src/pages/loyaltytier/loyaltytier-edit.tsx index cfd1213..6ac3fff 100644 --- a/frontend/src/pages/loyaltytier/loyaltytier-edit.tsx +++ b/frontend/src/pages/loyaltytier/loyaltytier-edit.tsx @@ -37,6 +37,12 @@ const EditLoyaltytierPage = () => { const dispatch = useAppDispatch(); const initVals = { name: '', + + annualfee: '', + + pointspersar: '', + + description: '', }; const [initialValues, setInitialValues] = useState(initVals); @@ -93,6 +99,22 @@ const EditLoyaltytierPage = () => { + + + + + + + + + + + + diff --git a/frontend/src/pages/loyaltytier/loyaltytier-list.tsx b/frontend/src/pages/loyaltytier/loyaltytier-list.tsx index 72689be..47a34a1 100644 --- a/frontend/src/pages/loyaltytier/loyaltytier-list.tsx +++ b/frontend/src/pages/loyaltytier/loyaltytier-list.tsx @@ -31,7 +31,13 @@ const LoyaltytierTablesPage = () => { const dispatch = useAppDispatch(); - const [filters] = useState([{ label: 'Name', title: 'name' }]); + const [filters] = useState([ + { label: 'Name', title: 'name' }, + { label: 'Description', title: 'description' }, + + { label: 'Annualfee', title: 'annualfee', number: 'true' }, + { label: 'Pointspersar', title: 'pointspersar', number: 'true' }, + ]); const hasCreatePermission = currentUser && hasPermission(currentUser, 'CREATE_LOYALTYTIER'); diff --git a/frontend/src/pages/loyaltytier/loyaltytier-new.tsx b/frontend/src/pages/loyaltytier/loyaltytier-new.tsx index 91458f3..e266f93 100644 --- a/frontend/src/pages/loyaltytier/loyaltytier-new.tsx +++ b/frontend/src/pages/loyaltytier/loyaltytier-new.tsx @@ -34,6 +34,12 @@ import moment from 'moment'; const initialValues = { name: '', + + annualfee: '', + + pointspersar: '', + + description: '', }; const LoyaltytierNew = () => { @@ -67,6 +73,22 @@ const LoyaltytierNew = () => { + + + + + + + + + + + + diff --git a/frontend/src/pages/loyaltytier/loyaltytier-table.tsx b/frontend/src/pages/loyaltytier/loyaltytier-table.tsx index dbfabab..cd51786 100644 --- a/frontend/src/pages/loyaltytier/loyaltytier-table.tsx +++ b/frontend/src/pages/loyaltytier/loyaltytier-table.tsx @@ -31,7 +31,13 @@ const LoyaltytierTablesPage = () => { const dispatch = useAppDispatch(); - const [filters] = useState([{ label: 'Name', title: 'name' }]); + const [filters] = useState([ + { label: 'Name', title: 'name' }, + { label: 'Description', title: 'description' }, + + { label: 'Annualfee', title: 'annualfee', number: 'true' }, + { label: 'Pointspersar', title: 'pointspersar', number: 'true' }, + ]); const hasCreatePermission = currentUser && hasPermission(currentUser, 'CREATE_LOYALTYTIER'); diff --git a/frontend/src/pages/loyaltytier/loyaltytier-view.tsx b/frontend/src/pages/loyaltytier/loyaltytier-view.tsx index 136711e..fc2e9d7 100644 --- a/frontend/src/pages/loyaltytier/loyaltytier-view.tsx +++ b/frontend/src/pages/loyaltytier/loyaltytier-view.tsx @@ -59,6 +59,84 @@ const LoyaltytierView = () => {

{loyaltytier?.name}

+
+

Annualfee

+

{loyaltytier?.annualfee || 'No data'}

+
+ +
+

Pointspersar

+

{loyaltytier?.pointspersar || 'No data'}

+
+ +
+

Description

+

{loyaltytier?.description}

+
+ + <> +

Users Loyaltytier

+ +
+ + + + + + + + + + + + + + + + + + + + {loyaltytier.users_loyaltytier && + Array.isArray(loyaltytier.users_loyaltytier) && + loyaltytier.users_loyaltytier.map((item: any) => ( + + router.push(`/users/users-view/?id=${item.id}`) + } + > + + + + + + + + + + + + + + + ))} + +
First NameLast NamePhone NumberE-MailDisabledPointsbalanceReferralcode
{item.firstName}{item.lastName}{item.phoneNumber}{item.email} + {dataFormatter.booleanFormatter(item.disabled)} + + {item.pointsbalance} + {item.referralcode}
+
+ {!loyaltytier?.users_loyaltytier?.length && ( +
No data
+ )} +
+ + { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + user: null, + + pointsused: '', + + redemptiontype: '', + + discountvalue: '', + + status: '', + + booking: null, + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { redemption } = useAppSelector((state) => state.redemption); + + const { redemptionId } = router.query; + + useEffect(() => { + dispatch(fetch({ id: redemptionId })); + }, [redemptionId]); + + useEffect(() => { + if (typeof redemption === 'object') { + setInitialValues(redemption); + } + }, [redemption]); + + useEffect(() => { + if (typeof redemption === 'object') { + const newInitialVal = { ...initVals }; + + Object.keys(initVals).forEach( + (el) => (newInitialVal[el] = redemption[el]), + ); + + setInitialValues(newInitialVal); + } + }, [redemption]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: redemptionId, data })); + await router.push('/redemption/redemption-list'); + }; + + return ( + <> + + {getPageTitle('Edit redemption')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + router.push('/redemption/redemption-list')} + /> + + +
+
+
+ + ); +}; + +EditRedemption.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditRedemption; diff --git a/frontend/src/pages/redemption/redemption-edit.tsx b/frontend/src/pages/redemption/redemption-edit.tsx new file mode 100644 index 0000000..a8fe30f --- /dev/null +++ b/frontend/src/pages/redemption/redemption-edit.tsx @@ -0,0 +1,184 @@ +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/redemption/redemptionSlice'; +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 EditRedemptionPage = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + user: null, + + pointsused: '', + + redemptiontype: '', + + discountvalue: '', + + status: '', + + booking: null, + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { redemption } = useAppSelector((state) => state.redemption); + + const { id } = router.query; + + useEffect(() => { + dispatch(fetch({ id: id })); + }, [id]); + + useEffect(() => { + if (typeof redemption === 'object') { + setInitialValues(redemption); + } + }, [redemption]); + + useEffect(() => { + if (typeof redemption === 'object') { + const newInitialVal = { ...initVals }; + Object.keys(initVals).forEach( + (el) => (newInitialVal[el] = redemption[el]), + ); + setInitialValues(newInitialVal); + } + }, [redemption]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: id, data })); + await router.push('/redemption/redemption-list'); + }; + + return ( + <> + + {getPageTitle('Edit redemption')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + router.push('/redemption/redemption-list')} + /> + + +
+
+
+ + ); +}; + +EditRedemptionPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditRedemptionPage; diff --git a/frontend/src/pages/redemption/redemption-list.tsx b/frontend/src/pages/redemption/redemption-list.tsx new file mode 100644 index 0000000..0cb8204 --- /dev/null +++ b/frontend/src/pages/redemption/redemption-list.tsx @@ -0,0 +1,177 @@ +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 TableRedemption from '../../components/Redemption/TableRedemption'; +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/redemption/redemptionSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const RedemptionTablesPage = () => { + 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: 'Pointsused', title: 'pointsused', number: 'true' }, + { label: 'Discountvalue', title: 'discountvalue', number: 'true' }, + + { label: 'User', title: 'user' }, + + { label: 'Booking', title: 'booking' }, + + { + label: 'Redemptiontype', + title: 'redemptiontype', + type: 'enum', + options: ['value'], + }, + { label: 'Status', title: 'status', type: 'enum', options: ['value'] }, + ]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_REDEMPTION'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getRedemptionCSV = async () => { + const response = await axios({ + url: '/redemption?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 = 'redemptionCSV.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('Redemption')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
+
+
+
+ + + + +
+ + + + + ); +}; + +RedemptionTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default RedemptionTablesPage; diff --git a/frontend/src/pages/redemption/redemption-new.tsx b/frontend/src/pages/redemption/redemption-new.tsx new file mode 100644 index 0000000..5e4e538 --- /dev/null +++ b/frontend/src/pages/redemption/redemption-new.tsx @@ -0,0 +1,156 @@ +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/redemption/redemptionSlice'; +import { useAppDispatch } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import moment from 'moment'; + +const initialValues = { + user: '', + + pointsused: '', + + redemptiontype: '', + + discountvalue: '', + + status: '', + + booking: '', +}; + +const RedemptionNew = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + + const handleSubmit = async (data) => { + await dispatch(create(data)); + await router.push('/redemption/redemption-list'); + }; + return ( + <> + + {getPageTitle('New Item')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + router.push('/redemption/redemption-list')} + /> + + +
+
+
+ + ); +}; + +RedemptionNew.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default RedemptionNew; diff --git a/frontend/src/pages/redemption/redemption-table.tsx b/frontend/src/pages/redemption/redemption-table.tsx new file mode 100644 index 0000000..20215e8 --- /dev/null +++ b/frontend/src/pages/redemption/redemption-table.tsx @@ -0,0 +1,176 @@ +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 TableRedemption from '../../components/Redemption/TableRedemption'; +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/redemption/redemptionSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const RedemptionTablesPage = () => { + 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: 'Pointsused', title: 'pointsused', number: 'true' }, + { label: 'Discountvalue', title: 'discountvalue', number: 'true' }, + + { label: 'User', title: 'user' }, + + { label: 'Booking', title: 'booking' }, + + { + label: 'Redemptiontype', + title: 'redemptiontype', + type: 'enum', + options: ['value'], + }, + { label: 'Status', title: 'status', type: 'enum', options: ['value'] }, + ]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_REDEMPTION'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getRedemptionCSV = async () => { + const response = await axios({ + url: '/redemption?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 = 'redemptionCSV.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('Redemption')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
+
+
+
+ + + +
+ + + + + ); +}; + +RedemptionTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default RedemptionTablesPage; diff --git a/frontend/src/pages/redemption/redemption-view.tsx b/frontend/src/pages/redemption/redemption-view.tsx new file mode 100644 index 0000000..94f8c8c --- /dev/null +++ b/frontend/src/pages/redemption/redemption-view.tsx @@ -0,0 +1,110 @@ +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/redemption/redemptionSlice'; +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 RedemptionView = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const { redemption } = useAppSelector((state) => state.redemption); + + 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 redemption')} + + + + + + +
+

User

+ +

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

+
+ +
+

Pointsused

+

{redemption?.pointsused || 'No data'}

+
+ +
+

Redemptiontype

+

{redemption?.redemptiontype ?? 'No data'}

+
+ +
+

Discountvalue

+

{redemption?.discountvalue || 'No data'}

+
+ +
+

Status

+

{redemption?.status ?? 'No data'}

+
+ +
+

Booking

+ +

{redemption?.booking?.service_type ?? 'No data'}

+
+ + + + router.push('/redemption/redemption-list')} + /> +
+
+ + ); +}; + +RedemptionView.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default RedemptionView; diff --git a/frontend/src/pages/referral/[referralId].tsx b/frontend/src/pages/referral/[referralId].tsx new file mode 100644 index 0000000..88edbc4 --- /dev/null +++ b/frontend/src/pages/referral/[referralId].tsx @@ -0,0 +1,147 @@ +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/referral/referralSlice'; +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 EditReferral = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + referrer: null, + + referredemail: '', + + status: '', + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { referral } = useAppSelector((state) => state.referral); + + const { referralId } = router.query; + + useEffect(() => { + dispatch(fetch({ id: referralId })); + }, [referralId]); + + useEffect(() => { + if (typeof referral === 'object') { + setInitialValues(referral); + } + }, [referral]); + + useEffect(() => { + if (typeof referral === 'object') { + const newInitialVal = { ...initVals }; + + Object.keys(initVals).forEach((el) => (newInitialVal[el] = referral[el])); + + setInitialValues(newInitialVal); + } + }, [referral]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: referralId, data })); + await router.push('/referral/referral-list'); + }; + + return ( + <> + + {getPageTitle('Edit referral')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + + + + + + + + + + + + + + + + + router.push('/referral/referral-list')} + /> + + +
+
+
+ + ); +}; + +EditReferral.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditReferral; diff --git a/frontend/src/pages/referral/referral-edit.tsx b/frontend/src/pages/referral/referral-edit.tsx new file mode 100644 index 0000000..7430389 --- /dev/null +++ b/frontend/src/pages/referral/referral-edit.tsx @@ -0,0 +1,145 @@ +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/referral/referralSlice'; +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 EditReferralPage = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + referrer: null, + + referredemail: '', + + status: '', + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { referral } = useAppSelector((state) => state.referral); + + const { id } = router.query; + + useEffect(() => { + dispatch(fetch({ id: id })); + }, [id]); + + useEffect(() => { + if (typeof referral === 'object') { + setInitialValues(referral); + } + }, [referral]); + + useEffect(() => { + if (typeof referral === 'object') { + const newInitialVal = { ...initVals }; + Object.keys(initVals).forEach((el) => (newInitialVal[el] = referral[el])); + setInitialValues(newInitialVal); + } + }, [referral]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: id, data })); + await router.push('/referral/referral-list'); + }; + + return ( + <> + + {getPageTitle('Edit referral')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + + + + + + + + + + + + + + + + + router.push('/referral/referral-list')} + /> + + +
+
+
+ + ); +}; + +EditReferralPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditReferralPage; diff --git a/frontend/src/pages/referral/referral-list.tsx b/frontend/src/pages/referral/referral-list.tsx new file mode 100644 index 0000000..c3391f2 --- /dev/null +++ b/frontend/src/pages/referral/referral-list.tsx @@ -0,0 +1,168 @@ +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 TableReferral from '../../components/Referral/TableReferral'; +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/referral/referralSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const ReferralTablesPage = () => { + 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: 'Referredemail', title: 'referredemail' }, + + { label: 'Referrer', title: 'referrer' }, + + { label: 'Status', title: 'status', type: 'enum', options: ['value'] }, + ]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_REFERRAL'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getReferralCSV = async () => { + const response = await axios({ + url: '/referral?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 = 'referralCSV.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('Referral')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
+
+
+
+ + + + +
+ + + + + ); +}; + +ReferralTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default ReferralTablesPage; diff --git a/frontend/src/pages/referral/referral-new.tsx b/frontend/src/pages/referral/referral-new.tsx new file mode 100644 index 0000000..a578d8b --- /dev/null +++ b/frontend/src/pages/referral/referral-new.tsx @@ -0,0 +1,120 @@ +import { + mdiAccount, + mdiChartTimelineVariant, + mdiMail, + mdiUpload, +} from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SwitchField } from '../../components/SwitchField'; + +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { RichTextField } from '../../components/RichTextField'; + +import { create } from '../../stores/referral/referralSlice'; +import { useAppDispatch } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import moment from 'moment'; + +const initialValues = { + referrer: '', + + referredemail: '', + + status: '', +}; + +const ReferralNew = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + + const handleSubmit = async (data) => { + await dispatch(create(data)); + await router.push('/referral/referral-list'); + }; + return ( + <> + + {getPageTitle('New Item')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + + + + + + + + + + + + + + + + + router.push('/referral/referral-list')} + /> + + +
+
+
+ + ); +}; + +ReferralNew.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default ReferralNew; diff --git a/frontend/src/pages/referral/referral-table.tsx b/frontend/src/pages/referral/referral-table.tsx new file mode 100644 index 0000000..56ade11 --- /dev/null +++ b/frontend/src/pages/referral/referral-table.tsx @@ -0,0 +1,167 @@ +import { mdiChartTimelineVariant } from '@mdi/js'; +import Head from 'next/head'; +import { uniqueId } from 'lodash'; +import React, { ReactElement, useState } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; +import TableReferral from '../../components/Referral/TableReferral'; +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/referral/referralSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const ReferralTablesPage = () => { + 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: 'Referredemail', title: 'referredemail' }, + + { label: 'Referrer', title: 'referrer' }, + + { label: 'Status', title: 'status', type: 'enum', options: ['value'] }, + ]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_REFERRAL'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getReferralCSV = async () => { + const response = await axios({ + url: '/referral?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 = 'referralCSV.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('Referral')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
+
+
+
+ + + +
+ + + + + ); +}; + +ReferralTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default ReferralTablesPage; diff --git a/frontend/src/pages/referral/referral-view.tsx b/frontend/src/pages/referral/referral-view.tsx new file mode 100644 index 0000000..d188b3c --- /dev/null +++ b/frontend/src/pages/referral/referral-view.tsx @@ -0,0 +1,94 @@ +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/referral/referralSlice'; +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 ReferralView = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const { referral } = useAppSelector((state) => state.referral); + + 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 referral')} + + + + + + +
+

Referrer

+ +

{referral?.referrer?.firstName ?? 'No data'}

+
+ +
+

Referredemail

+

{referral?.referredemail}

+
+ +
+

Status

+

{referral?.status ?? 'No data'}

+
+ + + + router.push('/referral/referral-list')} + /> +
+
+ + ); +}; + +ReferralView.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default ReferralView; diff --git a/frontend/src/pages/roles/roles-view.tsx b/frontend/src/pages/roles/roles-view.tsx index b7368ff..1a75133 100644 --- a/frontend/src/pages/roles/roles-view.tsx +++ b/frontend/src/pages/roles/roles-view.tsx @@ -115,6 +115,10 @@ const RolesView = () => { E-Mail Disabled + + Pointsbalance + + Referralcode @@ -138,6 +142,12 @@ const RolesView = () => { {dataFormatter.booleanFormatter(item.disabled)} + + + {item.pointsbalance} + + + {item.referralcode} ))} diff --git a/frontend/src/pages/users/[usersId].tsx b/frontend/src/pages/users/[usersId].tsx index e16077f..a21bdd8 100644 --- a/frontend/src/pages/users/[usersId].tsx +++ b/frontend/src/pages/users/[usersId].tsx @@ -52,6 +52,12 @@ const EditUsers = () => { custom_permissions: [], + loyaltytier: null, + + pointsbalance: '', + + referralcode: '', + password: '', }; const [initialValues, setInitialValues] = useState(initVals); @@ -170,6 +176,29 @@ const EditUsers = () => { >
+ + + + + + + + + + + + diff --git a/frontend/src/pages/users/users-edit.tsx b/frontend/src/pages/users/users-edit.tsx index 87d7090..7baef37 100644 --- a/frontend/src/pages/users/users-edit.tsx +++ b/frontend/src/pages/users/users-edit.tsx @@ -52,6 +52,12 @@ const EditUsersPage = () => { custom_permissions: [], + loyaltytier: null, + + pointsbalance: '', + + referralcode: '', + password: '', }; const [initialValues, setInitialValues] = useState(initVals); @@ -168,6 +174,29 @@ const EditUsersPage = () => { >
+ + + + + + + + + + + + diff --git a/frontend/src/pages/users/users-list.tsx b/frontend/src/pages/users/users-list.tsx index 6d03ebc..4768be2 100644 --- a/frontend/src/pages/users/users-list.tsx +++ b/frontend/src/pages/users/users-list.tsx @@ -33,9 +33,13 @@ const UsersTablesPage = () => { { label: 'Last Name', title: 'lastName' }, { label: 'Phone Number', title: 'phoneNumber' }, { label: 'E-Mail', title: 'email' }, + { label: 'Referralcode', title: 'referralcode' }, + { label: 'Pointsbalance', title: 'pointsbalance', number: 'true' }, { label: 'App Role', title: 'app_role' }, + { label: 'Loyaltytier', title: 'loyaltytier' }, + { label: 'Custom Permissions', title: 'custom_permissions' }, ]); diff --git a/frontend/src/pages/users/users-new.tsx b/frontend/src/pages/users/users-new.tsx index 4f3fa45..a7649dd 100644 --- a/frontend/src/pages/users/users-new.tsx +++ b/frontend/src/pages/users/users-new.tsx @@ -48,6 +48,12 @@ const initialValues = { app_role: '', custom_permissions: [], + + loyaltytier: '', + + pointsbalance: '', + + referralcode: '', }; const UsersNew = () => { @@ -140,6 +146,28 @@ const UsersNew = () => { >
+ + + + + + + + + + + + diff --git a/frontend/src/pages/users/users-table.tsx b/frontend/src/pages/users/users-table.tsx index a408cd6..bcf1102 100644 --- a/frontend/src/pages/users/users-table.tsx +++ b/frontend/src/pages/users/users-table.tsx @@ -33,9 +33,13 @@ const UsersTablesPage = () => { { label: 'Last Name', title: 'lastName' }, { label: 'Phone Number', title: 'phoneNumber' }, { label: 'E-Mail', title: 'email' }, + { label: 'Referralcode', title: 'referralcode' }, + { label: 'Pointsbalance', title: 'pointsbalance', number: 'true' }, { label: 'App Role', title: 'app_role' }, + { label: 'Loyaltytier', title: 'loyaltytier' }, + { label: 'Custom Permissions', title: 'custom_permissions' }, ]); diff --git a/frontend/src/pages/users/users-view.tsx b/frontend/src/pages/users/users-view.tsx index ce0851d..b7260b4 100644 --- a/frontend/src/pages/users/users-view.tsx +++ b/frontend/src/pages/users/users-view.tsx @@ -138,6 +138,118 @@ const UsersView = () => { +
+

Loyaltytier

+ +

{users?.loyaltytier?.name ?? 'No data'}

+
+ +
+

Pointsbalance

+

{users?.pointsbalance || 'No data'}

+
+ +
+

Referralcode

+

{users?.referralcode}

+
+ + <> +

Referral Referrer

+ +
+ + + + + + + + + + {users.referral_referrer && + Array.isArray(users.referral_referrer) && + users.referral_referrer.map((item: any) => ( + + router.push( + `/referral/referral-view/?id=${item.id}`, + ) + } + > + + + + + ))} + +
ReferredemailStatus
+ {item.referredemail} + {item.status}
+
+ {!users?.referral_referrer?.length && ( +
No data
+ )} +
+ + + <> +

Redemption User

+ +
+ + + + + + + + + + + + + + {users.redemption_user && + Array.isArray(users.redemption_user) && + users.redemption_user.map((item: any) => ( + + router.push( + `/redemption/redemption-view/?id=${item.id}`, + ) + } + > + + + + + + + + + ))} + +
PointsusedRedemptiontypeDiscountvalueStatus
{item.pointsused} + {item.redemptiontype} + + {item.discountvalue} + {item.status}
+
+ {!users?.redemption_user?.length && ( +
No data
+ )} +
+ + { + const { id, query } = data; + const result = await axios.get(`redemption${query || (id ? `/${id}` : '')}`); + return id + ? result.data + : { rows: result.data.rows, count: result.data.count }; +}); + +export const deleteItemsByIds = createAsyncThunk( + 'redemption/deleteByIds', + async (data: any, { rejectWithValue }) => { + try { + await axios.post('redemption/deleteByIds', { data }); + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const deleteItem = createAsyncThunk( + 'redemption/deleteRedemption', + async (id: string, { rejectWithValue }) => { + try { + await axios.delete(`redemption/${id}`); + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const create = createAsyncThunk( + 'redemption/createRedemption', + async (data: any, { rejectWithValue }) => { + try { + const result = await axios.post('redemption', { data }); + return result.data; + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const uploadCsv = createAsyncThunk( + 'redemption/uploadCsv', + async (file: File, { rejectWithValue }) => { + try { + const data = new FormData(); + data.append('file', file); + data.append('filename', file.name); + + const result = await axios.post('redemption/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( + 'redemption/updateRedemption', + async (payload: any, { rejectWithValue }) => { + try { + const result = await axios.put(`redemption/${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 redemptionSlice = createSlice({ + name: 'redemption', + 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.redemption = action.payload.rows; + state.count = action.payload.count; + } else { + state.redemption = 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, 'Redemption 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, `${'Redemption'.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, `${'Redemption'.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, `${'Redemption'.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, 'Redemption 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 } = redemptionSlice.actions; + +export default redemptionSlice.reducer; diff --git a/frontend/src/stores/referral/referralSlice.ts b/frontend/src/stores/referral/referralSlice.ts new file mode 100644 index 0000000..5f50910 --- /dev/null +++ b/frontend/src/stores/referral/referralSlice.ts @@ -0,0 +1,236 @@ +import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit'; +import axios from 'axios'; +import { + fulfilledNotify, + rejectNotify, + resetNotify, +} from '../../helpers/notifyStateHandler'; + +interface MainState { + referral: any; + loading: boolean; + count: number; + refetch: boolean; + rolesWidgets: any[]; + notify: { + showNotification: boolean; + textNotification: string; + typeNotification: string; + }; +} + +const initialState: MainState = { + referral: [], + loading: false, + count: 0, + refetch: false, + rolesWidgets: [], + notify: { + showNotification: false, + textNotification: '', + typeNotification: 'warn', + }, +}; + +export const fetch = createAsyncThunk('referral/fetch', async (data: any) => { + const { id, query } = data; + const result = await axios.get(`referral${query || (id ? `/${id}` : '')}`); + return id + ? result.data + : { rows: result.data.rows, count: result.data.count }; +}); + +export const deleteItemsByIds = createAsyncThunk( + 'referral/deleteByIds', + async (data: any, { rejectWithValue }) => { + try { + await axios.post('referral/deleteByIds', { data }); + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const deleteItem = createAsyncThunk( + 'referral/deleteReferral', + async (id: string, { rejectWithValue }) => { + try { + await axios.delete(`referral/${id}`); + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const create = createAsyncThunk( + 'referral/createReferral', + async (data: any, { rejectWithValue }) => { + try { + const result = await axios.post('referral', { data }); + return result.data; + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const uploadCsv = createAsyncThunk( + 'referral/uploadCsv', + async (file: File, { rejectWithValue }) => { + try { + const data = new FormData(); + data.append('file', file); + data.append('filename', file.name); + + const result = await axios.post('referral/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( + 'referral/updateReferral', + async (payload: any, { rejectWithValue }) => { + try { + const result = await axios.put(`referral/${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 referralSlice = createSlice({ + name: 'referral', + 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.referral = action.payload.rows; + state.count = action.payload.count; + } else { + state.referral = 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, 'Referral 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, `${'Referral'.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, `${'Referral'.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, `${'Referral'.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, 'Referral 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 } = referralSlice.actions; + +export default referralSlice.reducer; diff --git a/frontend/src/stores/store.ts b/frontend/src/stores/store.ts index 44cf2b8..498532f 100644 --- a/frontend/src/stores/store.ts +++ b/frontend/src/stores/store.ts @@ -13,6 +13,8 @@ import vouchersSlice from './vouchers/vouchersSlice'; import rolesSlice from './roles/rolesSlice'; import permissionsSlice from './permissions/permissionsSlice'; import loyaltytierSlice from './loyaltytier/loyaltytierSlice'; +import referralSlice from './referral/referralSlice'; +import redemptionSlice from './redemption/redemptionSlice'; export const store = configureStore({ reducer: { @@ -30,6 +32,8 @@ export const store = configureStore({ roles: rolesSlice, permissions: permissionsSlice, loyaltytier: loyaltytierSlice, + referral: referralSlice, + redemption: redemptionSlice, }, });