diff --git a/app-shell/src/_schema.json b/app-shell/src/_schema.json index 9b58d57..c88a816 100644 --- a/app-shell/src/_schema.json +++ b/app-shell/src/_schema.json @@ -2,5 +2,6 @@ "Initial version": "{\"iv\":\"ZtPqmPO7LWw2Twsd\",\"encryptedData\":\"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\"}", "Initial": "{\"iv\":\"hjlaCxCrMgn6YyLE\",\"encryptedData\":\"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\"}", "Added image support": "{\"iv\":\"lJDa9VFj/KwUB999\",\"encryptedData\":\"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\"}", - "Rolled back prior to loading image separate": "{\"iv\":\"PM3C5UNaRNdeI4cU\",\"encryptedData\":\"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\"}" + "Rolled back prior to loading image separate": "{\"iv\":\"PM3C5UNaRNdeI4cU\",\"encryptedData\":\"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\"}", + "Complete restart": "{\"iv\":\"uOqGpgOKQ/o5v8Q9\",\"encryptedData\":\"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\"}" } \ No newline at end of file diff --git a/backend/src/db/api/bottles.js b/backend/src/db/api/bottles.js index a0f63d8..f981c14 100644 --- a/backend/src/db/api/bottles.js +++ b/backend/src/db/api/bottles.js @@ -15,18 +15,20 @@ module.exports = class BottlesDBApi { { id: data.id || undefined, - name: data.name || null, proof: data.proof || null, - type: data.type || null, - notes: data.notes || null, - tasting_notes: data.tasting_notes || null, - msrp_range: data.msrp_range || null, - secondary_value_range: data.secondary_value_range || null, - opened_bottle_indicator: data.opened_bottle_indicator || false, - - quantity: data.quantity || null, - barcode: data.barcode || null, age: data.age || null, + rating: data.rating || null, + collectable: data.collectable || false, + + rickhouse: data.rickhouse || null, + rack: data.rack || null, + release: data.release || null, + barrelnumber: data.barrelnumber || null, + barreleddate: data.barreleddate || null, + bottlenumber: data.bottlenumber || null, + dateacquired: data.dateacquired || null, + volume: data.volume || null, + notes: data.notes || null, importHash: data.importHash || null, createdById: currentUser.id, updatedById: currentUser.id, @@ -34,27 +36,25 @@ module.exports = class BottlesDBApi { { transaction }, ); - await bottles.setBrand(data.brand || null, { - transaction, - }); - - await bottles.setDistillery(data.distillery || null, { - transaction, - }); - await bottles.setUser(data.user || null, { transaction, }); - await FileDBApi.replaceRelationFiles( - { - belongsTo: db.bottles.getTableName(), - belongsToColumn: 'picture', - belongsToId: bottles.id, - }, - data.picture, - options, - ); + await bottles.setProduct(data.product || null, { + transaction, + }); + + await bottles.setLocation(data.location || null, { + transaction, + }); + + await bottles.setPhotofront(data.photofront || null, { + transaction, + }); + + await bottles.setPhotoback(data.photoback || null, { + transaction, + }); return bottles; } @@ -67,18 +67,20 @@ module.exports = class BottlesDBApi { const bottlesData = data.map((item, index) => ({ id: item.id || undefined, - name: item.name || null, proof: item.proof || null, - type: item.type || null, - notes: item.notes || null, - tasting_notes: item.tasting_notes || null, - msrp_range: item.msrp_range || null, - secondary_value_range: item.secondary_value_range || null, - opened_bottle_indicator: item.opened_bottle_indicator || false, - - quantity: item.quantity || null, - barcode: item.barcode || null, age: item.age || null, + rating: item.rating || null, + collectable: item.collectable || false, + + rickhouse: item.rickhouse || null, + rack: item.rack || null, + release: item.release || null, + barrelnumber: item.barrelnumber || null, + barreleddate: item.barreleddate || null, + bottlenumber: item.bottlenumber || null, + dateacquired: item.dateacquired || null, + volume: item.volume || null, + notes: item.notes || null, importHash: item.importHash || null, createdById: currentUser.id, updatedById: currentUser.id, @@ -90,18 +92,6 @@ module.exports = class BottlesDBApi { // For each item created, replace relation files - for (let i = 0; i < bottles.length; i++) { - await FileDBApi.replaceRelationFiles( - { - belongsTo: db.bottles.getTableName(), - belongsToColumn: 'picture', - belongsToId: bottles[i].id, - }, - data[i].picture, - options, - ); - } - return bottles; } @@ -113,52 +103,41 @@ module.exports = class BottlesDBApi { const updatePayload = {}; - if (data.name !== undefined) updatePayload.name = data.name; - if (data.proof !== undefined) updatePayload.proof = data.proof; - if (data.type !== undefined) updatePayload.type = data.type; + if (data.age !== undefined) updatePayload.age = data.age; + + if (data.rating !== undefined) updatePayload.rating = data.rating; + + if (data.collectable !== undefined) + updatePayload.collectable = data.collectable; + + if (data.rickhouse !== undefined) updatePayload.rickhouse = data.rickhouse; + + if (data.rack !== undefined) updatePayload.rack = data.rack; + + if (data.release !== undefined) updatePayload.release = data.release; + + if (data.barrelnumber !== undefined) + updatePayload.barrelnumber = data.barrelnumber; + + if (data.barreleddate !== undefined) + updatePayload.barreleddate = data.barreleddate; + + if (data.bottlenumber !== undefined) + updatePayload.bottlenumber = data.bottlenumber; + + if (data.dateacquired !== undefined) + updatePayload.dateacquired = data.dateacquired; + + if (data.volume !== undefined) updatePayload.volume = data.volume; if (data.notes !== undefined) updatePayload.notes = data.notes; - if (data.tasting_notes !== undefined) - updatePayload.tasting_notes = data.tasting_notes; - - if (data.msrp_range !== undefined) - updatePayload.msrp_range = data.msrp_range; - - if (data.secondary_value_range !== undefined) - updatePayload.secondary_value_range = data.secondary_value_range; - - if (data.opened_bottle_indicator !== undefined) - updatePayload.opened_bottle_indicator = data.opened_bottle_indicator; - - if (data.quantity !== undefined) updatePayload.quantity = data.quantity; - - if (data.barcode !== undefined) updatePayload.barcode = data.barcode; - - if (data.age !== undefined) updatePayload.age = data.age; - updatePayload.updatedById = currentUser.id; await bottles.update(updatePayload, { transaction }); - if (data.brand !== undefined) { - await bottles.setBrand( - data.brand, - - { transaction }, - ); - } - - if (data.distillery !== undefined) { - await bottles.setDistillery( - data.distillery, - - { transaction }, - ); - } - if (data.user !== undefined) { await bottles.setUser( data.user, @@ -167,15 +146,37 @@ module.exports = class BottlesDBApi { ); } - await FileDBApi.replaceRelationFiles( - { - belongsTo: db.bottles.getTableName(), - belongsToColumn: 'picture', - belongsToId: bottles.id, - }, - data.picture, - options, - ); + if (data.product !== undefined) { + await bottles.setProduct( + data.product, + + { transaction }, + ); + } + + if (data.location !== undefined) { + await bottles.setLocation( + data.location, + + { transaction }, + ); + } + + if (data.photofront !== undefined) { + await bottles.setPhotofront( + data.photofront, + + { transaction }, + ); + } + + if (data.photoback !== undefined) { + await bottles.setPhotoback( + data.photoback, + + { transaction }, + ); + } return bottles; } @@ -238,15 +239,7 @@ module.exports = class BottlesDBApi { const output = bottles.get({ plain: true }); - output.brand = await bottles.getBrand({ - transaction, - }); - - output.picture = await bottles.getPicture({ - transaction, - }); - - output.distillery = await bottles.getDistillery({ + output.reviews_bottle = await bottles.getReviews_bottle({ transaction, }); @@ -254,6 +247,22 @@ module.exports = class BottlesDBApi { transaction, }); + output.product = await bottles.getProduct({ + transaction, + }); + + output.location = await bottles.getLocation({ + transaction, + }); + + output.photofront = await bottles.getPhotofront({ + transaction, + }); + + output.photoback = await bottles.getPhotoback({ + transaction, + }); + return output; } @@ -270,58 +279,6 @@ module.exports = class BottlesDBApi { const transaction = (options && options.transaction) || undefined; let include = [ - { - model: db.brands, - as: 'brand', - - where: filter.brand - ? { - [Op.or]: [ - { - id: { - [Op.in]: filter.brand - .split('|') - .map((term) => Utils.uuid(term)), - }, - }, - { - name: { - [Op.or]: filter.brand - .split('|') - .map((term) => ({ [Op.iLike]: `%${term}%` })), - }, - }, - ], - } - : {}, - }, - - { - model: db.distilleries, - as: 'distillery', - - where: filter.distillery - ? { - [Op.or]: [ - { - id: { - [Op.in]: filter.distillery - .split('|') - .map((term) => Utils.uuid(term)), - }, - }, - { - name: { - [Op.or]: filter.distillery - .split('|') - .map((term) => ({ [Op.iLike]: `%${term}%` })), - }, - }, - ], - } - : {}, - }, - { model: db.users, as: 'user', @@ -349,8 +306,107 @@ module.exports = class BottlesDBApi { }, { - model: db.file, - as: 'picture', + model: db.products, + as: 'product', + + where: filter.product + ? { + [Op.or]: [ + { + id: { + [Op.in]: filter.product + .split('|') + .map((term) => Utils.uuid(term)), + }, + }, + { + name: { + [Op.or]: filter.product + .split('|') + .map((term) => ({ [Op.iLike]: `%${term}%` })), + }, + }, + ], + } + : {}, + }, + + { + model: db.locations, + as: 'location', + + where: filter.location + ? { + [Op.or]: [ + { + id: { + [Op.in]: filter.location + .split('|') + .map((term) => Utils.uuid(term)), + }, + }, + { + name: { + [Op.or]: filter.location + .split('|') + .map((term) => ({ [Op.iLike]: `%${term}%` })), + }, + }, + ], + } + : {}, + }, + + { + model: db.photos, + as: 'photofront', + + where: filter.photofront + ? { + [Op.or]: [ + { + id: { + [Op.in]: filter.photofront + .split('|') + .map((term) => Utils.uuid(term)), + }, + }, + { + image: { + [Op.or]: filter.photofront + .split('|') + .map((term) => ({ [Op.iLike]: `%${term}%` })), + }, + }, + ], + } + : {}, + }, + + { + model: db.photos, + as: 'photoback', + + where: filter.photoback + ? { + [Op.or]: [ + { + id: { + [Op.in]: filter.photoback + .split('|') + .map((term) => Utils.uuid(term)), + }, + }, + { + image: { + [Op.or]: filter.photoback + .split('|') + .map((term) => ({ [Op.iLike]: `%${term}%` })), + }, + }, + ], + } + : {}, }, ]; @@ -362,10 +418,38 @@ module.exports = class BottlesDBApi { }; } - if (filter.name) { + if (filter.rickhouse) { where = { ...where, - [Op.and]: Utils.ilike('bottles', 'name', filter.name), + [Op.and]: Utils.ilike('bottles', 'rickhouse', filter.rickhouse), + }; + } + + if (filter.rack) { + where = { + ...where, + [Op.and]: Utils.ilike('bottles', 'rack', filter.rack), + }; + } + + if (filter.release) { + where = { + ...where, + [Op.and]: Utils.ilike('bottles', 'release', filter.release), + }; + } + + if (filter.barrelnumber) { + where = { + ...where, + [Op.and]: Utils.ilike('bottles', 'barrelnumber', filter.barrelnumber), + }; + } + + if (filter.bottlenumber) { + where = { + ...where, + [Op.and]: Utils.ilike('bottles', 'bottlenumber', filter.bottlenumber), }; } @@ -376,42 +460,6 @@ module.exports = class BottlesDBApi { }; } - if (filter.tasting_notes) { - where = { - ...where, - [Op.and]: Utils.ilike( - 'bottles', - 'tasting_notes', - filter.tasting_notes, - ), - }; - } - - if (filter.msrp_range) { - where = { - ...where, - [Op.and]: Utils.ilike('bottles', 'msrp_range', filter.msrp_range), - }; - } - - if (filter.secondary_value_range) { - where = { - ...where, - [Op.and]: Utils.ilike( - 'bottles', - 'secondary_value_range', - filter.secondary_value_range, - ), - }; - } - - if (filter.barcode) { - where = { - ...where, - [Op.and]: Utils.ilike('bottles', 'barcode', filter.barcode), - }; - } - if (filter.proofRange) { const [start, end] = filter.proofRange; @@ -436,30 +484,6 @@ module.exports = class BottlesDBApi { } } - if (filter.quantityRange) { - const [start, end] = filter.quantityRange; - - if (start !== undefined && start !== null && start !== '') { - where = { - ...where, - quantity: { - ...where.quantity, - [Op.gte]: start, - }, - }; - } - - if (end !== undefined && end !== null && end !== '') { - where = { - ...where, - quantity: { - ...where.quantity, - [Op.lte]: end, - }, - }; - } - } - if (filter.ageRange) { const [start, end] = filter.ageRange; @@ -484,6 +508,102 @@ module.exports = class BottlesDBApi { } } + if (filter.ratingRange) { + const [start, end] = filter.ratingRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + rating: { + ...where.rating, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + rating: { + ...where.rating, + [Op.lte]: end, + }, + }; + } + } + + if (filter.barreleddateRange) { + const [start, end] = filter.barreleddateRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + barreleddate: { + ...where.barreleddate, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + barreleddate: { + ...where.barreleddate, + [Op.lte]: end, + }, + }; + } + } + + if (filter.dateacquiredRange) { + const [start, end] = filter.dateacquiredRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + dateacquired: { + ...where.dateacquired, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + dateacquired: { + ...where.dateacquired, + [Op.lte]: end, + }, + }; + } + } + + if (filter.volumeRange) { + const [start, end] = filter.volumeRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + volume: { + ...where.volume, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + volume: { + ...where.volume, + [Op.lte]: end, + }, + }; + } + } + if (filter.active !== undefined) { where = { ...where, @@ -491,17 +611,10 @@ module.exports = class BottlesDBApi { }; } - if (filter.type) { + if (filter.collectable) { where = { ...where, - type: filter.type, - }; - } - - if (filter.opened_bottle_indicator) { - where = { - ...where, - opened_bottle_indicator: filter.opened_bottle_indicator, + collectable: filter.collectable, }; } @@ -567,22 +680,22 @@ module.exports = class BottlesDBApi { where = { [Op.or]: [ { ['id']: Utils.uuid(query) }, - Utils.ilike('bottles', 'name', query), + Utils.ilike('bottles', 'id', query), ], }; } const records = await db.bottles.findAll({ - attributes: ['id', 'name'], + attributes: ['id', 'id'], where, limit: limit ? Number(limit) : undefined, offset: offset ? Number(offset) : undefined, - orderBy: [['name', 'ASC']], + orderBy: [['id', 'ASC']], }); return records.map((record) => ({ id: record.id, - label: record.name, + label: record.id, })); } }; diff --git a/backend/src/db/api/brands.js b/backend/src/db/api/brands.js index df9fa52..2170498 100644 --- a/backend/src/db/api/brands.js +++ b/backend/src/db/api/brands.js @@ -16,6 +16,7 @@ module.exports = class BrandsDBApi { id: data.id || undefined, name: data.name || null, + status: data.status || null, importHash: data.importHash || null, createdById: currentUser.id, updatedById: currentUser.id, @@ -39,6 +40,7 @@ module.exports = class BrandsDBApi { id: item.id || undefined, name: item.name || null, + status: item.status || null, importHash: item.importHash || null, createdById: currentUser.id, updatedById: currentUser.id, @@ -63,6 +65,8 @@ module.exports = class BrandsDBApi { if (data.name !== undefined) updatePayload.name = data.name; + if (data.status !== undefined) updatePayload.status = data.status; + updatePayload.updatedById = currentUser.id; await brands.update(updatePayload, { transaction }); @@ -136,7 +140,7 @@ module.exports = class BrandsDBApi { const output = brands.get({ plain: true }); - output.bottles_brand = await brands.getBottles_brand({ + output.products_brand = await brands.getProducts_brand({ transaction, }); @@ -202,6 +206,30 @@ module.exports = class BrandsDBApi { }; } + if (filter.statusRange) { + const [start, end] = filter.statusRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + status: { + ...where.status, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + status: { + ...where.status, + [Op.lte]: end, + }, + }; + } + } + if (filter.active !== undefined) { where = { ...where, diff --git a/backend/src/db/api/conversationparticipants.js b/backend/src/db/api/conversationparticipants.js new file mode 100644 index 0000000..f0e41ec --- /dev/null +++ b/backend/src/db/api/conversationparticipants.js @@ -0,0 +1,335 @@ +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 ConversationparticipantsDBApi { + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const conversationparticipants = await db.conversationparticipants.create( + { + id: data.id || undefined, + + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + await conversationparticipants.setConversation(data.conversation || null, { + transaction, + }); + + await conversationparticipants.setUser(data.user || null, { + transaction, + }); + + return conversationparticipants; + } + + 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 conversationparticipantsData = data.map((item, index) => ({ + id: item.id || undefined, + + importHash: item.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + // Bulk create items + const conversationparticipants = + await db.conversationparticipants.bulkCreate( + conversationparticipantsData, + { transaction }, + ); + + // For each item created, replace relation files + + return conversationparticipants; + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const conversationparticipants = await db.conversationparticipants.findByPk( + id, + {}, + { transaction }, + ); + + const updatePayload = {}; + + updatePayload.updatedById = currentUser.id; + + await conversationparticipants.update(updatePayload, { transaction }); + + if (data.conversation !== undefined) { + await conversationparticipants.setConversation( + data.conversation, + + { transaction }, + ); + } + + if (data.user !== undefined) { + await conversationparticipants.setUser( + data.user, + + { transaction }, + ); + } + + return conversationparticipants; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const conversationparticipants = await db.conversationparticipants.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of conversationparticipants) { + await record.update({ deletedBy: currentUser.id }, { transaction }); + } + for (const record of conversationparticipants) { + await record.destroy({ transaction }); + } + }); + + return conversationparticipants; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const conversationparticipants = await db.conversationparticipants.findByPk( + id, + options, + ); + + await conversationparticipants.update( + { + deletedBy: currentUser.id, + }, + { + transaction, + }, + ); + + await conversationparticipants.destroy({ + transaction, + }); + + return conversationparticipants; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const conversationparticipants = await db.conversationparticipants.findOne( + { where }, + { transaction }, + ); + + if (!conversationparticipants) { + return conversationparticipants; + } + + const output = conversationparticipants.get({ plain: true }); + + output.conversation = await conversationparticipants.getConversation({ + transaction, + }); + + output.user = await conversationparticipants.getUser({ + 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.conversations, + as: 'conversation', + + where: filter.conversation + ? { + [Op.or]: [ + { + id: { + [Op.in]: filter.conversation + .split('|') + .map((term) => Utils.uuid(term)), + }, + }, + { + id: { + [Op.or]: filter.conversation + .split('|') + .map((term) => ({ [Op.iLike]: `%${term}%` })), + }, + }, + ], + } + : {}, + }, + + { + model: db.users, + as: 'user', + + where: filter.user + ? { + [Op.or]: [ + { + id: { + [Op.in]: filter.user + .split('|') + .map((term) => Utils.uuid(term)), + }, + }, + { + firstName: { + [Op.or]: filter.user + .split('|') + .map((term) => ({ [Op.iLike]: `%${term}%` })), + }, + }, + ], + } + : {}, + }, + ]; + + if (filter) { + if (filter.id) { + where = { + ...where, + ['id']: Utils.uuid(filter.id), + }; + } + + if (filter.active !== undefined) { + where = { + ...where, + active: filter.active === true || filter.active === 'true', + }; + } + + if (filter.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.conversationparticipants.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('conversationparticipants', 'id', query), + ], + }; + } + + const records = await db.conversationparticipants.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/conversations.js b/backend/src/db/api/conversations.js new file mode 100644 index 0000000..22f27ce --- /dev/null +++ b/backend/src/db/api/conversations.js @@ -0,0 +1,285 @@ +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 ConversationsDBApi { + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const conversations = await db.conversations.create( + { + id: data.id || undefined, + + createdat: data.createdat || null, + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + return conversations; + } + + 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 conversationsData = data.map((item, index) => ({ + id: item.id || undefined, + + createdat: item.createdat || null, + importHash: item.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + // Bulk create items + const conversations = await db.conversations.bulkCreate(conversationsData, { + transaction, + }); + + // For each item created, replace relation files + + return conversations; + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const conversations = await db.conversations.findByPk( + id, + {}, + { transaction }, + ); + + const updatePayload = {}; + + if (data.createdat !== undefined) updatePayload.createdat = data.createdat; + + updatePayload.updatedById = currentUser.id; + + await conversations.update(updatePayload, { transaction }); + + return conversations; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const conversations = await db.conversations.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of conversations) { + await record.update({ deletedBy: currentUser.id }, { transaction }); + } + for (const record of conversations) { + await record.destroy({ transaction }); + } + }); + + return conversations; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const conversations = await db.conversations.findByPk(id, options); + + await conversations.update( + { + deletedBy: currentUser.id, + }, + { + transaction, + }, + ); + + await conversations.destroy({ + transaction, + }); + + return conversations; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const conversations = await db.conversations.findOne( + { where }, + { transaction }, + ); + + if (!conversations) { + return conversations; + } + + const output = conversations.get({ plain: true }); + + output.messages_conversation = await conversations.getMessages_conversation( + { + transaction, + }, + ); + + output.conversationparticipants_conversation = + await conversations.getConversationparticipants_conversation({ + 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 = []; + + if (filter) { + if (filter.id) { + where = { + ...where, + ['id']: Utils.uuid(filter.id), + }; + } + + if (filter.createdatRange) { + const [start, end] = filter.createdatRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + createdat: { + ...where.createdat, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + createdat: { + ...where.createdat, + [Op.lte]: end, + }, + }; + } + } + + if (filter.active !== undefined) { + where = { + ...where, + active: filter.active === true || filter.active === 'true', + }; + } + + if (filter.createdAtRange) { + const [start, end] = filter.createdAtRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.lte]: end, + }, + }; + } + } + } + + const queryOptions = { + where, + include, + distinct: true, + order: + filter.field && filter.sort + ? [[filter.field, filter.sort]] + : [['createdAt', 'desc']], + transaction: options?.transaction, + logging: console.log, + }; + + if (!options?.countOnly) { + queryOptions.limit = limit ? Number(limit) : undefined; + queryOptions.offset = offset ? Number(offset) : undefined; + } + + try { + const { rows, count } = await db.conversations.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('conversations', 'id', query), + ], + }; + } + + const records = await db.conversations.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/distilleries.js b/backend/src/db/api/distilleries.js index c0255bf..9e5fc0b 100644 --- a/backend/src/db/api/distilleries.js +++ b/backend/src/db/api/distilleries.js @@ -16,6 +16,9 @@ module.exports = class DistilleriesDBApi { id: data.id || undefined, name: data.name || null, + city: data.city || null, + state: data.state || null, + status: data.status || null, importHash: data.importHash || null, createdById: currentUser.id, updatedById: currentUser.id, @@ -35,6 +38,9 @@ module.exports = class DistilleriesDBApi { id: item.id || undefined, name: item.name || null, + city: item.city || null, + state: item.state || null, + status: item.status || null, importHash: item.importHash || null, createdById: currentUser.id, updatedById: currentUser.id, @@ -65,6 +71,12 @@ module.exports = class DistilleriesDBApi { if (data.name !== undefined) updatePayload.name = data.name; + if (data.city !== undefined) updatePayload.city = data.city; + + if (data.state !== undefined) updatePayload.state = data.state; + + if (data.status !== undefined) updatePayload.status = data.status; + updatePayload.updatedById = currentUser.id; await distilleries.update(updatePayload, { transaction }); @@ -133,10 +145,6 @@ module.exports = class DistilleriesDBApi { const output = distilleries.get({ plain: true }); - output.bottles_distillery = await distilleries.getBottles_distillery({ - transaction, - }); - output.brands_distillery = await distilleries.getBrands_distillery({ transaction, }); @@ -173,6 +181,44 @@ module.exports = class DistilleriesDBApi { }; } + if (filter.city) { + where = { + ...where, + [Op.and]: Utils.ilike('distilleries', 'city', filter.city), + }; + } + + if (filter.state) { + where = { + ...where, + [Op.and]: Utils.ilike('distilleries', 'state', filter.state), + }; + } + + if (filter.statusRange) { + const [start, end] = filter.statusRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + status: { + ...where.status, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + status: { + ...where.status, + [Op.lte]: end, + }, + }; + } + } + if (filter.active !== undefined) { where = { ...where, diff --git a/backend/src/db/api/locations.js b/backend/src/db/api/locations.js new file mode 100644 index 0000000..0f9ff2e --- /dev/null +++ b/backend/src/db/api/locations.js @@ -0,0 +1,294 @@ +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 LocationsDBApi { + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const locations = await db.locations.create( + { + id: data.id || undefined, + + name: data.name || null, + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + await locations.setUser(data.user || null, { + transaction, + }); + + return locations; + } + + 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 locationsData = data.map((item, index) => ({ + id: item.id || undefined, + + name: item.name || null, + importHash: item.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + // Bulk create items + const locations = await db.locations.bulkCreate(locationsData, { + transaction, + }); + + // For each item created, replace relation files + + return locations; + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const locations = await db.locations.findByPk(id, {}, { transaction }); + + const updatePayload = {}; + + if (data.name !== undefined) updatePayload.name = data.name; + + updatePayload.updatedById = currentUser.id; + + await locations.update(updatePayload, { transaction }); + + if (data.user !== undefined) { + await locations.setUser( + data.user, + + { transaction }, + ); + } + + return locations; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const locations = await db.locations.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of locations) { + await record.update({ deletedBy: currentUser.id }, { transaction }); + } + for (const record of locations) { + await record.destroy({ transaction }); + } + }); + + return locations; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const locations = await db.locations.findByPk(id, options); + + await locations.update( + { + deletedBy: currentUser.id, + }, + { + transaction, + }, + ); + + await locations.destroy({ + transaction, + }); + + return locations; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const locations = await db.locations.findOne({ where }, { transaction }); + + if (!locations) { + return locations; + } + + const output = locations.get({ plain: true }); + + output.bottles_location = await locations.getBottles_location({ + transaction, + }); + + output.user = await locations.getUser({ + 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}%` })), + }, + }, + ], + } + : {}, + }, + ]; + + if (filter) { + if (filter.id) { + where = { + ...where, + ['id']: Utils.uuid(filter.id), + }; + } + + if (filter.name) { + where = { + ...where, + [Op.and]: Utils.ilike('locations', 'name', filter.name), + }; + } + + if (filter.active !== undefined) { + where = { + ...where, + active: filter.active === true || filter.active === 'true', + }; + } + + if (filter.createdAtRange) { + const [start, end] = filter.createdAtRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.lte]: end, + }, + }; + } + } + } + + const queryOptions = { + where, + include, + distinct: true, + order: + filter.field && filter.sort + ? [[filter.field, filter.sort]] + : [['createdAt', 'desc']], + transaction: options?.transaction, + logging: console.log, + }; + + if (!options?.countOnly) { + queryOptions.limit = limit ? Number(limit) : undefined; + queryOptions.offset = offset ? Number(offset) : undefined; + } + + try { + const { rows, count } = await db.locations.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('locations', 'name', query), + ], + }; + } + + const records = await db.locations.findAll({ + attributes: ['id', 'name'], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + orderBy: [['name', 'ASC']], + }); + + return records.map((record) => ({ + id: record.id, + label: record.name, + })); + } +}; diff --git a/backend/src/db/api/messages.js b/backend/src/db/api/messages.js new file mode 100644 index 0000000..cc04107 --- /dev/null +++ b/backend/src/db/api/messages.js @@ -0,0 +1,321 @@ +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 MessagesDBApi { + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const messages = await db.messages.create( + { + id: data.id || undefined, + + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + await messages.setConversation(data.conversation || null, { + transaction, + }); + + await messages.setSender(data.sender || null, { + transaction, + }); + + return messages; + } + + 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 messagesData = data.map((item, index) => ({ + id: item.id || undefined, + + importHash: item.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + // Bulk create items + const messages = await db.messages.bulkCreate(messagesData, { + transaction, + }); + + // For each item created, replace relation files + + return messages; + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const messages = await db.messages.findByPk(id, {}, { transaction }); + + const updatePayload = {}; + + updatePayload.updatedById = currentUser.id; + + await messages.update(updatePayload, { transaction }); + + if (data.conversation !== undefined) { + await messages.setConversation( + data.conversation, + + { transaction }, + ); + } + + if (data.sender !== undefined) { + await messages.setSender( + data.sender, + + { transaction }, + ); + } + + return messages; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const messages = await db.messages.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of messages) { + await record.update({ deletedBy: currentUser.id }, { transaction }); + } + for (const record of messages) { + await record.destroy({ transaction }); + } + }); + + return messages; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const messages = await db.messages.findByPk(id, options); + + await messages.update( + { + deletedBy: currentUser.id, + }, + { + transaction, + }, + ); + + await messages.destroy({ + transaction, + }); + + return messages; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const messages = await db.messages.findOne({ where }, { transaction }); + + if (!messages) { + return messages; + } + + const output = messages.get({ plain: true }); + + output.conversation = await messages.getConversation({ + transaction, + }); + + output.sender = await messages.getSender({ + 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.conversations, + as: 'conversation', + + where: filter.conversation + ? { + [Op.or]: [ + { + id: { + [Op.in]: filter.conversation + .split('|') + .map((term) => Utils.uuid(term)), + }, + }, + { + id: { + [Op.or]: filter.conversation + .split('|') + .map((term) => ({ [Op.iLike]: `%${term}%` })), + }, + }, + ], + } + : {}, + }, + + { + model: db.users, + as: 'sender', + + where: filter.sender + ? { + [Op.or]: [ + { + id: { + [Op.in]: filter.sender + .split('|') + .map((term) => Utils.uuid(term)), + }, + }, + { + firstName: { + [Op.or]: filter.sender + .split('|') + .map((term) => ({ [Op.iLike]: `%${term}%` })), + }, + }, + ], + } + : {}, + }, + ]; + + if (filter) { + if (filter.id) { + where = { + ...where, + ['id']: Utils.uuid(filter.id), + }; + } + + if (filter.active !== undefined) { + where = { + ...where, + active: filter.active === true || filter.active === 'true', + }; + } + + if (filter.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.messages.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('messages', 'id', query), + ], + }; + } + + const records = await db.messages.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/photos.js b/backend/src/db/api/photos.js new file mode 100644 index 0000000..6a5133e --- /dev/null +++ b/backend/src/db/api/photos.js @@ -0,0 +1,303 @@ +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 PhotosDBApi { + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const photos = await db.photos.create( + { + id: data.id || undefined, + + phototype: data.phototype || null, + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + await FileDBApi.replaceRelationFiles( + { + belongsTo: db.photos.getTableName(), + belongsToColumn: 'image', + belongsToId: photos.id, + }, + data.image, + options, + ); + + return photos; + } + + 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 photosData = data.map((item, index) => ({ + id: item.id || undefined, + + phototype: item.phototype || null, + importHash: item.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + // Bulk create items + const photos = await db.photos.bulkCreate(photosData, { transaction }); + + // For each item created, replace relation files + + for (let i = 0; i < photos.length; i++) { + await FileDBApi.replaceRelationFiles( + { + belongsTo: db.photos.getTableName(), + belongsToColumn: 'image', + belongsToId: photos[i].id, + }, + data[i].image, + options, + ); + } + + return photos; + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const photos = await db.photos.findByPk(id, {}, { transaction }); + + const updatePayload = {}; + + if (data.phototype !== undefined) updatePayload.phototype = data.phototype; + + updatePayload.updatedById = currentUser.id; + + await photos.update(updatePayload, { transaction }); + + await FileDBApi.replaceRelationFiles( + { + belongsTo: db.photos.getTableName(), + belongsToColumn: 'image', + belongsToId: photos.id, + }, + data.image, + options, + ); + + return photos; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const photos = await db.photos.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of photos) { + await record.update({ deletedBy: currentUser.id }, { transaction }); + } + for (const record of photos) { + await record.destroy({ transaction }); + } + }); + + return photos; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const photos = await db.photos.findByPk(id, options); + + await photos.update( + { + deletedBy: currentUser.id, + }, + { + transaction, + }, + ); + + await photos.destroy({ + transaction, + }); + + return photos; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const photos = await db.photos.findOne({ where }, { transaction }); + + if (!photos) { + return photos; + } + + const output = photos.get({ plain: true }); + + output.products_photofront = await photos.getProducts_photofront({ + transaction, + }); + + output.products_photoback = await photos.getProducts_photoback({ + transaction, + }); + + output.bottles_photofront = await photos.getBottles_photofront({ + transaction, + }); + + output.bottles_photoback = await photos.getBottles_photoback({ + transaction, + }); + + output.image = await photos.getImage({ + 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.file, + as: 'image', + }, + ]; + + if (filter) { + if (filter.id) { + where = { + ...where, + ['id']: Utils.uuid(filter.id), + }; + } + + if (filter.phototype) { + where = { + ...where, + [Op.and]: Utils.ilike('photos', 'phototype', filter.phototype), + }; + } + + if (filter.active !== undefined) { + where = { + ...where, + active: filter.active === true || filter.active === 'true', + }; + } + + if (filter.createdAtRange) { + const [start, end] = filter.createdAtRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.lte]: end, + }, + }; + } + } + } + + const queryOptions = { + where, + include, + distinct: true, + order: + filter.field && filter.sort + ? [[filter.field, filter.sort]] + : [['createdAt', 'desc']], + transaction: options?.transaction, + logging: console.log, + }; + + if (!options?.countOnly) { + queryOptions.limit = limit ? Number(limit) : undefined; + queryOptions.offset = offset ? Number(offset) : undefined; + } + + try { + const { rows, count } = await db.photos.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('photos', 'image', query), + ], + }; + } + + const records = await db.photos.findAll({ + attributes: ['id', 'image'], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + orderBy: [['image', 'ASC']], + }); + + return records.map((record) => ({ + id: record.id, + label: record.image, + })); + } +}; diff --git a/backend/src/db/api/products.js b/backend/src/db/api/products.js new file mode 100644 index 0000000..51381e7 --- /dev/null +++ b/backend/src/db/api/products.js @@ -0,0 +1,484 @@ +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 ProductsDBApi { + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const products = await db.products.create( + { + id: data.id || undefined, + + name: data.name || null, + proof: data.proof || null, + age: data.age || null, + barcode: data.barcode || null, + notes: data.notes || null, + status: data.status || null, + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + await products.setBrand(data.brand || null, { + transaction, + }); + + await products.setPhotofront(data.photofront || null, { + transaction, + }); + + await products.setPhotoback(data.photoback || null, { + transaction, + }); + + return products; + } + + 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 productsData = data.map((item, index) => ({ + id: item.id || undefined, + + name: item.name || null, + proof: item.proof || null, + age: item.age || null, + barcode: item.barcode || null, + notes: item.notes || 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 products = await db.products.bulkCreate(productsData, { + transaction, + }); + + // For each item created, replace relation files + + return products; + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const products = await db.products.findByPk(id, {}, { transaction }); + + const updatePayload = {}; + + if (data.name !== undefined) updatePayload.name = data.name; + + if (data.proof !== undefined) updatePayload.proof = data.proof; + + if (data.age !== undefined) updatePayload.age = data.age; + + if (data.barcode !== undefined) updatePayload.barcode = data.barcode; + + if (data.notes !== undefined) updatePayload.notes = data.notes; + + if (data.status !== undefined) updatePayload.status = data.status; + + updatePayload.updatedById = currentUser.id; + + await products.update(updatePayload, { transaction }); + + if (data.brand !== undefined) { + await products.setBrand( + data.brand, + + { transaction }, + ); + } + + if (data.photofront !== undefined) { + await products.setPhotofront( + data.photofront, + + { transaction }, + ); + } + + if (data.photoback !== undefined) { + await products.setPhotoback( + data.photoback, + + { transaction }, + ); + } + + return products; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const products = await db.products.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of products) { + await record.update({ deletedBy: currentUser.id }, { transaction }); + } + for (const record of products) { + await record.destroy({ transaction }); + } + }); + + return products; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const products = await db.products.findByPk(id, options); + + await products.update( + { + deletedBy: currentUser.id, + }, + { + transaction, + }, + ); + + await products.destroy({ + transaction, + }); + + return products; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const products = await db.products.findOne({ where }, { transaction }); + + if (!products) { + return products; + } + + const output = products.get({ plain: true }); + + output.bottles_product = await products.getBottles_product({ + transaction, + }); + + output.brand = await products.getBrand({ + transaction, + }); + + output.photofront = await products.getPhotofront({ + transaction, + }); + + output.photoback = await products.getPhotoback({ + 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.brands, + as: 'brand', + + where: filter.brand + ? { + [Op.or]: [ + { + id: { + [Op.in]: filter.brand + .split('|') + .map((term) => Utils.uuid(term)), + }, + }, + { + name: { + [Op.or]: filter.brand + .split('|') + .map((term) => ({ [Op.iLike]: `%${term}%` })), + }, + }, + ], + } + : {}, + }, + + { + model: db.photos, + as: 'photofront', + + where: filter.photofront + ? { + [Op.or]: [ + { + id: { + [Op.in]: filter.photofront + .split('|') + .map((term) => Utils.uuid(term)), + }, + }, + { + image: { + [Op.or]: filter.photofront + .split('|') + .map((term) => ({ [Op.iLike]: `%${term}%` })), + }, + }, + ], + } + : {}, + }, + + { + model: db.photos, + as: 'photoback', + + where: filter.photoback + ? { + [Op.or]: [ + { + id: { + [Op.in]: filter.photoback + .split('|') + .map((term) => Utils.uuid(term)), + }, + }, + { + image: { + [Op.or]: filter.photoback + .split('|') + .map((term) => ({ [Op.iLike]: `%${term}%` })), + }, + }, + ], + } + : {}, + }, + ]; + + if (filter) { + if (filter.id) { + where = { + ...where, + ['id']: Utils.uuid(filter.id), + }; + } + + if (filter.name) { + where = { + ...where, + [Op.and]: Utils.ilike('products', 'name', filter.name), + }; + } + + if (filter.barcode) { + where = { + ...where, + [Op.and]: Utils.ilike('products', 'barcode', filter.barcode), + }; + } + + if (filter.notes) { + where = { + ...where, + [Op.and]: Utils.ilike('products', 'notes', filter.notes), + }; + } + + if (filter.proofRange) { + const [start, end] = filter.proofRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + proof: { + ...where.proof, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + proof: { + ...where.proof, + [Op.lte]: end, + }, + }; + } + } + + if (filter.ageRange) { + const [start, end] = filter.ageRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + age: { + ...where.age, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + age: { + ...where.age, + [Op.lte]: end, + }, + }; + } + } + + if (filter.statusRange) { + const [start, end] = filter.statusRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + status: { + ...where.status, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + status: { + ...where.status, + [Op.lte]: end, + }, + }; + } + } + + if (filter.active !== undefined) { + where = { + ...where, + active: filter.active === true || filter.active === 'true', + }; + } + + if (filter.createdAtRange) { + const [start, end] = filter.createdAtRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.lte]: end, + }, + }; + } + } + } + + const queryOptions = { + where, + include, + distinct: true, + order: + filter.field && filter.sort + ? [[filter.field, filter.sort]] + : [['createdAt', 'desc']], + transaction: options?.transaction, + logging: console.log, + }; + + if (!options?.countOnly) { + queryOptions.limit = limit ? Number(limit) : undefined; + queryOptions.offset = offset ? Number(offset) : undefined; + } + + try { + const { rows, count } = await db.products.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('products', 'name', query), + ], + }; + } + + const records = await db.products.findAll({ + attributes: ['id', 'name'], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + orderBy: [['name', 'ASC']], + }); + + return records.map((record) => ({ + id: record.id, + label: record.name, + })); + } +}; diff --git a/backend/src/db/api/reviews.js b/backend/src/db/api/reviews.js new file mode 100644 index 0000000..109957f --- /dev/null +++ b/backend/src/db/api/reviews.js @@ -0,0 +1,386 @@ +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 ReviewsDBApi { + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const reviews = await db.reviews.create( + { + id: data.id || undefined, + + rating: data.rating || null, + notes: data.notes || null, + createdat: data.createdat || null, + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + await reviews.setUser(data.user || null, { + transaction, + }); + + await reviews.setBottle(data.bottle || null, { + transaction, + }); + + return reviews; + } + + 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 reviewsData = data.map((item, index) => ({ + id: item.id || undefined, + + rating: item.rating || null, + notes: item.notes || null, + createdat: item.createdat || null, + importHash: item.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + // Bulk create items + const reviews = await db.reviews.bulkCreate(reviewsData, { transaction }); + + // For each item created, replace relation files + + return reviews; + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const reviews = await db.reviews.findByPk(id, {}, { transaction }); + + const updatePayload = {}; + + if (data.rating !== undefined) updatePayload.rating = data.rating; + + if (data.notes !== undefined) updatePayload.notes = data.notes; + + if (data.createdat !== undefined) updatePayload.createdat = data.createdat; + + updatePayload.updatedById = currentUser.id; + + await reviews.update(updatePayload, { transaction }); + + if (data.user !== undefined) { + await reviews.setUser( + data.user, + + { transaction }, + ); + } + + if (data.bottle !== undefined) { + await reviews.setBottle( + data.bottle, + + { transaction }, + ); + } + + return reviews; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const reviews = await db.reviews.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of reviews) { + await record.update({ deletedBy: currentUser.id }, { transaction }); + } + for (const record of reviews) { + await record.destroy({ transaction }); + } + }); + + return reviews; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const reviews = await db.reviews.findByPk(id, options); + + await reviews.update( + { + deletedBy: currentUser.id, + }, + { + transaction, + }, + ); + + await reviews.destroy({ + transaction, + }); + + return reviews; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const reviews = await db.reviews.findOne({ where }, { transaction }); + + if (!reviews) { + return reviews; + } + + const output = reviews.get({ plain: true }); + + output.user = await reviews.getUser({ + transaction, + }); + + output.bottle = await reviews.getBottle({ + 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.bottles, + as: 'bottle', + + where: filter.bottle + ? { + [Op.or]: [ + { + id: { + [Op.in]: filter.bottle + .split('|') + .map((term) => Utils.uuid(term)), + }, + }, + { + id: { + [Op.or]: filter.bottle + .split('|') + .map((term) => ({ [Op.iLike]: `%${term}%` })), + }, + }, + ], + } + : {}, + }, + ]; + + if (filter) { + if (filter.id) { + where = { + ...where, + ['id']: Utils.uuid(filter.id), + }; + } + + if (filter.notes) { + where = { + ...where, + [Op.and]: Utils.ilike('reviews', 'notes', filter.notes), + }; + } + + if (filter.ratingRange) { + const [start, end] = filter.ratingRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + rating: { + ...where.rating, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + rating: { + ...where.rating, + [Op.lte]: end, + }, + }; + } + } + + if (filter.createdatRange) { + const [start, end] = filter.createdatRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + createdat: { + ...where.createdat, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + createdat: { + ...where.createdat, + [Op.lte]: end, + }, + }; + } + } + + if (filter.active !== undefined) { + where = { + ...where, + active: filter.active === true || filter.active === 'true', + }; + } + + if (filter.createdAtRange) { + const [start, end] = filter.createdAtRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.lte]: end, + }, + }; + } + } + } + + const queryOptions = { + where, + include, + distinct: true, + order: + filter.field && filter.sort + ? [[filter.field, filter.sort]] + : [['createdAt', 'desc']], + transaction: options?.transaction, + logging: console.log, + }; + + if (!options?.countOnly) { + queryOptions.limit = limit ? Number(limit) : undefined; + queryOptions.offset = offset ? Number(offset) : undefined; + } + + try { + const { rows, count } = await db.reviews.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('reviews', 'id', query), + ], + }; + } + + const records = await db.reviews.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 45e0429..f2abbd8 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, + address: data.data.address || null, + address2: data.data.address2 || null, importHash: data.data.importHash || null, createdById: currentUser.id, updatedById: currentUser.id, @@ -96,6 +98,8 @@ module.exports = class UsersDBApi { passwordResetToken: item.passwordResetToken || null, passwordResetTokenExpiresAt: item.passwordResetTokenExpiresAt || null, provider: item.provider || null, + address: item.address || null, + address2: item.address2 || null, importHash: item.importHash || null, createdById: currentUser.id, updatedById: currentUser.id, @@ -178,6 +182,10 @@ module.exports = class UsersDBApi { if (data.provider !== undefined) updatePayload.provider = data.provider; + if (data.address !== undefined) updatePayload.address = data.address; + + if (data.address2 !== undefined) updatePayload.address2 = data.address2; + updatePayload.updatedById = currentUser.id; await users.update(updatePayload, { transaction }); @@ -267,10 +275,27 @@ module.exports = class UsersDBApi { const output = users.get({ plain: true }); + output.locations_user = await users.getLocations_user({ + transaction, + }); + output.bottles_user = await users.getBottles_user({ transaction, }); + output.reviews_user = await users.getReviews_user({ + transaction, + }); + + output.messages_sender = await users.getMessages_sender({ + transaction, + }); + + output.conversationparticipants_user = + await users.getConversationparticipants_user({ + transaction, + }); + output.avatar = await users.getAvatar({ transaction, }); @@ -415,6 +440,20 @@ module.exports = class UsersDBApi { }; } + if (filter.address) { + where = { + ...where, + [Op.and]: Utils.ilike('users', 'address', filter.address), + }; + } + + if (filter.address2) { + where = { + ...where, + [Op.and]: Utils.ilike('users', 'address2', filter.address2), + }; + } + if (filter.emailVerificationTokenExpiresAtRange) { const [start, end] = filter.emailVerificationTokenExpiresAtRange; diff --git a/backend/src/db/migrations/1756232919789.js b/backend/src/db/migrations/1756232919789.js new file mode 100644 index 0000000..f06dac3 --- /dev/null +++ b/backend/src/db/migrations/1756232919789.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.dropTable('bottles', { 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.createTable( + 'bottles', + { + 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; + } + }, +}; diff --git a/backend/src/db/migrations/1756232938531.js b/backend/src/db/migrations/1756232938531.js new file mode 100644 index 0000000..63abd52 --- /dev/null +++ b/backend/src/db/migrations/1756232938531.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.dropTable('brands', { 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.createTable( + 'brands', + { + 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; + } + }, +}; diff --git a/backend/src/db/migrations/1756232961734.js b/backend/src/db/migrations/1756232961734.js new file mode 100644 index 0000000..e150bf9 --- /dev/null +++ b/backend/src/db/migrations/1756232961734.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.dropTable('distilleries', { 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.createTable( + 'distilleries', + { + 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; + } + }, +}; diff --git a/backend/src/db/migrations/1756232993973.js b/backend/src/db/migrations/1756232993973.js new file mode 100644 index 0000000..562b081 --- /dev/null +++ b/backend/src/db/migrations/1756232993973.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( + 'distilleries', + { + 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('distilleries', { transaction }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1756233026221.js b/backend/src/db/migrations/1756233026221.js new file mode 100644 index 0000000..0a363c4 --- /dev/null +++ b/backend/src/db/migrations/1756233026221.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( + 'distilleries', + 'name', + { + type: Sequelize.DataTypes.TEXT, + }, + { transaction }, + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async down(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.removeColumn('distilleries', 'name', { + transaction, + }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1756233047456.js b/backend/src/db/migrations/1756233047456.js new file mode 100644 index 0000000..e9567fd --- /dev/null +++ b/backend/src/db/migrations/1756233047456.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( + 'distilleries', + 'city', + { + 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('distilleries', 'city', { + transaction, + }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1756233083420.js b/backend/src/db/migrations/1756233083420.js new file mode 100644 index 0000000..e4bb435 --- /dev/null +++ b/backend/src/db/migrations/1756233083420.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( + 'distilleries', + 'state', + { + 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('distilleries', 'state', { + transaction, + }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1756233114262.js b/backend/src/db/migrations/1756233114262.js new file mode 100644 index 0000000..82251fc --- /dev/null +++ b/backend/src/db/migrations/1756233114262.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( + 'brands', + { + 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('brands', { transaction }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1756233134331.js b/backend/src/db/migrations/1756233134331.js new file mode 100644 index 0000000..061ee56 --- /dev/null +++ b/backend/src/db/migrations/1756233134331.js @@ -0,0 +1,47 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.addColumn( + 'brands', + 'name', + { + type: Sequelize.DataTypes.TEXT, + }, + { transaction }, + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async down(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.removeColumn('brands', 'name', { transaction }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1756233161698.js b/backend/src/db/migrations/1756233161698.js new file mode 100644 index 0000000..769c7d5 --- /dev/null +++ b/backend/src/db/migrations/1756233161698.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( + 'photos', + { + 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('photos', { transaction }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1756233186114.js b/backend/src/db/migrations/1756233186114.js new file mode 100644 index 0000000..80b2ebb --- /dev/null +++ b/backend/src/db/migrations/1756233186114.js @@ -0,0 +1,47 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.addColumn( + 'photos', + 'phototype', + { + 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('photos', 'phototype', { transaction }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1756233219308.js b/backend/src/db/migrations/1756233219308.js new file mode 100644 index 0000000..e6bfba3 --- /dev/null +++ b/backend/src/db/migrations/1756233219308.js @@ -0,0 +1,36 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + 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 transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1756233254738.js b/backend/src/db/migrations/1756233254738.js new file mode 100644 index 0000000..701dadb --- /dev/null +++ b/backend/src/db/migrations/1756233254738.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( + 'distilleries', + 'status', + { + 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('distilleries', 'status', { + transaction, + }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1756233282118.js b/backend/src/db/migrations/1756233282118.js new file mode 100644 index 0000000..2915c22 --- /dev/null +++ b/backend/src/db/migrations/1756233282118.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( + 'products', + { + 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('products', { transaction }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1756233314139.js b/backend/src/db/migrations/1756233314139.js new file mode 100644 index 0000000..e361ebd --- /dev/null +++ b/backend/src/db/migrations/1756233314139.js @@ -0,0 +1,47 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.addColumn( + 'users', + 'address', + { + 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', 'address', { transaction }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1756233343308.js b/backend/src/db/migrations/1756233343308.js new file mode 100644 index 0000000..3fa32ef --- /dev/null +++ b/backend/src/db/migrations/1756233343308.js @@ -0,0 +1,47 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.addColumn( + 'users', + 'address2', + { + 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', 'address2', { transaction }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1756233376774.js b/backend/src/db/migrations/1756233376774.js new file mode 100644 index 0000000..586ae85 --- /dev/null +++ b/backend/src/db/migrations/1756233376774.js @@ -0,0 +1,52 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.addColumn( + 'products', + 'brandId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'brands', + 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('products', 'brandId', { transaction }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1756233417905.js b/backend/src/db/migrations/1756233417905.js new file mode 100644 index 0000000..ca81d21 --- /dev/null +++ b/backend/src/db/migrations/1756233417905.js @@ -0,0 +1,47 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.addColumn( + 'brands', + 'status', + { + 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('brands', 'status', { transaction }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1756233460513.js b/backend/src/db/migrations/1756233460513.js new file mode 100644 index 0000000..eabce8c --- /dev/null +++ b/backend/src/db/migrations/1756233460513.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( + 'locations', + { + 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('locations', { transaction }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1756233493388.js b/backend/src/db/migrations/1756233493388.js new file mode 100644 index 0000000..82ea551 --- /dev/null +++ b/backend/src/db/migrations/1756233493388.js @@ -0,0 +1,47 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.addColumn( + 'locations', + 'name', + { + type: Sequelize.DataTypes.TEXT, + }, + { transaction }, + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async down(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.removeColumn('locations', 'name', { transaction }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1756233535083.js b/backend/src/db/migrations/1756233535083.js new file mode 100644 index 0000000..40c0006 --- /dev/null +++ b/backend/src/db/migrations/1756233535083.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( + 'brands', + 'distilleryId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'distilleries', + 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('brands', 'distilleryId', { + transaction, + }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1756233583823.js b/backend/src/db/migrations/1756233583823.js new file mode 100644 index 0000000..3c339da --- /dev/null +++ b/backend/src/db/migrations/1756233583823.js @@ -0,0 +1,47 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.addColumn( + 'products', + 'name', + { + type: Sequelize.DataTypes.TEXT, + }, + { transaction }, + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async down(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.removeColumn('products', 'name', { transaction }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1756233847816.js b/backend/src/db/migrations/1756233847816.js new file mode 100644 index 0000000..c235ffc --- /dev/null +++ b/backend/src/db/migrations/1756233847816.js @@ -0,0 +1,47 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.addColumn( + 'products', + 'proof', + { + 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('products', 'proof', { transaction }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1756233872842.js b/backend/src/db/migrations/1756233872842.js new file mode 100644 index 0000000..0720527 --- /dev/null +++ b/backend/src/db/migrations/1756233872842.js @@ -0,0 +1,47 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.addColumn( + 'products', + 'age', + { + 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('products', 'age', { transaction }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1756233895732.js b/backend/src/db/migrations/1756233895732.js new file mode 100644 index 0000000..5f1e108 --- /dev/null +++ b/backend/src/db/migrations/1756233895732.js @@ -0,0 +1,47 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.addColumn( + 'products', + 'barcode', + { + 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('products', 'barcode', { transaction }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1756233923545.js b/backend/src/db/migrations/1756233923545.js new file mode 100644 index 0000000..883c0c2 --- /dev/null +++ b/backend/src/db/migrations/1756233923545.js @@ -0,0 +1,47 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.addColumn( + 'products', + 'notes', + { + 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('products', 'notes', { transaction }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1756233946141.js b/backend/src/db/migrations/1756233946141.js new file mode 100644 index 0000000..13a8f82 --- /dev/null +++ b/backend/src/db/migrations/1756233946141.js @@ -0,0 +1,47 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.addColumn( + 'products', + 'status', + { + 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('products', 'status', { transaction }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1756233969555.js b/backend/src/db/migrations/1756233969555.js new file mode 100644 index 0000000..4232aa9 --- /dev/null +++ b/backend/src/db/migrations/1756233969555.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( + 'products', + 'photofrontId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'photos', + 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('products', 'photofrontId', { + transaction, + }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1756233991015.js b/backend/src/db/migrations/1756233991015.js new file mode 100644 index 0000000..82cd1bf --- /dev/null +++ b/backend/src/db/migrations/1756233991015.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( + 'products', + 'photobackId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'photos', + 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('products', 'photobackId', { + transaction, + }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1756234013059.js b/backend/src/db/migrations/1756234013059.js new file mode 100644 index 0000000..f821a9e --- /dev/null +++ b/backend/src/db/migrations/1756234013059.js @@ -0,0 +1,52 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.addColumn( + 'locations', + '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('locations', 'userId', { transaction }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1756234037032.js b/backend/src/db/migrations/1756234037032.js new file mode 100644 index 0000000..666e9b7 --- /dev/null +++ b/backend/src/db/migrations/1756234037032.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( + 'bottles', + { + 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('bottles', { transaction }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1756234060391.js b/backend/src/db/migrations/1756234060391.js new file mode 100644 index 0000000..e97bc9a --- /dev/null +++ b/backend/src/db/migrations/1756234060391.js @@ -0,0 +1,52 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.addColumn( + 'bottles', + '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('bottles', 'userId', { transaction }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1756234087868.js b/backend/src/db/migrations/1756234087868.js new file mode 100644 index 0000000..d96e2ec --- /dev/null +++ b/backend/src/db/migrations/1756234087868.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( + 'bottles', + 'productId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'products', + 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('bottles', 'productId', { + transaction, + }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1756234116962.js b/backend/src/db/migrations/1756234116962.js new file mode 100644 index 0000000..9997349 --- /dev/null +++ b/backend/src/db/migrations/1756234116962.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( + 'bottles', + 'locationId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'locations', + 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('bottles', 'locationId', { + transaction, + }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1756234140311.js b/backend/src/db/migrations/1756234140311.js new file mode 100644 index 0000000..6a9e4a0 --- /dev/null +++ b/backend/src/db/migrations/1756234140311.js @@ -0,0 +1,47 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.addColumn( + 'bottles', + 'proof', + { + 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('bottles', 'proof', { transaction }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1756234165049.js b/backend/src/db/migrations/1756234165049.js new file mode 100644 index 0000000..658fb3e --- /dev/null +++ b/backend/src/db/migrations/1756234165049.js @@ -0,0 +1,47 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.addColumn( + 'bottles', + 'age', + { + 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('bottles', 'age', { transaction }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1756234187286.js b/backend/src/db/migrations/1756234187286.js new file mode 100644 index 0000000..662ea89 --- /dev/null +++ b/backend/src/db/migrations/1756234187286.js @@ -0,0 +1,47 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.addColumn( + 'bottles', + 'rating', + { + 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('bottles', 'rating', { transaction }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1756234208774.js b/backend/src/db/migrations/1756234208774.js new file mode 100644 index 0000000..865c06d --- /dev/null +++ b/backend/src/db/migrations/1756234208774.js @@ -0,0 +1,52 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.addColumn( + 'bottles', + 'collectable', + { + type: Sequelize.DataTypes.BOOLEAN, + + defaultValue: false, + allowNull: false, + }, + { 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('bottles', 'collectable', { + transaction, + }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1756234237031.js b/backend/src/db/migrations/1756234237031.js new file mode 100644 index 0000000..c50a3c5 --- /dev/null +++ b/backend/src/db/migrations/1756234237031.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( + 'bottles', + 'rickhouse', + { + 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('bottles', 'rickhouse', { + transaction, + }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1756234265034.js b/backend/src/db/migrations/1756234265034.js new file mode 100644 index 0000000..9b5b449 --- /dev/null +++ b/backend/src/db/migrations/1756234265034.js @@ -0,0 +1,47 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.addColumn( + 'bottles', + 'rack', + { + 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('bottles', 'rack', { transaction }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1756234286983.js b/backend/src/db/migrations/1756234286983.js new file mode 100644 index 0000000..5562351 --- /dev/null +++ b/backend/src/db/migrations/1756234286983.js @@ -0,0 +1,47 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.addColumn( + 'bottles', + 'release', + { + 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('bottles', 'release', { transaction }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1756234308313.js b/backend/src/db/migrations/1756234308313.js new file mode 100644 index 0000000..b8f7114 --- /dev/null +++ b/backend/src/db/migrations/1756234308313.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( + 'bottles', + 'barrelnumber', + { + 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('bottles', 'barrelnumber', { + transaction, + }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1756234331914.js b/backend/src/db/migrations/1756234331914.js new file mode 100644 index 0000000..d6aa1e3 --- /dev/null +++ b/backend/src/db/migrations/1756234331914.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( + 'bottles', + 'barreleddate', + { + type: Sequelize.DataTypes.DATEONLY, + }, + { 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('bottles', 'barreleddate', { + transaction, + }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1756234359186.js b/backend/src/db/migrations/1756234359186.js new file mode 100644 index 0000000..0639bf1 --- /dev/null +++ b/backend/src/db/migrations/1756234359186.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( + 'bottles', + 'bottlenumber', + { + 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('bottles', 'bottlenumber', { + transaction, + }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1756234382031.js b/backend/src/db/migrations/1756234382031.js new file mode 100644 index 0000000..9f99184 --- /dev/null +++ b/backend/src/db/migrations/1756234382031.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( + 'bottles', + 'dateacquired', + { + type: Sequelize.DataTypes.DATEONLY, + }, + { 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('bottles', 'dateacquired', { + transaction, + }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1756234410684.js b/backend/src/db/migrations/1756234410684.js new file mode 100644 index 0000000..d124d78 --- /dev/null +++ b/backend/src/db/migrations/1756234410684.js @@ -0,0 +1,47 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.addColumn( + 'bottles', + 'volume', + { + 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('bottles', 'volume', { transaction }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1756234436241.js b/backend/src/db/migrations/1756234436241.js new file mode 100644 index 0000000..05a3d47 --- /dev/null +++ b/backend/src/db/migrations/1756234436241.js @@ -0,0 +1,47 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.addColumn( + 'bottles', + 'notes', + { + 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('bottles', 'notes', { transaction }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1756234471854.js b/backend/src/db/migrations/1756234471854.js new file mode 100644 index 0000000..05a6e21 --- /dev/null +++ b/backend/src/db/migrations/1756234471854.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( + 'bottles', + 'photofrontId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'photos', + 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('bottles', 'photofrontId', { + transaction, + }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1756234493098.js b/backend/src/db/migrations/1756234493098.js new file mode 100644 index 0000000..77e6572 --- /dev/null +++ b/backend/src/db/migrations/1756234493098.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( + 'bottles', + 'photobackId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'photos', + 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('bottles', 'photobackId', { + transaction, + }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1756234515450.js b/backend/src/db/migrations/1756234515450.js new file mode 100644 index 0000000..5cac6c2 --- /dev/null +++ b/backend/src/db/migrations/1756234515450.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( + 'reviews', + { + 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('reviews', { transaction }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1756234537835.js b/backend/src/db/migrations/1756234537835.js new file mode 100644 index 0000000..5a61b7b --- /dev/null +++ b/backend/src/db/migrations/1756234537835.js @@ -0,0 +1,52 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.addColumn( + 'reviews', + '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('reviews', 'userId', { transaction }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1756234563408.js b/backend/src/db/migrations/1756234563408.js new file mode 100644 index 0000000..70f5ab7 --- /dev/null +++ b/backend/src/db/migrations/1756234563408.js @@ -0,0 +1,52 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.addColumn( + 'reviews', + 'bottleId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'bottles', + 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('reviews', 'bottleId', { transaction }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1756234596232.js b/backend/src/db/migrations/1756234596232.js new file mode 100644 index 0000000..30dea79 --- /dev/null +++ b/backend/src/db/migrations/1756234596232.js @@ -0,0 +1,47 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.addColumn( + 'reviews', + 'rating', + { + 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('reviews', 'rating', { transaction }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1756234618834.js b/backend/src/db/migrations/1756234618834.js new file mode 100644 index 0000000..e5e9102 --- /dev/null +++ b/backend/src/db/migrations/1756234618834.js @@ -0,0 +1,47 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.addColumn( + 'reviews', + 'notes', + { + 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('reviews', 'notes', { transaction }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1756234642956.js b/backend/src/db/migrations/1756234642956.js new file mode 100644 index 0000000..223b667 --- /dev/null +++ b/backend/src/db/migrations/1756234642956.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( + 'reviews', + 'createdat', + { + type: Sequelize.DataTypes.DATE, + }, + { 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('reviews', 'createdat', { + transaction, + }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1756234669525.js b/backend/src/db/migrations/1756234669525.js new file mode 100644 index 0000000..5c4ab61 --- /dev/null +++ b/backend/src/db/migrations/1756234669525.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( + 'conversations', + { + 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('conversations', { transaction }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1756234695418.js b/backend/src/db/migrations/1756234695418.js new file mode 100644 index 0000000..12f4d29 --- /dev/null +++ b/backend/src/db/migrations/1756234695418.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( + 'conversations', + 'createdat', + { + type: Sequelize.DataTypes.DATE, + }, + { 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('conversations', 'createdat', { + transaction, + }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1756234721487.js b/backend/src/db/migrations/1756234721487.js new file mode 100644 index 0000000..85fbc72 --- /dev/null +++ b/backend/src/db/migrations/1756234721487.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( + 'messages', + { + 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('messages', { transaction }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1756234750821.js b/backend/src/db/migrations/1756234750821.js new file mode 100644 index 0000000..11ea395 --- /dev/null +++ b/backend/src/db/migrations/1756234750821.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( + 'messages', + 'conversationId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'conversations', + 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('messages', 'conversationId', { + transaction, + }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1756234775299.js b/backend/src/db/migrations/1756234775299.js new file mode 100644 index 0000000..dfb96d7 --- /dev/null +++ b/backend/src/db/migrations/1756234775299.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( + 'messages', + 'senderId', + { + 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('messages', 'senderId', { + transaction, + }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1756234799569.js b/backend/src/db/migrations/1756234799569.js new file mode 100644 index 0000000..bbe7b2e --- /dev/null +++ b/backend/src/db/migrations/1756234799569.js @@ -0,0 +1,74 @@ +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( + 'conversationparticipants', + { + 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('conversationparticipants', { + transaction, + }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1756234822685.js b/backend/src/db/migrations/1756234822685.js new file mode 100644 index 0000000..1fef055 --- /dev/null +++ b/backend/src/db/migrations/1756234822685.js @@ -0,0 +1,56 @@ +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( + 'conversationparticipants', + 'conversationId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'conversations', + 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( + 'conversationparticipants', + 'conversationId', + { transaction }, + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1756234849724.js b/backend/src/db/migrations/1756234849724.js new file mode 100644 index 0000000..d47441a --- /dev/null +++ b/backend/src/db/migrations/1756234849724.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( + 'conversationparticipants', + '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('conversationparticipants', 'userId', { + transaction, + }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/models/bottles.js b/backend/src/db/models/bottles.js index 23d8edf..27d0c76 100644 --- a/backend/src/db/models/bottles.js +++ b/backend/src/db/models/bottles.js @@ -14,53 +14,71 @@ module.exports = function (sequelize, DataTypes) { primaryKey: true, }, - name: { - type: DataTypes.TEXT, - }, - proof: { type: DataTypes.DECIMAL, }, - type: { - type: DataTypes.ENUM, - - values: ['Bourbon', 'Scotch', 'Rye', 'Irish', 'Other'], + age: { + type: DataTypes.INTEGER, }, - notes: { - type: DataTypes.TEXT, + rating: { + type: DataTypes.INTEGER, }, - tasting_notes: { - type: DataTypes.TEXT, - }, - - msrp_range: { - type: DataTypes.TEXT, - }, - - secondary_value_range: { - type: DataTypes.TEXT, - }, - - opened_bottle_indicator: { + collectable: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: false, }, - quantity: { - type: DataTypes.INTEGER, - }, - - barcode: { + rickhouse: { type: DataTypes.TEXT, }, - age: { - type: DataTypes.DECIMAL, + rack: { + type: DataTypes.TEXT, + }, + + release: { + type: DataTypes.TEXT, + }, + + barrelnumber: { + type: DataTypes.TEXT, + }, + + barreleddate: { + type: DataTypes.DATEONLY, + + get: function () { + return this.getDataValue('barreleddate') + ? moment.utc(this.getDataValue('barreleddate')).format('YYYY-MM-DD') + : null; + }, + }, + + bottlenumber: { + type: DataTypes.TEXT, + }, + + dateacquired: { + type: DataTypes.DATEONLY, + + get: function () { + return this.getDataValue('dateacquired') + ? moment.utc(this.getDataValue('dateacquired')).format('YYYY-MM-DD') + : null; + }, + }, + + volume: { + type: DataTypes.INTEGER, + }, + + notes: { + type: DataTypes.TEXT, }, importHash: { @@ -79,24 +97,16 @@ module.exports = function (sequelize, DataTypes) { bottles.associate = (db) => { /// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity + db.bottles.hasMany(db.reviews, { + as: 'reviews_bottle', + foreignKey: { + name: 'bottleId', + }, + constraints: false, + }); + //end loop - db.bottles.belongsTo(db.brands, { - as: 'brand', - foreignKey: { - name: 'brandId', - }, - constraints: false, - }); - - db.bottles.belongsTo(db.distilleries, { - as: 'distillery', - foreignKey: { - name: 'distilleryId', - }, - constraints: false, - }); - db.bottles.belongsTo(db.users, { as: 'user', foreignKey: { @@ -105,14 +115,36 @@ module.exports = function (sequelize, DataTypes) { constraints: false, }); - db.bottles.hasMany(db.file, { - as: 'picture', - foreignKey: 'belongsToId', - constraints: false, - scope: { - belongsTo: db.bottles.getTableName(), - belongsToColumn: 'picture', + db.bottles.belongsTo(db.products, { + as: 'product', + foreignKey: { + name: 'productId', }, + constraints: false, + }); + + db.bottles.belongsTo(db.locations, { + as: 'location', + foreignKey: { + name: 'locationId', + }, + constraints: false, + }); + + db.bottles.belongsTo(db.photos, { + as: 'photofront', + foreignKey: { + name: 'photofrontId', + }, + constraints: false, + }); + + db.bottles.belongsTo(db.photos, { + as: 'photoback', + foreignKey: { + name: 'photobackId', + }, + constraints: false, }); db.bottles.belongsTo(db.users, { diff --git a/backend/src/db/models/brands.js b/backend/src/db/models/brands.js index e30b44b..0b57391 100644 --- a/backend/src/db/models/brands.js +++ b/backend/src/db/models/brands.js @@ -18,6 +18,10 @@ module.exports = function (sequelize, DataTypes) { type: DataTypes.TEXT, }, + status: { + type: DataTypes.INTEGER, + }, + importHash: { type: DataTypes.STRING(255), allowNull: true, @@ -34,8 +38,8 @@ module.exports = function (sequelize, DataTypes) { brands.associate = (db) => { /// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity - db.brands.hasMany(db.bottles, { - as: 'bottles_brand', + db.brands.hasMany(db.products, { + as: 'products_brand', foreignKey: { name: 'brandId', }, diff --git a/backend/src/db/models/conversationparticipants.js b/backend/src/db/models/conversationparticipants.js new file mode 100644 index 0000000..2222a13 --- /dev/null +++ b/backend/src/db/models/conversationparticipants.js @@ -0,0 +1,61 @@ +const config = require('../../config'); +const providers = config.providers; +const crypto = require('crypto'); +const bcrypt = require('bcrypt'); +const moment = require('moment'); + +module.exports = function (sequelize, DataTypes) { + const conversationparticipants = sequelize.define( + 'conversationparticipants', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + conversationparticipants.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.conversationparticipants.belongsTo(db.conversations, { + as: 'conversation', + foreignKey: { + name: 'conversationId', + }, + constraints: false, + }); + + db.conversationparticipants.belongsTo(db.users, { + as: 'user', + foreignKey: { + name: 'userId', + }, + constraints: false, + }); + + db.conversationparticipants.belongsTo(db.users, { + as: 'createdBy', + }); + + db.conversationparticipants.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return conversationparticipants; +}; diff --git a/backend/src/db/models/conversations.js b/backend/src/db/models/conversations.js new file mode 100644 index 0000000..da1cd2e --- /dev/null +++ b/backend/src/db/models/conversations.js @@ -0,0 +1,65 @@ +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 conversations = sequelize.define( + 'conversations', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + + createdat: { + type: DataTypes.DATE, + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + conversations.associate = (db) => { + /// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity + + db.conversations.hasMany(db.messages, { + as: 'messages_conversation', + foreignKey: { + name: 'conversationId', + }, + constraints: false, + }); + + db.conversations.hasMany(db.conversationparticipants, { + as: 'conversationparticipants_conversation', + foreignKey: { + name: 'conversationId', + }, + constraints: false, + }); + + //end loop + + db.conversations.belongsTo(db.users, { + as: 'createdBy', + }); + + db.conversations.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return conversations; +}; diff --git a/backend/src/db/models/distilleries.js b/backend/src/db/models/distilleries.js index 88e041e..fe721ca 100644 --- a/backend/src/db/models/distilleries.js +++ b/backend/src/db/models/distilleries.js @@ -18,6 +18,18 @@ module.exports = function (sequelize, DataTypes) { type: DataTypes.TEXT, }, + city: { + type: DataTypes.TEXT, + }, + + state: { + type: DataTypes.TEXT, + }, + + status: { + type: DataTypes.INTEGER, + }, + importHash: { type: DataTypes.STRING(255), allowNull: true, @@ -34,14 +46,6 @@ module.exports = function (sequelize, DataTypes) { distilleries.associate = (db) => { /// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity - db.distilleries.hasMany(db.bottles, { - as: 'bottles_distillery', - foreignKey: { - name: 'distilleryId', - }, - constraints: false, - }); - db.distilleries.hasMany(db.brands, { as: 'brands_distillery', foreignKey: { diff --git a/backend/src/db/models/locations.js b/backend/src/db/models/locations.js new file mode 100644 index 0000000..c186da1 --- /dev/null +++ b/backend/src/db/models/locations.js @@ -0,0 +1,65 @@ +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 locations = sequelize.define( + 'locations', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + + name: { + type: DataTypes.TEXT, + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + locations.associate = (db) => { + /// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity + + db.locations.hasMany(db.bottles, { + as: 'bottles_location', + foreignKey: { + name: 'locationId', + }, + constraints: false, + }); + + //end loop + + db.locations.belongsTo(db.users, { + as: 'user', + foreignKey: { + name: 'userId', + }, + constraints: false, + }); + + db.locations.belongsTo(db.users, { + as: 'createdBy', + }); + + db.locations.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return locations; +}; diff --git a/backend/src/db/models/messages.js b/backend/src/db/models/messages.js new file mode 100644 index 0000000..8a776f9 --- /dev/null +++ b/backend/src/db/models/messages.js @@ -0,0 +1,61 @@ +const config = require('../../config'); +const providers = config.providers; +const crypto = require('crypto'); +const bcrypt = require('bcrypt'); +const moment = require('moment'); + +module.exports = function (sequelize, DataTypes) { + const messages = sequelize.define( + 'messages', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + messages.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.messages.belongsTo(db.conversations, { + as: 'conversation', + foreignKey: { + name: 'conversationId', + }, + constraints: false, + }); + + db.messages.belongsTo(db.users, { + as: 'sender', + foreignKey: { + name: 'senderId', + }, + constraints: false, + }); + + db.messages.belongsTo(db.users, { + as: 'createdBy', + }); + + db.messages.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return messages; +}; diff --git a/backend/src/db/models/photos.js b/backend/src/db/models/photos.js new file mode 100644 index 0000000..af905bc --- /dev/null +++ b/backend/src/db/models/photos.js @@ -0,0 +1,91 @@ +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 photos = sequelize.define( + 'photos', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + + phototype: { + type: DataTypes.TEXT, + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + photos.associate = (db) => { + /// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity + + db.photos.hasMany(db.products, { + as: 'products_photofront', + foreignKey: { + name: 'photofrontId', + }, + constraints: false, + }); + + db.photos.hasMany(db.products, { + as: 'products_photoback', + foreignKey: { + name: 'photobackId', + }, + constraints: false, + }); + + db.photos.hasMany(db.bottles, { + as: 'bottles_photofront', + foreignKey: { + name: 'photofrontId', + }, + constraints: false, + }); + + db.photos.hasMany(db.bottles, { + as: 'bottles_photoback', + foreignKey: { + name: 'photobackId', + }, + constraints: false, + }); + + //end loop + + db.photos.hasMany(db.file, { + as: 'image', + foreignKey: 'belongsToId', + constraints: false, + scope: { + belongsTo: db.photos.getTableName(), + belongsToColumn: 'image', + }, + }); + + db.photos.belongsTo(db.users, { + as: 'createdBy', + }); + + db.photos.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return photos; +}; diff --git a/backend/src/db/models/products.js b/backend/src/db/models/products.js new file mode 100644 index 0000000..ff187a5 --- /dev/null +++ b/backend/src/db/models/products.js @@ -0,0 +1,101 @@ +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 products = sequelize.define( + 'products', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + + name: { + type: DataTypes.TEXT, + }, + + proof: { + type: DataTypes.DECIMAL, + }, + + age: { + type: DataTypes.INTEGER, + }, + + barcode: { + type: DataTypes.TEXT, + }, + + notes: { + type: DataTypes.TEXT, + }, + + status: { + type: DataTypes.INTEGER, + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + products.associate = (db) => { + /// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity + + db.products.hasMany(db.bottles, { + as: 'bottles_product', + foreignKey: { + name: 'productId', + }, + constraints: false, + }); + + //end loop + + db.products.belongsTo(db.brands, { + as: 'brand', + foreignKey: { + name: 'brandId', + }, + constraints: false, + }); + + db.products.belongsTo(db.photos, { + as: 'photofront', + foreignKey: { + name: 'photofrontId', + }, + constraints: false, + }); + + db.products.belongsTo(db.photos, { + as: 'photoback', + foreignKey: { + name: 'photobackId', + }, + constraints: false, + }); + + db.products.belongsTo(db.users, { + as: 'createdBy', + }); + + db.products.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return products; +}; diff --git a/backend/src/db/models/reviews.js b/backend/src/db/models/reviews.js new file mode 100644 index 0000000..1e9d916 --- /dev/null +++ b/backend/src/db/models/reviews.js @@ -0,0 +1,73 @@ +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 reviews = sequelize.define( + 'reviews', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + + rating: { + type: DataTypes.INTEGER, + }, + + notes: { + type: DataTypes.TEXT, + }, + + createdat: { + type: DataTypes.DATE, + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + reviews.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.reviews.belongsTo(db.users, { + as: 'user', + foreignKey: { + name: 'userId', + }, + constraints: false, + }); + + db.reviews.belongsTo(db.bottles, { + as: 'bottle', + foreignKey: { + name: 'bottleId', + }, + constraints: false, + }); + + db.reviews.belongsTo(db.users, { + as: 'createdBy', + }); + + db.reviews.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return reviews; +}; diff --git a/backend/src/db/models/users.js b/backend/src/db/models/users.js index af07cd6..4414c07 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, }, + address: { + type: DataTypes.TEXT, + }, + + address2: { + type: DataTypes.TEXT, + }, + importHash: { type: DataTypes.STRING(255), allowNull: true, @@ -102,6 +110,14 @@ 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.locations, { + as: 'locations_user', + foreignKey: { + name: 'userId', + }, + constraints: false, + }); + db.users.hasMany(db.bottles, { as: 'bottles_user', foreignKey: { @@ -110,6 +126,30 @@ module.exports = function (sequelize, DataTypes) { constraints: false, }); + db.users.hasMany(db.reviews, { + as: 'reviews_user', + foreignKey: { + name: 'userId', + }, + constraints: false, + }); + + db.users.hasMany(db.messages, { + as: 'messages_sender', + foreignKey: { + name: 'senderId', + }, + constraints: false, + }); + + db.users.hasMany(db.conversationparticipants, { + as: 'conversationparticipants_user', + foreignKey: { + name: 'userId', + }, + constraints: false, + }); + //end loop db.users.belongsTo(db.roles, { diff --git a/backend/src/db/seeders/20200430130760-user-roles.js b/backend/src/db/seeders/20200430130760-user-roles.js index 67a934b..3b54060 100644 --- a/backend/src/db/seeders/20200430130760-user-roles.js +++ b/backend/src/db/seeders/20200430130760-user-roles.js @@ -105,11 +105,18 @@ module.exports = { const entities = [ 'users', - 'bottles', - 'brands', - 'distilleries', 'roles', 'permissions', + 'distilleries', + 'brands', + 'photos', + 'products', + 'locations', + 'bottles', + 'reviews', + 'conversations', + 'messages', + 'conversationparticipants', , ]; await queryInterface.bulkInsert( @@ -211,91 +218,63 @@ primary key ("roles_permissionsId", "permissionId") createdAt, updatedAt, roles_permissionsId: getId('WhiskeyMaster'), - permissionId: getId('CREATE_BOTTLES'), + permissionId: getId('CREATE_DISTILLERIES'), }, { createdAt, updatedAt, roles_permissionsId: getId('WhiskeyMaster'), - permissionId: getId('READ_BOTTLES'), + permissionId: getId('READ_DISTILLERIES'), }, { createdAt, updatedAt, roles_permissionsId: getId('WhiskeyMaster'), - permissionId: getId('UPDATE_BOTTLES'), + permissionId: getId('UPDATE_DISTILLERIES'), }, { createdAt, updatedAt, roles_permissionsId: getId('WhiskeyMaster'), - permissionId: getId('DELETE_BOTTLES'), + permissionId: getId('DELETE_DISTILLERIES'), }, { createdAt, updatedAt, roles_permissionsId: getId('InventoryManager'), - permissionId: getId('CREATE_BOTTLES'), + permissionId: getId('READ_DISTILLERIES'), }, { createdAt, updatedAt, roles_permissionsId: getId('InventoryManager'), - permissionId: getId('READ_BOTTLES'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('InventoryManager'), - permissionId: getId('UPDATE_BOTTLES'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('InventoryManager'), - permissionId: getId('DELETE_BOTTLES'), + permissionId: getId('UPDATE_DISTILLERIES'), }, { createdAt, updatedAt, roles_permissionsId: getId('TastingExpert'), - permissionId: getId('READ_BOTTLES'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('TastingExpert'), - permissionId: getId('UPDATE_BOTTLES'), + permissionId: getId('READ_DISTILLERIES'), }, { createdAt, updatedAt, roles_permissionsId: getId('BottleCollector'), - permissionId: getId('READ_BOTTLES'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('BottleCollector'), - permissionId: getId('UPDATE_BOTTLES'), + permissionId: getId('READ_DISTILLERIES'), }, { createdAt, updatedAt, roles_permissionsId: getId('WhiskeyEnthusiast'), - permissionId: getId('READ_BOTTLES'), + permissionId: getId('READ_DISTILLERIES'), }, { @@ -365,63 +344,91 @@ primary key ("roles_permissionsId", "permissionId") createdAt, updatedAt, roles_permissionsId: getId('WhiskeyMaster'), - permissionId: getId('CREATE_DISTILLERIES'), + permissionId: getId('CREATE_BOTTLES'), }, { createdAt, updatedAt, roles_permissionsId: getId('WhiskeyMaster'), - permissionId: getId('READ_DISTILLERIES'), + permissionId: getId('READ_BOTTLES'), }, { createdAt, updatedAt, roles_permissionsId: getId('WhiskeyMaster'), - permissionId: getId('UPDATE_DISTILLERIES'), + permissionId: getId('UPDATE_BOTTLES'), }, { createdAt, updatedAt, roles_permissionsId: getId('WhiskeyMaster'), - permissionId: getId('DELETE_DISTILLERIES'), + permissionId: getId('DELETE_BOTTLES'), }, { createdAt, updatedAt, roles_permissionsId: getId('InventoryManager'), - permissionId: getId('READ_DISTILLERIES'), + permissionId: getId('CREATE_BOTTLES'), }, { createdAt, updatedAt, roles_permissionsId: getId('InventoryManager'), - permissionId: getId('UPDATE_DISTILLERIES'), + permissionId: getId('READ_BOTTLES'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('InventoryManager'), + permissionId: getId('UPDATE_BOTTLES'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('InventoryManager'), + permissionId: getId('DELETE_BOTTLES'), }, { createdAt, updatedAt, roles_permissionsId: getId('TastingExpert'), - permissionId: getId('READ_DISTILLERIES'), + permissionId: getId('READ_BOTTLES'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('TastingExpert'), + permissionId: getId('UPDATE_BOTTLES'), }, { createdAt, updatedAt, roles_permissionsId: getId('BottleCollector'), - permissionId: getId('READ_DISTILLERIES'), + permissionId: getId('READ_BOTTLES'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('BottleCollector'), + permissionId: getId('UPDATE_BOTTLES'), }, { createdAt, updatedAt, roles_permissionsId: getId('WhiskeyEnthusiast'), - permissionId: getId('READ_DISTILLERIES'), + permissionId: getId('READ_BOTTLES'), }, { @@ -484,81 +491,6 @@ primary key ("roles_permissionsId", "permissionId") permissionId: getId('DELETE_USERS'), }, - { - createdAt, - updatedAt, - roles_permissionsId: getId('Administrator'), - permissionId: getId('CREATE_BOTTLES'), - }, - { - createdAt, - updatedAt, - roles_permissionsId: getId('Administrator'), - permissionId: getId('READ_BOTTLES'), - }, - { - createdAt, - updatedAt, - roles_permissionsId: getId('Administrator'), - permissionId: getId('UPDATE_BOTTLES'), - }, - { - createdAt, - updatedAt, - roles_permissionsId: getId('Administrator'), - permissionId: getId('DELETE_BOTTLES'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('Administrator'), - permissionId: getId('CREATE_BRANDS'), - }, - { - createdAt, - updatedAt, - roles_permissionsId: getId('Administrator'), - permissionId: getId('READ_BRANDS'), - }, - { - createdAt, - updatedAt, - roles_permissionsId: getId('Administrator'), - permissionId: getId('UPDATE_BRANDS'), - }, - { - createdAt, - updatedAt, - roles_permissionsId: getId('Administrator'), - permissionId: getId('DELETE_BRANDS'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('Administrator'), - permissionId: getId('CREATE_DISTILLERIES'), - }, - { - createdAt, - updatedAt, - roles_permissionsId: getId('Administrator'), - permissionId: getId('READ_DISTILLERIES'), - }, - { - createdAt, - updatedAt, - roles_permissionsId: getId('Administrator'), - permissionId: getId('UPDATE_DISTILLERIES'), - }, - { - createdAt, - updatedAt, - roles_permissionsId: getId('Administrator'), - permissionId: getId('DELETE_DISTILLERIES'), - }, - { createdAt, updatedAt, @@ -609,6 +541,256 @@ primary key ("roles_permissionsId", "permissionId") permissionId: getId('DELETE_PERMISSIONS'), }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('CREATE_DISTILLERIES'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('READ_DISTILLERIES'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('UPDATE_DISTILLERIES'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('DELETE_DISTILLERIES'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('CREATE_BRANDS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('READ_BRANDS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('UPDATE_BRANDS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('DELETE_BRANDS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('CREATE_PHOTOS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('READ_PHOTOS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('UPDATE_PHOTOS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('DELETE_PHOTOS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('CREATE_PRODUCTS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('READ_PRODUCTS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('UPDATE_PRODUCTS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('DELETE_PRODUCTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('CREATE_LOCATIONS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('READ_LOCATIONS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('UPDATE_LOCATIONS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('DELETE_LOCATIONS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('CREATE_BOTTLES'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('READ_BOTTLES'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('UPDATE_BOTTLES'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('DELETE_BOTTLES'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('CREATE_REVIEWS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('READ_REVIEWS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('UPDATE_REVIEWS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('DELETE_REVIEWS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('CREATE_CONVERSATIONS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('READ_CONVERSATIONS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('UPDATE_CONVERSATIONS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('DELETE_CONVERSATIONS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('CREATE_MESSAGES'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('READ_MESSAGES'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('UPDATE_MESSAGES'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('DELETE_MESSAGES'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('CREATE_CONVERSATIONPARTICIPANTS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('READ_CONVERSATIONPARTICIPANTS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('UPDATE_CONVERSATIONPARTICIPANTS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('DELETE_CONVERSATIONPARTICIPANTS'), + }, + { createdAt, updatedAt, diff --git a/backend/src/db/seeders/20231127130745-sample-data.js b/backend/src/db/seeders/20231127130745-sample-data.js index 207d2f7..435ae40 100644 --- a/backend/src/db/seeders/20231127130745-sample-data.js +++ b/backend/src/db/seeders/20231127130745-sample-data.js @@ -1,171 +1,75 @@ const db = require('../models'); const Users = db.users; -const Bottles = db.bottles; +const Distilleries = db.distilleries; const Brands = db.brands; -const Distilleries = db.distilleries; +const Photos = db.photos; -const BottlesData = [ +const Products = db.products; + +const Locations = db.locations; + +const Bottles = db.bottles; + +const Reviews = db.reviews; + +const Conversations = db.conversations; + +const Messages = db.messages; + +const Conversationparticipants = db.conversationparticipants; + +const DistilleriesData = [ { - name: 'Old Forester 1920', + name: 'Old Forester', - // type code here for "relation_one" field + city: 'James Clerk Maxwell', - proof: 115, + state: 'Alfred Kinsey', - type: 'Rye', - - notes: 'Rich and full-bodied', - - tasting_notes: 'Caramel, vanilla, and oak', - - msrp_range: '$60-$70', - - secondary_value_range: '$80-$100', - - opened_bottle_indicator: false, - - quantity: 1, - - barcode: '123456789012', - - // type code here for "images" field - - age: 4, - - // type code here for "relation_one" field - - // type code here for "relation_one" field + status: 6, }, { - name: 'Lagavulin 16', + name: 'Lagavulin', - // type code here for "relation_one" field + city: 'J. Robert Oppenheimer', - proof: 86, + state: 'Galileo Galilei', - type: 'Bourbon', - - notes: 'Peaty and smoky', - - tasting_notes: 'Smoke, seaweed, and vanilla', - - msrp_range: '$100-$120', - - secondary_value_range: '$150-$180', - - opened_bottle_indicator: true, - - quantity: 1, - - barcode: '234567890123', - - // type code here for "images" field - - age: 16, - - // type code here for "relation_one" field - - // type code here for "relation_one" field + status: 1, }, { name: 'Buffalo Trace', - // type code here for "relation_one" field + city: 'Comte de Buffon', - proof: 90, + state: 'Thomas Hunt Morgan', - type: 'Bourbon', - - notes: 'Smooth and balanced', - - tasting_notes: 'Vanilla, caramel, and spice', - - msrp_range: '$25-$35', - - secondary_value_range: '$40-$50', - - opened_bottle_indicator: false, - - quantity: 2, - - barcode: '345678901234', - - // type code here for "images" field - - age: 8, - - // type code here for "relation_one" field - - // type code here for "relation_one" field + status: 4, }, { - name: 'Redbreast 12', + name: 'Midleton', - // type code here for "relation_one" field + city: 'Linus Pauling', - proof: 80, + state: 'Edward Teller', - type: 'Rye', - - notes: 'Rich and complex', - - tasting_notes: 'Sherry, fruit, and spice', - - msrp_range: '$50-$60', - - secondary_value_range: '$70-$90', - - opened_bottle_indicator: true, - - quantity: 1, - - barcode: '456789012345', - - // type code here for "images" field - - age: 12, - - // type code here for "relation_one" field - - // type code here for "relation_one" field + status: 5, }, { - name: 'Pappy Van Winkle 15', + name: 'Old Rip Van Winkle', - // type code here for "relation_one" field + city: 'Archimedes', - proof: 107, + state: 'Werner Heisenberg', - type: 'Bourbon', - - notes: 'Rare and exquisite', - - tasting_notes: 'Vanilla, oak, and spice', - - msrp_range: '$120-$150', - - secondary_value_range: '$2000-$2500', - - opened_bottle_indicator: true, - - quantity: 1, - - barcode: '567890123456', - - // type code here for "images" field - - age: 15, - - // type code here for "relation_one" field - - // type code here for "relation_one" field + status: 8, }, ]; @@ -173,169 +77,824 @@ const BrandsData = [ { name: 'Old Forester', + status: 4, + // type code here for "relation_one" field }, { name: 'Lagavulin', + status: 7, + // type code here for "relation_one" field }, { name: 'Buffalo Trace', + status: 1, + // type code here for "relation_one" field }, { name: 'Redbreast', + status: 4, + // type code here for "relation_one" field }, { name: 'Pappy Van Winkle', + status: 6, + // type code here for "relation_one" field }, ]; -const DistilleriesData = [ +const PhotosData = [ { - name: 'Old Forester', + phototype: 'William Bayliss', + + // type code here for "images" field }, { - name: 'Lagavulin', + phototype: 'William Harvey', + + // type code here for "images" field }, { - name: 'Buffalo Trace', + phototype: 'George Gaylord Simpson', + + // type code here for "images" field }, { - name: 'Midleton', + phototype: 'Marcello Malpighi', + + // type code here for "images" field }, { - name: 'Old Rip Van Winkle', + phototype: 'Claude Levi-Strauss', + + // type code here for "images" field + }, +]; + +const ProductsData = [ + { + // type code here for "relation_one" field + + name: 'Max von Laue', + + proof: 64.07, + + age: 4, + + barcode: 'George Gaylord Simpson', + + notes: 'Ernest Rutherford', + + status: 2, + + // type code here for "relation_one" field + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + name: 'John Bardeen', + + proof: 14.86, + + age: 7, + + barcode: 'Alfred Binet', + + notes: 'James Clerk Maxwell', + + status: 4, + + // type code here for "relation_one" field + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + name: 'John von Neumann', + + proof: 52.62, + + age: 3, + + barcode: 'Richard Feynman', + + notes: 'Karl Landsteiner', + + status: 9, + + // type code here for "relation_one" field + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + name: 'Heike Kamerlingh Onnes', + + proof: 87.82, + + age: 8, + + barcode: 'J. Robert Oppenheimer', + + notes: 'Charles Lyell', + + status: 6, + + // type code here for "relation_one" field + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + name: 'Max Planck', + + proof: 76.09, + + age: 6, + + barcode: 'Ernst Haeckel', + + notes: 'Ludwig Boltzmann', + + status: 3, + + // type code here for "relation_one" field + + // type code here for "relation_one" field + }, +]; + +const LocationsData = [ + { + name: 'George Gaylord Simpson', + + // type code here for "relation_one" field + }, + + { + name: 'Galileo Galilei', + + // type code here for "relation_one" field + }, + + { + name: 'Anton van Leeuwenhoek', + + // type code here for "relation_one" field + }, + + { + name: 'Emil Kraepelin', + + // type code here for "relation_one" field + }, + + { + name: 'James Watson', + + // type code here for "relation_one" field + }, +]; + +const BottlesData = [ + { + // type code here for "relation_one" field + + // type code here for "relation_one" field + + // type code here for "relation_one" field + + proof: 115, + + age: 4, + + rating: 8, + + collectable: false, + + rickhouse: 'Enrico Fermi', + + rack: 'Paul Ehrlich', + + release: 'Michael Faraday', + + barrelnumber: 'J. Robert Oppenheimer', + + barreleddate: new Date(Date.now()), + + bottlenumber: 'Albrecht von Haller', + + dateacquired: new Date(Date.now()), + + volume: 9, + + notes: 'Rich and full-bodied', + + // type code here for "relation_one" field + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + // type code here for "relation_one" field + + // type code here for "relation_one" field + + proof: 86, + + age: 16, + + rating: 2, + + collectable: false, + + rickhouse: 'William Bayliss', + + rack: 'Johannes Kepler', + + release: 'Sheldon Glashow', + + barrelnumber: 'Claude Levi-Strauss', + + barreleddate: new Date(Date.now()), + + bottlenumber: 'William Herschel', + + dateacquired: new Date(Date.now()), + + volume: 5, + + notes: 'Peaty and smoky', + + // type code here for "relation_one" field + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + // type code here for "relation_one" field + + // type code here for "relation_one" field + + proof: 90, + + age: 8, + + rating: 1, + + collectable: false, + + rickhouse: 'Jean Baptiste Lamarck', + + rack: 'Frederick Gowland Hopkins', + + release: 'Emil Kraepelin', + + barrelnumber: 'Jean Piaget', + + barreleddate: new Date(Date.now()), + + bottlenumber: 'William Herschel', + + dateacquired: new Date(Date.now()), + + volume: 3, + + notes: 'Smooth and balanced', + + // type code here for "relation_one" field + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + // type code here for "relation_one" field + + // type code here for "relation_one" field + + proof: 80, + + age: 12, + + rating: 4, + + collectable: false, + + rickhouse: 'Frederick Gowland Hopkins', + + rack: 'Alexander Fleming', + + release: 'Ernst Mayr', + + barrelnumber: 'James Clerk Maxwell', + + barreleddate: new Date(Date.now()), + + bottlenumber: 'Francis Galton', + + dateacquired: new Date(Date.now()), + + volume: 8, + + notes: 'Rich and complex', + + // type code here for "relation_one" field + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + // type code here for "relation_one" field + + // type code here for "relation_one" field + + proof: 107, + + age: 15, + + rating: 3, + + collectable: true, + + rickhouse: 'Heike Kamerlingh Onnes', + + rack: 'Trofim Lysenko', + + release: 'Willard Libby', + + barrelnumber: 'Enrico Fermi', + + barreleddate: new Date(Date.now()), + + bottlenumber: 'Max von Laue', + + dateacquired: new Date(Date.now()), + + volume: 2, + + notes: 'Rare and exquisite', + + // type code here for "relation_one" field + + // type code here for "relation_one" field + }, +]; + +const ReviewsData = [ + { + // type code here for "relation_one" field + + // type code here for "relation_one" field + + rating: 7, + + notes: 'Sheldon Glashow', + + createdat: new Date(Date.now()), + }, + + { + // type code here for "relation_one" field + + // type code here for "relation_one" field + + rating: 6, + + notes: 'James Watson', + + createdat: new Date(Date.now()), + }, + + { + // type code here for "relation_one" field + + // type code here for "relation_one" field + + rating: 2, + + notes: 'Anton van Leeuwenhoek', + + createdat: new Date(Date.now()), + }, + + { + // type code here for "relation_one" field + + // type code here for "relation_one" field + + rating: 1, + + notes: 'Karl Landsteiner', + + createdat: new Date(Date.now()), + }, + + { + // type code here for "relation_one" field + + // type code here for "relation_one" field + + rating: 3, + + notes: 'Edward O. Wilson', + + createdat: new Date(Date.now()), + }, +]; + +const ConversationsData = [ + { + createdat: new Date(Date.now()), + }, + + { + createdat: new Date(Date.now()), + }, + + { + createdat: new Date(Date.now()), + }, + + { + createdat: new Date(Date.now()), + }, + + { + createdat: new Date(Date.now()), + }, +]; + +const MessagesData = [ + { + // type code here for "relation_one" field + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + // type code here for "relation_one" field + }, +]; + +const ConversationparticipantsData = [ + { + // type code here for "relation_one" field + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + // type code here for "relation_one" field }, ]; // Similar logic for "relation_many" -async function associateBottleWithBrand() { - const relatedBrand0 = await Brands.findOne({ - offset: Math.floor(Math.random() * (await Brands.count())), - }); - const Bottle0 = await Bottles.findOne({ - order: [['id', 'ASC']], - offset: 0, - }); - if (Bottle0?.setBrand) { - await Bottle0.setBrand(relatedBrand0); - } - - const relatedBrand1 = await Brands.findOne({ - offset: Math.floor(Math.random() * (await Brands.count())), - }); - const Bottle1 = await Bottles.findOne({ - order: [['id', 'ASC']], - offset: 1, - }); - if (Bottle1?.setBrand) { - await Bottle1.setBrand(relatedBrand1); - } - - const relatedBrand2 = await Brands.findOne({ - offset: Math.floor(Math.random() * (await Brands.count())), - }); - const Bottle2 = await Bottles.findOne({ - order: [['id', 'ASC']], - offset: 2, - }); - if (Bottle2?.setBrand) { - await Bottle2.setBrand(relatedBrand2); - } - - const relatedBrand3 = await Brands.findOne({ - offset: Math.floor(Math.random() * (await Brands.count())), - }); - const Bottle3 = await Bottles.findOne({ - order: [['id', 'ASC']], - offset: 3, - }); - if (Bottle3?.setBrand) { - await Bottle3.setBrand(relatedBrand3); - } - - const relatedBrand4 = await Brands.findOne({ - offset: Math.floor(Math.random() * (await Brands.count())), - }); - const Bottle4 = await Bottles.findOne({ - order: [['id', 'ASC']], - offset: 4, - }); - if (Bottle4?.setBrand) { - await Bottle4.setBrand(relatedBrand4); - } -} - -async function associateBottleWithDistillery() { +async function associateBrandWithDistillery() { const relatedDistillery0 = await Distilleries.findOne({ offset: Math.floor(Math.random() * (await Distilleries.count())), }); - const Bottle0 = await Bottles.findOne({ + const Brand0 = await Brands.findOne({ order: [['id', 'ASC']], offset: 0, }); - if (Bottle0?.setDistillery) { - await Bottle0.setDistillery(relatedDistillery0); + if (Brand0?.setDistillery) { + await Brand0.setDistillery(relatedDistillery0); } const relatedDistillery1 = await Distilleries.findOne({ offset: Math.floor(Math.random() * (await Distilleries.count())), }); - const Bottle1 = await Bottles.findOne({ + const Brand1 = await Brands.findOne({ order: [['id', 'ASC']], offset: 1, }); - if (Bottle1?.setDistillery) { - await Bottle1.setDistillery(relatedDistillery1); + if (Brand1?.setDistillery) { + await Brand1.setDistillery(relatedDistillery1); } const relatedDistillery2 = await Distilleries.findOne({ offset: Math.floor(Math.random() * (await Distilleries.count())), }); - const Bottle2 = await Bottles.findOne({ + const Brand2 = await Brands.findOne({ order: [['id', 'ASC']], offset: 2, }); - if (Bottle2?.setDistillery) { - await Bottle2.setDistillery(relatedDistillery2); + if (Brand2?.setDistillery) { + await Brand2.setDistillery(relatedDistillery2); } const relatedDistillery3 = await Distilleries.findOne({ offset: Math.floor(Math.random() * (await Distilleries.count())), }); - const Bottle3 = await Bottles.findOne({ + const Brand3 = await Brands.findOne({ order: [['id', 'ASC']], offset: 3, }); - if (Bottle3?.setDistillery) { - await Bottle3.setDistillery(relatedDistillery3); + if (Brand3?.setDistillery) { + await Brand3.setDistillery(relatedDistillery3); } const relatedDistillery4 = await Distilleries.findOne({ offset: Math.floor(Math.random() * (await Distilleries.count())), }); - const Bottle4 = await Bottles.findOne({ + const Brand4 = await Brands.findOne({ order: [['id', 'ASC']], offset: 4, }); - if (Bottle4?.setDistillery) { - await Bottle4.setDistillery(relatedDistillery4); + if (Brand4?.setDistillery) { + await Brand4.setDistillery(relatedDistillery4); + } +} + +async function associateProductWithBrand() { + const relatedBrand0 = await Brands.findOne({ + offset: Math.floor(Math.random() * (await Brands.count())), + }); + const Product0 = await Products.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (Product0?.setBrand) { + await Product0.setBrand(relatedBrand0); + } + + const relatedBrand1 = await Brands.findOne({ + offset: Math.floor(Math.random() * (await Brands.count())), + }); + const Product1 = await Products.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (Product1?.setBrand) { + await Product1.setBrand(relatedBrand1); + } + + const relatedBrand2 = await Brands.findOne({ + offset: Math.floor(Math.random() * (await Brands.count())), + }); + const Product2 = await Products.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (Product2?.setBrand) { + await Product2.setBrand(relatedBrand2); + } + + const relatedBrand3 = await Brands.findOne({ + offset: Math.floor(Math.random() * (await Brands.count())), + }); + const Product3 = await Products.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (Product3?.setBrand) { + await Product3.setBrand(relatedBrand3); + } + + const relatedBrand4 = await Brands.findOne({ + offset: Math.floor(Math.random() * (await Brands.count())), + }); + const Product4 = await Products.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (Product4?.setBrand) { + await Product4.setBrand(relatedBrand4); + } +} + +async function associateProductWithPhotofront() { + const relatedPhotofront0 = await Photos.findOne({ + offset: Math.floor(Math.random() * (await Photos.count())), + }); + const Product0 = await Products.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (Product0?.setPhotofront) { + await Product0.setPhotofront(relatedPhotofront0); + } + + const relatedPhotofront1 = await Photos.findOne({ + offset: Math.floor(Math.random() * (await Photos.count())), + }); + const Product1 = await Products.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (Product1?.setPhotofront) { + await Product1.setPhotofront(relatedPhotofront1); + } + + const relatedPhotofront2 = await Photos.findOne({ + offset: Math.floor(Math.random() * (await Photos.count())), + }); + const Product2 = await Products.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (Product2?.setPhotofront) { + await Product2.setPhotofront(relatedPhotofront2); + } + + const relatedPhotofront3 = await Photos.findOne({ + offset: Math.floor(Math.random() * (await Photos.count())), + }); + const Product3 = await Products.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (Product3?.setPhotofront) { + await Product3.setPhotofront(relatedPhotofront3); + } + + const relatedPhotofront4 = await Photos.findOne({ + offset: Math.floor(Math.random() * (await Photos.count())), + }); + const Product4 = await Products.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (Product4?.setPhotofront) { + await Product4.setPhotofront(relatedPhotofront4); + } +} + +async function associateProductWithPhotoback() { + const relatedPhotoback0 = await Photos.findOne({ + offset: Math.floor(Math.random() * (await Photos.count())), + }); + const Product0 = await Products.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (Product0?.setPhotoback) { + await Product0.setPhotoback(relatedPhotoback0); + } + + const relatedPhotoback1 = await Photos.findOne({ + offset: Math.floor(Math.random() * (await Photos.count())), + }); + const Product1 = await Products.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (Product1?.setPhotoback) { + await Product1.setPhotoback(relatedPhotoback1); + } + + const relatedPhotoback2 = await Photos.findOne({ + offset: Math.floor(Math.random() * (await Photos.count())), + }); + const Product2 = await Products.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (Product2?.setPhotoback) { + await Product2.setPhotoback(relatedPhotoback2); + } + + const relatedPhotoback3 = await Photos.findOne({ + offset: Math.floor(Math.random() * (await Photos.count())), + }); + const Product3 = await Products.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (Product3?.setPhotoback) { + await Product3.setPhotoback(relatedPhotoback3); + } + + const relatedPhotoback4 = await Photos.findOne({ + offset: Math.floor(Math.random() * (await Photos.count())), + }); + const Product4 = await Products.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (Product4?.setPhotoback) { + await Product4.setPhotoback(relatedPhotoback4); + } +} + +async function associateLocationWithUser() { + const relatedUser0 = await Users.findOne({ + offset: Math.floor(Math.random() * (await Users.count())), + }); + const Location0 = await Locations.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (Location0?.setUser) { + await Location0.setUser(relatedUser0); + } + + const relatedUser1 = await Users.findOne({ + offset: Math.floor(Math.random() * (await Users.count())), + }); + const Location1 = await Locations.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (Location1?.setUser) { + await Location1.setUser(relatedUser1); + } + + const relatedUser2 = await Users.findOne({ + offset: Math.floor(Math.random() * (await Users.count())), + }); + const Location2 = await Locations.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (Location2?.setUser) { + await Location2.setUser(relatedUser2); + } + + const relatedUser3 = await Users.findOne({ + offset: Math.floor(Math.random() * (await Users.count())), + }); + const Location3 = await Locations.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (Location3?.setUser) { + await Location3.setUser(relatedUser3); + } + + const relatedUser4 = await Users.findOne({ + offset: Math.floor(Math.random() * (await Users.count())), + }); + const Location4 = await Locations.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (Location4?.setUser) { + await Location4.setUser(relatedUser4); } } @@ -396,89 +955,654 @@ async function associateBottleWithUser() { } } -async function associateBrandWithDistillery() { - const relatedDistillery0 = await Distilleries.findOne({ - offset: Math.floor(Math.random() * (await Distilleries.count())), +async function associateBottleWithProduct() { + const relatedProduct0 = await Products.findOne({ + offset: Math.floor(Math.random() * (await Products.count())), }); - const Brand0 = await Brands.findOne({ + const Bottle0 = await Bottles.findOne({ order: [['id', 'ASC']], offset: 0, }); - if (Brand0?.setDistillery) { - await Brand0.setDistillery(relatedDistillery0); + if (Bottle0?.setProduct) { + await Bottle0.setProduct(relatedProduct0); } - const relatedDistillery1 = await Distilleries.findOne({ - offset: Math.floor(Math.random() * (await Distilleries.count())), + const relatedProduct1 = await Products.findOne({ + offset: Math.floor(Math.random() * (await Products.count())), }); - const Brand1 = await Brands.findOne({ + const Bottle1 = await Bottles.findOne({ order: [['id', 'ASC']], offset: 1, }); - if (Brand1?.setDistillery) { - await Brand1.setDistillery(relatedDistillery1); + if (Bottle1?.setProduct) { + await Bottle1.setProduct(relatedProduct1); } - const relatedDistillery2 = await Distilleries.findOne({ - offset: Math.floor(Math.random() * (await Distilleries.count())), + const relatedProduct2 = await Products.findOne({ + offset: Math.floor(Math.random() * (await Products.count())), }); - const Brand2 = await Brands.findOne({ + const Bottle2 = await Bottles.findOne({ order: [['id', 'ASC']], offset: 2, }); - if (Brand2?.setDistillery) { - await Brand2.setDistillery(relatedDistillery2); + if (Bottle2?.setProduct) { + await Bottle2.setProduct(relatedProduct2); } - const relatedDistillery3 = await Distilleries.findOne({ - offset: Math.floor(Math.random() * (await Distilleries.count())), + const relatedProduct3 = await Products.findOne({ + offset: Math.floor(Math.random() * (await Products.count())), }); - const Brand3 = await Brands.findOne({ + const Bottle3 = await Bottles.findOne({ order: [['id', 'ASC']], offset: 3, }); - if (Brand3?.setDistillery) { - await Brand3.setDistillery(relatedDistillery3); + if (Bottle3?.setProduct) { + await Bottle3.setProduct(relatedProduct3); } - const relatedDistillery4 = await Distilleries.findOne({ - offset: Math.floor(Math.random() * (await Distilleries.count())), + const relatedProduct4 = await Products.findOne({ + offset: Math.floor(Math.random() * (await Products.count())), }); - const Brand4 = await Brands.findOne({ + const Bottle4 = await Bottles.findOne({ order: [['id', 'ASC']], offset: 4, }); - if (Brand4?.setDistillery) { - await Brand4.setDistillery(relatedDistillery4); + if (Bottle4?.setProduct) { + await Bottle4.setProduct(relatedProduct4); + } +} + +async function associateBottleWithLocation() { + const relatedLocation0 = await Locations.findOne({ + offset: Math.floor(Math.random() * (await Locations.count())), + }); + const Bottle0 = await Bottles.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (Bottle0?.setLocation) { + await Bottle0.setLocation(relatedLocation0); + } + + const relatedLocation1 = await Locations.findOne({ + offset: Math.floor(Math.random() * (await Locations.count())), + }); + const Bottle1 = await Bottles.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (Bottle1?.setLocation) { + await Bottle1.setLocation(relatedLocation1); + } + + const relatedLocation2 = await Locations.findOne({ + offset: Math.floor(Math.random() * (await Locations.count())), + }); + const Bottle2 = await Bottles.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (Bottle2?.setLocation) { + await Bottle2.setLocation(relatedLocation2); + } + + const relatedLocation3 = await Locations.findOne({ + offset: Math.floor(Math.random() * (await Locations.count())), + }); + const Bottle3 = await Bottles.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (Bottle3?.setLocation) { + await Bottle3.setLocation(relatedLocation3); + } + + const relatedLocation4 = await Locations.findOne({ + offset: Math.floor(Math.random() * (await Locations.count())), + }); + const Bottle4 = await Bottles.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (Bottle4?.setLocation) { + await Bottle4.setLocation(relatedLocation4); + } +} + +async function associateBottleWithPhotofront() { + const relatedPhotofront0 = await Photos.findOne({ + offset: Math.floor(Math.random() * (await Photos.count())), + }); + const Bottle0 = await Bottles.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (Bottle0?.setPhotofront) { + await Bottle0.setPhotofront(relatedPhotofront0); + } + + const relatedPhotofront1 = await Photos.findOne({ + offset: Math.floor(Math.random() * (await Photos.count())), + }); + const Bottle1 = await Bottles.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (Bottle1?.setPhotofront) { + await Bottle1.setPhotofront(relatedPhotofront1); + } + + const relatedPhotofront2 = await Photos.findOne({ + offset: Math.floor(Math.random() * (await Photos.count())), + }); + const Bottle2 = await Bottles.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (Bottle2?.setPhotofront) { + await Bottle2.setPhotofront(relatedPhotofront2); + } + + const relatedPhotofront3 = await Photos.findOne({ + offset: Math.floor(Math.random() * (await Photos.count())), + }); + const Bottle3 = await Bottles.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (Bottle3?.setPhotofront) { + await Bottle3.setPhotofront(relatedPhotofront3); + } + + const relatedPhotofront4 = await Photos.findOne({ + offset: Math.floor(Math.random() * (await Photos.count())), + }); + const Bottle4 = await Bottles.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (Bottle4?.setPhotofront) { + await Bottle4.setPhotofront(relatedPhotofront4); + } +} + +async function associateBottleWithPhotoback() { + const relatedPhotoback0 = await Photos.findOne({ + offset: Math.floor(Math.random() * (await Photos.count())), + }); + const Bottle0 = await Bottles.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (Bottle0?.setPhotoback) { + await Bottle0.setPhotoback(relatedPhotoback0); + } + + const relatedPhotoback1 = await Photos.findOne({ + offset: Math.floor(Math.random() * (await Photos.count())), + }); + const Bottle1 = await Bottles.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (Bottle1?.setPhotoback) { + await Bottle1.setPhotoback(relatedPhotoback1); + } + + const relatedPhotoback2 = await Photos.findOne({ + offset: Math.floor(Math.random() * (await Photos.count())), + }); + const Bottle2 = await Bottles.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (Bottle2?.setPhotoback) { + await Bottle2.setPhotoback(relatedPhotoback2); + } + + const relatedPhotoback3 = await Photos.findOne({ + offset: Math.floor(Math.random() * (await Photos.count())), + }); + const Bottle3 = await Bottles.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (Bottle3?.setPhotoback) { + await Bottle3.setPhotoback(relatedPhotoback3); + } + + const relatedPhotoback4 = await Photos.findOne({ + offset: Math.floor(Math.random() * (await Photos.count())), + }); + const Bottle4 = await Bottles.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (Bottle4?.setPhotoback) { + await Bottle4.setPhotoback(relatedPhotoback4); + } +} + +async function associateReviewWithUser() { + const relatedUser0 = await Users.findOne({ + offset: Math.floor(Math.random() * (await Users.count())), + }); + const Review0 = await Reviews.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (Review0?.setUser) { + await Review0.setUser(relatedUser0); + } + + const relatedUser1 = await Users.findOne({ + offset: Math.floor(Math.random() * (await Users.count())), + }); + const Review1 = await Reviews.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (Review1?.setUser) { + await Review1.setUser(relatedUser1); + } + + const relatedUser2 = await Users.findOne({ + offset: Math.floor(Math.random() * (await Users.count())), + }); + const Review2 = await Reviews.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (Review2?.setUser) { + await Review2.setUser(relatedUser2); + } + + const relatedUser3 = await Users.findOne({ + offset: Math.floor(Math.random() * (await Users.count())), + }); + const Review3 = await Reviews.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (Review3?.setUser) { + await Review3.setUser(relatedUser3); + } + + const relatedUser4 = await Users.findOne({ + offset: Math.floor(Math.random() * (await Users.count())), + }); + const Review4 = await Reviews.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (Review4?.setUser) { + await Review4.setUser(relatedUser4); + } +} + +async function associateReviewWithBottle() { + const relatedBottle0 = await Bottles.findOne({ + offset: Math.floor(Math.random() * (await Bottles.count())), + }); + const Review0 = await Reviews.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (Review0?.setBottle) { + await Review0.setBottle(relatedBottle0); + } + + const relatedBottle1 = await Bottles.findOne({ + offset: Math.floor(Math.random() * (await Bottles.count())), + }); + const Review1 = await Reviews.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (Review1?.setBottle) { + await Review1.setBottle(relatedBottle1); + } + + const relatedBottle2 = await Bottles.findOne({ + offset: Math.floor(Math.random() * (await Bottles.count())), + }); + const Review2 = await Reviews.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (Review2?.setBottle) { + await Review2.setBottle(relatedBottle2); + } + + const relatedBottle3 = await Bottles.findOne({ + offset: Math.floor(Math.random() * (await Bottles.count())), + }); + const Review3 = await Reviews.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (Review3?.setBottle) { + await Review3.setBottle(relatedBottle3); + } + + const relatedBottle4 = await Bottles.findOne({ + offset: Math.floor(Math.random() * (await Bottles.count())), + }); + const Review4 = await Reviews.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (Review4?.setBottle) { + await Review4.setBottle(relatedBottle4); + } +} + +async function associateMessageWithConversation() { + const relatedConversation0 = await Conversations.findOne({ + offset: Math.floor(Math.random() * (await Conversations.count())), + }); + const Message0 = await Messages.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (Message0?.setConversation) { + await Message0.setConversation(relatedConversation0); + } + + const relatedConversation1 = await Conversations.findOne({ + offset: Math.floor(Math.random() * (await Conversations.count())), + }); + const Message1 = await Messages.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (Message1?.setConversation) { + await Message1.setConversation(relatedConversation1); + } + + const relatedConversation2 = await Conversations.findOne({ + offset: Math.floor(Math.random() * (await Conversations.count())), + }); + const Message2 = await Messages.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (Message2?.setConversation) { + await Message2.setConversation(relatedConversation2); + } + + const relatedConversation3 = await Conversations.findOne({ + offset: Math.floor(Math.random() * (await Conversations.count())), + }); + const Message3 = await Messages.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (Message3?.setConversation) { + await Message3.setConversation(relatedConversation3); + } + + const relatedConversation4 = await Conversations.findOne({ + offset: Math.floor(Math.random() * (await Conversations.count())), + }); + const Message4 = await Messages.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (Message4?.setConversation) { + await Message4.setConversation(relatedConversation4); + } +} + +async function associateMessageWithSender() { + const relatedSender0 = await Users.findOne({ + offset: Math.floor(Math.random() * (await Users.count())), + }); + const Message0 = await Messages.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (Message0?.setSender) { + await Message0.setSender(relatedSender0); + } + + const relatedSender1 = await Users.findOne({ + offset: Math.floor(Math.random() * (await Users.count())), + }); + const Message1 = await Messages.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (Message1?.setSender) { + await Message1.setSender(relatedSender1); + } + + const relatedSender2 = await Users.findOne({ + offset: Math.floor(Math.random() * (await Users.count())), + }); + const Message2 = await Messages.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (Message2?.setSender) { + await Message2.setSender(relatedSender2); + } + + const relatedSender3 = await Users.findOne({ + offset: Math.floor(Math.random() * (await Users.count())), + }); + const Message3 = await Messages.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (Message3?.setSender) { + await Message3.setSender(relatedSender3); + } + + const relatedSender4 = await Users.findOne({ + offset: Math.floor(Math.random() * (await Users.count())), + }); + const Message4 = await Messages.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (Message4?.setSender) { + await Message4.setSender(relatedSender4); + } +} + +async function associateConversationparticipantWithConversation() { + const relatedConversation0 = await Conversations.findOne({ + offset: Math.floor(Math.random() * (await Conversations.count())), + }); + const Conversationparticipant0 = await Conversationparticipants.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (Conversationparticipant0?.setConversation) { + await Conversationparticipant0.setConversation(relatedConversation0); + } + + const relatedConversation1 = await Conversations.findOne({ + offset: Math.floor(Math.random() * (await Conversations.count())), + }); + const Conversationparticipant1 = await Conversationparticipants.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (Conversationparticipant1?.setConversation) { + await Conversationparticipant1.setConversation(relatedConversation1); + } + + const relatedConversation2 = await Conversations.findOne({ + offset: Math.floor(Math.random() * (await Conversations.count())), + }); + const Conversationparticipant2 = await Conversationparticipants.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (Conversationparticipant2?.setConversation) { + await Conversationparticipant2.setConversation(relatedConversation2); + } + + const relatedConversation3 = await Conversations.findOne({ + offset: Math.floor(Math.random() * (await Conversations.count())), + }); + const Conversationparticipant3 = await Conversationparticipants.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (Conversationparticipant3?.setConversation) { + await Conversationparticipant3.setConversation(relatedConversation3); + } + + const relatedConversation4 = await Conversations.findOne({ + offset: Math.floor(Math.random() * (await Conversations.count())), + }); + const Conversationparticipant4 = await Conversationparticipants.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (Conversationparticipant4?.setConversation) { + await Conversationparticipant4.setConversation(relatedConversation4); + } +} + +async function associateConversationparticipantWithUser() { + const relatedUser0 = await Users.findOne({ + offset: Math.floor(Math.random() * (await Users.count())), + }); + const Conversationparticipant0 = await Conversationparticipants.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (Conversationparticipant0?.setUser) { + await Conversationparticipant0.setUser(relatedUser0); + } + + const relatedUser1 = await Users.findOne({ + offset: Math.floor(Math.random() * (await Users.count())), + }); + const Conversationparticipant1 = await Conversationparticipants.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (Conversationparticipant1?.setUser) { + await Conversationparticipant1.setUser(relatedUser1); + } + + const relatedUser2 = await Users.findOne({ + offset: Math.floor(Math.random() * (await Users.count())), + }); + const Conversationparticipant2 = await Conversationparticipants.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (Conversationparticipant2?.setUser) { + await Conversationparticipant2.setUser(relatedUser2); + } + + const relatedUser3 = await Users.findOne({ + offset: Math.floor(Math.random() * (await Users.count())), + }); + const Conversationparticipant3 = await Conversationparticipants.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (Conversationparticipant3?.setUser) { + await Conversationparticipant3.setUser(relatedUser3); + } + + const relatedUser4 = await Users.findOne({ + offset: Math.floor(Math.random() * (await Users.count())), + }); + const Conversationparticipant4 = await Conversationparticipants.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (Conversationparticipant4?.setUser) { + await Conversationparticipant4.setUser(relatedUser4); } } module.exports = { up: async (queryInterface, Sequelize) => { - await Bottles.bulkCreate(BottlesData); + await Distilleries.bulkCreate(DistilleriesData); await Brands.bulkCreate(BrandsData); - await Distilleries.bulkCreate(DistilleriesData); + await Photos.bulkCreate(PhotosData); + + await Products.bulkCreate(ProductsData); + + await Locations.bulkCreate(LocationsData); + + await Bottles.bulkCreate(BottlesData); + + await Reviews.bulkCreate(ReviewsData); + + await Conversations.bulkCreate(ConversationsData); + + await Messages.bulkCreate(MessagesData); + + await Conversationparticipants.bulkCreate(ConversationparticipantsData); await Promise.all([ // Similar logic for "relation_many" - await associateBottleWithBrand(), + await associateBrandWithDistillery(), - await associateBottleWithDistillery(), + await associateProductWithBrand(), + + await associateProductWithPhotofront(), + + await associateProductWithPhotoback(), + + await associateLocationWithUser(), await associateBottleWithUser(), - await associateBrandWithDistillery(), + await associateBottleWithProduct(), + + await associateBottleWithLocation(), + + await associateBottleWithPhotofront(), + + await associateBottleWithPhotoback(), + + await associateReviewWithUser(), + + await associateReviewWithBottle(), + + await associateMessageWithConversation(), + + await associateMessageWithSender(), + + await associateConversationparticipantWithConversation(), + + await associateConversationparticipantWithUser(), ]); }, down: async (queryInterface, Sequelize) => { - await queryInterface.bulkDelete('bottles', null, {}); + await queryInterface.bulkDelete('distilleries', null, {}); await queryInterface.bulkDelete('brands', null, {}); - await queryInterface.bulkDelete('distilleries', null, {}); + await queryInterface.bulkDelete('photos', null, {}); + + await queryInterface.bulkDelete('products', null, {}); + + await queryInterface.bulkDelete('locations', null, {}); + + await queryInterface.bulkDelete('bottles', null, {}); + + await queryInterface.bulkDelete('reviews', null, {}); + + await queryInterface.bulkDelete('conversations', null, {}); + + await queryInterface.bulkDelete('messages', null, {}); + + await queryInterface.bulkDelete('conversationparticipants', null, {}); }, }; diff --git a/backend/src/db/seeders/20250826182953.js b/backend/src/db/seeders/20250826182953.js new file mode 100644 index 0000000..b2ab8cb --- /dev/null +++ b/backend/src/db/seeders/20250826182953.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 = ['distilleries']; + + 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/20250826183154.js b/backend/src/db/seeders/20250826183154.js new file mode 100644 index 0000000..11a8ae8 --- /dev/null +++ b/backend/src/db/seeders/20250826183154.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 = ['brands']; + + 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/20250826183241.js b/backend/src/db/seeders/20250826183241.js new file mode 100644 index 0000000..b656561 --- /dev/null +++ b/backend/src/db/seeders/20250826183241.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 = ['photos']; + + 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/20250826183442.js b/backend/src/db/seeders/20250826183442.js new file mode 100644 index 0000000..8d90697 --- /dev/null +++ b/backend/src/db/seeders/20250826183442.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 = ['products']; + + 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/20250826183740.js b/backend/src/db/seeders/20250826183740.js new file mode 100644 index 0000000..962dba6 --- /dev/null +++ b/backend/src/db/seeders/20250826183740.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 = ['locations']; + + 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/20250826184717.js b/backend/src/db/seeders/20250826184717.js new file mode 100644 index 0000000..332f374 --- /dev/null +++ b/backend/src/db/seeders/20250826184717.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 = ['bottles']; + + 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/20250826185515.js b/backend/src/db/seeders/20250826185515.js new file mode 100644 index 0000000..8647e0a --- /dev/null +++ b/backend/src/db/seeders/20250826185515.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 = ['reviews']; + + 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/20250826185749.js b/backend/src/db/seeders/20250826185749.js new file mode 100644 index 0000000..2133d5d --- /dev/null +++ b/backend/src/db/seeders/20250826185749.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 = ['conversations']; + + 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/20250826185841.js b/backend/src/db/seeders/20250826185841.js new file mode 100644 index 0000000..be900db --- /dev/null +++ b/backend/src/db/seeders/20250826185841.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 = ['messages']; + + 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/20250826185959.js b/backend/src/db/seeders/20250826185959.js new file mode 100644 index 0000000..5425655 --- /dev/null +++ b/backend/src/db/seeders/20250826185959.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 = ['conversationparticipants']; + + 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 48986df..17cf23e 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -21,16 +21,30 @@ const contactFormRoutes = require('./routes/contactForm'); const usersRoutes = require('./routes/users'); -const bottlesRoutes = require('./routes/bottles'); - -const brandsRoutes = require('./routes/brands'); - -const distilleriesRoutes = require('./routes/distilleries'); - const rolesRoutes = require('./routes/roles'); const permissionsRoutes = require('./routes/permissions'); +const distilleriesRoutes = require('./routes/distilleries'); + +const brandsRoutes = require('./routes/brands'); + +const photosRoutes = require('./routes/photos'); + +const productsRoutes = require('./routes/products'); + +const locationsRoutes = require('./routes/locations'); + +const bottlesRoutes = require('./routes/bottles'); + +const reviewsRoutes = require('./routes/reviews'); + +const conversationsRoutes = require('./routes/conversations'); + +const messagesRoutes = require('./routes/messages'); + +const conversationparticipantsRoutes = require('./routes/conversationparticipants'); + const getBaseUrl = (url) => { if (!url) return ''; return url.endsWith('/api') ? url.slice(0, -4) : url; @@ -102,24 +116,6 @@ app.use( usersRoutes, ); -app.use( - '/api/bottles', - passport.authenticate('jwt', { session: false }), - bottlesRoutes, -); - -app.use( - '/api/brands', - passport.authenticate('jwt', { session: false }), - brandsRoutes, -); - -app.use( - '/api/distilleries', - passport.authenticate('jwt', { session: false }), - distilleriesRoutes, -); - app.use( '/api/roles', passport.authenticate('jwt', { session: false }), @@ -132,6 +128,66 @@ app.use( permissionsRoutes, ); +app.use( + '/api/distilleries', + passport.authenticate('jwt', { session: false }), + distilleriesRoutes, +); + +app.use( + '/api/brands', + passport.authenticate('jwt', { session: false }), + brandsRoutes, +); + +app.use( + '/api/photos', + passport.authenticate('jwt', { session: false }), + photosRoutes, +); + +app.use( + '/api/products', + passport.authenticate('jwt', { session: false }), + productsRoutes, +); + +app.use( + '/api/locations', + passport.authenticate('jwt', { session: false }), + locationsRoutes, +); + +app.use( + '/api/bottles', + passport.authenticate('jwt', { session: false }), + bottlesRoutes, +); + +app.use( + '/api/reviews', + passport.authenticate('jwt', { session: false }), + reviewsRoutes, +); + +app.use( + '/api/conversations', + passport.authenticate('jwt', { session: false }), + conversationsRoutes, +); + +app.use( + '/api/messages', + passport.authenticate('jwt', { session: false }), + messagesRoutes, +); + +app.use( + '/api/conversationparticipants', + passport.authenticate('jwt', { session: false }), + conversationparticipantsRoutes, +); + app.use( '/api/openai', passport.authenticate('jwt', { session: false }), diff --git a/backend/src/routes/bottles.js b/backend/src/routes/bottles.js index f66fb05..5ae1ee2 100644 --- a/backend/src/routes/bottles.js +++ b/backend/src/routes/bottles.js @@ -20,37 +20,39 @@ router.use(checkCrudPermissions('bottles')); * type: object * properties: - * name: + * rickhouse: * type: string - * default: name + * default: rickhouse + * rack: + * type: string + * default: rack + * release: + * type: string + * default: release + * barrelnumber: + * type: string + * default: barrelnumber + * bottlenumber: + * type: string + * default: bottlenumber * notes: * type: string * default: notes - * tasting_notes: - * type: string - * default: tasting_notes - * msrp_range: - * type: string - * default: msrp_range - * secondary_value_range: - * type: string - * default: secondary_value_range - * barcode: - * type: string - * default: barcode - * quantity: + * age: + * type: integer + * format: int64 + * rating: + * type: integer + * format: int64 + * volume: * type: integer * format: int64 * proof: * type: integer * format: int64 - * age: - * type: integer - * format: int64 - * */ /** @@ -333,15 +335,19 @@ router.get( if (filetype && filetype === 'csv') { const fields = [ 'id', - 'name', + 'rickhouse', + 'rack', + 'release', + 'barrelnumber', + 'bottlenumber', 'notes', - 'tasting_notes', - 'msrp_range', - 'secondary_value_range', - 'barcode', - 'quantity', - 'proof', 'age', + 'rating', + 'volume', + 'proof', + + 'barreleddate', + 'dateacquired', ]; const opts = { fields }; try { diff --git a/backend/src/routes/brands.js b/backend/src/routes/brands.js index f5a6622..c5f16a5 100644 --- a/backend/src/routes/brands.js +++ b/backend/src/routes/brands.js @@ -24,6 +24,10 @@ router.use(checkCrudPermissions('brands')); * type: string * default: name + * status: + * type: integer + * format: int64 + */ /** @@ -299,7 +303,7 @@ router.get( const currentUser = req.currentUser; const payload = await BrandsDBApi.findAll(req.query, { currentUser }); if (filetype && filetype === 'csv') { - const fields = ['id', 'name']; + const fields = ['id', 'name', 'status']; const opts = { fields }; try { const csv = parse(payload.rows, opts); diff --git a/backend/src/routes/conversationparticipants.js b/backend/src/routes/conversationparticipants.js new file mode 100644 index 0000000..3b5148d --- /dev/null +++ b/backend/src/routes/conversationparticipants.js @@ -0,0 +1,449 @@ +const express = require('express'); + +const ConversationparticipantsService = require('../services/conversationparticipants'); +const ConversationparticipantsDBApi = require('../db/api/conversationparticipants'); +const wrapAsync = require('../helpers').wrapAsync; + +const router = express.Router(); + +const { parse } = require('json2csv'); + +const { checkCrudPermissions } = require('../middlewares/check-permissions'); + +router.use(checkCrudPermissions('conversationparticipants')); + +/** + * @swagger + * components: + * schemas: + * Conversationparticipants: + * type: object + * properties: + + */ + +/** + * @swagger + * tags: + * name: Conversationparticipants + * description: The Conversationparticipants managing API + */ + +/** + * @swagger + * /api/conversationparticipants: + * post: + * security: + * - bearerAuth: [] + * tags: [Conversationparticipants] + * 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/Conversationparticipants" + * responses: + * 200: + * description: The item was successfully added + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Conversationparticipants" + * 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 ConversationparticipantsService.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: [Conversationparticipants] + * 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/Conversationparticipants" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Conversationparticipants" + * 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 ConversationparticipantsService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/conversationparticipants/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Conversationparticipants] + * 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/Conversationparticipants" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Conversationparticipants" + * 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 ConversationparticipantsService.update( + req.body.data, + req.body.id, + req.currentUser, + ); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/conversationparticipants/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Conversationparticipants] + * 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/Conversationparticipants" + * 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 ConversationparticipantsService.remove( + req.params.id, + req.currentUser, + ); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/conversationparticipants/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Conversationparticipants] + * 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/Conversationparticipants" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post( + '/deleteByIds', + wrapAsync(async (req, res) => { + await ConversationparticipantsService.deleteByIds( + req.body.data, + req.currentUser, + ); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/conversationparticipants: + * get: + * security: + * - bearerAuth: [] + * tags: [Conversationparticipants] + * summary: Get all conversationparticipants + * description: Get all conversationparticipants + * responses: + * 200: + * description: Conversationparticipants list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Conversationparticipants" + * 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 ConversationparticipantsDBApi.findAll(req.query, { + currentUser, + }); + if (filetype && filetype === 'csv') { + const fields = ['id']; + const opts = { fields }; + try { + const csv = parse(payload.rows, opts); + res.status(200).attachment(csv); + res.send(csv); + } catch (err) { + console.error(err); + } + } else { + res.status(200).send(payload); + } + }), +); + +/** + * @swagger + * /api/conversationparticipants/count: + * get: + * security: + * - bearerAuth: [] + * tags: [Conversationparticipants] + * summary: Count all conversationparticipants + * description: Count all conversationparticipants + * responses: + * 200: + * description: Conversationparticipants count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Conversationparticipants" + * 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 ConversationparticipantsDBApi.findAll( + req.query, + null, + { countOnly: true, currentUser }, + ); + + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/conversationparticipants/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [Conversationparticipants] + * summary: Find all conversationparticipants that match search criteria + * description: Find all conversationparticipants that match search criteria + * responses: + * 200: + * description: Conversationparticipants list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Conversationparticipants" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/autocomplete', async (req, res) => { + const payload = await ConversationparticipantsDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/conversationparticipants/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Conversationparticipants] + * 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/Conversationparticipants" + * 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 ConversationparticipantsDBApi.findBy({ + id: req.params.id, + }); + + res.status(200).send(payload); + }), +); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/routes/conversations.js b/backend/src/routes/conversations.js new file mode 100644 index 0000000..0f681ef --- /dev/null +++ b/backend/src/routes/conversations.js @@ -0,0 +1,440 @@ +const express = require('express'); + +const ConversationsService = require('../services/conversations'); +const ConversationsDBApi = require('../db/api/conversations'); +const wrapAsync = require('../helpers').wrapAsync; + +const router = express.Router(); + +const { parse } = require('json2csv'); + +const { checkCrudPermissions } = require('../middlewares/check-permissions'); + +router.use(checkCrudPermissions('conversations')); + +/** + * @swagger + * components: + * schemas: + * Conversations: + * type: object + * properties: + + */ + +/** + * @swagger + * tags: + * name: Conversations + * description: The Conversations managing API + */ + +/** + * @swagger + * /api/conversations: + * post: + * security: + * - bearerAuth: [] + * tags: [Conversations] + * 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/Conversations" + * responses: + * 200: + * description: The item was successfully added + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Conversations" + * 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 ConversationsService.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: [Conversations] + * 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/Conversations" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Conversations" + * 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 ConversationsService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/conversations/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Conversations] + * 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/Conversations" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Conversations" + * 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 ConversationsService.update( + req.body.data, + req.body.id, + req.currentUser, + ); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/conversations/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Conversations] + * 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/Conversations" + * 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 ConversationsService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/conversations/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Conversations] + * 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/Conversations" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post( + '/deleteByIds', + wrapAsync(async (req, res) => { + await ConversationsService.deleteByIds(req.body.data, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/conversations: + * get: + * security: + * - bearerAuth: [] + * tags: [Conversations] + * summary: Get all conversations + * description: Get all conversations + * responses: + * 200: + * description: Conversations list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Conversations" + * 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 ConversationsDBApi.findAll(req.query, { + currentUser, + }); + if (filetype && filetype === 'csv') { + const fields = ['id', 'createdat']; + 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/conversations/count: + * get: + * security: + * - bearerAuth: [] + * tags: [Conversations] + * summary: Count all conversations + * description: Count all conversations + * responses: + * 200: + * description: Conversations count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Conversations" + * 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 ConversationsDBApi.findAll(req.query, null, { + countOnly: true, + currentUser, + }); + + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/conversations/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [Conversations] + * summary: Find all conversations that match search criteria + * description: Find all conversations that match search criteria + * responses: + * 200: + * description: Conversations list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Conversations" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/autocomplete', async (req, res) => { + const payload = await ConversationsDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/conversations/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Conversations] + * 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/Conversations" + * 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 ConversationsDBApi.findBy({ id: req.params.id }); + + res.status(200).send(payload); + }), +); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/routes/distilleries.js b/backend/src/routes/distilleries.js index 78d04b0..5c8959e 100644 --- a/backend/src/routes/distilleries.js +++ b/backend/src/routes/distilleries.js @@ -23,6 +23,16 @@ router.use(checkCrudPermissions('distilleries')); * name: * type: string * default: name + * city: + * type: string + * default: city + * state: + * type: string + * default: state + + * status: + * type: integer + * format: int64 */ @@ -308,7 +318,7 @@ router.get( const currentUser = req.currentUser; const payload = await DistilleriesDBApi.findAll(req.query, { currentUser }); if (filetype && filetype === 'csv') { - const fields = ['id', 'name']; + const fields = ['id', 'name', 'city', 'state', 'status']; const opts = { fields }; try { const csv = parse(payload.rows, opts); diff --git a/backend/src/routes/locations.js b/backend/src/routes/locations.js new file mode 100644 index 0000000..6859012 --- /dev/null +++ b/backend/src/routes/locations.js @@ -0,0 +1,438 @@ +const express = require('express'); + +const LocationsService = require('../services/locations'); +const LocationsDBApi = require('../db/api/locations'); +const wrapAsync = require('../helpers').wrapAsync; + +const router = express.Router(); + +const { parse } = require('json2csv'); + +const { checkCrudPermissions } = require('../middlewares/check-permissions'); + +router.use(checkCrudPermissions('locations')); + +/** + * @swagger + * components: + * schemas: + * Locations: + * type: object + * properties: + + * name: + * type: string + * default: name + + */ + +/** + * @swagger + * tags: + * name: Locations + * description: The Locations managing API + */ + +/** + * @swagger + * /api/locations: + * post: + * security: + * - bearerAuth: [] + * tags: [Locations] + * 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/Locations" + * responses: + * 200: + * description: The item was successfully added + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Locations" + * 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 LocationsService.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: [Locations] + * 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/Locations" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Locations" + * 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 LocationsService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/locations/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Locations] + * 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/Locations" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Locations" + * 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 LocationsService.update(req.body.data, req.body.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/locations/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Locations] + * 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/Locations" + * 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 LocationsService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/locations/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Locations] + * 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/Locations" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post( + '/deleteByIds', + wrapAsync(async (req, res) => { + await LocationsService.deleteByIds(req.body.data, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/locations: + * get: + * security: + * - bearerAuth: [] + * tags: [Locations] + * summary: Get all locations + * description: Get all locations + * responses: + * 200: + * description: Locations list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Locations" + * 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 LocationsDBApi.findAll(req.query, { currentUser }); + if (filetype && filetype === 'csv') { + const fields = ['id', 'name']; + const opts = { fields }; + try { + const csv = parse(payload.rows, opts); + res.status(200).attachment(csv); + res.send(csv); + } catch (err) { + console.error(err); + } + } else { + res.status(200).send(payload); + } + }), +); + +/** + * @swagger + * /api/locations/count: + * get: + * security: + * - bearerAuth: [] + * tags: [Locations] + * summary: Count all locations + * description: Count all locations + * responses: + * 200: + * description: Locations count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Locations" + * 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 LocationsDBApi.findAll(req.query, null, { + countOnly: true, + currentUser, + }); + + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/locations/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [Locations] + * summary: Find all locations that match search criteria + * description: Find all locations that match search criteria + * responses: + * 200: + * description: Locations list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Locations" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/autocomplete', async (req, res) => { + const payload = await LocationsDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/locations/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Locations] + * 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/Locations" + * 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 LocationsDBApi.findBy({ id: req.params.id }); + + res.status(200).send(payload); + }), +); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/routes/messages.js b/backend/src/routes/messages.js new file mode 100644 index 0000000..7722f84 --- /dev/null +++ b/backend/src/routes/messages.js @@ -0,0 +1,434 @@ +const express = require('express'); + +const MessagesService = require('../services/messages'); +const MessagesDBApi = require('../db/api/messages'); +const wrapAsync = require('../helpers').wrapAsync; + +const router = express.Router(); + +const { parse } = require('json2csv'); + +const { checkCrudPermissions } = require('../middlewares/check-permissions'); + +router.use(checkCrudPermissions('messages')); + +/** + * @swagger + * components: + * schemas: + * Messages: + * type: object + * properties: + + */ + +/** + * @swagger + * tags: + * name: Messages + * description: The Messages managing API + */ + +/** + * @swagger + * /api/messages: + * post: + * security: + * - bearerAuth: [] + * tags: [Messages] + * 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/Messages" + * responses: + * 200: + * description: The item was successfully added + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Messages" + * 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 MessagesService.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: [Messages] + * 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/Messages" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Messages" + * 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 MessagesService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/messages/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Messages] + * 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/Messages" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Messages" + * 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 MessagesService.update(req.body.data, req.body.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/messages/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Messages] + * 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/Messages" + * 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 MessagesService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/messages/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Messages] + * 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/Messages" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post( + '/deleteByIds', + wrapAsync(async (req, res) => { + await MessagesService.deleteByIds(req.body.data, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/messages: + * get: + * security: + * - bearerAuth: [] + * tags: [Messages] + * summary: Get all messages + * description: Get all messages + * responses: + * 200: + * description: Messages list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Messages" + * 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 MessagesDBApi.findAll(req.query, { currentUser }); + if (filetype && filetype === 'csv') { + const fields = ['id']; + const opts = { fields }; + try { + const csv = parse(payload.rows, opts); + res.status(200).attachment(csv); + res.send(csv); + } catch (err) { + console.error(err); + } + } else { + res.status(200).send(payload); + } + }), +); + +/** + * @swagger + * /api/messages/count: + * get: + * security: + * - bearerAuth: [] + * tags: [Messages] + * summary: Count all messages + * description: Count all messages + * responses: + * 200: + * description: Messages count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Messages" + * 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 MessagesDBApi.findAll(req.query, null, { + countOnly: true, + currentUser, + }); + + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/messages/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [Messages] + * summary: Find all messages that match search criteria + * description: Find all messages that match search criteria + * responses: + * 200: + * description: Messages list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Messages" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/autocomplete', async (req, res) => { + const payload = await MessagesDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/messages/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Messages] + * 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/Messages" + * 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 MessagesDBApi.findBy({ id: req.params.id }); + + res.status(200).send(payload); + }), +); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/routes/photos.js b/backend/src/routes/photos.js new file mode 100644 index 0000000..c862d62 --- /dev/null +++ b/backend/src/routes/photos.js @@ -0,0 +1,433 @@ +const express = require('express'); + +const PhotosService = require('../services/photos'); +const PhotosDBApi = require('../db/api/photos'); +const wrapAsync = require('../helpers').wrapAsync; + +const router = express.Router(); + +const { parse } = require('json2csv'); + +const { checkCrudPermissions } = require('../middlewares/check-permissions'); + +router.use(checkCrudPermissions('photos')); + +/** + * @swagger + * components: + * schemas: + * Photos: + * type: object + * properties: + + * phototype: + * type: string + * default: phototype + + */ + +/** + * @swagger + * tags: + * name: Photos + * description: The Photos managing API + */ + +/** + * @swagger + * /api/photos: + * post: + * security: + * - bearerAuth: [] + * tags: [Photos] + * 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/Photos" + * responses: + * 200: + * description: The item was successfully added + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Photos" + * 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 PhotosService.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: [Photos] + * 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/Photos" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Photos" + * 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 PhotosService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/photos/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Photos] + * 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/Photos" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Photos" + * 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 PhotosService.update(req.body.data, req.body.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/photos/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Photos] + * 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/Photos" + * 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 PhotosService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/photos/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Photos] + * 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/Photos" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post( + '/deleteByIds', + wrapAsync(async (req, res) => { + await PhotosService.deleteByIds(req.body.data, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/photos: + * get: + * security: + * - bearerAuth: [] + * tags: [Photos] + * summary: Get all photos + * description: Get all photos + * responses: + * 200: + * description: Photos list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Photos" + * 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 PhotosDBApi.findAll(req.query, { currentUser }); + if (filetype && filetype === 'csv') { + const fields = ['id', 'phototype']; + 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/photos/count: + * get: + * security: + * - bearerAuth: [] + * tags: [Photos] + * summary: Count all photos + * description: Count all photos + * responses: + * 200: + * description: Photos count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Photos" + * 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 PhotosDBApi.findAll(req.query, null, { + countOnly: true, + currentUser, + }); + + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/photos/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [Photos] + * summary: Find all photos that match search criteria + * description: Find all photos that match search criteria + * responses: + * 200: + * description: Photos list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Photos" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/autocomplete', async (req, res) => { + const payload = await PhotosDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/photos/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Photos] + * 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/Photos" + * 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 PhotosDBApi.findBy({ id: req.params.id }); + + res.status(200).send(payload); + }), +); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/routes/products.js b/backend/src/routes/products.js new file mode 100644 index 0000000..a4ffa79 --- /dev/null +++ b/backend/src/routes/products.js @@ -0,0 +1,463 @@ +const express = require('express'); + +const ProductsService = require('../services/products'); +const ProductsDBApi = require('../db/api/products'); +const wrapAsync = require('../helpers').wrapAsync; + +const router = express.Router(); + +const { parse } = require('json2csv'); + +const { checkCrudPermissions } = require('../middlewares/check-permissions'); + +router.use(checkCrudPermissions('products')); + +/** + * @swagger + * components: + * schemas: + * Products: + * type: object + * properties: + + * name: + * type: string + * default: name + * barcode: + * type: string + * default: barcode + * notes: + * type: string + * default: notes + + * age: + * type: integer + * format: int64 + * status: + * type: integer + * format: int64 + + * proof: + * type: integer + * format: int64 + + */ + +/** + * @swagger + * tags: + * name: Products + * description: The Products managing API + */ + +/** + * @swagger + * /api/products: + * post: + * security: + * - bearerAuth: [] + * tags: [Products] + * 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/Products" + * responses: + * 200: + * description: The item was successfully added + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Products" + * 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 ProductsService.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: [Products] + * 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/Products" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Products" + * 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 ProductsService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/products/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Products] + * 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/Products" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Products" + * 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 ProductsService.update(req.body.data, req.body.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/products/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Products] + * 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/Products" + * 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 ProductsService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/products/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Products] + * 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/Products" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post( + '/deleteByIds', + wrapAsync(async (req, res) => { + await ProductsService.deleteByIds(req.body.data, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/products: + * get: + * security: + * - bearerAuth: [] + * tags: [Products] + * summary: Get all products + * description: Get all products + * responses: + * 200: + * description: Products list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Products" + * 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 ProductsDBApi.findAll(req.query, { currentUser }); + if (filetype && filetype === 'csv') { + const fields = [ + 'id', + 'name', + 'barcode', + 'notes', + 'age', + 'status', + 'proof', + ]; + 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/products/count: + * get: + * security: + * - bearerAuth: [] + * tags: [Products] + * summary: Count all products + * description: Count all products + * responses: + * 200: + * description: Products count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Products" + * 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 ProductsDBApi.findAll(req.query, null, { + countOnly: true, + currentUser, + }); + + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/products/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [Products] + * summary: Find all products that match search criteria + * description: Find all products that match search criteria + * responses: + * 200: + * description: Products list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Products" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/autocomplete', async (req, res) => { + const payload = await ProductsDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/products/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Products] + * 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/Products" + * 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 ProductsDBApi.findBy({ id: req.params.id }); + + res.status(200).send(payload); + }), +); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/routes/reviews.js b/backend/src/routes/reviews.js new file mode 100644 index 0000000..95e9665 --- /dev/null +++ b/backend/src/routes/reviews.js @@ -0,0 +1,442 @@ +const express = require('express'); + +const ReviewsService = require('../services/reviews'); +const ReviewsDBApi = require('../db/api/reviews'); +const wrapAsync = require('../helpers').wrapAsync; + +const router = express.Router(); + +const { parse } = require('json2csv'); + +const { checkCrudPermissions } = require('../middlewares/check-permissions'); + +router.use(checkCrudPermissions('reviews')); + +/** + * @swagger + * components: + * schemas: + * Reviews: + * type: object + * properties: + + * notes: + * type: string + * default: notes + + * rating: + * type: integer + * format: int64 + + */ + +/** + * @swagger + * tags: + * name: Reviews + * description: The Reviews managing API + */ + +/** + * @swagger + * /api/reviews: + * post: + * security: + * - bearerAuth: [] + * tags: [Reviews] + * 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/Reviews" + * responses: + * 200: + * description: The item was successfully added + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Reviews" + * 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 ReviewsService.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: [Reviews] + * 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/Reviews" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Reviews" + * 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 ReviewsService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/reviews/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Reviews] + * 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/Reviews" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Reviews" + * 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 ReviewsService.update(req.body.data, req.body.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/reviews/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Reviews] + * 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/Reviews" + * 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 ReviewsService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/reviews/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Reviews] + * 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/Reviews" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post( + '/deleteByIds', + wrapAsync(async (req, res) => { + await ReviewsService.deleteByIds(req.body.data, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/reviews: + * get: + * security: + * - bearerAuth: [] + * tags: [Reviews] + * summary: Get all reviews + * description: Get all reviews + * responses: + * 200: + * description: Reviews list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Reviews" + * 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 ReviewsDBApi.findAll(req.query, { currentUser }); + if (filetype && filetype === 'csv') { + const fields = ['id', 'notes', 'rating', 'createdat']; + 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/reviews/count: + * get: + * security: + * - bearerAuth: [] + * tags: [Reviews] + * summary: Count all reviews + * description: Count all reviews + * responses: + * 200: + * description: Reviews count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Reviews" + * 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 ReviewsDBApi.findAll(req.query, null, { + countOnly: true, + currentUser, + }); + + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/reviews/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [Reviews] + * summary: Find all reviews that match search criteria + * description: Find all reviews that match search criteria + * responses: + * 200: + * description: Reviews list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Reviews" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/autocomplete', async (req, res) => { + const payload = await ReviewsDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/reviews/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Reviews] + * 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/Reviews" + * 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 ReviewsDBApi.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..6d500e7 100644 --- a/backend/src/routes/users.js +++ b/backend/src/routes/users.js @@ -32,6 +32,12 @@ router.use(checkCrudPermissions('users')); * email: * type: string * default: email + * address: + * type: string + * default: address + * address2: + * type: string + * default: address2 */ @@ -308,7 +314,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', + 'address', + 'address2', + ]; const opts = { fields }; try { const csv = parse(payload.rows, opts); diff --git a/backend/src/services/conversationparticipants.js b/backend/src/services/conversationparticipants.js new file mode 100644 index 0000000..8d003be --- /dev/null +++ b/backend/src/services/conversationparticipants.js @@ -0,0 +1,118 @@ +const db = require('../db/models'); +const ConversationparticipantsDBApi = require('../db/api/conversationparticipants'); +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 ConversationparticipantsService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await ConversationparticipantsDBApi.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 ConversationparticipantsDBApi.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 conversationparticipants = await ConversationparticipantsDBApi.findBy( + { id }, + { transaction }, + ); + + if (!conversationparticipants) { + throw new ValidationError('conversationparticipantsNotFound'); + } + + const updatedConversationparticipants = + await ConversationparticipantsDBApi.update(id, data, { + currentUser, + transaction, + }); + + await transaction.commit(); + return updatedConversationparticipants; + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await ConversationparticipantsDBApi.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 ConversationparticipantsDBApi.remove(id, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; diff --git a/backend/src/services/conversations.js b/backend/src/services/conversations.js new file mode 100644 index 0000000..5a67c76 --- /dev/null +++ b/backend/src/services/conversations.js @@ -0,0 +1,117 @@ +const db = require('../db/models'); +const ConversationsDBApi = require('../db/api/conversations'); +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 ConversationsService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await ConversationsDBApi.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 ConversationsDBApi.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 conversations = await ConversationsDBApi.findBy( + { id }, + { transaction }, + ); + + if (!conversations) { + throw new ValidationError('conversationsNotFound'); + } + + const updatedConversations = await ConversationsDBApi.update(id, data, { + currentUser, + transaction, + }); + + await transaction.commit(); + return updatedConversations; + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await ConversationsDBApi.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 ConversationsDBApi.remove(id, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; diff --git a/backend/src/services/locations.js b/backend/src/services/locations.js new file mode 100644 index 0000000..9c8082b --- /dev/null +++ b/backend/src/services/locations.js @@ -0,0 +1,114 @@ +const db = require('../db/models'); +const LocationsDBApi = require('../db/api/locations'); +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 LocationsService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await LocationsDBApi.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 LocationsDBApi.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 locations = await LocationsDBApi.findBy({ id }, { transaction }); + + if (!locations) { + throw new ValidationError('locationsNotFound'); + } + + const updatedLocations = await LocationsDBApi.update(id, data, { + currentUser, + transaction, + }); + + await transaction.commit(); + return updatedLocations; + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await LocationsDBApi.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 LocationsDBApi.remove(id, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; diff --git a/backend/src/services/messages.js b/backend/src/services/messages.js new file mode 100644 index 0000000..bdc275c --- /dev/null +++ b/backend/src/services/messages.js @@ -0,0 +1,114 @@ +const db = require('../db/models'); +const MessagesDBApi = require('../db/api/messages'); +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 MessagesService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await MessagesDBApi.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 MessagesDBApi.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 messages = await MessagesDBApi.findBy({ id }, { transaction }); + + if (!messages) { + throw new ValidationError('messagesNotFound'); + } + + const updatedMessages = await MessagesDBApi.update(id, data, { + currentUser, + transaction, + }); + + await transaction.commit(); + return updatedMessages; + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await MessagesDBApi.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 MessagesDBApi.remove(id, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; diff --git a/backend/src/services/photos.js b/backend/src/services/photos.js new file mode 100644 index 0000000..18bcf10 --- /dev/null +++ b/backend/src/services/photos.js @@ -0,0 +1,114 @@ +const db = require('../db/models'); +const PhotosDBApi = require('../db/api/photos'); +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 PhotosService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await PhotosDBApi.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 PhotosDBApi.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 photos = await PhotosDBApi.findBy({ id }, { transaction }); + + if (!photos) { + throw new ValidationError('photosNotFound'); + } + + const updatedPhotos = await PhotosDBApi.update(id, data, { + currentUser, + transaction, + }); + + await transaction.commit(); + return updatedPhotos; + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await PhotosDBApi.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 PhotosDBApi.remove(id, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; diff --git a/backend/src/services/products.js b/backend/src/services/products.js new file mode 100644 index 0000000..0f7a762 --- /dev/null +++ b/backend/src/services/products.js @@ -0,0 +1,114 @@ +const db = require('../db/models'); +const ProductsDBApi = require('../db/api/products'); +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 ProductsService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await ProductsDBApi.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 ProductsDBApi.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 products = await ProductsDBApi.findBy({ id }, { transaction }); + + if (!products) { + throw new ValidationError('productsNotFound'); + } + + const updatedProducts = await ProductsDBApi.update(id, data, { + currentUser, + transaction, + }); + + await transaction.commit(); + return updatedProducts; + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await ProductsDBApi.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 ProductsDBApi.remove(id, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; diff --git a/backend/src/services/reviews.js b/backend/src/services/reviews.js new file mode 100644 index 0000000..2ea9dd6 --- /dev/null +++ b/backend/src/services/reviews.js @@ -0,0 +1,114 @@ +const db = require('../db/models'); +const ReviewsDBApi = require('../db/api/reviews'); +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 ReviewsService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await ReviewsDBApi.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 ReviewsDBApi.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 reviews = await ReviewsDBApi.findBy({ id }, { transaction }); + + if (!reviews) { + throw new ValidationError('reviewsNotFound'); + } + + const updatedReviews = await ReviewsDBApi.update(id, data, { + currentUser, + transaction, + }); + + await transaction.commit(); + return updatedReviews; + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await ReviewsDBApi.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 ReviewsDBApi.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 4f1772b..f238b62 100644 --- a/backend/src/services/search.js +++ b/backend/src/services/search.js @@ -41,28 +41,56 @@ module.exports = class SearchService { throw new ValidationError('iam.errors.searchQueryRequired'); } const tableColumns = { - users: ['firstName', 'lastName', 'phoneNumber', 'email'], + users: [ + 'firstName', - bottles: [ - 'name', + 'lastName', - 'notes', + 'phoneNumber', - 'tasting_notes', + 'email', - 'msrp_range', + 'address', - 'secondary_value_range', - - 'barcode', + 'address2', ], + distilleries: ['name', 'city', 'state'], + brands: ['name'], - distilleries: ['name'], + photos: ['phototype'], + + products: ['name', 'barcode', 'notes'], + + locations: ['name'], + + bottles: [ + 'rickhouse', + + 'rack', + + 'release', + + 'barrelnumber', + + 'bottlenumber', + + 'notes', + ], + + reviews: ['notes'], }; const columnsInt = { - bottles: ['proof', 'quantity', 'age'], + distilleries: ['status'], + + brands: ['status'], + + products: ['proof', 'age', 'status'], + + bottles: ['proof', 'age', 'rating', 'volume'], + + reviews: ['rating'], }; let allFoundRecords = []; diff --git a/frontend/src/components/Bottles/CardBottles.tsx b/frontend/src/components/Bottles/CardBottles.tsx index b0318f2..720b17e 100644 --- a/frontend/src/components/Bottles/CardBottles.tsx +++ b/frontend/src/components/Bottles/CardBottles.tsx @@ -56,22 +56,16 @@ const CardBottles = ({ }`} >
- -

{item.name}

+ {item.id} -
+
-
Name
+
User
-
{item.name}
+
+ {dataFormatter.usersOneListFormatter(item.user)} +
-
Brand
+
+ Product +
- {dataFormatter.brandsOneListFormatter(item.brand)} + {dataFormatter.productsOneListFormatter(item.product)} +
+
+
+ +
+
+ Location +
+
+
+ {dataFormatter.locationsOneListFormatter(item.location)}
@@ -106,9 +115,115 @@ const CardBottles = ({
-
Type
+
Age
-
{item.type}
+
{item.age}
+
+
+ +
+
+ Rating +
+
+
+ {item.rating} +
+
+
+ +
+
+ Collectable +
+
+
+ {dataFormatter.booleanFormatter(item.collectable)} +
+
+
+ +
+
+ Rickhouse +
+
+
+ {item.rickhouse} +
+
+
+ +
+
Rack
+
+
{item.rack}
+
+
+ +
+
+ Release +
+
+
+ {item.release} +
+
+
+ +
+
+ Barrelnumber +
+
+
+ {item.barrelnumber} +
+
+
+ +
+
+ Barreleddate +
+
+
+ {dataFormatter.dateFormatter(item.barreleddate)} +
+
+
+ +
+
+ Bottlenumber +
+
+
+ {item.bottlenumber} +
+
+
+ +
+
+ Dateacquired +
+
+
+ {dataFormatter.dateFormatter(item.dateacquired)} +
+
+
+ +
+
+ Volume +
+
+
+ {item.volume} +
@@ -121,112 +236,22 @@ const CardBottles = ({
- TastingNotes + Photofront
- {item.tasting_notes} + {dataFormatter.photosOneListFormatter(item.photofront)}
- MSRPRange + Photoback
- {item.msrp_range} -
-
-
- -
-
- SecondaryValueRange -
-
-
- {item.secondary_value_range} -
-
-
- -
-
- OpenedBottleIndicator -
-
-
- {dataFormatter.booleanFormatter( - item.opened_bottle_indicator, - )} -
-
-
- -
-
- Quantity -
-
-
- {item.quantity} -
-
-
- -
-
- Barcode -
-
-
- {item.barcode} -
-
-
- -
-
- Picture -
-
-
- -
-
-
- -
-
Age
-
-
{item.age}
-
-
- -
-
- Distillery -
-
-
- {dataFormatter.distilleriesOneListFormatter( - item.distillery, - )} -
-
-
- -
-
User
-
-
- {dataFormatter.usersOneListFormatter(item.user)} + {dataFormatter.photosOneListFormatter(item.photoback)}
diff --git a/frontend/src/components/Bottles/ListBottles.tsx b/frontend/src/components/Bottles/ListBottles.tsx index 9b33565..6f8205c 100644 --- a/frontend/src/components/Bottles/ListBottles.tsx +++ b/frontend/src/components/Bottles/ListBottles.tsx @@ -45,15 +45,6 @@ const ListBottles = ({
- -
-

Name

-

{item.name}

+

User

+

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

-

Brand

+

Product

- {dataFormatter.brandsOneListFormatter(item.brand)} + {dataFormatter.productsOneListFormatter(item.product)} +

+
+ +
+

Location

+

+ {dataFormatter.locationsOneListFormatter(item.location)}

@@ -78,8 +78,64 @@ const ListBottles = ({
-

Type

-

{item.type}

+

Age

+

{item.age}

+
+ +
+

Rating

+

{item.rating}

+
+ +
+

Collectable

+

+ {dataFormatter.booleanFormatter(item.collectable)} +

+
+ +
+

Rickhouse

+

{item.rickhouse}

+
+ +
+

Rack

+

{item.rack}

+
+ +
+

Release

+

{item.release}

+
+ +
+

Barrelnumber

+

{item.barrelnumber}

+
+ +
+

Barreleddate

+

+ {dataFormatter.dateFormatter(item.barreleddate)} +

+
+ +
+

Bottlenumber

+

{item.bottlenumber}

+
+ +
+

Dateacquired

+

+ {dataFormatter.dateFormatter(item.dateacquired)} +

+
+ +
+

Volume

+

{item.volume}

@@ -88,72 +144,16 @@ const ListBottles = ({
-

TastingNotes

-

{item.tasting_notes}

-
- -
-

MSRPRange

-

{item.msrp_range}

-
- -
-

- SecondaryValueRange -

+

Photofront

- {item.secondary_value_range} + {dataFormatter.photosOneListFormatter(item.photofront)}

-

- OpenedBottleIndicator -

+

Photoback

- {dataFormatter.booleanFormatter( - item.opened_bottle_indicator, - )} -

-
- -
-

Quantity

-

{item.quantity}

-
- -
-

Barcode

-

{item.barcode}

-
- -
-

Picture

- -
- -
-

Age

-

{item.age}

-
- -
-

Distillery

-

- {dataFormatter.distilleriesOneListFormatter( - item.distillery, - )} -

-
- -
-

User

-

- {dataFormatter.usersOneListFormatter(item.user)} + {dataFormatter.photosOneListFormatter(item.photoback)}

diff --git a/frontend/src/components/Bottles/TableBottles.tsx b/frontend/src/components/Bottles/TableBottles.tsx index 20d442b..3935e3a 100644 --- a/frontend/src/components/Bottles/TableBottles.tsx +++ b/frontend/src/components/Bottles/TableBottles.tsx @@ -20,8 +20,6 @@ import _ from 'lodash'; import dataFormatter from '../../helpers/dataFormatter'; import { dataGridStyles } from '../../styles'; -import CardBottles from './CardBottles'; - const perPage = 10; const TableSampleBottles = ({ @@ -463,18 +461,7 @@ const TableSampleBottles = ({

Are you sure you want to delete this item?

- {bottles && Array.isArray(bottles) && !showGrid && ( - - )} - - {showGrid && dataGrid} + {dataGrid} {selectedRows.length > 0 && createPortal( diff --git a/frontend/src/components/Bottles/configureBottlesCols.tsx b/frontend/src/components/Bottles/configureBottlesCols.tsx index 66a8bf0..e6d8678 100644 --- a/frontend/src/components/Bottles/configureBottlesCols.tsx +++ b/frontend/src/components/Bottles/configureBottlesCols.tsx @@ -39,20 +39,8 @@ export const loadColumns = async ( return [ { - field: 'name', - headerName: 'Name', - flex: 1, - minWidth: 120, - filterable: false, - headerClassName: 'datagrid--header', - cellClassName: 'datagrid--cell', - - editable: hasUpdatePermission, - }, - - { - field: 'brand', - headerName: 'Brand', + field: 'user', + headerName: 'User', flex: 1, minWidth: 120, filterable: false, @@ -65,7 +53,47 @@ export const loadColumns = async ( type: 'singleSelect', getOptionValue: (value: any) => value?.id, getOptionLabel: (value: any) => value?.label, - valueOptions: await callOptionsApi('brands'), + valueOptions: await callOptionsApi('users'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + }, + + { + field: 'product', + headerName: 'Product', + 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('products'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + }, + + { + field: 'location', + headerName: 'Location', + 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('locations'), valueGetter: (params: GridValueGetterParams) => params?.value?.id ?? params?.value, }, @@ -85,8 +113,8 @@ export const loadColumns = async ( }, { - field: 'type', - headerName: 'Type', + field: 'age', + headerName: 'Age', flex: 1, minWidth: 120, filterable: false, @@ -94,6 +122,142 @@ export const loadColumns = async ( cellClassName: 'datagrid--cell', editable: hasUpdatePermission, + + type: 'number', + }, + + { + field: 'rating', + headerName: 'Rating', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + type: 'number', + }, + + { + field: 'collectable', + headerName: 'Collectable', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + type: 'boolean', + }, + + { + field: 'rickhouse', + headerName: 'Rickhouse', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + + { + field: 'rack', + headerName: 'Rack', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + + { + field: 'release', + headerName: 'Release', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + + { + field: 'barrelnumber', + headerName: 'Barrelnumber', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + + { + field: 'barreleddate', + headerName: 'Barreleddate', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + type: 'date', + valueGetter: (params: GridValueGetterParams) => + new Date(params.row.barreleddate), + }, + + { + field: 'bottlenumber', + headerName: 'Bottlenumber', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + + { + field: 'dateacquired', + headerName: 'Dateacquired', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + type: 'date', + valueGetter: (params: GridValueGetterParams) => + new Date(params.row.dateacquired), + }, + + { + field: 'volume', + headerName: 'Volume', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + type: 'number', }, { @@ -109,118 +273,8 @@ export const loadColumns = async ( }, { - field: 'tasting_notes', - headerName: 'TastingNotes', - flex: 1, - minWidth: 120, - filterable: false, - headerClassName: 'datagrid--header', - cellClassName: 'datagrid--cell', - - editable: hasUpdatePermission, - }, - - { - field: 'msrp_range', - headerName: 'MSRPRange', - flex: 1, - minWidth: 120, - filterable: false, - headerClassName: 'datagrid--header', - cellClassName: 'datagrid--cell', - - editable: hasUpdatePermission, - }, - - { - field: 'secondary_value_range', - headerName: 'SecondaryValueRange', - flex: 1, - minWidth: 120, - filterable: false, - headerClassName: 'datagrid--header', - cellClassName: 'datagrid--cell', - - editable: hasUpdatePermission, - }, - - { - field: 'opened_bottle_indicator', - headerName: 'OpenedBottleIndicator', - flex: 1, - minWidth: 120, - filterable: false, - headerClassName: 'datagrid--header', - cellClassName: 'datagrid--cell', - - editable: hasUpdatePermission, - - type: 'boolean', - }, - - { - field: 'quantity', - headerName: 'Quantity', - flex: 1, - minWidth: 120, - filterable: false, - headerClassName: 'datagrid--header', - cellClassName: 'datagrid--cell', - - editable: hasUpdatePermission, - - type: 'number', - }, - - { - field: 'barcode', - headerName: 'Barcode', - flex: 1, - minWidth: 120, - filterable: false, - headerClassName: 'datagrid--header', - cellClassName: 'datagrid--cell', - - editable: hasUpdatePermission, - }, - - { - field: 'picture', - headerName: 'Picture', - flex: 1, - minWidth: 120, - filterable: false, - headerClassName: 'datagrid--header', - cellClassName: 'datagrid--cell', - - editable: false, - sortable: false, - renderCell: (params: GridValueGetterParams) => ( - - ), - }, - - { - field: 'age', - headerName: 'Age', - flex: 1, - minWidth: 120, - filterable: false, - headerClassName: 'datagrid--header', - cellClassName: 'datagrid--cell', - - editable: hasUpdatePermission, - - type: 'number', - }, - - { - field: 'distillery', - headerName: 'Distillery', + field: 'photofront', + headerName: 'Photofront', flex: 1, minWidth: 120, filterable: false, @@ -233,14 +287,14 @@ export const loadColumns = async ( type: 'singleSelect', getOptionValue: (value: any) => value?.id, getOptionLabel: (value: any) => value?.label, - valueOptions: await callOptionsApi('distilleries'), + valueOptions: await callOptionsApi('photos'), valueGetter: (params: GridValueGetterParams) => params?.value?.id ?? params?.value, }, { - field: 'user', - headerName: 'User', + field: 'photoback', + headerName: 'Photoback', flex: 1, minWidth: 120, filterable: false, @@ -253,7 +307,7 @@ export const loadColumns = async ( type: 'singleSelect', getOptionValue: (value: any) => value?.id, getOptionLabel: (value: any) => value?.label, - valueOptions: await callOptionsApi('users'), + valueOptions: await callOptionsApi('photos'), valueGetter: (params: GridValueGetterParams) => params?.value?.id ?? params?.value, }, diff --git a/frontend/src/components/Brands/CardBrands.tsx b/frontend/src/components/Brands/CardBrands.tsx index 6c02f89..1574059 100644 --- a/frontend/src/components/Brands/CardBrands.tsx +++ b/frontend/src/components/Brands/CardBrands.tsx @@ -83,6 +83,17 @@ const CardBrands = ({
+
+
+ Status +
+
+
+ {item.status} +
+
+
+
Distillery diff --git a/frontend/src/components/Brands/ListBrands.tsx b/frontend/src/components/Brands/ListBrands.tsx index 8c508c7..0d30be2 100644 --- a/frontend/src/components/Brands/ListBrands.tsx +++ b/frontend/src/components/Brands/ListBrands.tsx @@ -56,6 +56,11 @@ const ListBrands = ({

{item.name}

+
+

Status

+

{item.status}

+
+

Distillery

diff --git a/frontend/src/components/Brands/TableBrands.tsx b/frontend/src/components/Brands/TableBrands.tsx index 17e3c07..afc54d8 100644 --- a/frontend/src/components/Brands/TableBrands.tsx +++ b/frontend/src/components/Brands/TableBrands.tsx @@ -20,8 +20,6 @@ import _ from 'lodash'; import dataFormatter from '../../helpers/dataFormatter'; import { dataGridStyles } from '../../styles'; -import ListBrands from './ListBrands'; - const perPage = 10; const TableSampleBrands = ({ @@ -463,18 +461,7 @@ const TableSampleBrands = ({

Are you sure you want to delete this item?

- {brands && Array.isArray(brands) && !showGrid && ( - - )} - - {showGrid && dataGrid} + {dataGrid} {selectedRows.length > 0 && createPortal( diff --git a/frontend/src/components/Brands/configureBrandsCols.tsx b/frontend/src/components/Brands/configureBrandsCols.tsx index 2674b8f..826f965 100644 --- a/frontend/src/components/Brands/configureBrandsCols.tsx +++ b/frontend/src/components/Brands/configureBrandsCols.tsx @@ -50,6 +50,20 @@ export const loadColumns = async ( editable: hasUpdatePermission, }, + { + field: 'status', + headerName: 'Status', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + type: 'number', + }, + { field: 'distillery', headerName: 'Distillery', diff --git a/frontend/src/components/Conversationparticipants/CardConversationparticipants.tsx b/frontend/src/components/Conversationparticipants/CardConversationparticipants.tsx new file mode 100644 index 0000000..4a0e2c6 --- /dev/null +++ b/frontend/src/components/Conversationparticipants/CardConversationparticipants.tsx @@ -0,0 +1,123 @@ +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 = { + conversationparticipants: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const CardConversationparticipants = ({ + conversationparticipants, + 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_CONVERSATIONPARTICIPANTS', + ); + + return ( +
+ {loading && } +
    + {!loading && + conversationparticipants.map((item, index) => ( +
  • +
    + + {item.id} + + +
    + +
    +
    +
    +
    +
    + Conversation +
    +
    +
    + {dataFormatter.conversationsOneListFormatter( + item.conversation, + )} +
    +
    +
    + +
    +
    User
    +
    +
    + {dataFormatter.usersOneListFormatter(item.user)} +
    +
    +
    +
    +
  • + ))} + {!loading && conversationparticipants.length === 0 && ( +
    +

    No data to display

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

Conversation

+

+ {dataFormatter.conversationsOneListFormatter( + item.conversation, + )} +

+
+ +
+

User

+

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

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

No data to display

+
+ )} +
+
+ +
+ + ); +}; + +export default ListConversationparticipants; diff --git a/frontend/src/components/Conversationparticipants/TableConversationparticipants.tsx b/frontend/src/components/Conversationparticipants/TableConversationparticipants.tsx new file mode 100644 index 0000000..26432db --- /dev/null +++ b/frontend/src/components/Conversationparticipants/TableConversationparticipants.tsx @@ -0,0 +1,486 @@ +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/conversationparticipants/conversationparticipantsSlice'; +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 './configureConversationparticipantsCols'; +import _ from 'lodash'; +import dataFormatter from '../../helpers/dataFormatter'; +import { dataGridStyles } from '../../styles'; + +const perPage = 10; + +const TableSampleConversationparticipants = ({ + 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 { + conversationparticipants, + loading, + count, + notify: conversationparticipantsNotify, + refetch, + } = useAppSelector((state) => state.conversationparticipants); + 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 (conversationparticipantsNotify.showNotification) { + notify( + conversationparticipantsNotify.typeNotification, + conversationparticipantsNotify.textNotification, + ); + } + }, [conversationparticipantsNotify.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, + `conversationparticipants`, + 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={conversationparticipants ?? []} + 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 TableSampleConversationparticipants; diff --git a/frontend/src/components/Conversationparticipants/configureConversationparticipantsCols.tsx b/frontend/src/components/Conversationparticipants/configureConversationparticipantsCols.tsx new file mode 100644 index 0000000..0e86907 --- /dev/null +++ b/frontend/src/components/Conversationparticipants/configureConversationparticipantsCols.tsx @@ -0,0 +1,105 @@ +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_CONVERSATIONPARTICIPANTS', + ); + + return [ + { + field: 'conversation', + headerName: 'Conversation', + 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('conversations'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + }, + + { + field: 'user', + headerName: 'User', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + sortable: false, + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('users'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + }, + + { + field: 'actions', + type: 'actions', + minWidth: 30, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + getActions: (params: GridRowParams) => { + return [ +
+ +
, + ]; + }, + }, + ]; +}; diff --git a/frontend/src/components/Conversations/CardConversations.tsx b/frontend/src/components/Conversations/CardConversations.tsx new file mode 100644 index 0000000..d864fd4 --- /dev/null +++ b/frontend/src/components/Conversations/CardConversations.tsx @@ -0,0 +1,112 @@ +import React from 'react'; +import ImageField from '../ImageField'; +import ListActionsPopover from '../ListActionsPopover'; +import { useAppSelector } from '../../stores/hooks'; +import dataFormatter from '../../helpers/dataFormatter'; +import { Pagination } from '../Pagination'; +import { saveFile } from '../../helpers/fileSaver'; +import LoadingSpinner from '../LoadingSpinner'; +import Link from 'next/link'; + +import { hasPermission } from '../../helpers/userPermissions'; + +type Props = { + conversations: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const CardConversations = ({ + conversations, + 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_CONVERSATIONS', + ); + + return ( +
+ {loading && } +
    + {!loading && + conversations.map((item, index) => ( +
  • +
    + + {item.id} + + +
    + +
    +
    +
    +
    +
    + Createdat +
    +
    +
    + {dataFormatter.dateTimeFormatter(item.createdat)} +
    +
    +
    +
    +
  • + ))} + {!loading && conversations.length === 0 && ( +
    +

    No data to display

    +
    + )} +
+
+ +
+
+ ); +}; + +export default CardConversations; diff --git a/frontend/src/components/Conversations/ListConversations.tsx b/frontend/src/components/Conversations/ListConversations.tsx new file mode 100644 index 0000000..2d9efd6 --- /dev/null +++ b/frontend/src/components/Conversations/ListConversations.tsx @@ -0,0 +1,92 @@ +import React from 'react'; +import CardBox from '../CardBox'; +import ImageField from '../ImageField'; +import dataFormatter from '../../helpers/dataFormatter'; +import { saveFile } from '../../helpers/fileSaver'; +import ListActionsPopover from '../ListActionsPopover'; +import { useAppSelector } from '../../stores/hooks'; +import { Pagination } from '../Pagination'; +import LoadingSpinner from '../LoadingSpinner'; +import Link from 'next/link'; + +import { hasPermission } from '../../helpers/userPermissions'; + +type Props = { + conversations: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const ListConversations = ({ + conversations, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const currentUser = useAppSelector((state) => state.auth.currentUser); + const hasUpdatePermission = hasPermission( + currentUser, + 'UPDATE_CONVERSATIONS', + ); + + const corners = useAppSelector((state) => state.style.corners); + const bgColor = useAppSelector((state) => state.style.cardsColor); + + return ( + <> +
+ {loading && } + {!loading && + conversations.map((item) => ( +
+ +
+ dark:divide-dark-700 overflow-x-auto' + } + > +
+

Createdat

+

+ {dataFormatter.dateTimeFormatter(item.createdat)} +

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

No data to display

+
+ )} +
+
+ +
+ + ); +}; + +export default ListConversations; diff --git a/frontend/src/components/Conversations/TableConversations.tsx b/frontend/src/components/Conversations/TableConversations.tsx new file mode 100644 index 0000000..9705281 --- /dev/null +++ b/frontend/src/components/Conversations/TableConversations.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/conversations/conversationsSlice'; +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 './configureConversationsCols'; +import _ from 'lodash'; +import dataFormatter from '../../helpers/dataFormatter'; +import { dataGridStyles } from '../../styles'; + +const perPage = 10; + +const TableSampleConversations = ({ + 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 { + conversations, + loading, + count, + notify: conversationsNotify, + refetch, + } = useAppSelector((state) => state.conversations); + 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 (conversationsNotify.showNotification) { + notify( + conversationsNotify.typeNotification, + conversationsNotify.textNotification, + ); + } + }, [conversationsNotify.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, `conversations`, 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={conversations ?? []} + 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 TableSampleConversations; diff --git a/frontend/src/components/Conversations/configureConversationsCols.tsx b/frontend/src/components/Conversations/configureConversationsCols.tsx new file mode 100644 index 0000000..05bdfe3 --- /dev/null +++ b/frontend/src/components/Conversations/configureConversationsCols.tsx @@ -0,0 +1,78 @@ +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_CONVERSATIONS'); + + return [ + { + field: 'createdat', + headerName: 'Createdat', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + type: 'dateTime', + valueGetter: (params: GridValueGetterParams) => + new Date(params.row.createdat), + }, + + { + field: 'actions', + type: 'actions', + minWidth: 30, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + getActions: (params: GridRowParams) => { + return [ +
+ +
, + ]; + }, + }, + ]; +}; diff --git a/frontend/src/components/Distilleries/CardDistilleries.tsx b/frontend/src/components/Distilleries/CardDistilleries.tsx index c4740e7..aa31d1d 100644 --- a/frontend/src/components/Distilleries/CardDistilleries.tsx +++ b/frontend/src/components/Distilleries/CardDistilleries.tsx @@ -82,6 +82,31 @@ const CardDistilleries = ({
{item.name}
+ +
+
City
+
+
{item.city}
+
+
+ +
+
State
+
+
{item.state}
+
+
+ +
+
+ Status +
+
+
+ {item.status} +
+
+
))} diff --git a/frontend/src/components/Distilleries/ListDistilleries.tsx b/frontend/src/components/Distilleries/ListDistilleries.tsx index b6686e5..6d72729 100644 --- a/frontend/src/components/Distilleries/ListDistilleries.tsx +++ b/frontend/src/components/Distilleries/ListDistilleries.tsx @@ -55,6 +55,21 @@ const ListDistilleries = ({

Name

{item.name}

+ +
+

City

+

{item.city}

+
+ +
+

State

+

{item.state}

+
+ +
+

Status

+

{item.status}

+
Are you sure you want to delete this item?

- {distilleries && Array.isArray(distilleries) && !showGrid && ( - - )} - - {showGrid && dataGrid} + {dataGrid} {selectedRows.length > 0 && createPortal( diff --git a/frontend/src/components/Distilleries/configureDistilleriesCols.tsx b/frontend/src/components/Distilleries/configureDistilleriesCols.tsx index 02fa87d..cf9af8b 100644 --- a/frontend/src/components/Distilleries/configureDistilleriesCols.tsx +++ b/frontend/src/components/Distilleries/configureDistilleriesCols.tsx @@ -50,6 +50,44 @@ export const loadColumns = async ( editable: hasUpdatePermission, }, + { + field: 'city', + headerName: 'City', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + + { + field: 'state', + headerName: 'State', + 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: 'number', + }, + { field: 'actions', type: 'actions', diff --git a/frontend/src/components/Locations/CardLocations.tsx b/frontend/src/components/Locations/CardLocations.tsx new file mode 100644 index 0000000..6fdee93 --- /dev/null +++ b/frontend/src/components/Locations/CardLocations.tsx @@ -0,0 +1,114 @@ +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 = { + locations: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const CardLocations = ({ + locations, + 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_LOCATIONS'); + + return ( +
+ {loading && } +
    + {!loading && + locations.map((item, index) => ( +
  • +
    + + {item.name} + + +
    + +
    +
    +
    +
    +
    Name
    +
    +
    {item.name}
    +
    +
    + +
    +
    User
    +
    +
    + {dataFormatter.usersOneListFormatter(item.user)} +
    +
    +
    +
    +
  • + ))} + {!loading && locations.length === 0 && ( +
    +

    No data to display

    +
    + )} +
+
+ +
+
+ ); +}; + +export default CardLocations; diff --git a/frontend/src/components/Locations/ListLocations.tsx b/frontend/src/components/Locations/ListLocations.tsx new file mode 100644 index 0000000..e34b067 --- /dev/null +++ b/frontend/src/components/Locations/ListLocations.tsx @@ -0,0 +1,94 @@ +import React from 'react'; +import CardBox from '../CardBox'; +import ImageField from '../ImageField'; +import dataFormatter from '../../helpers/dataFormatter'; +import { saveFile } from '../../helpers/fileSaver'; +import ListActionsPopover from '../ListActionsPopover'; +import { useAppSelector } from '../../stores/hooks'; +import { Pagination } from '../Pagination'; +import LoadingSpinner from '../LoadingSpinner'; +import Link from 'next/link'; + +import { hasPermission } from '../../helpers/userPermissions'; + +type Props = { + locations: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const ListLocations = ({ + locations, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const currentUser = useAppSelector((state) => state.auth.currentUser); + const hasUpdatePermission = hasPermission(currentUser, 'UPDATE_LOCATIONS'); + + const corners = useAppSelector((state) => state.style.corners); + const bgColor = useAppSelector((state) => state.style.cardsColor); + + return ( + <> +
+ {loading && } + {!loading && + locations.map((item) => ( +
+ +
+ dark:divide-dark-700 overflow-x-auto' + } + > +
+

Name

+

{item.name}

+
+ +
+

User

+

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

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

No data to display

+
+ )} +
+
+ +
+ + ); +}; + +export default ListLocations; diff --git a/frontend/src/components/Locations/TableLocations.tsx b/frontend/src/components/Locations/TableLocations.tsx new file mode 100644 index 0000000..c0a84fc --- /dev/null +++ b/frontend/src/components/Locations/TableLocations.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/locations/locationsSlice'; +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 './configureLocationsCols'; +import _ from 'lodash'; +import dataFormatter from '../../helpers/dataFormatter'; +import { dataGridStyles } from '../../styles'; + +const perPage = 10; + +const TableSampleLocations = ({ + 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 { + locations, + loading, + count, + notify: locationsNotify, + refetch, + } = useAppSelector((state) => state.locations); + 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 (locationsNotify.showNotification) { + notify( + locationsNotify.typeNotification, + locationsNotify.textNotification, + ); + } + }, [locationsNotify.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, `locations`, 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={locations ?? []} + 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 TableSampleLocations; diff --git a/frontend/src/components/Locations/configureLocationsCols.tsx b/frontend/src/components/Locations/configureLocationsCols.tsx new file mode 100644 index 0000000..ede479e --- /dev/null +++ b/frontend/src/components/Locations/configureLocationsCols.tsx @@ -0,0 +1,94 @@ +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_LOCATIONS'); + + return [ + { + field: 'name', + headerName: 'Name', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + + { + field: 'user', + headerName: 'User', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + sortable: false, + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('users'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + }, + + { + field: 'actions', + type: 'actions', + minWidth: 30, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + getActions: (params: GridRowParams) => { + return [ +
+ +
, + ]; + }, + }, + ]; +}; diff --git a/frontend/src/components/Messages/CardMessages.tsx b/frontend/src/components/Messages/CardMessages.tsx new file mode 100644 index 0000000..491275b --- /dev/null +++ b/frontend/src/components/Messages/CardMessages.tsx @@ -0,0 +1,122 @@ +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 = { + messages: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const CardMessages = ({ + messages, + 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_MESSAGES'); + + return ( +
+ {loading && } +
    + {!loading && + messages.map((item, index) => ( +
  • +
    + + {item.id} + + +
    + +
    +
    +
    +
    +
    + Conversation +
    +
    +
    + {dataFormatter.conversationsOneListFormatter( + item.conversation, + )} +
    +
    +
    + +
    +
    + Sender +
    +
    +
    + {dataFormatter.usersOneListFormatter(item.sender)} +
    +
    +
    +
    +
  • + ))} + {!loading && messages.length === 0 && ( +
    +

    No data to display

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

Conversation

+

+ {dataFormatter.conversationsOneListFormatter( + item.conversation, + )} +

+
+ +
+

Sender

+

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

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

No data to display

+
+ )} +
+
+ +
+ + ); +}; + +export default ListMessages; diff --git a/frontend/src/components/Messages/TableMessages.tsx b/frontend/src/components/Messages/TableMessages.tsx new file mode 100644 index 0000000..f0e476a --- /dev/null +++ b/frontend/src/components/Messages/TableMessages.tsx @@ -0,0 +1,481 @@ +import React, { useEffect, useState, useMemo } from 'react'; +import { createPortal } from 'react-dom'; +import { ToastContainer, toast } from 'react-toastify'; +import BaseButton from '../BaseButton'; +import CardBoxModal from '../CardBoxModal'; +import CardBox from '../CardBox'; +import { + fetch, + update, + deleteItem, + setRefetch, + deleteItemsByIds, +} from '../../stores/messages/messagesSlice'; +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 './configureMessagesCols'; +import _ from 'lodash'; +import dataFormatter from '../../helpers/dataFormatter'; +import { dataGridStyles } from '../../styles'; + +const perPage = 10; + +const TableSampleMessages = ({ + 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 { + messages, + loading, + count, + notify: messagesNotify, + refetch, + } = useAppSelector((state) => state.messages); + 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 (messagesNotify.showNotification) { + notify(messagesNotify.typeNotification, messagesNotify.textNotification); + } + }, [messagesNotify.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, `messages`, 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={messages ?? []} + 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 TableSampleMessages; diff --git a/frontend/src/components/Messages/configureMessagesCols.tsx b/frontend/src/components/Messages/configureMessagesCols.tsx new file mode 100644 index 0000000..d6446dc --- /dev/null +++ b/frontend/src/components/Messages/configureMessagesCols.tsx @@ -0,0 +1,102 @@ +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_MESSAGES'); + + return [ + { + field: 'conversation', + headerName: 'Conversation', + 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('conversations'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + }, + + { + field: 'sender', + headerName: 'Sender', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + sortable: false, + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('users'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + }, + + { + field: 'actions', + type: 'actions', + minWidth: 30, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + getActions: (params: GridRowParams) => { + return [ +
+ +
, + ]; + }, + }, + ]; +}; diff --git a/frontend/src/components/Photos/CardPhotos.tsx b/frontend/src/components/Photos/CardPhotos.tsx new file mode 100644 index 0000000..051101b --- /dev/null +++ b/frontend/src/components/Photos/CardPhotos.tsx @@ -0,0 +1,128 @@ +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 = { + photos: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const CardPhotos = ({ + photos, + 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_PHOTOS'); + + return ( +
+ {loading && } +
    + {!loading && + photos.map((item, index) => ( +
  • +
    + + +

    {item.image}

    + + +
    + +
    +
    +
    +
    +
    + Phototype +
    +
    +
    + {item.phototype} +
    +
    +
    + +
    +
    Image
    +
    +
    + +
    +
    +
    +
    +
  • + ))} + {!loading && photos.length === 0 && ( +
    +

    No data to display

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

Phototype

+

{item.phototype}

+
+ +
+

Image

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

No data to display

+
+ )} +
+
+ +
+ + ); +}; + +export default ListPhotos; diff --git a/frontend/src/components/Photos/TablePhotos.tsx b/frontend/src/components/Photos/TablePhotos.tsx new file mode 100644 index 0000000..51fc0b9 --- /dev/null +++ b/frontend/src/components/Photos/TablePhotos.tsx @@ -0,0 +1,481 @@ +import React, { useEffect, useState, useMemo } from 'react'; +import { createPortal } from 'react-dom'; +import { ToastContainer, toast } from 'react-toastify'; +import BaseButton from '../BaseButton'; +import CardBoxModal from '../CardBoxModal'; +import CardBox from '../CardBox'; +import { + fetch, + update, + deleteItem, + setRefetch, + deleteItemsByIds, +} from '../../stores/photos/photosSlice'; +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 './configurePhotosCols'; +import _ from 'lodash'; +import dataFormatter from '../../helpers/dataFormatter'; +import { dataGridStyles } from '../../styles'; + +const perPage = 10; + +const TableSamplePhotos = ({ + 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 { + photos, + loading, + count, + notify: photosNotify, + refetch, + } = useAppSelector((state) => state.photos); + 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 (photosNotify.showNotification) { + notify(photosNotify.typeNotification, photosNotify.textNotification); + } + }, [photosNotify.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, `photos`, 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={photos ?? []} + 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 TableSamplePhotos; diff --git a/frontend/src/components/Photos/configurePhotosCols.tsx b/frontend/src/components/Photos/configurePhotosCols.tsx new file mode 100644 index 0000000..06c8603 --- /dev/null +++ b/frontend/src/components/Photos/configurePhotosCols.tsx @@ -0,0 +1,94 @@ +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_PHOTOS'); + + return [ + { + field: 'phototype', + headerName: 'Phototype', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + + { + field: 'image', + headerName: 'Image', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: false, + sortable: false, + renderCell: (params: GridValueGetterParams) => ( + + ), + }, + + { + field: 'actions', + type: 'actions', + minWidth: 30, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + getActions: (params: GridRowParams) => { + return [ +
+ +
, + ]; + }, + }, + ]; +}; diff --git a/frontend/src/components/Products/CardProducts.tsx b/frontend/src/components/Products/CardProducts.tsx new file mode 100644 index 0000000..5be612c --- /dev/null +++ b/frontend/src/components/Products/CardProducts.tsx @@ -0,0 +1,179 @@ +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 = { + products: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const CardProducts = ({ + products, + 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_PRODUCTS'); + + return ( +
+ {loading && } +
    + {!loading && + products.map((item, index) => ( +
  • +
    + + {item.name} + + +
    + +
    +
    +
    +
    +
    Brand
    +
    +
    + {dataFormatter.brandsOneListFormatter(item.brand)} +
    +
    +
    + +
    +
    Name
    +
    +
    {item.name}
    +
    +
    + +
    +
    Proof
    +
    +
    {item.proof}
    +
    +
    + +
    +
    Age
    +
    +
    {item.age}
    +
    +
    + +
    +
    + Barcode +
    +
    +
    + {item.barcode} +
    +
    +
    + +
    +
    Notes
    +
    +
    {item.notes}
    +
    +
    + +
    +
    + Status +
    +
    +
    + {item.status} +
    +
    +
    + +
    +
    + Photofront +
    +
    +
    + {dataFormatter.photosOneListFormatter(item.photofront)} +
    +
    +
    + +
    +
    + Photoback +
    +
    +
    + {dataFormatter.photosOneListFormatter(item.photoback)} +
    +
    +
    +
    +
  • + ))} + {!loading && products.length === 0 && ( +
    +

    No data to display

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

Brand

+

+ {dataFormatter.brandsOneListFormatter(item.brand)} +

+
+ +
+

Name

+

{item.name}

+
+ +
+

Proof

+

{item.proof}

+
+ +
+

Age

+

{item.age}

+
+ +
+

Barcode

+

{item.barcode}

+
+ +
+

Notes

+

{item.notes}

+
+ +
+

Status

+

{item.status}

+
+ +
+

Photofront

+

+ {dataFormatter.photosOneListFormatter(item.photofront)} +

+
+ +
+

Photoback

+

+ {dataFormatter.photosOneListFormatter(item.photoback)} +

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

No data to display

+
+ )} +
+
+ +
+ + ); +}; + +export default ListProducts; diff --git a/frontend/src/components/Products/TableProducts.tsx b/frontend/src/components/Products/TableProducts.tsx new file mode 100644 index 0000000..87183e1 --- /dev/null +++ b/frontend/src/components/Products/TableProducts.tsx @@ -0,0 +1,481 @@ +import React, { useEffect, useState, useMemo } from 'react'; +import { createPortal } from 'react-dom'; +import { ToastContainer, toast } from 'react-toastify'; +import BaseButton from '../BaseButton'; +import CardBoxModal from '../CardBoxModal'; +import CardBox from '../CardBox'; +import { + fetch, + update, + deleteItem, + setRefetch, + deleteItemsByIds, +} from '../../stores/products/productsSlice'; +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 './configureProductsCols'; +import _ from 'lodash'; +import dataFormatter from '../../helpers/dataFormatter'; +import { dataGridStyles } from '../../styles'; + +const perPage = 10; + +const TableSampleProducts = ({ + 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 { + products, + loading, + count, + notify: productsNotify, + refetch, + } = useAppSelector((state) => state.products); + 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 (productsNotify.showNotification) { + notify(productsNotify.typeNotification, productsNotify.textNotification); + } + }, [productsNotify.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, `products`, 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={products ?? []} + 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 TableSampleProducts; diff --git a/frontend/src/components/Products/configureProductsCols.tsx b/frontend/src/components/Products/configureProductsCols.tsx new file mode 100644 index 0000000..8adef37 --- /dev/null +++ b/frontend/src/components/Products/configureProductsCols.tsx @@ -0,0 +1,200 @@ +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_PRODUCTS'); + + return [ + { + field: 'brand', + headerName: 'Brand', + 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('brands'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + }, + + { + field: 'name', + headerName: 'Name', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + + { + field: 'proof', + headerName: 'Proof', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + type: 'number', + }, + + { + field: 'age', + headerName: 'Age', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + type: 'number', + }, + + { + field: 'barcode', + headerName: 'Barcode', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + + { + field: 'notes', + headerName: 'Notes', + 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: 'number', + }, + + { + field: 'photofront', + headerName: 'Photofront', + 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('photos'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + }, + + { + field: 'photoback', + headerName: 'Photoback', + 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('photos'), + 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/Reviews/CardReviews.tsx b/frontend/src/components/Reviews/CardReviews.tsx new file mode 100644 index 0000000..da4b26c --- /dev/null +++ b/frontend/src/components/Reviews/CardReviews.tsx @@ -0,0 +1,147 @@ +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 = { + reviews: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const CardReviews = ({ + reviews, + 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_REVIEWS'); + + return ( +
+ {loading && } +
    + {!loading && + reviews.map((item, index) => ( +
  • +
    + + {item.id} + + +
    + +
    +
    +
    +
    +
    User
    +
    +
    + {dataFormatter.usersOneListFormatter(item.user)} +
    +
    +
    + +
    +
    + Bottle +
    +
    +
    + {dataFormatter.bottlesOneListFormatter(item.bottle)} +
    +
    +
    + +
    +
    + Rating +
    +
    +
    + {item.rating} +
    +
    +
    + +
    +
    Notes
    +
    +
    {item.notes}
    +
    +
    + +
    +
    + Createdat +
    +
    +
    + {dataFormatter.dateTimeFormatter(item.createdat)} +
    +
    +
    +
    +
  • + ))} + {!loading && reviews.length === 0 && ( +
    +

    No data to display

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

User

+

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

+
+ +
+

Bottle

+

+ {dataFormatter.bottlesOneListFormatter(item.bottle)} +

+
+ +
+

Rating

+

{item.rating}

+
+ +
+

Notes

+

{item.notes}

+
+ +
+

Createdat

+

+ {dataFormatter.dateTimeFormatter(item.createdat)} +

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

No data to display

+
+ )} +
+
+ +
+ + ); +}; + +export default ListReviews; diff --git a/frontend/src/components/Reviews/TableReviews.tsx b/frontend/src/components/Reviews/TableReviews.tsx new file mode 100644 index 0000000..5769c29 --- /dev/null +++ b/frontend/src/components/Reviews/TableReviews.tsx @@ -0,0 +1,481 @@ +import React, { useEffect, useState, useMemo } from 'react'; +import { createPortal } from 'react-dom'; +import { ToastContainer, toast } from 'react-toastify'; +import BaseButton from '../BaseButton'; +import CardBoxModal from '../CardBoxModal'; +import CardBox from '../CardBox'; +import { + fetch, + update, + deleteItem, + setRefetch, + deleteItemsByIds, +} from '../../stores/reviews/reviewsSlice'; +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 './configureReviewsCols'; +import _ from 'lodash'; +import dataFormatter from '../../helpers/dataFormatter'; +import { dataGridStyles } from '../../styles'; + +const perPage = 10; + +const TableSampleReviews = ({ + 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 { + reviews, + loading, + count, + notify: reviewsNotify, + refetch, + } = useAppSelector((state) => state.reviews); + 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 (reviewsNotify.showNotification) { + notify(reviewsNotify.typeNotification, reviewsNotify.textNotification); + } + }, [reviewsNotify.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, `reviews`, 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={reviews ?? []} + 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 TableSampleReviews; diff --git a/frontend/src/components/Reviews/configureReviewsCols.tsx b/frontend/src/components/Reviews/configureReviewsCols.tsx new file mode 100644 index 0000000..da2d3a7 --- /dev/null +++ b/frontend/src/components/Reviews/configureReviewsCols.tsx @@ -0,0 +1,144 @@ +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_REVIEWS'); + + 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: 'bottle', + headerName: 'Bottle', + 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('bottles'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + }, + + { + field: 'rating', + headerName: 'Rating', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + type: 'number', + }, + + { + field: 'notes', + headerName: 'Notes', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + + { + field: 'createdat', + headerName: 'Createdat', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + type: 'dateTime', + valueGetter: (params: GridValueGetterParams) => + new Date(params.row.createdat), + }, + + { + 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 f4ce406..718090b 100644 --- a/frontend/src/components/Users/CardUsers.tsx +++ b/frontend/src/components/Users/CardUsers.tsx @@ -173,6 +173,28 @@ const CardUsers = ({ + +
+
+ Address +
+
+
+ {item.address} +
+
+
+ +
+
+ Address2 +
+
+
+ {item.address2} +
+
+
))} diff --git a/frontend/src/components/Users/ListUsers.tsx b/frontend/src/components/Users/ListUsers.tsx index 61fe76d..76796db 100644 --- a/frontend/src/components/Users/ListUsers.tsx +++ b/frontend/src/components/Users/ListUsers.tsx @@ -113,6 +113,16 @@ const ListUsers = ({ .join(', ')}

+ +
+

Address

+

{item.address}

+
+ +
+

Address2

+

{item.address2}

+
state.style.websiteHeder); const borders = useAppSelector((state) => state.style.borders); - const style = HeaderStyle.PAGES_RIGHT; + const style = HeaderStyle.PAGES_LEFT; const design = HeaderDesigns.DESIGN_DIVERSITY; return ( diff --git a/frontend/src/helpers/dataFormatter.js b/frontend/src/helpers/dataFormatter.js index f3620b8..ee5fda9 100644 --- a/frontend/src/helpers/dataFormatter.js +++ b/frontend/src/helpers/dataFormatter.js @@ -58,44 +58,6 @@ export default { return { label: val.firstName, id: val.id }; }, - brandsManyListFormatter(val) { - if (!val || !val.length) return []; - return val.map((item) => item.name); - }, - brandsOneListFormatter(val) { - if (!val) return ''; - return val.name; - }, - brandsManyListFormatterEdit(val) { - if (!val || !val.length) return []; - return val.map((item) => { - return { id: item.id, label: item.name }; - }); - }, - brandsOneListFormatterEdit(val) { - if (!val) return ''; - return { label: val.name, id: val.id }; - }, - - distilleriesManyListFormatter(val) { - if (!val || !val.length) return []; - return val.map((item) => item.name); - }, - distilleriesOneListFormatter(val) { - if (!val) return ''; - return val.name; - }, - distilleriesManyListFormatterEdit(val) { - if (!val || !val.length) return []; - return val.map((item) => { - return { id: item.id, label: item.name }; - }); - }, - distilleriesOneListFormatterEdit(val) { - if (!val) return ''; - return { label: val.name, id: val.id }; - }, - rolesManyListFormatter(val) { if (!val || !val.length) return []; return val.map((item) => item.name); @@ -133,4 +95,137 @@ export default { if (!val) return ''; return { label: val.name, id: val.id }; }, + + distilleriesManyListFormatter(val) { + if (!val || !val.length) return []; + return val.map((item) => item.name); + }, + distilleriesOneListFormatter(val) { + if (!val) return ''; + return val.name; + }, + distilleriesManyListFormatterEdit(val) { + if (!val || !val.length) return []; + return val.map((item) => { + return { id: item.id, label: item.name }; + }); + }, + distilleriesOneListFormatterEdit(val) { + if (!val) return ''; + return { label: val.name, id: val.id }; + }, + + brandsManyListFormatter(val) { + if (!val || !val.length) return []; + return val.map((item) => item.name); + }, + brandsOneListFormatter(val) { + if (!val) return ''; + return val.name; + }, + brandsManyListFormatterEdit(val) { + if (!val || !val.length) return []; + return val.map((item) => { + return { id: item.id, label: item.name }; + }); + }, + brandsOneListFormatterEdit(val) { + if (!val) return ''; + return { label: val.name, id: val.id }; + }, + + photosManyListFormatter(val) { + if (!val || !val.length) return []; + return val.map((item) => item.image); + }, + photosOneListFormatter(val) { + if (!val) return ''; + return val.image; + }, + photosManyListFormatterEdit(val) { + if (!val || !val.length) return []; + return val.map((item) => { + return { id: item.id, label: item.image }; + }); + }, + photosOneListFormatterEdit(val) { + if (!val) return ''; + return { label: val.image, id: val.id }; + }, + + productsManyListFormatter(val) { + if (!val || !val.length) return []; + return val.map((item) => item.name); + }, + productsOneListFormatter(val) { + if (!val) return ''; + return val.name; + }, + productsManyListFormatterEdit(val) { + if (!val || !val.length) return []; + return val.map((item) => { + return { id: item.id, label: item.name }; + }); + }, + productsOneListFormatterEdit(val) { + if (!val) return ''; + return { label: val.name, id: val.id }; + }, + + locationsManyListFormatter(val) { + if (!val || !val.length) return []; + return val.map((item) => item.name); + }, + locationsOneListFormatter(val) { + if (!val) return ''; + return val.name; + }, + locationsManyListFormatterEdit(val) { + if (!val || !val.length) return []; + return val.map((item) => { + return { id: item.id, label: item.name }; + }); + }, + locationsOneListFormatterEdit(val) { + if (!val) return ''; + return { label: val.name, id: val.id }; + }, + + bottlesManyListFormatter(val) { + if (!val || !val.length) return []; + return val.map((item) => item.id); + }, + bottlesOneListFormatter(val) { + if (!val) return ''; + return val.id; + }, + bottlesManyListFormatterEdit(val) { + if (!val || !val.length) return []; + return val.map((item) => { + return { id: item.id, label: item.id }; + }); + }, + bottlesOneListFormatterEdit(val) { + if (!val) return ''; + return { label: val.id, id: val.id }; + }, + + conversationsManyListFormatter(val) { + if (!val || !val.length) return []; + return val.map((item) => item.id); + }, + conversationsOneListFormatter(val) { + if (!val) return ''; + return val.id; + }, + conversationsManyListFormatterEdit(val) { + if (!val || !val.length) return []; + return val.map((item) => { + return { id: item.id, label: item.id }; + }); + }, + conversationsOneListFormatterEdit(val) { + if (!val) return ''; + return { label: val.id, id: val.id }; + }, }; diff --git a/frontend/src/menuAside.ts b/frontend/src/menuAside.ts index 69ad530..860a203 100644 --- a/frontend/src/menuAside.ts +++ b/frontend/src/menuAside.ts @@ -16,39 +16,6 @@ const menuAside: MenuAsideItem[] = [ icon: icon.mdiAccountGroup ?? icon.mdiTable, permissions: 'READ_USERS', }, - { - href: '/bottles/bottles-list', - label: 'Bottles', - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - icon: - 'mdiBottleWine' in icon - ? icon['mdiBottleWine' as keyof typeof icon] - : icon.mdiTable ?? icon.mdiTable, - permissions: 'READ_BOTTLES', - }, - { - href: '/brands/brands-list', - label: 'Brands', - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - icon: - 'mdiTrademark' in icon - ? icon['mdiTrademark' as keyof typeof icon] - : icon.mdiTable ?? icon.mdiTable, - permissions: 'READ_BRANDS', - }, - { - href: '/distilleries/distilleries-list', - label: 'Distilleries', - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - icon: - 'mdiFactory' in icon - ? icon['mdiFactory' as keyof typeof icon] - : icon.mdiTable ?? icon.mdiTable, - permissions: 'READ_DISTILLERIES', - }, { href: '/roles/roles-list', label: 'Roles', @@ -65,6 +32,86 @@ const menuAside: MenuAsideItem[] = [ icon: icon.mdiShieldAccountOutline ?? icon.mdiTable, permissions: 'READ_PERMISSIONS', }, + { + href: '/distilleries/distilleries-list', + label: 'Distilleries', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: icon.mdiTable ?? icon.mdiTable, + permissions: 'READ_DISTILLERIES', + }, + { + href: '/brands/brands-list', + label: 'Brands', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: icon.mdiTable ?? icon.mdiTable, + permissions: 'READ_BRANDS', + }, + { + href: '/photos/photos-list', + label: 'Photos', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: icon.mdiTable ?? icon.mdiTable, + permissions: 'READ_PHOTOS', + }, + { + href: '/products/products-list', + label: 'Products', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: icon.mdiTable ?? icon.mdiTable, + permissions: 'READ_PRODUCTS', + }, + { + href: '/locations/locations-list', + label: 'Locations', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: icon.mdiTable ?? icon.mdiTable, + permissions: 'READ_LOCATIONS', + }, + { + href: '/bottles/bottles-list', + label: 'Bottles', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: icon.mdiTable ?? icon.mdiTable, + permissions: 'READ_BOTTLES', + }, + { + href: '/reviews/reviews-list', + label: 'Reviews', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: icon.mdiTable ?? icon.mdiTable, + permissions: 'READ_REVIEWS', + }, + { + href: '/conversations/conversations-list', + label: 'Conversations', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: icon.mdiTable ?? icon.mdiTable, + permissions: 'READ_CONVERSATIONS', + }, + { + href: '/messages/messages-list', + label: 'Messages', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: icon.mdiTable ?? icon.mdiTable, + permissions: 'READ_MESSAGES', + }, + { + href: '/conversationparticipants/conversationparticipants-list', + label: 'Conversationparticipants', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: icon.mdiTable ?? icon.mdiTable, + permissions: 'READ_CONVERSATIONPARTICIPANTS', + }, { href: '/profile', label: 'Profile', diff --git a/frontend/src/pages/bottles/[bottlesId].tsx b/frontend/src/pages/bottles/[bottlesId].tsx index 0f0d404..8625fd6 100644 --- a/frontend/src/pages/bottles/[bottlesId].tsx +++ b/frontend/src/pages/bottles/[bottlesId].tsx @@ -36,35 +36,41 @@ const EditBottles = () => { const router = useRouter(); const dispatch = useAppDispatch(); const initVals = { - name: '', + user: null, - brand: null, + product: null, + + location: null, proof: '', - type: '', + age: '', + + rating: '', + + collectable: false, + + rickhouse: '', + + rack: '', + + release: '', + + barrelnumber: '', + + barreleddate: new Date(), + + bottlenumber: '', + + dateacquired: new Date(), + + volume: '', notes: '', - tasting_notes: '', + photofront: null, - msrp_range: '', - - secondary_value_range: '', - - opened_bottle_indicator: false, - - quantity: '', - - barcode: '', - - picture: [], - - age: '', - - distillery: null, - - user: null, + photoback: null, }; const [initialValues, setInitialValues] = useState(initVals); @@ -117,116 +123,6 @@ const EditBottles = () => { onSubmit={(values) => handleSubmit(values)} >
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - { > + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + setInitialValues({ ...initialValues, barreleddate: date }) + } + /> + + + + + + + + + setInitialValues({ ...initialValues, dateacquired: date }) + } + /> + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/pages/bottles/bottles-edit.tsx b/frontend/src/pages/bottles/bottles-edit.tsx index 525a738..c775a65 100644 --- a/frontend/src/pages/bottles/bottles-edit.tsx +++ b/frontend/src/pages/bottles/bottles-edit.tsx @@ -36,35 +36,41 @@ const EditBottlesPage = () => { const router = useRouter(); const dispatch = useAppDispatch(); const initVals = { - name: '', + user: null, - brand: null, + product: null, + + location: null, proof: '', - type: '', + age: '', + + rating: '', + + collectable: false, + + rickhouse: '', + + rack: '', + + release: '', + + barrelnumber: '', + + barreleddate: new Date(), + + bottlenumber: '', + + dateacquired: new Date(), + + volume: '', notes: '', - tasting_notes: '', + photofront: null, - msrp_range: '', - - secondary_value_range: '', - - opened_bottle_indicator: false, - - quantity: '', - - barcode: '', - - picture: [], - - age: '', - - distillery: null, - - user: null, + photoback: null, }; const [initialValues, setInitialValues] = useState(initVals); @@ -115,118 +121,6 @@ const EditBottlesPage = () => { onSubmit={(values) => handleSubmit(values)} > - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - { > + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + setInitialValues({ ...initialValues, barreleddate: date }) + } + /> + + + + + + + + + setInitialValues({ ...initialValues, dateacquired: date }) + } + /> + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/pages/bottles/bottles-list.tsx b/frontend/src/pages/bottles/bottles-list.tsx index 84b14cb..eb11596 100644 --- a/frontend/src/pages/bottles/bottles-list.tsx +++ b/frontend/src/pages/bottles/bottles-list.tsx @@ -29,28 +29,26 @@ const BottlesTablesPage = () => { const dispatch = useAppDispatch(); const [filters] = useState([ - { label: 'Name', title: 'name' }, + { label: 'Rickhouse', title: 'rickhouse' }, + { label: 'Rack', title: 'rack' }, + { label: 'Release', title: 'release' }, + { label: 'Barrelnumber', title: 'barrelnumber' }, + { label: 'Bottlenumber', title: 'bottlenumber' }, { label: 'Notes', title: 'notes' }, - { label: 'TastingNotes', title: 'tasting_notes' }, - { label: 'MSRPRange', title: 'msrp_range' }, - { label: 'SecondaryValueRange', title: 'secondary_value_range' }, - { label: 'Barcode', title: 'barcode' }, - { label: 'Quantity', title: 'quantity', number: 'true' }, - { label: 'Proof', title: 'proof', number: 'true' }, { label: 'Age', title: 'age', number: 'true' }, - - { label: 'Brand', title: 'brand' }, - - { label: 'Distillery', title: 'distillery' }, + { label: 'Rating', title: 'rating', number: 'true' }, + { label: 'Volume', title: 'volume', number: 'true' }, + { label: 'Proof', title: 'proof', number: 'true' }, { label: 'User', title: 'user' }, - { - label: 'Type', - title: 'type', - type: 'enum', - options: ['Bourbon', 'Scotch', 'Rye', 'Irish', 'Other'], - }, + { label: 'Product', title: 'product' }, + + { label: 'Location', title: 'location' }, + + { label: 'Photofront', title: 'photofront' }, + + { label: 'Photoback', title: 'photoback' }, ]); const hasCreatePermission = @@ -144,10 +142,6 @@ const BottlesTablesPage = () => {
- -
- Switch to Table -
diff --git a/frontend/src/pages/bottles/bottles-new.tsx b/frontend/src/pages/bottles/bottles-new.tsx index 177261a..b165623 100644 --- a/frontend/src/pages/bottles/bottles-new.tsx +++ b/frontend/src/pages/bottles/bottles-new.tsx @@ -33,35 +33,43 @@ import { useRouter } from 'next/router'; import moment from 'moment'; const initialValues = { - name: '', + user: '', - brand: '', + product: '', + + location: '', proof: '', - type: 'Bourbon', + age: '', + + rating: '', + + collectable: false, + + rickhouse: '', + + rack: '', + + release: '', + + barrelnumber: '', + + barreleddate: '', + dateBarreleddate: '', + + bottlenumber: '', + + dateacquired: '', + dateDateacquired: '', + + volume: '', notes: '', - tasting_notes: '', + photofront: '', - msrp_range: '', - - secondary_value_range: '', - - opened_bottle_indicator: false, - - quantity: '', - - barcode: '', - - picture: [], - - age: '', - - distillery: '', - - user: '', + photoback: '', }; const BottlesNew = () => { @@ -91,116 +99,6 @@ const BottlesNew = () => { onSubmit={(values) => handleSubmit(values)} > - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - { > + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/pages/bottles/bottles-table.tsx b/frontend/src/pages/bottles/bottles-table.tsx index 4d7f3cc..1d8d1a4 100644 --- a/frontend/src/pages/bottles/bottles-table.tsx +++ b/frontend/src/pages/bottles/bottles-table.tsx @@ -29,28 +29,26 @@ const BottlesTablesPage = () => { const dispatch = useAppDispatch(); const [filters] = useState([ - { label: 'Name', title: 'name' }, + { label: 'Rickhouse', title: 'rickhouse' }, + { label: 'Rack', title: 'rack' }, + { label: 'Release', title: 'release' }, + { label: 'Barrelnumber', title: 'barrelnumber' }, + { label: 'Bottlenumber', title: 'bottlenumber' }, { label: 'Notes', title: 'notes' }, - { label: 'TastingNotes', title: 'tasting_notes' }, - { label: 'MSRPRange', title: 'msrp_range' }, - { label: 'SecondaryValueRange', title: 'secondary_value_range' }, - { label: 'Barcode', title: 'barcode' }, - { label: 'Quantity', title: 'quantity', number: 'true' }, - { label: 'Proof', title: 'proof', number: 'true' }, { label: 'Age', title: 'age', number: 'true' }, - - { label: 'Brand', title: 'brand' }, - - { label: 'Distillery', title: 'distillery' }, + { label: 'Rating', title: 'rating', number: 'true' }, + { label: 'Volume', title: 'volume', number: 'true' }, + { label: 'Proof', title: 'proof', number: 'true' }, { label: 'User', title: 'user' }, - { - label: 'Type', - title: 'type', - type: 'enum', - options: ['Bourbon', 'Scotch', 'Rye', 'Irish', 'Other'], - }, + { label: 'Product', title: 'product' }, + + { label: 'Location', title: 'location' }, + + { label: 'Photofront', title: 'photofront' }, + + { label: 'Photoback', title: 'photoback' }, ]); const hasCreatePermission = @@ -143,10 +141,6 @@ const BottlesTablesPage = () => {
- - - Back to card -
diff --git a/frontend/src/pages/bottles/bottles-view.tsx b/frontend/src/pages/bottles/bottles-view.tsx index 57060b4..7ca1413 100644 --- a/frontend/src/pages/bottles/bottles-view.tsx +++ b/frontend/src/pages/bottles/bottles-view.tsx @@ -55,14 +55,21 @@ const BottlesView = () => {
-

Name

-

{bottles?.name}

+

User

+ +

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

-

Brand

+

Product

-

{bottles?.brand?.name ?? 'No data'}

+

{bottles?.product?.name ?? 'No data'}

+
+ +
+

Location

+ +

{bottles?.location?.name ?? 'No data'}

@@ -71,89 +78,153 @@ const BottlesView = () => {
-

Type

-

{bottles?.type ?? 'No data'}

+

Age

+

{bottles?.age || 'No data'}

-

Notes

- {bottles.notes ? ( -

- ) : ( -

No data

- )} +

Rating

+

{bottles?.rating || 'No data'}

-
-

TastingNotes

- {bottles.tasting_notes ? ( -

- ) : ( -

No data

- )} -
- -
-

MSRPRange

-

{bottles?.msrp_range}

-
- -
-

SecondaryValueRange

-

{bottles?.secondary_value_range}

-
- - + null }} disabled />
-

Quantity

-

{bottles?.quantity || 'No data'}

+

Rickhouse

+

{bottles?.rickhouse}

-

Barcode

-

{bottles?.barcode}

+

Rack

+

{bottles?.rack}

-

Picture

- {bottles?.picture?.length ? ( - Release

+

{bottles?.release}

+
+ +
+

Barrelnumber

+

{bottles?.barrelnumber}

+
+ + + {bottles.barreleddate ? ( + ) : ( -

No Picture

+

No Barreleddate

)} +
+ +
+

Bottlenumber

+

{bottles?.bottlenumber}

+
+ + + {bottles.dateacquired ? ( + + ) : ( +

No Dateacquired

+ )} +
+ +
+

Volume

+

{bottles?.volume || 'No data'}

-

Age

-

{bottles?.age || 'No data'}

+

Notes

+

{bottles?.notes}

-

Distillery

+

Photofront

-

{bottles?.distillery?.name ?? 'No data'}

+

{bottles?.photofront?.image ?? 'No data'}

-

User

+

Photoback

-

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

+

{bottles?.photoback?.image ?? 'No data'}

+ <> +

Reviews Bottle

+ +
+ + + + + + + + + + + + {bottles.reviews_bottle && + Array.isArray(bottles.reviews_bottle) && + bottles.reviews_bottle.map((item: any) => ( + + router.push(`/reviews/reviews-view/?id=${item.id}`) + } + > + + + + + + + ))} + +
RatingNotesCreatedat
{item.rating}{item.notes} + {dataFormatter.dateTimeFormatter(item.createdat)} +
+
+ {!bottles?.reviews_bottle?.length && ( +
No data
+ )} +
+ + { const initVals = { name: '', + status: '', + distillery: null, }; const [initialValues, setInitialValues] = useState(initVals); @@ -95,6 +97,10 @@ const EditBrands = () => {
+ + + + { const initVals = { name: '', + status: '', + distillery: null, }; const [initialValues, setInitialValues] = useState(initVals); @@ -93,6 +95,10 @@ const EditBrandsPage = () => { + + + + { const [filters] = useState([ { label: 'Name', title: 'name' }, + { label: 'Status', title: 'status', number: 'true' }, { label: 'Distillery', title: 'distillery' }, ]); @@ -125,10 +126,6 @@ const BrandsTablesPage = () => {
- -
- Switch to Table -
diff --git a/frontend/src/pages/brands/brands-new.tsx b/frontend/src/pages/brands/brands-new.tsx index 94a5a76..df1edec 100644 --- a/frontend/src/pages/brands/brands-new.tsx +++ b/frontend/src/pages/brands/brands-new.tsx @@ -35,6 +35,8 @@ import moment from 'moment'; const initialValues = { name: '', + status: '', + distillery: '', }; @@ -69,6 +71,10 @@ const BrandsNew = () => { + + + + { const [filters] = useState([ { label: 'Name', title: 'name' }, + { label: 'Status', title: 'status', number: 'true' }, { label: 'Distillery', title: 'distillery' }, ]); @@ -124,10 +125,6 @@ const BrandsTablesPage = () => {
- - - Back to list -
diff --git a/frontend/src/pages/brands/brands-view.tsx b/frontend/src/pages/brands/brands-view.tsx index a2b310b..af14276 100644 --- a/frontend/src/pages/brands/brands-view.tsx +++ b/frontend/src/pages/brands/brands-view.tsx @@ -59,6 +59,11 @@ const BrandsView = () => {

{brands?.name}

+
+

Status

+

{brands?.status || 'No data'}

+
+

Distillery

@@ -66,7 +71,7 @@ const BrandsView = () => {
<> -

Bottles Brand

+

Products Brand

{ Proof - Type - - MSRPRange - - SecondaryValueRange - - OpenedBottleIndicator - - Quantity + Age Barcode - Age + Notes + + Status - {brands.bottles_brand && - Array.isArray(brands.bottles_brand) && - brands.bottles_brand.map((item: any) => ( + {brands.products_brand && + Array.isArray(brands.products_brand) && + brands.products_brand.map((item: any) => ( - router.push(`/bottles/bottles-view/?id=${item.id}`) + router.push( + `/products/products-view/?id=${item.id}`, + ) } > {item.name} {item.proof} - {item.type} - - {item.msrp_range} - - - {item.secondary_value_range} - - - - {dataFormatter.booleanFormatter( - item.opened_bottle_indicator, - )} - - - {item.quantity} + {item.age} {item.barcode} - {item.age} + {item.notes} + + {item.status} ))} - {!brands?.bottles_brand?.length && ( + {!brands?.products_brand?.length && (
No data
)}
diff --git a/frontend/src/pages/conversationparticipants/[conversationparticipantsId].tsx b/frontend/src/pages/conversationparticipants/[conversationparticipantsId].tsx new file mode 100644 index 0000000..5ffb959 --- /dev/null +++ b/frontend/src/pages/conversationparticipants/[conversationparticipantsId].tsx @@ -0,0 +1,159 @@ +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/conversationparticipants/conversationparticipantsSlice'; +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 EditConversationparticipants = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + conversation: null, + + user: null, + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { conversationparticipants } = useAppSelector( + (state) => state.conversationparticipants, + ); + + const { conversationparticipantsId } = router.query; + + useEffect(() => { + dispatch(fetch({ id: conversationparticipantsId })); + }, [conversationparticipantsId]); + + useEffect(() => { + if (typeof conversationparticipants === 'object') { + setInitialValues(conversationparticipants); + } + }, [conversationparticipants]); + + useEffect(() => { + if (typeof conversationparticipants === 'object') { + const newInitialVal = { ...initVals }; + + Object.keys(initVals).forEach( + (el) => (newInitialVal[el] = conversationparticipants[el]), + ); + + setInitialValues(newInitialVal); + } + }, [conversationparticipants]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: conversationparticipantsId, data })); + await router.push( + '/conversationparticipants/conversationparticipants-list', + ); + }; + + return ( + <> + + {getPageTitle('Edit conversationparticipants')} + + + + {''} + + + handleSubmit(values)} + > + + + + + + + + + + + + + + + router.push( + '/conversationparticipants/conversationparticipants-list', + ) + } + /> + + + + + + + ); +}; + +EditConversationparticipants.getLayout = function getLayout( + page: ReactElement, +) { + return ( + + {page} + + ); +}; + +export default EditConversationparticipants; diff --git a/frontend/src/pages/conversationparticipants/conversationparticipants-edit.tsx b/frontend/src/pages/conversationparticipants/conversationparticipants-edit.tsx new file mode 100644 index 0000000..04865ff --- /dev/null +++ b/frontend/src/pages/conversationparticipants/conversationparticipants-edit.tsx @@ -0,0 +1,157 @@ +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/conversationparticipants/conversationparticipantsSlice'; +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 EditConversationparticipantsPage = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + conversation: null, + + user: null, + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { conversationparticipants } = useAppSelector( + (state) => state.conversationparticipants, + ); + + const { id } = router.query; + + useEffect(() => { + dispatch(fetch({ id: id })); + }, [id]); + + useEffect(() => { + if (typeof conversationparticipants === 'object') { + setInitialValues(conversationparticipants); + } + }, [conversationparticipants]); + + useEffect(() => { + if (typeof conversationparticipants === 'object') { + const newInitialVal = { ...initVals }; + Object.keys(initVals).forEach( + (el) => (newInitialVal[el] = conversationparticipants[el]), + ); + setInitialValues(newInitialVal); + } + }, [conversationparticipants]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: id, data })); + await router.push( + '/conversationparticipants/conversationparticipants-list', + ); + }; + + return ( + <> + + {getPageTitle('Edit conversationparticipants')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + + + + + + + + + + router.push( + '/conversationparticipants/conversationparticipants-list', + ) + } + /> + + +
+
+
+ + ); +}; + +EditConversationparticipantsPage.getLayout = function getLayout( + page: ReactElement, +) { + return ( + + {page} + + ); +}; + +export default EditConversationparticipantsPage; diff --git a/frontend/src/pages/conversationparticipants/conversationparticipants-list.tsx b/frontend/src/pages/conversationparticipants/conversationparticipants-list.tsx new file mode 100644 index 0000000..6a5262b --- /dev/null +++ b/frontend/src/pages/conversationparticipants/conversationparticipants-list.tsx @@ -0,0 +1,172 @@ +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 TableConversationparticipants from '../../components/Conversationparticipants/TableConversationparticipants'; +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/conversationparticipants/conversationparticipantsSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const ConversationparticipantsTablesPage = () => { + 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: 'Conversation', title: 'conversation' }, + + { label: 'User', title: 'user' }, + ]); + + const hasCreatePermission = + currentUser && + hasPermission(currentUser, 'CREATE_CONVERSATIONPARTICIPANTS'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getConversationparticipantsCSV = async () => { + const response = await axios({ + url: '/conversationparticipants?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 = 'conversationparticipantsCSV.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('Conversationparticipants')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
+
+
+
+ + + + +
+ + + + + ); +}; + +ConversationparticipantsTablesPage.getLayout = function getLayout( + page: ReactElement, +) { + return ( + + {page} + + ); +}; + +export default ConversationparticipantsTablesPage; diff --git a/frontend/src/pages/conversationparticipants/conversationparticipants-new.tsx b/frontend/src/pages/conversationparticipants/conversationparticipants-new.tsx new file mode 100644 index 0000000..89a7b54 --- /dev/null +++ b/frontend/src/pages/conversationparticipants/conversationparticipants-new.tsx @@ -0,0 +1,122 @@ +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/conversationparticipants/conversationparticipantsSlice'; +import { useAppDispatch } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import moment from 'moment'; + +const initialValues = { + conversation: '', + + user: '', +}; + +const ConversationparticipantsNew = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + + const handleSubmit = async (data) => { + await dispatch(create(data)); + await router.push( + '/conversationparticipants/conversationparticipants-list', + ); + }; + return ( + <> + + {getPageTitle('New Item')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + + + + + + + + + + router.push( + '/conversationparticipants/conversationparticipants-list', + ) + } + /> + + +
+
+
+ + ); +}; + +ConversationparticipantsNew.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default ConversationparticipantsNew; diff --git a/frontend/src/pages/conversationparticipants/conversationparticipants-table.tsx b/frontend/src/pages/conversationparticipants/conversationparticipants-table.tsx new file mode 100644 index 0000000..591c259 --- /dev/null +++ b/frontend/src/pages/conversationparticipants/conversationparticipants-table.tsx @@ -0,0 +1,171 @@ +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 TableConversationparticipants from '../../components/Conversationparticipants/TableConversationparticipants'; +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/conversationparticipants/conversationparticipantsSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const ConversationparticipantsTablesPage = () => { + 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: 'Conversation', title: 'conversation' }, + + { label: 'User', title: 'user' }, + ]); + + const hasCreatePermission = + currentUser && + hasPermission(currentUser, 'CREATE_CONVERSATIONPARTICIPANTS'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getConversationparticipantsCSV = async () => { + const response = await axios({ + url: '/conversationparticipants?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 = 'conversationparticipantsCSV.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('Conversationparticipants')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
+
+
+
+ + + +
+ + + + + ); +}; + +ConversationparticipantsTablesPage.getLayout = function getLayout( + page: ReactElement, +) { + return ( + + {page} + + ); +}; + +export default ConversationparticipantsTablesPage; diff --git a/frontend/src/pages/conversationparticipants/conversationparticipants-view.tsx b/frontend/src/pages/conversationparticipants/conversationparticipants-view.tsx new file mode 100644 index 0000000..bab5c9c --- /dev/null +++ b/frontend/src/pages/conversationparticipants/conversationparticipants-view.tsx @@ -0,0 +1,98 @@ +import React, { ReactElement, useEffect } from 'react'; +import Head from 'next/head'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { fetch } from '../../stores/conversationparticipants/conversationparticipantsSlice'; +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 ConversationparticipantsView = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const { conversationparticipants } = useAppSelector( + (state) => state.conversationparticipants, + ); + + 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 conversationparticipants')} + + + + + + +
+

Conversation

+ +

{conversationparticipants?.conversation?.id ?? 'No data'}

+
+ +
+

User

+ +

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

+
+ + + + + router.push( + '/conversationparticipants/conversationparticipants-list', + ) + } + /> +
+
+ + ); +}; + +ConversationparticipantsView.getLayout = function getLayout( + page: ReactElement, +) { + return ( + + {page} + + ); +}; + +export default ConversationparticipantsView; diff --git a/frontend/src/pages/conversations/[conversationsId].tsx b/frontend/src/pages/conversations/[conversationsId].tsx new file mode 100644 index 0000000..a0752d9 --- /dev/null +++ b/frontend/src/pages/conversations/[conversationsId].tsx @@ -0,0 +1,143 @@ +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/conversations/conversationsSlice'; +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 EditConversations = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + createdat: new Date(), + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { conversations } = useAppSelector((state) => state.conversations); + + const { conversationsId } = router.query; + + useEffect(() => { + dispatch(fetch({ id: conversationsId })); + }, [conversationsId]); + + useEffect(() => { + if (typeof conversations === 'object') { + setInitialValues(conversations); + } + }, [conversations]); + + useEffect(() => { + if (typeof conversations === 'object') { + const newInitialVal = { ...initVals }; + + Object.keys(initVals).forEach( + (el) => (newInitialVal[el] = conversations[el]), + ); + + setInitialValues(newInitialVal); + } + }, [conversations]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: conversationsId, data })); + await router.push('/conversations/conversations-list'); + }; + + return ( + <> + + {getPageTitle('Edit conversations')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + setInitialValues({ ...initialValues, createdat: date }) + } + /> + + + + + + + + router.push('/conversations/conversations-list') + } + /> + + +
+
+
+ + ); +}; + +EditConversations.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditConversations; diff --git a/frontend/src/pages/conversations/conversations-edit.tsx b/frontend/src/pages/conversations/conversations-edit.tsx new file mode 100644 index 0000000..90fc1a2 --- /dev/null +++ b/frontend/src/pages/conversations/conversations-edit.tsx @@ -0,0 +1,141 @@ +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/conversations/conversationsSlice'; +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 EditConversationsPage = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + createdat: new Date(), + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { conversations } = useAppSelector((state) => state.conversations); + + const { id } = router.query; + + useEffect(() => { + dispatch(fetch({ id: id })); + }, [id]); + + useEffect(() => { + if (typeof conversations === 'object') { + setInitialValues(conversations); + } + }, [conversations]); + + useEffect(() => { + if (typeof conversations === 'object') { + const newInitialVal = { ...initVals }; + Object.keys(initVals).forEach( + (el) => (newInitialVal[el] = conversations[el]), + ); + setInitialValues(newInitialVal); + } + }, [conversations]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: id, data })); + await router.push('/conversations/conversations-list'); + }; + + return ( + <> + + {getPageTitle('Edit conversations')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + setInitialValues({ ...initialValues, createdat: date }) + } + /> + + + + + + + + router.push('/conversations/conversations-list') + } + /> + + +
+
+
+ + ); +}; + +EditConversationsPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditConversationsPage; diff --git a/frontend/src/pages/conversations/conversations-list.tsx b/frontend/src/pages/conversations/conversations-list.tsx new file mode 100644 index 0000000..90a555d --- /dev/null +++ b/frontend/src/pages/conversations/conversations-list.tsx @@ -0,0 +1,167 @@ +import { mdiChartTimelineVariant } from '@mdi/js'; +import Head from 'next/head'; +import { uniqueId } from 'lodash'; +import React, { ReactElement, useState } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; +import TableConversations from '../../components/Conversations/TableConversations'; +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/conversations/conversationsSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const ConversationsTablesPage = () => { + 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: 'Createdat', title: 'createdat', date: 'true' }, + ]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_CONVERSATIONS'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getConversationsCSV = async () => { + const response = await axios({ + url: '/conversations?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 = 'conversationsCSV.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('Conversations')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
+
+
+
+ + + + +
+ + + + + ); +}; + +ConversationsTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default ConversationsTablesPage; diff --git a/frontend/src/pages/conversations/conversations-new.tsx b/frontend/src/pages/conversations/conversations-new.tsx new file mode 100644 index 0000000..f19171e --- /dev/null +++ b/frontend/src/pages/conversations/conversations-new.tsx @@ -0,0 +1,104 @@ +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/conversations/conversationsSlice'; +import { useAppDispatch } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import moment from 'moment'; + +const initialValues = { + createdat: '', +}; + +const ConversationsNew = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + + const handleSubmit = async (data) => { + await dispatch(create(data)); + await router.push('/conversations/conversations-list'); + }; + return ( + <> + + {getPageTitle('New Item')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + + + + + + router.push('/conversations/conversations-list') + } + /> + + +
+
+
+ + ); +}; + +ConversationsNew.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default ConversationsNew; diff --git a/frontend/src/pages/conversations/conversations-table.tsx b/frontend/src/pages/conversations/conversations-table.tsx new file mode 100644 index 0000000..775583c --- /dev/null +++ b/frontend/src/pages/conversations/conversations-table.tsx @@ -0,0 +1,166 @@ +import { mdiChartTimelineVariant } from '@mdi/js'; +import Head from 'next/head'; +import { uniqueId } from 'lodash'; +import React, { ReactElement, useState } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; +import TableConversations from '../../components/Conversations/TableConversations'; +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/conversations/conversationsSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const ConversationsTablesPage = () => { + 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: 'Createdat', title: 'createdat', date: 'true' }, + ]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_CONVERSATIONS'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getConversationsCSV = async () => { + const response = await axios({ + url: '/conversations?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 = 'conversationsCSV.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('Conversations')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
+
+
+
+ + + +
+ + + + + ); +}; + +ConversationsTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default ConversationsTablesPage; diff --git a/frontend/src/pages/conversations/conversations-view.tsx b/frontend/src/pages/conversations/conversations-view.tsx new file mode 100644 index 0000000..d552dd6 --- /dev/null +++ b/frontend/src/pages/conversations/conversations-view.tsx @@ -0,0 +1,170 @@ +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/conversations/conversationsSlice'; +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 ConversationsView = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const { conversations } = useAppSelector((state) => state.conversations); + + 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 conversations')} + + + + + + + + {conversations.createdat ? ( + + ) : ( +

No Createdat

+ )} +
+ + <> +

Messages Conversation

+ +
+ + + + + + {conversations.messages_conversation && + Array.isArray(conversations.messages_conversation) && + conversations.messages_conversation.map((item: any) => ( + + router.push( + `/messages/messages-view/?id=${item.id}`, + ) + } + > + ))} + +
+
+ {!conversations?.messages_conversation?.length && ( +
No data
+ )} +
+ + + <> +

+ Conversationparticipants Conversation +

+ +
+ + + + + + {conversations.conversationparticipants_conversation && + Array.isArray( + conversations.conversationparticipants_conversation, + ) && + conversations.conversationparticipants_conversation.map( + (item: any) => ( + + router.push( + `/conversationparticipants/conversationparticipants-view/?id=${item.id}`, + ) + } + > + ), + )} + +
+
+ {!conversations?.conversationparticipants_conversation + ?.length &&
No data
} +
+ + + + + router.push('/conversations/conversations-list')} + /> +
+
+ + ); +}; + +ConversationsView.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default ConversationsView; diff --git a/frontend/src/pages/dashboard.tsx b/frontend/src/pages/dashboard.tsx index 67b3a64..74d45c1 100644 --- a/frontend/src/pages/dashboard.tsx +++ b/frontend/src/pages/dashboard.tsx @@ -29,11 +29,19 @@ const Dashboard = () => { }); const [users, setUsers] = React.useState(loadingMessage); - const [bottles, setBottles] = React.useState(loadingMessage); - const [brands, setBrands] = React.useState(loadingMessage); - const [distilleries, setDistilleries] = React.useState(loadingMessage); const [roles, setRoles] = React.useState(loadingMessage); const [permissions, setPermissions] = React.useState(loadingMessage); + const [distilleries, setDistilleries] = React.useState(loadingMessage); + const [brands, setBrands] = React.useState(loadingMessage); + const [photos, setPhotos] = React.useState(loadingMessage); + const [products, setProducts] = React.useState(loadingMessage); + const [locations, setLocations] = React.useState(loadingMessage); + const [bottles, setBottles] = React.useState(loadingMessage); + const [reviews, setReviews] = React.useState(loadingMessage); + const [conversations, setConversations] = React.useState(loadingMessage); + const [messages, setMessages] = React.useState(loadingMessage); + const [conversationparticipants, setConversationparticipants] = + React.useState(loadingMessage); const [widgetsRole, setWidgetsRole] = React.useState({ role: { value: '', label: '' }, @@ -46,19 +54,33 @@ const Dashboard = () => { async function loadData() { const entities = [ 'users', - 'bottles', - 'brands', - 'distilleries', 'roles', 'permissions', + 'distilleries', + 'brands', + 'photos', + 'products', + 'locations', + 'bottles', + 'reviews', + 'conversations', + 'messages', + 'conversationparticipants', ]; const fns = [ setUsers, - setBottles, - setBrands, - setDistilleries, setRoles, setPermissions, + setDistilleries, + setBrands, + setPhotos, + setProducts, + setLocations, + setBottles, + setReviews, + setConversations, + setMessages, + setConversationparticipants, ]; const requests = entities.map((entity, index) => { @@ -203,114 +225,6 @@ const Dashboard = () => { )} - {hasPermission(currentUser, 'READ_BOTTLES') && ( - -
-
-
-
- Bottles -
-
- {bottles} -
-
-
- -
-
-
- - )} - - {hasPermission(currentUser, 'READ_BRANDS') && ( - -
-
-
-
- Brands -
-
- {brands} -
-
-
- -
-
-
- - )} - - {hasPermission(currentUser, 'READ_DISTILLERIES') && ( - -
-
-
-
- Distilleries -
-
- {distilleries} -
-
-
- -
-
-
- - )} - {hasPermission(currentUser, 'READ_ROLES') && (
{
)} + + {hasPermission(currentUser, 'READ_DISTILLERIES') && ( + +
+
+
+
+ Distilleries +
+
+ {distilleries} +
+
+
+ +
+
+
+ + )} + + {hasPermission(currentUser, 'READ_BRANDS') && ( + +
+
+
+
+ Brands +
+
+ {brands} +
+
+
+ +
+
+
+ + )} + + {hasPermission(currentUser, 'READ_PHOTOS') && ( + +
+
+
+
+ Photos +
+
+ {photos} +
+
+
+ +
+
+
+ + )} + + {hasPermission(currentUser, 'READ_PRODUCTS') && ( + +
+
+
+
+ Products +
+
+ {products} +
+
+
+ +
+
+
+ + )} + + {hasPermission(currentUser, 'READ_LOCATIONS') && ( + +
+
+
+
+ Locations +
+
+ {locations} +
+
+
+ +
+
+
+ + )} + + {hasPermission(currentUser, 'READ_BOTTLES') && ( + +
+
+
+
+ Bottles +
+
+ {bottles} +
+
+
+ +
+
+
+ + )} + + {hasPermission(currentUser, 'READ_REVIEWS') && ( + +
+
+
+
+ Reviews +
+
+ {reviews} +
+
+
+ +
+
+
+ + )} + + {hasPermission(currentUser, 'READ_CONVERSATIONS') && ( + +
+
+
+
+ Conversations +
+
+ {conversations} +
+
+
+ +
+
+
+ + )} + + {hasPermission(currentUser, 'READ_MESSAGES') && ( + +
+
+
+
+ Messages +
+
+ {messages} +
+
+
+ +
+
+
+ + )} + + {hasPermission(currentUser, 'READ_CONVERSATIONPARTICIPANTS') && ( + +
+
+
+
+ Conversationparticipants +
+
+ {conversationparticipants} +
+
+
+ +
+
+
+ + )} diff --git a/frontend/src/pages/distilleries/[distilleriesId].tsx b/frontend/src/pages/distilleries/[distilleriesId].tsx index 0152bc8..116efb3 100644 --- a/frontend/src/pages/distilleries/[distilleriesId].tsx +++ b/frontend/src/pages/distilleries/[distilleriesId].tsx @@ -37,6 +37,12 @@ const EditDistilleries = () => { const dispatch = useAppDispatch(); const initVals = { name: '', + + city: '', + + state: '', + + status: '', }; const [initialValues, setInitialValues] = useState(initVals); @@ -95,6 +101,18 @@ const EditDistilleries = () => { + + + + + + + + + + + + diff --git a/frontend/src/pages/distilleries/distilleries-edit.tsx b/frontend/src/pages/distilleries/distilleries-edit.tsx index 1eb92bf..3d041f0 100644 --- a/frontend/src/pages/distilleries/distilleries-edit.tsx +++ b/frontend/src/pages/distilleries/distilleries-edit.tsx @@ -37,6 +37,12 @@ const EditDistilleriesPage = () => { const dispatch = useAppDispatch(); const initVals = { name: '', + + city: '', + + state: '', + + status: '', }; const [initialValues, setInitialValues] = useState(initVals); @@ -93,6 +99,18 @@ const EditDistilleriesPage = () => { + + + + + + + + + + + + diff --git a/frontend/src/pages/distilleries/distilleries-list.tsx b/frontend/src/pages/distilleries/distilleries-list.tsx index ec7a601..6cfee79 100644 --- a/frontend/src/pages/distilleries/distilleries-list.tsx +++ b/frontend/src/pages/distilleries/distilleries-list.tsx @@ -31,7 +31,12 @@ const DistilleriesTablesPage = () => { const dispatch = useAppDispatch(); - const [filters] = useState([{ label: 'Name', title: 'name' }]); + const [filters] = useState([ + { label: 'Name', title: 'name' }, + { label: 'City', title: 'city' }, + { label: 'State', title: 'state' }, + { label: 'Status', title: 'status', number: 'true' }, + ]); const hasCreatePermission = currentUser && hasPermission(currentUser, 'CREATE_DISTILLERIES'); @@ -124,12 +129,6 @@ const DistilleriesTablesPage = () => {
- -
- - Switch to Table - -
diff --git a/frontend/src/pages/distilleries/distilleries-new.tsx b/frontend/src/pages/distilleries/distilleries-new.tsx index 4abfa67..4978925 100644 --- a/frontend/src/pages/distilleries/distilleries-new.tsx +++ b/frontend/src/pages/distilleries/distilleries-new.tsx @@ -34,6 +34,12 @@ import moment from 'moment'; const initialValues = { name: '', + + city: '', + + state: '', + + status: '', }; const DistilleriesNew = () => { @@ -67,6 +73,18 @@ const DistilleriesNew = () => { + + + + + + + + + + + + diff --git a/frontend/src/pages/distilleries/distilleries-table.tsx b/frontend/src/pages/distilleries/distilleries-table.tsx index fa90493..5d1abff 100644 --- a/frontend/src/pages/distilleries/distilleries-table.tsx +++ b/frontend/src/pages/distilleries/distilleries-table.tsx @@ -31,7 +31,12 @@ const DistilleriesTablesPage = () => { const dispatch = useAppDispatch(); - const [filters] = useState([{ label: 'Name', title: 'name' }]); + const [filters] = useState([ + { label: 'Name', title: 'name' }, + { label: 'City', title: 'city' }, + { label: 'State', title: 'state' }, + { label: 'Status', title: 'status', number: 'true' }, + ]); const hasCreatePermission = currentUser && hasPermission(currentUser, 'CREATE_DISTILLERIES'); @@ -123,10 +128,6 @@ const DistilleriesTablesPage = () => {
- - - Back to list -
diff --git a/frontend/src/pages/distilleries/distilleries-view.tsx b/frontend/src/pages/distilleries/distilleries-view.tsx index 9c95805..4cd674d 100644 --- a/frontend/src/pages/distilleries/distilleries-view.tsx +++ b/frontend/src/pages/distilleries/distilleries-view.tsx @@ -59,78 +59,20 @@ const DistilleriesView = () => {

{distilleries?.name}

- <> -

Bottles Distillery

- -
- - - - +
+

City

+

{distilleries?.city}

+
- +
+

State

+

{distilleries?.state}

+
- - - - - - - - - - - - - - - - - {distilleries.bottles_distillery && - Array.isArray(distilleries.bottles_distillery) && - distilleries.bottles_distillery.map((item: any) => ( - - router.push(`/bottles/bottles-view/?id=${item.id}`) - } - > - - - - - - - - - - - - - - - - - - - ))} - -
NameProofTypeMSRPRangeSecondaryValueRangeOpenedBottleIndicatorQuantityBarcodeAge
{item.name}{item.proof}{item.type}{item.msrp_range} - {item.secondary_value_range} - - {dataFormatter.booleanFormatter( - item.opened_bottle_indicator, - )} - {item.quantity}{item.barcode}{item.age}
-
- {!distilleries?.bottles_distillery?.length && ( -
No data
- )} -
- +
+

Status

+

{distilleries?.status || 'No data'}

+
<>

Brands Distillery

@@ -143,6 +85,8 @@ const DistilleriesView = () => { Name + + Status @@ -156,6 +100,8 @@ const DistilleriesView = () => { } > {item.name} + + {item.status} ))} diff --git a/frontend/src/pages/locations/[locationsId].tsx b/frontend/src/pages/locations/[locationsId].tsx new file mode 100644 index 0000000..7a78761 --- /dev/null +++ b/frontend/src/pages/locations/[locationsId].tsx @@ -0,0 +1,139 @@ +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/locations/locationsSlice'; +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 EditLocations = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + name: '', + + user: null, + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { locations } = useAppSelector((state) => state.locations); + + const { locationsId } = router.query; + + useEffect(() => { + dispatch(fetch({ id: locationsId })); + }, [locationsId]); + + useEffect(() => { + if (typeof locations === 'object') { + setInitialValues(locations); + } + }, [locations]); + + useEffect(() => { + if (typeof locations === 'object') { + const newInitialVal = { ...initVals }; + + Object.keys(initVals).forEach( + (el) => (newInitialVal[el] = locations[el]), + ); + + setInitialValues(newInitialVal); + } + }, [locations]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: locationsId, data })); + await router.push('/locations/locations-list'); + }; + + return ( + <> + + {getPageTitle('Edit locations')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + + + + + + + + + router.push('/locations/locations-list')} + /> + + +
+
+
+ + ); +}; + +EditLocations.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditLocations; diff --git a/frontend/src/pages/locations/locations-edit.tsx b/frontend/src/pages/locations/locations-edit.tsx new file mode 100644 index 0000000..c4f73d1 --- /dev/null +++ b/frontend/src/pages/locations/locations-edit.tsx @@ -0,0 +1,137 @@ +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/locations/locationsSlice'; +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 EditLocationsPage = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + name: '', + + user: null, + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { locations } = useAppSelector((state) => state.locations); + + const { id } = router.query; + + useEffect(() => { + dispatch(fetch({ id: id })); + }, [id]); + + useEffect(() => { + if (typeof locations === 'object') { + setInitialValues(locations); + } + }, [locations]); + + useEffect(() => { + if (typeof locations === 'object') { + const newInitialVal = { ...initVals }; + Object.keys(initVals).forEach( + (el) => (newInitialVal[el] = locations[el]), + ); + setInitialValues(newInitialVal); + } + }, [locations]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: id, data })); + await router.push('/locations/locations-list'); + }; + + return ( + <> + + {getPageTitle('Edit locations')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + + + + + + + + + router.push('/locations/locations-list')} + /> + + +
+
+
+ + ); +}; + +EditLocationsPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditLocationsPage; diff --git a/frontend/src/pages/locations/locations-list.tsx b/frontend/src/pages/locations/locations-list.tsx new file mode 100644 index 0000000..17bca2a --- /dev/null +++ b/frontend/src/pages/locations/locations-list.tsx @@ -0,0 +1,166 @@ +import { mdiChartTimelineVariant } from '@mdi/js'; +import Head from 'next/head'; +import { uniqueId } from 'lodash'; +import React, { ReactElement, useState } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; +import TableLocations from '../../components/Locations/TableLocations'; +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/locations/locationsSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const LocationsTablesPage = () => { + const [filterItems, setFilterItems] = useState([]); + const [csvFile, setCsvFile] = useState(null); + const [isModalActive, setIsModalActive] = useState(false); + const [showTableView, setShowTableView] = useState(false); + + const { currentUser } = useAppSelector((state) => state.auth); + + const dispatch = useAppDispatch(); + + const [filters] = useState([ + { label: 'Name', title: 'name' }, + + { label: 'User', title: 'user' }, + ]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_LOCATIONS'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getLocationsCSV = async () => { + const response = await axios({ + url: '/locations?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 = 'locationsCSV.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('Locations')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
+
+
+
+ + + + +
+ + + + + ); +}; + +LocationsTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default LocationsTablesPage; diff --git a/frontend/src/pages/locations/locations-new.tsx b/frontend/src/pages/locations/locations-new.tsx new file mode 100644 index 0000000..0313330 --- /dev/null +++ b/frontend/src/pages/locations/locations-new.tsx @@ -0,0 +1,110 @@ +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/locations/locationsSlice'; +import { useAppDispatch } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import moment from 'moment'; + +const initialValues = { + name: '', + + user: '', +}; + +const LocationsNew = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + + const handleSubmit = async (data) => { + await dispatch(create(data)); + await router.push('/locations/locations-list'); + }; + return ( + <> + + {getPageTitle('New Item')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + + + + + + + + + router.push('/locations/locations-list')} + /> + + +
+
+
+ + ); +}; + +LocationsNew.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default LocationsNew; diff --git a/frontend/src/pages/locations/locations-table.tsx b/frontend/src/pages/locations/locations-table.tsx new file mode 100644 index 0000000..1a76ef7 --- /dev/null +++ b/frontend/src/pages/locations/locations-table.tsx @@ -0,0 +1,165 @@ +import { mdiChartTimelineVariant } from '@mdi/js'; +import Head from 'next/head'; +import { uniqueId } from 'lodash'; +import React, { ReactElement, useState } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; +import TableLocations from '../../components/Locations/TableLocations'; +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/locations/locationsSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const LocationsTablesPage = () => { + const [filterItems, setFilterItems] = useState([]); + const [csvFile, setCsvFile] = useState(null); + const [isModalActive, setIsModalActive] = useState(false); + const [showTableView, setShowTableView] = useState(false); + + const { currentUser } = useAppSelector((state) => state.auth); + + const dispatch = useAppDispatch(); + + const [filters] = useState([ + { label: 'Name', title: 'name' }, + + { label: 'User', title: 'user' }, + ]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_LOCATIONS'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getLocationsCSV = async () => { + const response = await axios({ + url: '/locations?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 = 'locationsCSV.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('Locations')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
+
+
+
+ + + +
+ + + + + ); +}; + +LocationsTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default LocationsTablesPage; diff --git a/frontend/src/pages/locations/locations-view.tsx b/frontend/src/pages/locations/locations-view.tsx new file mode 100644 index 0000000..4d0fb6c --- /dev/null +++ b/frontend/src/pages/locations/locations-view.tsx @@ -0,0 +1,178 @@ +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/locations/locationsSlice'; +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 LocationsView = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const { locations } = useAppSelector((state) => state.locations); + + 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 locations')} + + + + + + +
+

Name

+

{locations?.name}

+
+ +
+

User

+ +

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

+
+ + <> +

Bottles Location

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {locations.bottles_location && + Array.isArray(locations.bottles_location) && + locations.bottles_location.map((item: any) => ( + + router.push(`/bottles/bottles-view/?id=${item.id}`) + } + > + + + + + + + + + + + + + + + + + + + + + + + + + + + ))} + +
ProofAgeRatingCollectableRickhouseRackReleaseBarrelnumberBarreleddateBottlenumberDateacquiredVolumeNotes
{item.proof}{item.age}{item.rating} + {dataFormatter.booleanFormatter(item.collectable)} + {item.rickhouse}{item.rack}{item.release}{item.barrelnumber} + {dataFormatter.dateFormatter(item.barreleddate)} + {item.bottlenumber} + {dataFormatter.dateFormatter(item.dateacquired)} + {item.volume}{item.notes}
+
+ {!locations?.bottles_location?.length && ( +
No data
+ )} +
+ + + + + router.push('/locations/locations-list')} + /> +
+
+ + ); +}; + +LocationsView.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default LocationsView; diff --git a/frontend/src/pages/messages/[messagesId].tsx b/frontend/src/pages/messages/[messagesId].tsx new file mode 100644 index 0000000..50be4de --- /dev/null +++ b/frontend/src/pages/messages/[messagesId].tsx @@ -0,0 +1,144 @@ +import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement, useEffect, useState } from 'react'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; + +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { SwitchField } from '../../components/SwitchField'; +import { RichTextField } from '../../components/RichTextField'; + +import { update, fetch } from '../../stores/messages/messagesSlice'; +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 EditMessages = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + conversation: null, + + sender: null, + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { messages } = useAppSelector((state) => state.messages); + + const { messagesId } = router.query; + + useEffect(() => { + dispatch(fetch({ id: messagesId })); + }, [messagesId]); + + useEffect(() => { + if (typeof messages === 'object') { + setInitialValues(messages); + } + }, [messages]); + + useEffect(() => { + if (typeof messages === 'object') { + const newInitialVal = { ...initVals }; + + Object.keys(initVals).forEach((el) => (newInitialVal[el] = messages[el])); + + setInitialValues(newInitialVal); + } + }, [messages]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: messagesId, data })); + await router.push('/messages/messages-list'); + }; + + return ( + <> + + {getPageTitle('Edit messages')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + + + + + + + + + router.push('/messages/messages-list')} + /> + + +
+
+
+ + ); +}; + +EditMessages.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditMessages; diff --git a/frontend/src/pages/messages/messages-edit.tsx b/frontend/src/pages/messages/messages-edit.tsx new file mode 100644 index 0000000..c3ff1ee --- /dev/null +++ b/frontend/src/pages/messages/messages-edit.tsx @@ -0,0 +1,142 @@ +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/messages/messagesSlice'; +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 EditMessagesPage = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + conversation: null, + + sender: null, + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { messages } = useAppSelector((state) => state.messages); + + const { id } = router.query; + + useEffect(() => { + dispatch(fetch({ id: id })); + }, [id]); + + useEffect(() => { + if (typeof messages === 'object') { + setInitialValues(messages); + } + }, [messages]); + + useEffect(() => { + if (typeof messages === 'object') { + const newInitialVal = { ...initVals }; + Object.keys(initVals).forEach((el) => (newInitialVal[el] = messages[el])); + setInitialValues(newInitialVal); + } + }, [messages]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: id, data })); + await router.push('/messages/messages-list'); + }; + + return ( + <> + + {getPageTitle('Edit messages')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + + + + + + + + + router.push('/messages/messages-list')} + /> + + +
+
+
+ + ); +}; + +EditMessagesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditMessagesPage; diff --git a/frontend/src/pages/messages/messages-list.tsx b/frontend/src/pages/messages/messages-list.tsx new file mode 100644 index 0000000..1603b4e --- /dev/null +++ b/frontend/src/pages/messages/messages-list.tsx @@ -0,0 +1,166 @@ +import { mdiChartTimelineVariant } from '@mdi/js'; +import Head from 'next/head'; +import { uniqueId } from 'lodash'; +import React, { ReactElement, useState } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; +import TableMessages from '../../components/Messages/TableMessages'; +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/messages/messagesSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const MessagesTablesPage = () => { + 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: 'Conversation', title: 'conversation' }, + + { label: 'Sender', title: 'sender' }, + ]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_MESSAGES'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getMessagesCSV = async () => { + const response = await axios({ + url: '/messages?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 = 'messagesCSV.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('Messages')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
+
+
+
+ + + + +
+ + + + + ); +}; + +MessagesTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default MessagesTablesPage; diff --git a/frontend/src/pages/messages/messages-new.tsx b/frontend/src/pages/messages/messages-new.tsx new file mode 100644 index 0000000..a49a2de --- /dev/null +++ b/frontend/src/pages/messages/messages-new.tsx @@ -0,0 +1,116 @@ +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/messages/messagesSlice'; +import { useAppDispatch } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import moment from 'moment'; + +const initialValues = { + conversation: '', + + sender: '', +}; + +const MessagesNew = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + + const handleSubmit = async (data) => { + await dispatch(create(data)); + await router.push('/messages/messages-list'); + }; + return ( + <> + + {getPageTitle('New Item')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + + + + + + + + + router.push('/messages/messages-list')} + /> + + +
+
+
+ + ); +}; + +MessagesNew.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default MessagesNew; diff --git a/frontend/src/pages/messages/messages-table.tsx b/frontend/src/pages/messages/messages-table.tsx new file mode 100644 index 0000000..bc7a43e --- /dev/null +++ b/frontend/src/pages/messages/messages-table.tsx @@ -0,0 +1,165 @@ +import { mdiChartTimelineVariant } from '@mdi/js'; +import Head from 'next/head'; +import { uniqueId } from 'lodash'; +import React, { ReactElement, useState } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; +import TableMessages from '../../components/Messages/TableMessages'; +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/messages/messagesSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const MessagesTablesPage = () => { + 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: 'Conversation', title: 'conversation' }, + + { label: 'Sender', title: 'sender' }, + ]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_MESSAGES'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getMessagesCSV = async () => { + const response = await axios({ + url: '/messages?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 = 'messagesCSV.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('Messages')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
+
+
+
+ + + +
+ + + + + ); +}; + +MessagesTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default MessagesTablesPage; diff --git a/frontend/src/pages/messages/messages-view.tsx b/frontend/src/pages/messages/messages-view.tsx new file mode 100644 index 0000000..1fbb378 --- /dev/null +++ b/frontend/src/pages/messages/messages-view.tsx @@ -0,0 +1,90 @@ +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/messages/messagesSlice'; +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 MessagesView = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const { messages } = useAppSelector((state) => state.messages); + + 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 messages')} + + + + + + +
+

Conversation

+ +

{messages?.conversation?.id ?? 'No data'}

+
+ +
+

Sender

+ +

{messages?.sender?.firstName ?? 'No data'}

+
+ + + + router.push('/messages/messages-list')} + /> +
+
+ + ); +}; + +MessagesView.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default MessagesView; diff --git a/frontend/src/pages/photos/[photosId].tsx b/frontend/src/pages/photos/[photosId].tsx new file mode 100644 index 0000000..95c0f1f --- /dev/null +++ b/frontend/src/pages/photos/[photosId].tsx @@ -0,0 +1,142 @@ +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/photos/photosSlice'; +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 EditPhotos = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + phototype: '', + + image: [], + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { photos } = useAppSelector((state) => state.photos); + + const { photosId } = router.query; + + useEffect(() => { + dispatch(fetch({ id: photosId })); + }, [photosId]); + + useEffect(() => { + if (typeof photos === 'object') { + setInitialValues(photos); + } + }, [photos]); + + useEffect(() => { + if (typeof photos === 'object') { + const newInitialVal = { ...initVals }; + + Object.keys(initVals).forEach((el) => (newInitialVal[el] = photos[el])); + + setInitialValues(newInitialVal); + } + }, [photos]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: photosId, data })); + await router.push('/photos/photos-list'); + }; + + return ( + <> + + {getPageTitle('Edit photos')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + + + + + + + + + router.push('/photos/photos-list')} + /> + + +
+
+
+ + ); +}; + +EditPhotos.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditPhotos; diff --git a/frontend/src/pages/photos/photos-edit.tsx b/frontend/src/pages/photos/photos-edit.tsx new file mode 100644 index 0000000..8513484 --- /dev/null +++ b/frontend/src/pages/photos/photos-edit.tsx @@ -0,0 +1,140 @@ +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/photos/photosSlice'; +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 EditPhotosPage = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + phototype: '', + + image: [], + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { photos } = useAppSelector((state) => state.photos); + + const { id } = router.query; + + useEffect(() => { + dispatch(fetch({ id: id })); + }, [id]); + + useEffect(() => { + if (typeof photos === 'object') { + setInitialValues(photos); + } + }, [photos]); + + useEffect(() => { + if (typeof photos === 'object') { + const newInitialVal = { ...initVals }; + Object.keys(initVals).forEach((el) => (newInitialVal[el] = photos[el])); + setInitialValues(newInitialVal); + } + }, [photos]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: id, data })); + await router.push('/photos/photos-list'); + }; + + return ( + <> + + {getPageTitle('Edit photos')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + + + + + + + + + router.push('/photos/photos-list')} + /> + + +
+
+
+ + ); +}; + +EditPhotosPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditPhotosPage; diff --git a/frontend/src/pages/photos/photos-list.tsx b/frontend/src/pages/photos/photos-list.tsx new file mode 100644 index 0000000..102e890 --- /dev/null +++ b/frontend/src/pages/photos/photos-list.tsx @@ -0,0 +1,160 @@ +import { mdiChartTimelineVariant } from '@mdi/js'; +import Head from 'next/head'; +import { uniqueId } from 'lodash'; +import React, { ReactElement, useState } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; +import TablePhotos from '../../components/Photos/TablePhotos'; +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/photos/photosSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const PhotosTablesPage = () => { + 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: 'Phototype', title: 'phototype' }]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_PHOTOS'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getPhotosCSV = async () => { + const response = await axios({ + url: '/photos?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 = 'photosCSV.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('Photos')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
+
+
+
+ + + + +
+ + + + + ); +}; + +PhotosTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + {page} + ); +}; + +export default PhotosTablesPage; diff --git a/frontend/src/pages/photos/photos-new.tsx b/frontend/src/pages/photos/photos-new.tsx new file mode 100644 index 0000000..a9314c3 --- /dev/null +++ b/frontend/src/pages/photos/photos-new.tsx @@ -0,0 +1,116 @@ +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/photos/photosSlice'; +import { useAppDispatch } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import moment from 'moment'; + +const initialValues = { + phototype: '', + + image: [], +}; + +const PhotosNew = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + + const handleSubmit = async (data) => { + await dispatch(create(data)); + await router.push('/photos/photos-list'); + }; + return ( + <> + + {getPageTitle('New Item')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + + + + + + + + + router.push('/photos/photos-list')} + /> + + +
+
+
+ + ); +}; + +PhotosNew.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default PhotosNew; diff --git a/frontend/src/pages/photos/photos-table.tsx b/frontend/src/pages/photos/photos-table.tsx new file mode 100644 index 0000000..16360f9 --- /dev/null +++ b/frontend/src/pages/photos/photos-table.tsx @@ -0,0 +1,159 @@ +import { mdiChartTimelineVariant } from '@mdi/js'; +import Head from 'next/head'; +import { uniqueId } from 'lodash'; +import React, { ReactElement, useState } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; +import TablePhotos from '../../components/Photos/TablePhotos'; +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/photos/photosSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const PhotosTablesPage = () => { + 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: 'Phototype', title: 'phototype' }]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_PHOTOS'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getPhotosCSV = async () => { + const response = await axios({ + url: '/photos?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 = 'photosCSV.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('Photos')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
+
+
+
+ + + +
+ + + + + ); +}; + +PhotosTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + {page} + ); +}; + +export default PhotosTablesPage; diff --git a/frontend/src/pages/photos/photos-view.tsx b/frontend/src/pages/photos/photos-view.tsx new file mode 100644 index 0000000..cf97af8 --- /dev/null +++ b/frontend/src/pages/photos/photos-view.tsx @@ -0,0 +1,386 @@ +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/photos/photosSlice'; +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 PhotosView = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const { photos } = useAppSelector((state) => state.photos); + + 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 photos')} + + + + + + +
+

Phototype

+

{photos?.phototype}

+
+ +
+

Image

+ {photos?.image?.length ? ( + + ) : ( +

No Image

+ )} +
+ + <> +

Products Photofront

+ +
+ + + + + + + + + + + + + + + + + + {photos.products_photofront && + Array.isArray(photos.products_photofront) && + photos.products_photofront.map((item: any) => ( + + router.push( + `/products/products-view/?id=${item.id}`, + ) + } + > + + + + + + + + + + + + + ))} + +
NameProofAgeBarcodeNotesStatus
{item.name}{item.proof}{item.age}{item.barcode}{item.notes}{item.status}
+
+ {!photos?.products_photofront?.length && ( +
No data
+ )} +
+ + + <> +

Products Photoback

+ +
+ + + + + + + + + + + + + + + + + + {photos.products_photoback && + Array.isArray(photos.products_photoback) && + photos.products_photoback.map((item: any) => ( + + router.push( + `/products/products-view/?id=${item.id}`, + ) + } + > + + + + + + + + + + + + + ))} + +
NameProofAgeBarcodeNotesStatus
{item.name}{item.proof}{item.age}{item.barcode}{item.notes}{item.status}
+
+ {!photos?.products_photoback?.length && ( +
No data
+ )} +
+ + + <> +

Bottles Photofront

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {photos.bottles_photofront && + Array.isArray(photos.bottles_photofront) && + photos.bottles_photofront.map((item: any) => ( + + router.push(`/bottles/bottles-view/?id=${item.id}`) + } + > + + + + + + + + + + + + + + + + + + + + + + + + + + + ))} + +
ProofAgeRatingCollectableRickhouseRackReleaseBarrelnumberBarreleddateBottlenumberDateacquiredVolumeNotes
{item.proof}{item.age}{item.rating} + {dataFormatter.booleanFormatter(item.collectable)} + {item.rickhouse}{item.rack}{item.release}{item.barrelnumber} + {dataFormatter.dateFormatter(item.barreleddate)} + {item.bottlenumber} + {dataFormatter.dateFormatter(item.dateacquired)} + {item.volume}{item.notes}
+
+ {!photos?.bottles_photofront?.length && ( +
No data
+ )} +
+ + + <> +

Bottles Photoback

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {photos.bottles_photoback && + Array.isArray(photos.bottles_photoback) && + photos.bottles_photoback.map((item: any) => ( + + router.push(`/bottles/bottles-view/?id=${item.id}`) + } + > + + + + + + + + + + + + + + + + + + + + + + + + + + + ))} + +
ProofAgeRatingCollectableRickhouseRackReleaseBarrelnumberBarreleddateBottlenumberDateacquiredVolumeNotes
{item.proof}{item.age}{item.rating} + {dataFormatter.booleanFormatter(item.collectable)} + {item.rickhouse}{item.rack}{item.release}{item.barrelnumber} + {dataFormatter.dateFormatter(item.barreleddate)} + {item.bottlenumber} + {dataFormatter.dateFormatter(item.dateacquired)} + {item.volume}{item.notes}
+
+ {!photos?.bottles_photoback?.length && ( +
No data
+ )} +
+ + + + + router.push('/photos/photos-list')} + /> +
+
+ + ); +}; + +PhotosView.getLayout = function getLayout(page: ReactElement) { + return ( + {page} + ); +}; + +export default PhotosView; diff --git a/frontend/src/pages/products/[productsId].tsx b/frontend/src/pages/products/[productsId].tsx new file mode 100644 index 0000000..4c1e34f --- /dev/null +++ b/frontend/src/pages/products/[productsId].tsx @@ -0,0 +1,193 @@ +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/products/productsSlice'; +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 EditProducts = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + brand: null, + + name: '', + + proof: '', + + age: '', + + barcode: '', + + notes: '', + + status: '', + + photofront: null, + + photoback: null, + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { products } = useAppSelector((state) => state.products); + + const { productsId } = router.query; + + useEffect(() => { + dispatch(fetch({ id: productsId })); + }, [productsId]); + + useEffect(() => { + if (typeof products === 'object') { + setInitialValues(products); + } + }, [products]); + + useEffect(() => { + if (typeof products === 'object') { + const newInitialVal = { ...initVals }; + + Object.keys(initVals).forEach((el) => (newInitialVal[el] = products[el])); + + setInitialValues(newInitialVal); + } + }, [products]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: productsId, data })); + await router.push('/products/products-list'); + }; + + return ( + <> + + {getPageTitle('Edit products')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + router.push('/products/products-list')} + /> + + +
+
+
+ + ); +}; + +EditProducts.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditProducts; diff --git a/frontend/src/pages/products/products-edit.tsx b/frontend/src/pages/products/products-edit.tsx new file mode 100644 index 0000000..8f69506 --- /dev/null +++ b/frontend/src/pages/products/products-edit.tsx @@ -0,0 +1,191 @@ +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/products/productsSlice'; +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 EditProductsPage = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + brand: null, + + name: '', + + proof: '', + + age: '', + + barcode: '', + + notes: '', + + status: '', + + photofront: null, + + photoback: null, + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { products } = useAppSelector((state) => state.products); + + const { id } = router.query; + + useEffect(() => { + dispatch(fetch({ id: id })); + }, [id]); + + useEffect(() => { + if (typeof products === 'object') { + setInitialValues(products); + } + }, [products]); + + useEffect(() => { + if (typeof products === 'object') { + const newInitialVal = { ...initVals }; + Object.keys(initVals).forEach((el) => (newInitialVal[el] = products[el])); + setInitialValues(newInitialVal); + } + }, [products]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: id, data })); + await router.push('/products/products-list'); + }; + + return ( + <> + + {getPageTitle('Edit products')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + router.push('/products/products-list')} + /> + + +
+
+
+ + ); +}; + +EditProductsPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditProductsPage; diff --git a/frontend/src/pages/products/products-list.tsx b/frontend/src/pages/products/products-list.tsx new file mode 100644 index 0000000..3ee281b --- /dev/null +++ b/frontend/src/pages/products/products-list.tsx @@ -0,0 +1,175 @@ +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 TableProducts from '../../components/Products/TableProducts'; +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/products/productsSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const ProductsTablesPage = () => { + const [filterItems, setFilterItems] = useState([]); + const [csvFile, setCsvFile] = useState(null); + const [isModalActive, setIsModalActive] = useState(false); + const [showTableView, setShowTableView] = useState(false); + + const { currentUser } = useAppSelector((state) => state.auth); + + const dispatch = useAppDispatch(); + + const [filters] = useState([ + { label: 'Name', title: 'name' }, + { label: 'Barcode', title: 'barcode' }, + { label: 'Notes', title: 'notes' }, + { label: 'Age', title: 'age', number: 'true' }, + { label: 'Status', title: 'status', number: 'true' }, + { label: 'Proof', title: 'proof', number: 'true' }, + + { label: 'Brand', title: 'brand' }, + + { label: 'Photofront', title: 'photofront' }, + + { label: 'Photoback', title: 'photoback' }, + ]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_PRODUCTS'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getProductsCSV = async () => { + const response = await axios({ + url: '/products?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 = 'productsCSV.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('Products')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
+
+
+
+ + + + +
+ + + + + ); +}; + +ProductsTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default ProductsTablesPage; diff --git a/frontend/src/pages/products/products-new.tsx b/frontend/src/pages/products/products-new.tsx new file mode 100644 index 0000000..e839f20 --- /dev/null +++ b/frontend/src/pages/products/products-new.tsx @@ -0,0 +1,164 @@ +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/products/productsSlice'; +import { useAppDispatch } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import moment from 'moment'; + +const initialValues = { + brand: '', + + name: '', + + proof: '', + + age: '', + + barcode: '', + + notes: '', + + status: '', + + photofront: '', + + photoback: '', +}; + +const ProductsNew = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + + const handleSubmit = async (data) => { + await dispatch(create(data)); + await router.push('/products/products-list'); + }; + return ( + <> + + {getPageTitle('New Item')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + router.push('/products/products-list')} + /> + + +
+
+
+ + ); +}; + +ProductsNew.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default ProductsNew; diff --git a/frontend/src/pages/products/products-table.tsx b/frontend/src/pages/products/products-table.tsx new file mode 100644 index 0000000..7e95359 --- /dev/null +++ b/frontend/src/pages/products/products-table.tsx @@ -0,0 +1,174 @@ +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 TableProducts from '../../components/Products/TableProducts'; +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/products/productsSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const ProductsTablesPage = () => { + const [filterItems, setFilterItems] = useState([]); + const [csvFile, setCsvFile] = useState(null); + const [isModalActive, setIsModalActive] = useState(false); + const [showTableView, setShowTableView] = useState(false); + + const { currentUser } = useAppSelector((state) => state.auth); + + const dispatch = useAppDispatch(); + + const [filters] = useState([ + { label: 'Name', title: 'name' }, + { label: 'Barcode', title: 'barcode' }, + { label: 'Notes', title: 'notes' }, + { label: 'Age', title: 'age', number: 'true' }, + { label: 'Status', title: 'status', number: 'true' }, + { label: 'Proof', title: 'proof', number: 'true' }, + + { label: 'Brand', title: 'brand' }, + + { label: 'Photofront', title: 'photofront' }, + + { label: 'Photoback', title: 'photoback' }, + ]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_PRODUCTS'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getProductsCSV = async () => { + const response = await axios({ + url: '/products?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 = 'productsCSV.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('Products')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
+
+
+
+ + + +
+ + + + + ); +}; + +ProductsTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default ProductsTablesPage; diff --git a/frontend/src/pages/products/products-view.tsx b/frontend/src/pages/products/products-view.tsx new file mode 100644 index 0000000..cd77f88 --- /dev/null +++ b/frontend/src/pages/products/products-view.tsx @@ -0,0 +1,215 @@ +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/products/productsSlice'; +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 ProductsView = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const { products } = useAppSelector((state) => state.products); + + 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 products')} + + + + + + +
+

Brand

+ +

{products?.brand?.name ?? 'No data'}

+
+ +
+

Name

+

{products?.name}

+
+ +
+

Proof

+

{products?.proof || 'No data'}

+
+ +
+

Age

+

{products?.age || 'No data'}

+
+ +
+

Barcode

+

{products?.barcode}

+
+ +
+

Notes

+

{products?.notes}

+
+ +
+

Status

+

{products?.status || 'No data'}

+
+ +
+

Photofront

+ +

{products?.photofront?.image ?? 'No data'}

+
+ +
+

Photoback

+ +

{products?.photoback?.image ?? 'No data'}

+
+ + <> +

Bottles Product

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {products.bottles_product && + Array.isArray(products.bottles_product) && + products.bottles_product.map((item: any) => ( + + router.push(`/bottles/bottles-view/?id=${item.id}`) + } + > + + + + + + + + + + + + + + + + + + + + + + + + + + + ))} + +
ProofAgeRatingCollectableRickhouseRackReleaseBarrelnumberBarreleddateBottlenumberDateacquiredVolumeNotes
{item.proof}{item.age}{item.rating} + {dataFormatter.booleanFormatter(item.collectable)} + {item.rickhouse}{item.rack}{item.release}{item.barrelnumber} + {dataFormatter.dateFormatter(item.barreleddate)} + {item.bottlenumber} + {dataFormatter.dateFormatter(item.dateacquired)} + {item.volume}{item.notes}
+
+ {!products?.bottles_product?.length && ( +
No data
+ )} +
+ + + + + router.push('/products/products-list')} + /> +
+
+ + ); +}; + +ProductsView.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default ProductsView; diff --git a/frontend/src/pages/reviews/[reviewsId].tsx b/frontend/src/pages/reviews/[reviewsId].tsx new file mode 100644 index 0000000..17d1914 --- /dev/null +++ b/frontend/src/pages/reviews/[reviewsId].tsx @@ -0,0 +1,177 @@ +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/reviews/reviewsSlice'; +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 EditReviews = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + user: null, + + bottle: null, + + rating: '', + + notes: '', + + createdat: new Date(), + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { reviews } = useAppSelector((state) => state.reviews); + + const { reviewsId } = router.query; + + useEffect(() => { + dispatch(fetch({ id: reviewsId })); + }, [reviewsId]); + + useEffect(() => { + if (typeof reviews === 'object') { + setInitialValues(reviews); + } + }, [reviews]); + + useEffect(() => { + if (typeof reviews === 'object') { + const newInitialVal = { ...initVals }; + + Object.keys(initVals).forEach((el) => (newInitialVal[el] = reviews[el])); + + setInitialValues(newInitialVal); + } + }, [reviews]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: reviewsId, data })); + await router.push('/reviews/reviews-list'); + }; + + return ( + <> + + {getPageTitle('Edit reviews')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + + + + + + + + + + + + + + + setInitialValues({ ...initialValues, createdat: date }) + } + /> + + + + + + + router.push('/reviews/reviews-list')} + /> + + +
+
+
+ + ); +}; + +EditReviews.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditReviews; diff --git a/frontend/src/pages/reviews/reviews-edit.tsx b/frontend/src/pages/reviews/reviews-edit.tsx new file mode 100644 index 0000000..4fb4373 --- /dev/null +++ b/frontend/src/pages/reviews/reviews-edit.tsx @@ -0,0 +1,175 @@ +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/reviews/reviewsSlice'; +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 EditReviewsPage = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + user: null, + + bottle: null, + + rating: '', + + notes: '', + + createdat: new Date(), + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { reviews } = useAppSelector((state) => state.reviews); + + const { id } = router.query; + + useEffect(() => { + dispatch(fetch({ id: id })); + }, [id]); + + useEffect(() => { + if (typeof reviews === 'object') { + setInitialValues(reviews); + } + }, [reviews]); + + useEffect(() => { + if (typeof reviews === 'object') { + const newInitialVal = { ...initVals }; + Object.keys(initVals).forEach((el) => (newInitialVal[el] = reviews[el])); + setInitialValues(newInitialVal); + } + }, [reviews]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: id, data })); + await router.push('/reviews/reviews-list'); + }; + + return ( + <> + + {getPageTitle('Edit reviews')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + + + + + + + + + + + + + + + setInitialValues({ ...initialValues, createdat: date }) + } + /> + + + + + + + router.push('/reviews/reviews-list')} + /> + + +
+
+
+ + ); +}; + +EditReviewsPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditReviewsPage; diff --git a/frontend/src/pages/reviews/reviews-list.tsx b/frontend/src/pages/reviews/reviews-list.tsx new file mode 100644 index 0000000..eac372b --- /dev/null +++ b/frontend/src/pages/reviews/reviews-list.tsx @@ -0,0 +1,171 @@ +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 TableReviews from '../../components/Reviews/TableReviews'; +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/reviews/reviewsSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const ReviewsTablesPage = () => { + 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: 'Notes', title: 'notes' }, + { label: 'Rating', title: 'rating', number: 'true' }, + + { label: 'Createdat', title: 'createdat', date: 'true' }, + + { label: 'User', title: 'user' }, + + { label: 'Bottle', title: 'bottle' }, + ]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_REVIEWS'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getReviewsCSV = async () => { + const response = await axios({ + url: '/reviews?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 = 'reviewsCSV.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('Reviews')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
+
+
+
+ + + + +
+ + + + + ); +}; + +ReviewsTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default ReviewsTablesPage; diff --git a/frontend/src/pages/reviews/reviews-new.tsx b/frontend/src/pages/reviews/reviews-new.tsx new file mode 100644 index 0000000..ac2b9df --- /dev/null +++ b/frontend/src/pages/reviews/reviews-new.tsx @@ -0,0 +1,138 @@ +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/reviews/reviewsSlice'; +import { useAppDispatch } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import moment from 'moment'; + +const initialValues = { + user: '', + + bottle: '', + + rating: '', + + notes: '', + + createdat: '', +}; + +const ReviewsNew = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + + const handleSubmit = async (data) => { + await dispatch(create(data)); + await router.push('/reviews/reviews-list'); + }; + return ( + <> + + {getPageTitle('New Item')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + + + + + + + + + + + + + + + + + + + + + router.push('/reviews/reviews-list')} + /> + + +
+
+
+ + ); +}; + +ReviewsNew.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default ReviewsNew; diff --git a/frontend/src/pages/reviews/reviews-table.tsx b/frontend/src/pages/reviews/reviews-table.tsx new file mode 100644 index 0000000..df5ef37 --- /dev/null +++ b/frontend/src/pages/reviews/reviews-table.tsx @@ -0,0 +1,170 @@ +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 TableReviews from '../../components/Reviews/TableReviews'; +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/reviews/reviewsSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const ReviewsTablesPage = () => { + 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: 'Notes', title: 'notes' }, + { label: 'Rating', title: 'rating', number: 'true' }, + + { label: 'Createdat', title: 'createdat', date: 'true' }, + + { label: 'User', title: 'user' }, + + { label: 'Bottle', title: 'bottle' }, + ]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_REVIEWS'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getReviewsCSV = async () => { + const response = await axios({ + url: '/reviews?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 = 'reviewsCSV.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('Reviews')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
+
+
+
+ + + +
+ + + + + ); +}; + +ReviewsTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default ReviewsTablesPage; diff --git a/frontend/src/pages/reviews/reviews-view.tsx b/frontend/src/pages/reviews/reviews-view.tsx new file mode 100644 index 0000000..dbb0bcd --- /dev/null +++ b/frontend/src/pages/reviews/reviews-view.tsx @@ -0,0 +1,119 @@ +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/reviews/reviewsSlice'; +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 ReviewsView = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const { reviews } = useAppSelector((state) => state.reviews); + + 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 reviews')} + + + + + + +
+

User

+ +

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

+
+ +
+

Bottle

+ +

{reviews?.bottle?.id ?? 'No data'}

+
+ +
+

Rating

+

{reviews?.rating || 'No data'}

+
+ +
+

Notes

+

{reviews?.notes}

+
+ + + {reviews.createdat ? ( + + ) : ( +

No Createdat

+ )} +
+ + + + router.push('/reviews/reviews-list')} + /> +
+
+ + ); +}; + +ReviewsView.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default ReviewsView; diff --git a/frontend/src/pages/roles/roles-view.tsx b/frontend/src/pages/roles/roles-view.tsx index b7368ff..bed2814 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 + + Address + + Address2 @@ -138,6 +142,10 @@ const RolesView = () => { {dataFormatter.booleanFormatter(item.disabled)} + + {item.address} + + {item.address2} ))} diff --git a/frontend/src/pages/users/[usersId].tsx b/frontend/src/pages/users/[usersId].tsx index e16077f..aea3885 100644 --- a/frontend/src/pages/users/[usersId].tsx +++ b/frontend/src/pages/users/[usersId].tsx @@ -52,6 +52,10 @@ const EditUsers = () => { custom_permissions: [], + address: '', + + address2: '', + password: '', }; const [initialValues, setInitialValues] = useState(initVals); @@ -170,6 +174,14 @@ const EditUsers = () => { > + + + + + + + + diff --git a/frontend/src/pages/users/users-edit.tsx b/frontend/src/pages/users/users-edit.tsx index 87d7090..697ae46 100644 --- a/frontend/src/pages/users/users-edit.tsx +++ b/frontend/src/pages/users/users-edit.tsx @@ -52,6 +52,10 @@ const EditUsersPage = () => { custom_permissions: [], + address: '', + + address2: '', + password: '', }; const [initialValues, setInitialValues] = useState(initVals); @@ -168,6 +172,14 @@ const EditUsersPage = () => { > + + + + + + + + diff --git a/frontend/src/pages/users/users-list.tsx b/frontend/src/pages/users/users-list.tsx index 6d03ebc..a5d2eb6 100644 --- a/frontend/src/pages/users/users-list.tsx +++ b/frontend/src/pages/users/users-list.tsx @@ -33,6 +33,8 @@ const UsersTablesPage = () => { { label: 'Last Name', title: 'lastName' }, { label: 'Phone Number', title: 'phoneNumber' }, { label: 'E-Mail', title: 'email' }, + { label: 'Address', title: 'address' }, + { label: 'Address2', title: 'address2' }, { label: 'App Role', title: 'app_role' }, diff --git a/frontend/src/pages/users/users-new.tsx b/frontend/src/pages/users/users-new.tsx index 4f3fa45..1315f43 100644 --- a/frontend/src/pages/users/users-new.tsx +++ b/frontend/src/pages/users/users-new.tsx @@ -48,6 +48,10 @@ const initialValues = { app_role: '', custom_permissions: [], + + address: '', + + address2: '', }; const UsersNew = () => { @@ -140,6 +144,14 @@ const UsersNew = () => { > + + + + + + + + diff --git a/frontend/src/pages/users/users-table.tsx b/frontend/src/pages/users/users-table.tsx index a408cd6..c4728de 100644 --- a/frontend/src/pages/users/users-table.tsx +++ b/frontend/src/pages/users/users-table.tsx @@ -33,6 +33,8 @@ const UsersTablesPage = () => { { label: 'Last Name', title: 'lastName' }, { label: 'Phone Number', title: 'phoneNumber' }, { label: 'E-Mail', title: 'email' }, + { label: 'Address', title: 'address' }, + { label: 'Address2', title: 'address2' }, { label: 'App Role', title: 'app_role' }, diff --git a/frontend/src/pages/users/users-view.tsx b/frontend/src/pages/users/users-view.tsx index 6273d2e..18d8651 100644 --- a/frontend/src/pages/users/users-view.tsx +++ b/frontend/src/pages/users/users-view.tsx @@ -138,6 +138,53 @@ const UsersView = () => {
+
+

Address

+

{users?.address}

+
+ +
+

Address2

+

{users?.address2}

+
+ + <> +

Locations User

+ +
+ + + + + + + + {users.locations_user && + Array.isArray(users.locations_user) && + users.locations_user.map((item: any) => ( + + router.push( + `/locations/locations-view/?id=${item.id}`, + ) + } + > + + + ))} + +
Name
{item.name}
+
+ {!users?.locations_user?.length && ( +
No data
+ )} +
+ + <>

Bottles User

{ - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + @@ -177,29 +232,37 @@ const UsersView = () => { router.push(`/bottles/bottles-view/?id=${item.id}`) } > - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + ))} @@ -211,6 +274,119 @@ const UsersView = () => { + <> +

Reviews User

+ +
+
NameProofTypeMSRPRangeSecondaryValueRangeOpenedBottleIndicatorQuantityBarcodeAgeRatingCollectableRickhouseRackReleaseBarrelnumberBarreleddateBottlenumberDateacquiredVolumeNotes
{item.name}{item.proof}{item.type}{item.msrp_range} - {item.secondary_value_range} - - {dataFormatter.booleanFormatter( - item.opened_bottle_indicator, - )} - {item.quantity}{item.barcode}{item.age}{item.rating} + {dataFormatter.booleanFormatter(item.collectable)} + {item.rickhouse}{item.rack}{item.release}{item.barrelnumber} + {dataFormatter.dateFormatter(item.barreleddate)} + {item.bottlenumber} + {dataFormatter.dateFormatter(item.dateacquired)} + {item.volume}{item.notes}
+ + + + + + + + + + + {users.reviews_user && + Array.isArray(users.reviews_user) && + users.reviews_user.map((item: any) => ( + + router.push(`/reviews/reviews-view/?id=${item.id}`) + } + > + + + + + + + ))} + +
RatingNotesCreatedat
{item.rating}{item.notes} + {dataFormatter.dateTimeFormatter(item.createdat)} +
+ + {!users?.reviews_user?.length && ( +
No data
+ )} +
+ + + <> +

Messages Sender

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

+ Conversationparticipants User +

+ +
+ + + + + + {users.conversationparticipants_user && + Array.isArray(users.conversationparticipants_user) && + users.conversationparticipants_user.map((item: any) => ( + + router.push( + `/conversationparticipants/conversationparticipants-view/?id=${item.id}`, + ) + } + > + ))} + +
+
+ {!users?.conversationparticipants_user?.length && ( +
No data
+ )} +
+ + diff --git a/frontend/src/pages/web_pages/products.tsx b/frontend/src/pages/web_pages/products.tsx index 7548e72..a67632a 100644 --- a/frontend/src/pages/web_pages/products.tsx +++ b/frontend/src/pages/web_pages/products.tsx @@ -126,7 +126,7 @@ export default function WebSite() { diff --git a/frontend/src/stores/conversationparticipants/conversationparticipantsSlice.ts b/frontend/src/stores/conversationparticipants/conversationparticipantsSlice.ts new file mode 100644 index 0000000..71fc3c7 --- /dev/null +++ b/frontend/src/stores/conversationparticipants/conversationparticipantsSlice.ts @@ -0,0 +1,254 @@ +import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit'; +import axios from 'axios'; +import { + fulfilledNotify, + rejectNotify, + resetNotify, +} from '../../helpers/notifyStateHandler'; + +interface MainState { + conversationparticipants: any; + loading: boolean; + count: number; + refetch: boolean; + rolesWidgets: any[]; + notify: { + showNotification: boolean; + textNotification: string; + typeNotification: string; + }; +} + +const initialState: MainState = { + conversationparticipants: [], + loading: false, + count: 0, + refetch: false, + rolesWidgets: [], + notify: { + showNotification: false, + textNotification: '', + typeNotification: 'warn', + }, +}; + +export const fetch = createAsyncThunk( + 'conversationparticipants/fetch', + async (data: any) => { + const { id, query } = data; + const result = await axios.get( + `conversationparticipants${query || (id ? `/${id}` : '')}`, + ); + return id + ? result.data + : { rows: result.data.rows, count: result.data.count }; + }, +); + +export const deleteItemsByIds = createAsyncThunk( + 'conversationparticipants/deleteByIds', + async (data: any, { rejectWithValue }) => { + try { + await axios.post('conversationparticipants/deleteByIds', { data }); + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const deleteItem = createAsyncThunk( + 'conversationparticipants/deleteConversationparticipants', + async (id: string, { rejectWithValue }) => { + try { + await axios.delete(`conversationparticipants/${id}`); + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const create = createAsyncThunk( + 'conversationparticipants/createConversationparticipants', + async (data: any, { rejectWithValue }) => { + try { + const result = await axios.post('conversationparticipants', { data }); + return result.data; + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const uploadCsv = createAsyncThunk( + 'conversationparticipants/uploadCsv', + async (file: File, { rejectWithValue }) => { + try { + const data = new FormData(); + data.append('file', file); + data.append('filename', file.name); + + const result = await axios.post( + 'conversationparticipants/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( + 'conversationparticipants/updateConversationparticipants', + async (payload: any, { rejectWithValue }) => { + try { + const result = await axios.put(`conversationparticipants/${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 conversationparticipantsSlice = createSlice({ + name: 'conversationparticipants', + 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.conversationparticipants = action.payload.rows; + state.count = action.payload.count; + } else { + state.conversationparticipants = 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, 'Conversationparticipants 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, + `${'Conversationparticipants'.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, + `${'Conversationparticipants'.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, + `${'Conversationparticipants'.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, 'Conversationparticipants 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 } = conversationparticipantsSlice.actions; + +export default conversationparticipantsSlice.reducer; diff --git a/frontend/src/stores/conversations/conversationsSlice.ts b/frontend/src/stores/conversations/conversationsSlice.ts new file mode 100644 index 0000000..52faae8 --- /dev/null +++ b/frontend/src/stores/conversations/conversationsSlice.ts @@ -0,0 +1,250 @@ +import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit'; +import axios from 'axios'; +import { + fulfilledNotify, + rejectNotify, + resetNotify, +} from '../../helpers/notifyStateHandler'; + +interface MainState { + conversations: any; + loading: boolean; + count: number; + refetch: boolean; + rolesWidgets: any[]; + notify: { + showNotification: boolean; + textNotification: string; + typeNotification: string; + }; +} + +const initialState: MainState = { + conversations: [], + loading: false, + count: 0, + refetch: false, + rolesWidgets: [], + notify: { + showNotification: false, + textNotification: '', + typeNotification: 'warn', + }, +}; + +export const fetch = createAsyncThunk( + 'conversations/fetch', + async (data: any) => { + const { id, query } = data; + const result = await axios.get( + `conversations${query || (id ? `/${id}` : '')}`, + ); + return id + ? result.data + : { rows: result.data.rows, count: result.data.count }; + }, +); + +export const deleteItemsByIds = createAsyncThunk( + 'conversations/deleteByIds', + async (data: any, { rejectWithValue }) => { + try { + await axios.post('conversations/deleteByIds', { data }); + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const deleteItem = createAsyncThunk( + 'conversations/deleteConversations', + async (id: string, { rejectWithValue }) => { + try { + await axios.delete(`conversations/${id}`); + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const create = createAsyncThunk( + 'conversations/createConversations', + async (data: any, { rejectWithValue }) => { + try { + const result = await axios.post('conversations', { data }); + return result.data; + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const uploadCsv = createAsyncThunk( + 'conversations/uploadCsv', + async (file: File, { rejectWithValue }) => { + try { + const data = new FormData(); + data.append('file', file); + data.append('filename', file.name); + + const result = await axios.post('conversations/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( + 'conversations/updateConversations', + async (payload: any, { rejectWithValue }) => { + try { + const result = await axios.put(`conversations/${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 conversationsSlice = createSlice({ + name: 'conversations', + 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.conversations = action.payload.rows; + state.count = action.payload.count; + } else { + state.conversations = 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, 'Conversations 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, + `${'Conversations'.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, + `${'Conversations'.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, + `${'Conversations'.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, 'Conversations 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 } = conversationsSlice.actions; + +export default conversationsSlice.reducer; diff --git a/frontend/src/stores/locations/locationsSlice.ts b/frontend/src/stores/locations/locationsSlice.ts new file mode 100644 index 0000000..7f3ee07 --- /dev/null +++ b/frontend/src/stores/locations/locationsSlice.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 { + locations: any; + loading: boolean; + count: number; + refetch: boolean; + rolesWidgets: any[]; + notify: { + showNotification: boolean; + textNotification: string; + typeNotification: string; + }; +} + +const initialState: MainState = { + locations: [], + loading: false, + count: 0, + refetch: false, + rolesWidgets: [], + notify: { + showNotification: false, + textNotification: '', + typeNotification: 'warn', + }, +}; + +export const fetch = createAsyncThunk('locations/fetch', async (data: any) => { + const { id, query } = data; + const result = await axios.get(`locations${query || (id ? `/${id}` : '')}`); + return id + ? result.data + : { rows: result.data.rows, count: result.data.count }; +}); + +export const deleteItemsByIds = createAsyncThunk( + 'locations/deleteByIds', + async (data: any, { rejectWithValue }) => { + try { + await axios.post('locations/deleteByIds', { data }); + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const deleteItem = createAsyncThunk( + 'locations/deleteLocations', + async (id: string, { rejectWithValue }) => { + try { + await axios.delete(`locations/${id}`); + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const create = createAsyncThunk( + 'locations/createLocations', + async (data: any, { rejectWithValue }) => { + try { + const result = await axios.post('locations', { data }); + return result.data; + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const uploadCsv = createAsyncThunk( + 'locations/uploadCsv', + async (file: File, { rejectWithValue }) => { + try { + const data = new FormData(); + data.append('file', file); + data.append('filename', file.name); + + const result = await axios.post('locations/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( + 'locations/updateLocations', + async (payload: any, { rejectWithValue }) => { + try { + const result = await axios.put(`locations/${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 locationsSlice = createSlice({ + name: 'locations', + 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.locations = action.payload.rows; + state.count = action.payload.count; + } else { + state.locations = 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, 'Locations 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, `${'Locations'.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, `${'Locations'.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, `${'Locations'.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, 'Locations 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 } = locationsSlice.actions; + +export default locationsSlice.reducer; diff --git a/frontend/src/stores/messages/messagesSlice.ts b/frontend/src/stores/messages/messagesSlice.ts new file mode 100644 index 0000000..765dc1c --- /dev/null +++ b/frontend/src/stores/messages/messagesSlice.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 { + messages: any; + loading: boolean; + count: number; + refetch: boolean; + rolesWidgets: any[]; + notify: { + showNotification: boolean; + textNotification: string; + typeNotification: string; + }; +} + +const initialState: MainState = { + messages: [], + loading: false, + count: 0, + refetch: false, + rolesWidgets: [], + notify: { + showNotification: false, + textNotification: '', + typeNotification: 'warn', + }, +}; + +export const fetch = createAsyncThunk('messages/fetch', async (data: any) => { + const { id, query } = data; + const result = await axios.get(`messages${query || (id ? `/${id}` : '')}`); + return id + ? result.data + : { rows: result.data.rows, count: result.data.count }; +}); + +export const deleteItemsByIds = createAsyncThunk( + 'messages/deleteByIds', + async (data: any, { rejectWithValue }) => { + try { + await axios.post('messages/deleteByIds', { data }); + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const deleteItem = createAsyncThunk( + 'messages/deleteMessages', + async (id: string, { rejectWithValue }) => { + try { + await axios.delete(`messages/${id}`); + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const create = createAsyncThunk( + 'messages/createMessages', + async (data: any, { rejectWithValue }) => { + try { + const result = await axios.post('messages', { data }); + return result.data; + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const uploadCsv = createAsyncThunk( + 'messages/uploadCsv', + async (file: File, { rejectWithValue }) => { + try { + const data = new FormData(); + data.append('file', file); + data.append('filename', file.name); + + const result = await axios.post('messages/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( + 'messages/updateMessages', + async (payload: any, { rejectWithValue }) => { + try { + const result = await axios.put(`messages/${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 messagesSlice = createSlice({ + name: 'messages', + 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.messages = action.payload.rows; + state.count = action.payload.count; + } else { + state.messages = 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, 'Messages 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, `${'Messages'.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, `${'Messages'.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, `${'Messages'.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, 'Messages 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 } = messagesSlice.actions; + +export default messagesSlice.reducer; diff --git a/frontend/src/stores/photos/photosSlice.ts b/frontend/src/stores/photos/photosSlice.ts new file mode 100644 index 0000000..de0831c --- /dev/null +++ b/frontend/src/stores/photos/photosSlice.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 { + photos: any; + loading: boolean; + count: number; + refetch: boolean; + rolesWidgets: any[]; + notify: { + showNotification: boolean; + textNotification: string; + typeNotification: string; + }; +} + +const initialState: MainState = { + photos: [], + loading: false, + count: 0, + refetch: false, + rolesWidgets: [], + notify: { + showNotification: false, + textNotification: '', + typeNotification: 'warn', + }, +}; + +export const fetch = createAsyncThunk('photos/fetch', async (data: any) => { + const { id, query } = data; + const result = await axios.get(`photos${query || (id ? `/${id}` : '')}`); + return id + ? result.data + : { rows: result.data.rows, count: result.data.count }; +}); + +export const deleteItemsByIds = createAsyncThunk( + 'photos/deleteByIds', + async (data: any, { rejectWithValue }) => { + try { + await axios.post('photos/deleteByIds', { data }); + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const deleteItem = createAsyncThunk( + 'photos/deletePhotos', + async (id: string, { rejectWithValue }) => { + try { + await axios.delete(`photos/${id}`); + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const create = createAsyncThunk( + 'photos/createPhotos', + async (data: any, { rejectWithValue }) => { + try { + const result = await axios.post('photos', { data }); + return result.data; + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const uploadCsv = createAsyncThunk( + 'photos/uploadCsv', + async (file: File, { rejectWithValue }) => { + try { + const data = new FormData(); + data.append('file', file); + data.append('filename', file.name); + + const result = await axios.post('photos/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( + 'photos/updatePhotos', + async (payload: any, { rejectWithValue }) => { + try { + const result = await axios.put(`photos/${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 photosSlice = createSlice({ + name: 'photos', + 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.photos = action.payload.rows; + state.count = action.payload.count; + } else { + state.photos = 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, 'Photos 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, `${'Photos'.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, `${'Photos'.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, `${'Photos'.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, 'Photos 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 } = photosSlice.actions; + +export default photosSlice.reducer; diff --git a/frontend/src/stores/products/productsSlice.ts b/frontend/src/stores/products/productsSlice.ts new file mode 100644 index 0000000..03c9269 --- /dev/null +++ b/frontend/src/stores/products/productsSlice.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 { + products: any; + loading: boolean; + count: number; + refetch: boolean; + rolesWidgets: any[]; + notify: { + showNotification: boolean; + textNotification: string; + typeNotification: string; + }; +} + +const initialState: MainState = { + products: [], + loading: false, + count: 0, + refetch: false, + rolesWidgets: [], + notify: { + showNotification: false, + textNotification: '', + typeNotification: 'warn', + }, +}; + +export const fetch = createAsyncThunk('products/fetch', async (data: any) => { + const { id, query } = data; + const result = await axios.get(`products${query || (id ? `/${id}` : '')}`); + return id + ? result.data + : { rows: result.data.rows, count: result.data.count }; +}); + +export const deleteItemsByIds = createAsyncThunk( + 'products/deleteByIds', + async (data: any, { rejectWithValue }) => { + try { + await axios.post('products/deleteByIds', { data }); + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const deleteItem = createAsyncThunk( + 'products/deleteProducts', + async (id: string, { rejectWithValue }) => { + try { + await axios.delete(`products/${id}`); + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const create = createAsyncThunk( + 'products/createProducts', + async (data: any, { rejectWithValue }) => { + try { + const result = await axios.post('products', { data }); + return result.data; + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const uploadCsv = createAsyncThunk( + 'products/uploadCsv', + async (file: File, { rejectWithValue }) => { + try { + const data = new FormData(); + data.append('file', file); + data.append('filename', file.name); + + const result = await axios.post('products/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( + 'products/updateProducts', + async (payload: any, { rejectWithValue }) => { + try { + const result = await axios.put(`products/${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 productsSlice = createSlice({ + name: 'products', + 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.products = action.payload.rows; + state.count = action.payload.count; + } else { + state.products = 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, 'Products 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, `${'Products'.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, `${'Products'.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, `${'Products'.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, 'Products 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 } = productsSlice.actions; + +export default productsSlice.reducer; diff --git a/frontend/src/stores/reviews/reviewsSlice.ts b/frontend/src/stores/reviews/reviewsSlice.ts new file mode 100644 index 0000000..de632ab --- /dev/null +++ b/frontend/src/stores/reviews/reviewsSlice.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 { + reviews: any; + loading: boolean; + count: number; + refetch: boolean; + rolesWidgets: any[]; + notify: { + showNotification: boolean; + textNotification: string; + typeNotification: string; + }; +} + +const initialState: MainState = { + reviews: [], + loading: false, + count: 0, + refetch: false, + rolesWidgets: [], + notify: { + showNotification: false, + textNotification: '', + typeNotification: 'warn', + }, +}; + +export const fetch = createAsyncThunk('reviews/fetch', async (data: any) => { + const { id, query } = data; + const result = await axios.get(`reviews${query || (id ? `/${id}` : '')}`); + return id + ? result.data + : { rows: result.data.rows, count: result.data.count }; +}); + +export const deleteItemsByIds = createAsyncThunk( + 'reviews/deleteByIds', + async (data: any, { rejectWithValue }) => { + try { + await axios.post('reviews/deleteByIds', { data }); + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const deleteItem = createAsyncThunk( + 'reviews/deleteReviews', + async (id: string, { rejectWithValue }) => { + try { + await axios.delete(`reviews/${id}`); + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const create = createAsyncThunk( + 'reviews/createReviews', + async (data: any, { rejectWithValue }) => { + try { + const result = await axios.post('reviews', { data }); + return result.data; + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const uploadCsv = createAsyncThunk( + 'reviews/uploadCsv', + async (file: File, { rejectWithValue }) => { + try { + const data = new FormData(); + data.append('file', file); + data.append('filename', file.name); + + const result = await axios.post('reviews/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( + 'reviews/updateReviews', + async (payload: any, { rejectWithValue }) => { + try { + const result = await axios.put(`reviews/${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 reviewsSlice = createSlice({ + name: 'reviews', + 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.reviews = action.payload.rows; + state.count = action.payload.count; + } else { + state.reviews = 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, 'Reviews 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, `${'Reviews'.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, `${'Reviews'.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, `${'Reviews'.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, 'Reviews 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 } = reviewsSlice.actions; + +export default reviewsSlice.reducer; diff --git a/frontend/src/stores/store.ts b/frontend/src/stores/store.ts index fb00656..f801ac7 100644 --- a/frontend/src/stores/store.ts +++ b/frontend/src/stores/store.ts @@ -5,11 +5,18 @@ import authSlice from './authSlice'; import openAiSlice from './openAiSlice'; import usersSlice from './users/usersSlice'; -import bottlesSlice from './bottles/bottlesSlice'; -import brandsSlice from './brands/brandsSlice'; -import distilleriesSlice from './distilleries/distilleriesSlice'; import rolesSlice from './roles/rolesSlice'; import permissionsSlice from './permissions/permissionsSlice'; +import distilleriesSlice from './distilleries/distilleriesSlice'; +import brandsSlice from './brands/brandsSlice'; +import photosSlice from './photos/photosSlice'; +import productsSlice from './products/productsSlice'; +import locationsSlice from './locations/locationsSlice'; +import bottlesSlice from './bottles/bottlesSlice'; +import reviewsSlice from './reviews/reviewsSlice'; +import conversationsSlice from './conversations/conversationsSlice'; +import messagesSlice from './messages/messagesSlice'; +import conversationparticipantsSlice from './conversationparticipants/conversationparticipantsSlice'; export const store = configureStore({ reducer: { @@ -19,11 +26,18 @@ export const store = configureStore({ openAi: openAiSlice, users: usersSlice, - bottles: bottlesSlice, - brands: brandsSlice, - distilleries: distilleriesSlice, roles: rolesSlice, permissions: permissionsSlice, + distilleries: distilleriesSlice, + brands: brandsSlice, + photos: photosSlice, + products: productsSlice, + locations: locationsSlice, + bottles: bottlesSlice, + reviews: reviewsSlice, + conversations: conversationsSlice, + messages: messagesSlice, + conversationparticipants: conversationparticipantsSlice, }, });