diff --git a/backend/src/db/api/breeders.js b/backend/src/db/api/breeders.js new file mode 100644 index 0000000..39d8a2a --- /dev/null +++ b/backend/src/db/api/breeders.js @@ -0,0 +1,141 @@ +const db = require('../models'); +const Utils = require('../utils'); +const Sequelize = db.Sequelize; +const Op = Sequelize.Op; + +module.exports = class BreedersDBApi { + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const record = await db.breeders.create( + { + id: data.id || undefined, + name: data.name || null, + importHash: data.importHash || null, + tenantId: data.tenant || currentUser.tenantId || null, + organizationsId: data.organizations || currentUser.organizationsId || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + return record; + } + + static async bulkImport(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const recordsData = data.map((item, index) => ({ + id: item.id || undefined, + name: item.name || null, + importHash: item.importHash || null, + tenantId: item.tenant || currentUser.tenantId || null, + organizationsId: item.organizations || currentUser.organizationsId || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + return await db.breeders.bulkCreate(recordsData, { transaction }); + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const record = await db.breeders.findByPk(id, {}, { transaction }); + const updatePayload = {}; + if (data.name !== undefined) updatePayload.name = data.name; + updatePayload.updatedById = currentUser.id; + + await record.update(updatePayload, { transaction }); + + if (data.tenant !== undefined) await record.setTenant(data.tenant, { transaction }); + if (data.organizations !== undefined) await record.setOrganizations(data.organizations, { transaction }); + + return record; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const records = await db.breeders.findAll({ + where: { id: { [Op.in]: ids } }, + transaction, + }); + + for (const record of records) { + await record.destroy({ transaction }); + } + return records; + } + + static async remove(id, options) { + const transaction = (options && options.transaction) || undefined; + const record = await db.breeders.findByPk(id, options); + await record.destroy({ transaction }); + return record; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + return await db.breeders.findOne({ where }, { transaction }); + } + + static async findAll(filter, globalAccess, options) { + const limit = filter.limit || 0; + const currentPage = +filter.page || 0; + const offset = currentPage * limit; + let where = {}; + + const user = (options && options.currentUser) || null; + if (!globalAccess && user?.organizationsId) { + where.organizationsId = user.organizationsId; + } + + if (filter) { + if (filter.id) where.id = Utils.uuid(filter.id); + if (filter.name) where[Op.and] = Utils.ilike('breeders', 'name', filter.name); + } + + const { rows, count } = await db.breeders.findAndCountAll({ + where, + include: [ + { model: db.tenants, as: 'tenant' }, + { model: db.organizations, as: 'organizations' } + ], + distinct: true, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + order: filter.field && filter.sort ? [[filter.field, filter.sort]] : [['createdAt', 'desc']], + transaction: options?.transaction, + }); + + return { rows, count }; + } + + static async findAllAutocomplete(query, limit, offset, globalAccess, organizationId) { + let where = {}; + if (!globalAccess && organizationId) where.organizationsId = organizationId; + + if (query) { + where[Op.or] = [ + { id: Utils.uuid(query) }, + Utils.ilike('breeders', 'name', query), + ]; + } + + const records = await db.breeders.findAll({ + attributes: ['id', 'name'], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + order: [['name', 'ASC']], + }); + + return records.map((record) => ({ id: record.id, label: record.name })); + } +}; diff --git a/backend/src/db/api/containers.js b/backend/src/db/api/containers.js new file mode 100644 index 0000000..01e0c4a --- /dev/null +++ b/backend/src/db/api/containers.js @@ -0,0 +1,142 @@ + +const db = require('../models'); +const Utils = require('../utils'); +const Sequelize = db.Sequelize; +const Op = Sequelize.Op; + +module.exports = class ContainersDBApi { + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const record = await db.containers.create( + { + id: data.id || undefined, + name: data.name || null, + importHash: data.importHash || null, + tenantId: data.tenant || currentUser.tenantId || null, + organizationsId: data.organizations || currentUser.organizationsId || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + return record; + } + + static async bulkImport(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const recordsData = data.map((item, index) => ({ + id: item.id || undefined, + name: item.name || null, + importHash: item.importHash || null, + tenantId: item.tenant || currentUser.tenantId || null, + organizationsId: item.organizations || currentUser.organizationsId || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + return await db.containers.bulkCreate(recordsData, { transaction }); + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const record = await db.containers.findByPk(id, {}, { transaction }); + const updatePayload = {}; + if (data.name !== undefined) updatePayload.name = data.name; + updatePayload.updatedById = currentUser.id; + + await record.update(updatePayload, { transaction }); + + if (data.tenant !== undefined) await record.setTenant(data.tenant, { transaction }); + if (data.organizations !== undefined) await record.setOrganizations(data.organizations, { transaction }); + + return record; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const records = await db.containers.findAll({ + where: { id: { [Op.in]: ids } }, + transaction, + }); + + for (const record of records) { + await record.destroy({ transaction }); + } + return records; + } + + static async remove(id, options) { + const transaction = (options && options.transaction) || undefined; + const record = await db.containers.findByPk(id, options); + await record.destroy({ transaction }); + return record; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + return await db.containers.findOne({ where }, { transaction }); + } + + static async findAll(filter, globalAccess, options) { + const limit = filter.limit || 0; + const currentPage = +filter.page || 0; + const offset = currentPage * limit; + let where = {}; + + const user = (options && options.currentUser) || null; + if (!globalAccess && user?.organizationsId) { + where.organizationsId = user.organizationsId; + } + + if (filter) { + if (filter.id) where.id = Utils.uuid(filter.id); + if (filter.name) where[Op.and] = Utils.ilike('containers', 'name', filter.name); + } + + const { rows, count } = await db.containers.findAndCountAll({ + where, + include: [ + { model: db.tenants, as: 'tenant' }, + { model: db.organizations, as: 'organizations' } + ], + distinct: true, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + order: filter.field && filter.sort ? [[filter.field, filter.sort]] : [['createdAt', 'desc']], + transaction: options?.transaction, + }); + + return { rows, count }; + } + + static async findAllAutocomplete(query, limit, offset, globalAccess, organizationId) { + let where = {}; + if (!globalAccess && organizationId) where.organizationsId = organizationId; + + if (query) { + where[Op.or] = [ + { id: Utils.uuid(query) }, + Utils.ilike('containers', 'name', query), + ]; + } + + const records = await db.containers.findAll({ + attributes: ['id', 'name'], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + order: [['name', 'ASC']], + }); + + return records.map((record) => ({ id: record.id, label: record.name })); + } +}; diff --git a/backend/src/db/api/crop_types.js b/backend/src/db/api/crop_types.js new file mode 100644 index 0000000..08cf123 --- /dev/null +++ b/backend/src/db/api/crop_types.js @@ -0,0 +1,142 @@ + +const db = require('../models'); +const Utils = require('../utils'); +const Sequelize = db.Sequelize; +const Op = Sequelize.Op; + +module.exports = class Crop_typesDBApi { + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const record = await db.crop_types.create( + { + id: data.id || undefined, + name: data.name || null, + importHash: data.importHash || null, + tenantId: data.tenant || currentUser.tenantId || null, + organizationsId: data.organizations || currentUser.organizationsId || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + return record; + } + + static async bulkImport(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const recordsData = data.map((item, index) => ({ + id: item.id || undefined, + name: item.name || null, + importHash: item.importHash || null, + tenantId: item.tenant || currentUser.tenantId || null, + organizationsId: item.organizations || currentUser.organizationsId || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + return await db.crop_types.bulkCreate(recordsData, { transaction }); + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const record = await db.crop_types.findByPk(id, {}, { transaction }); + const updatePayload = {}; + if (data.name !== undefined) updatePayload.name = data.name; + updatePayload.updatedById = currentUser.id; + + await record.update(updatePayload, { transaction }); + + if (data.tenant !== undefined) await record.setTenant(data.tenant, { transaction }); + if (data.organizations !== undefined) await record.setOrganizations(data.organizations, { transaction }); + + return record; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const records = await db.crop_types.findAll({ + where: { id: { [Op.in]: ids } }, + transaction, + }); + + for (const record of records) { + await record.destroy({ transaction }); + } + return records; + } + + static async remove(id, options) { + const transaction = (options && options.transaction) || undefined; + const record = await db.crop_types.findByPk(id, options); + await record.destroy({ transaction }); + return record; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + return await db.crop_types.findOne({ where }, { transaction }); + } + + static async findAll(filter, globalAccess, options) { + const limit = filter.limit || 0; + const currentPage = +filter.page || 0; + const offset = currentPage * limit; + let where = {}; + + const user = (options && options.currentUser) || null; + if (!globalAccess && user?.organizationsId) { + where.organizationsId = user.organizationsId; + } + + if (filter) { + if (filter.id) where.id = Utils.uuid(filter.id); + if (filter.name) where[Op.and] = Utils.ilike('crop_types', 'name', filter.name); + } + + const { rows, count } = await db.crop_types.findAndCountAll({ + where, + include: [ + { model: db.tenants, as: 'tenant' }, + { model: db.organizations, as: 'organizations' } + ], + distinct: true, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + order: filter.field && filter.sort ? [[filter.field, filter.sort]] : [['createdAt', 'desc']], + transaction: options?.transaction, + }); + + return { rows, count }; + } + + static async findAllAutocomplete(query, limit, offset, globalAccess, organizationId) { + let where = {}; + if (!globalAccess && organizationId) where.organizationsId = organizationId; + + if (query) { + where[Op.or] = [ + { id: Utils.uuid(query) }, + Utils.ilike('crop_types', 'name', query), + ]; + } + + const records = await db.crop_types.findAll({ + attributes: ['id', 'name'], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + order: [['name', 'ASC']], + }); + + return records.map((record) => ({ id: record.id, label: record.name })); + } +}; diff --git a/backend/src/db/api/groups.js b/backend/src/db/api/groups.js new file mode 100644 index 0000000..ce2ae26 --- /dev/null +++ b/backend/src/db/api/groups.js @@ -0,0 +1,141 @@ +const db = require('../models'); +const Utils = require('../utils'); +const Sequelize = db.Sequelize; +const Op = Sequelize.Op; + +module.exports = class groupsDBApi { + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const record = await db.groups.create( + { + id: data.id || undefined, + name: data.name || null, + importHash: data.importHash || null, + tenantId: data.tenant || currentUser.tenantId || null, + organizationsId: data.organizations || currentUser.organizationsId || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + return record; + } + + static async bulkImport(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const recordsData = data.map((item, index) => ({ + id: item.id || undefined, + name: item.name || null, + importHash: item.importHash || null, + tenantId: item.tenant || currentUser.tenantId || null, + organizationsId: item.organizations || currentUser.organizationsId || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + return await db.groups.bulkCreate(recordsData, { transaction }); + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const record = await db.groups.findByPk(id, {}, { transaction }); + const updatePayload = {}; + if (data.name !== undefined) updatePayload.name = data.name; + updatePayload.updatedById = currentUser.id; + + await record.update(updatePayload, { transaction }); + + if (data.tenant !== undefined) await record.setTenant(data.tenant, { transaction }); + if (data.organizations !== undefined) await record.setOrganizations(data.organizations, { transaction }); + + return record; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const records = await db.groups.findAll({ + where: { id: { [Op.in]: ids } }, + transaction, + }); + + for (const record of records) { + await record.destroy({ transaction }); + } + return records; + } + + static async remove(id, options) { + const transaction = (options && options.transaction) || undefined; + const record = await db.groups.findByPk(id, options); + await record.destroy({ transaction }); + return record; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + return await db.groups.findOne({ where }, { transaction }); + } + + static async findAll(filter, globalAccess, options) { + const limit = filter.limit || 0; + const currentPage = +filter.page || 0; + const offset = currentPage * limit; + let where = {}; + + const user = (options && options.currentUser) || null; + if (!globalAccess && user?.organizationsId) { + where.organizationsId = user.organizationsId; + } + + if (filter) { + if (filter.id) where.id = Utils.uuid(filter.id); + if (filter.name) where[Op.and] = Utils.ilike('groups', 'name', filter.name); + } + + const { rows, count } = await db.groups.findAndCountAll({ + where, + include: [ + { model: db.tenants, as: 'tenant' }, + { model: db.organizations, as: 'organizations' } + ], + distinct: true, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + order: filter.field && filter.sort ? [[filter.field, filter.sort]] : [['createdAt', 'desc']], + transaction: options?.transaction, + }); + + return { rows, count }; + } + + static async findAllAutocomplete(query, limit, offset, globalAccess, organizationId) { + let where = {}; + if (!globalAccess && organizationId) where.organizationsId = organizationId; + + if (query) { + where[Op.or] = [ + { id: Utils.uuid(query) }, + Utils.ilike('groups', 'name', query), + ]; + } + + const records = await db.groups.findAll({ + attributes: ['id', 'name'], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + order: [['name', 'ASC']], + }); + + return records.map((record) => ({ id: record.id, label: record.name })); + } +}; diff --git a/backend/src/db/api/growth_habits.js b/backend/src/db/api/growth_habits.js new file mode 100644 index 0000000..dc71f65 --- /dev/null +++ b/backend/src/db/api/growth_habits.js @@ -0,0 +1,142 @@ + +const db = require('../models'); +const Utils = require('../utils'); +const Sequelize = db.Sequelize; +const Op = Sequelize.Op; + +module.exports = class Growth_habitsDBApi { + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const record = await db.growth_habits.create( + { + id: data.id || undefined, + name: data.name || null, + importHash: data.importHash || null, + tenantId: data.tenant || currentUser.tenantId || null, + organizationsId: data.organizations || currentUser.organizationsId || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + return record; + } + + static async bulkImport(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const recordsData = data.map((item, index) => ({ + id: item.id || undefined, + name: item.name || null, + importHash: item.importHash || null, + tenantId: item.tenant || currentUser.tenantId || null, + organizationsId: item.organizations || currentUser.organizationsId || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + return await db.growth_habits.bulkCreate(recordsData, { transaction }); + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const record = await db.growth_habits.findByPk(id, {}, { transaction }); + const updatePayload = {}; + if (data.name !== undefined) updatePayload.name = data.name; + updatePayload.updatedById = currentUser.id; + + await record.update(updatePayload, { transaction }); + + if (data.tenant !== undefined) await record.setTenant(data.tenant, { transaction }); + if (data.organizations !== undefined) await record.setOrganizations(data.organizations, { transaction }); + + return record; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const records = await db.growth_habits.findAll({ + where: { id: { [Op.in]: ids } }, + transaction, + }); + + for (const record of records) { + await record.destroy({ transaction }); + } + return records; + } + + static async remove(id, options) { + const transaction = (options && options.transaction) || undefined; + const record = await db.growth_habits.findByPk(id, options); + await record.destroy({ transaction }); + return record; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + return await db.growth_habits.findOne({ where }, { transaction }); + } + + static async findAll(filter, globalAccess, options) { + const limit = filter.limit || 0; + const currentPage = +filter.page || 0; + const offset = currentPage * limit; + let where = {}; + + const user = (options && options.currentUser) || null; + if (!globalAccess && user?.organizationsId) { + where.organizationsId = user.organizationsId; + } + + if (filter) { + if (filter.id) where.id = Utils.uuid(filter.id); + if (filter.name) where[Op.and] = Utils.ilike('growth_habits', 'name', filter.name); + } + + const { rows, count } = await db.growth_habits.findAndCountAll({ + where, + include: [ + { model: db.tenants, as: 'tenant' }, + { model: db.organizations, as: 'organizations' } + ], + distinct: true, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + order: filter.field && filter.sort ? [[filter.field, filter.sort]] : [['createdAt', 'desc']], + transaction: options?.transaction, + }); + + return { rows, count }; + } + + static async findAllAutocomplete(query, limit, offset, globalAccess, organizationId) { + let where = {}; + if (!globalAccess && organizationId) where.organizationsId = organizationId; + + if (query) { + where[Op.or] = [ + { id: Utils.uuid(query) }, + Utils.ilike('growth_habits', 'name', query), + ]; + } + + const records = await db.growth_habits.findAll({ + attributes: ['id', 'name'], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + order: [['name', 'ASC']], + }); + + return records.map((record) => ({ id: record.id, label: record.name })); + } +}; diff --git a/backend/src/db/api/locations.js b/backend/src/db/api/locations.js new file mode 100644 index 0000000..24df145 --- /dev/null +++ b/backend/src/db/api/locations.js @@ -0,0 +1,141 @@ +const db = require('../models'); +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 record = await db.locations.create( + { + id: data.id || undefined, + name: data.name || null, + importHash: data.importHash || null, + tenantId: data.tenant || currentUser.tenantId || null, + organizationsId: data.organizations || currentUser.organizationsId || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + return record; + } + + static async bulkImport(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const recordsData = data.map((item, index) => ({ + id: item.id || undefined, + name: item.name || null, + importHash: item.importHash || null, + tenantId: item.tenant || currentUser.tenantId || null, + organizationsId: item.organizations || currentUser.organizationsId || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + return await db.locations.bulkCreate(recordsData, { transaction }); + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const record = await db.locations.findByPk(id, {}, { transaction }); + const updatePayload = {}; + if (data.name !== undefined) updatePayload.name = data.name; + updatePayload.updatedById = currentUser.id; + + await record.update(updatePayload, { transaction }); + + if (data.tenant !== undefined) await record.setTenant(data.tenant, { transaction }); + if (data.organizations !== undefined) await record.setOrganizations(data.organizations, { transaction }); + + return record; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const records = await db.locations.findAll({ + where: { id: { [Op.in]: ids } }, + transaction, + }); + + for (const record of records) { + await record.destroy({ transaction }); + } + return records; + } + + static async remove(id, options) { + const transaction = (options && options.transaction) || undefined; + const record = await db.locations.findByPk(id, options); + await record.destroy({ transaction }); + return record; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + return await db.locations.findOne({ where }, { transaction }); + } + + static async findAll(filter, globalAccess, options) { + const limit = filter.limit || 0; + const currentPage = +filter.page || 0; + const offset = currentPage * limit; + let where = {}; + + const user = (options && options.currentUser) || null; + if (!globalAccess && user?.organizationsId) { + where.organizationsId = user.organizationsId; + } + + if (filter) { + if (filter.id) where.id = Utils.uuid(filter.id); + if (filter.name) where[Op.and] = Utils.ilike('locations', 'name', filter.name); + } + + const { rows, count } = await db.locations.findAndCountAll({ + where, + include: [ + { model: db.tenants, as: 'tenant' }, + { model: db.organizations, as: 'organizations' } + ], + distinct: true, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + order: filter.field && filter.sort ? [[filter.field, filter.sort]] : [['createdAt', 'desc']], + transaction: options?.transaction, + }); + + return { rows, count }; + } + + static async findAllAutocomplete(query, limit, offset, globalAccess, organizationId) { + let where = {}; + if (!globalAccess && organizationId) where.organizationsId = organizationId; + + 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, + order: [['name', 'ASC']], + }); + + return records.map((record) => ({ id: record.id, label: record.name })); + } +}; diff --git a/backend/src/db/api/programs.js b/backend/src/db/api/programs.js new file mode 100644 index 0000000..f7fed39 --- /dev/null +++ b/backend/src/db/api/programs.js @@ -0,0 +1,141 @@ +const db = require('../models'); +const Utils = require('../utils'); +const Sequelize = db.Sequelize; +const Op = Sequelize.Op; + +module.exports = class programsDBApi { + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const record = await db.programs.create( + { + id: data.id || undefined, + name: data.name || null, + importHash: data.importHash || null, + tenantId: data.tenant || currentUser.tenantId || null, + organizationsId: data.organizations || currentUser.organizationsId || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + return record; + } + + static async bulkImport(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const recordsData = data.map((item, index) => ({ + id: item.id || undefined, + name: item.name || null, + importHash: item.importHash || null, + tenantId: item.tenant || currentUser.tenantId || null, + organizationsId: item.organizations || currentUser.organizationsId || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + return await db.programs.bulkCreate(recordsData, { transaction }); + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const record = await db.programs.findByPk(id, {}, { transaction }); + const updatePayload = {}; + if (data.name !== undefined) updatePayload.name = data.name; + updatePayload.updatedById = currentUser.id; + + await record.update(updatePayload, { transaction }); + + if (data.tenant !== undefined) await record.setTenant(data.tenant, { transaction }); + if (data.organizations !== undefined) await record.setOrganizations(data.organizations, { transaction }); + + return record; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const records = await db.programs.findAll({ + where: { id: { [Op.in]: ids } }, + transaction, + }); + + for (const record of records) { + await record.destroy({ transaction }); + } + return records; + } + + static async remove(id, options) { + const transaction = (options && options.transaction) || undefined; + const record = await db.programs.findByPk(id, options); + await record.destroy({ transaction }); + return record; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + return await db.programs.findOne({ where }, { transaction }); + } + + static async findAll(filter, globalAccess, options) { + const limit = filter.limit || 0; + const currentPage = +filter.page || 0; + const offset = currentPage * limit; + let where = {}; + + const user = (options && options.currentUser) || null; + if (!globalAccess && user?.organizationsId) { + where.organizationsId = user.organizationsId; + } + + if (filter) { + if (filter.id) where.id = Utils.uuid(filter.id); + if (filter.name) where[Op.and] = Utils.ilike('programs', 'name', filter.name); + } + + const { rows, count } = await db.programs.findAndCountAll({ + where, + include: [ + { model: db.tenants, as: 'tenant' }, + { model: db.organizations, as: 'organizations' } + ], + distinct: true, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + order: filter.field && filter.sort ? [[filter.field, filter.sort]] : [['createdAt', 'desc']], + transaction: options?.transaction, + }); + + return { rows, count }; + } + + static async findAllAutocomplete(query, limit, offset, globalAccess, organizationId) { + let where = {}; + if (!globalAccess && organizationId) where.organizationsId = organizationId; + + if (query) { + where[Op.or] = [ + { id: Utils.uuid(query) }, + Utils.ilike('programs', 'name', query), + ]; + } + + const records = await db.programs.findAll({ + attributes: ['id', 'name'], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + order: [['name', 'ASC']], + }); + + return records.map((record) => ({ id: record.id, label: record.name })); + } +}; diff --git a/backend/src/db/api/propagation_types.js b/backend/src/db/api/propagation_types.js new file mode 100644 index 0000000..5246758 --- /dev/null +++ b/backend/src/db/api/propagation_types.js @@ -0,0 +1,142 @@ + +const db = require('../models'); +const Utils = require('../utils'); +const Sequelize = db.Sequelize; +const Op = Sequelize.Op; + +module.exports = class Propagation_typesDBApi { + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const record = await db.propagation_types.create( + { + id: data.id || undefined, + name: data.name || null, + importHash: data.importHash || null, + tenantId: data.tenant || currentUser.tenantId || null, + organizationsId: data.organizations || currentUser.organizationsId || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + return record; + } + + static async bulkImport(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const recordsData = data.map((item, index) => ({ + id: item.id || undefined, + name: item.name || null, + importHash: item.importHash || null, + tenantId: item.tenant || currentUser.tenantId || null, + organizationsId: item.organizations || currentUser.organizationsId || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + return await db.propagation_types.bulkCreate(recordsData, { transaction }); + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const record = await db.propagation_types.findByPk(id, {}, { transaction }); + const updatePayload = {}; + if (data.name !== undefined) updatePayload.name = data.name; + updatePayload.updatedById = currentUser.id; + + await record.update(updatePayload, { transaction }); + + if (data.tenant !== undefined) await record.setTenant(data.tenant, { transaction }); + if (data.organizations !== undefined) await record.setOrganizations(data.organizations, { transaction }); + + return record; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const records = await db.propagation_types.findAll({ + where: { id: { [Op.in]: ids } }, + transaction, + }); + + for (const record of records) { + await record.destroy({ transaction }); + } + return records; + } + + static async remove(id, options) { + const transaction = (options && options.transaction) || undefined; + const record = await db.propagation_types.findByPk(id, options); + await record.destroy({ transaction }); + return record; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + return await db.propagation_types.findOne({ where }, { transaction }); + } + + static async findAll(filter, globalAccess, options) { + const limit = filter.limit || 0; + const currentPage = +filter.page || 0; + const offset = currentPage * limit; + let where = {}; + + const user = (options && options.currentUser) || null; + if (!globalAccess && user?.organizationsId) { + where.organizationsId = user.organizationsId; + } + + if (filter) { + if (filter.id) where.id = Utils.uuid(filter.id); + if (filter.name) where[Op.and] = Utils.ilike('propagation_types', 'name', filter.name); + } + + const { rows, count } = await db.propagation_types.findAndCountAll({ + where, + include: [ + { model: db.tenants, as: 'tenant' }, + { model: db.organizations, as: 'organizations' } + ], + distinct: true, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + order: filter.field && filter.sort ? [[filter.field, filter.sort]] : [['createdAt', 'desc']], + transaction: options?.transaction, + }); + + return { rows, count }; + } + + static async findAllAutocomplete(query, limit, offset, globalAccess, organizationId) { + let where = {}; + if (!globalAccess && organizationId) where.organizationsId = organizationId; + + if (query) { + where[Op.or] = [ + { id: Utils.uuid(query) }, + Utils.ilike('propagation_types', 'name', query), + ]; + } + + const records = await db.propagation_types.findAll({ + attributes: ['id', 'name'], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + order: [['name', 'ASC']], + }); + + return records.map((record) => ({ id: record.id, label: record.name })); + } +}; diff --git a/backend/src/db/api/retailer_programs.js b/backend/src/db/api/retailer_programs.js new file mode 100644 index 0000000..b05625b --- /dev/null +++ b/backend/src/db/api/retailer_programs.js @@ -0,0 +1,142 @@ + +const db = require('../models'); +const Utils = require('../utils'); +const Sequelize = db.Sequelize; +const Op = Sequelize.Op; + +module.exports = class Retailer_programsDBApi { + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const record = await db.retailer_programs.create( + { + id: data.id || undefined, + name: data.name || null, + importHash: data.importHash || null, + tenantId: data.tenant || currentUser.tenantId || null, + organizationsId: data.organizations || currentUser.organizationsId || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + return record; + } + + static async bulkImport(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const recordsData = data.map((item, index) => ({ + id: item.id || undefined, + name: item.name || null, + importHash: item.importHash || null, + tenantId: item.tenant || currentUser.tenantId || null, + organizationsId: item.organizations || currentUser.organizationsId || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + return await db.retailer_programs.bulkCreate(recordsData, { transaction }); + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const record = await db.retailer_programs.findByPk(id, {}, { transaction }); + const updatePayload = {}; + if (data.name !== undefined) updatePayload.name = data.name; + updatePayload.updatedById = currentUser.id; + + await record.update(updatePayload, { transaction }); + + if (data.tenant !== undefined) await record.setTenant(data.tenant, { transaction }); + if (data.organizations !== undefined) await record.setOrganizations(data.organizations, { transaction }); + + return record; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const records = await db.retailer_programs.findAll({ + where: { id: { [Op.in]: ids } }, + transaction, + }); + + for (const record of records) { + await record.destroy({ transaction }); + } + return records; + } + + static async remove(id, options) { + const transaction = (options && options.transaction) || undefined; + const record = await db.retailer_programs.findByPk(id, options); + await record.destroy({ transaction }); + return record; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + return await db.retailer_programs.findOne({ where }, { transaction }); + } + + static async findAll(filter, globalAccess, options) { + const limit = filter.limit || 0; + const currentPage = +filter.page || 0; + const offset = currentPage * limit; + let where = {}; + + const user = (options && options.currentUser) || null; + if (!globalAccess && user?.organizationsId) { + where.organizationsId = user.organizationsId; + } + + if (filter) { + if (filter.id) where.id = Utils.uuid(filter.id); + if (filter.name) where[Op.and] = Utils.ilike('retailer_programs', 'name', filter.name); + } + + const { rows, count } = await db.retailer_programs.findAndCountAll({ + where, + include: [ + { model: db.tenants, as: 'tenant' }, + { model: db.organizations, as: 'organizations' } + ], + distinct: true, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + order: filter.field && filter.sort ? [[filter.field, filter.sort]] : [['createdAt', 'desc']], + transaction: options?.transaction, + }); + + return { rows, count }; + } + + static async findAllAutocomplete(query, limit, offset, globalAccess, organizationId) { + let where = {}; + if (!globalAccess && organizationId) where.organizationsId = organizationId; + + if (query) { + where[Op.or] = [ + { id: Utils.uuid(query) }, + Utils.ilike('retailer_programs', 'name', query), + ]; + } + + const records = await db.retailer_programs.findAll({ + attributes: ['id', 'name'], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + order: [['name', 'ASC']], + }); + + return records.map((record) => ({ id: record.id, label: record.name })); + } +}; diff --git a/backend/src/db/api/retailers.js b/backend/src/db/api/retailers.js new file mode 100644 index 0000000..c66145d --- /dev/null +++ b/backend/src/db/api/retailers.js @@ -0,0 +1,141 @@ +const db = require('../models'); +const Utils = require('../utils'); +const Sequelize = db.Sequelize; +const Op = Sequelize.Op; + +module.exports = class retailersDBApi { + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const record = await db.retailers.create( + { + id: data.id || undefined, + name: data.name || null, + importHash: data.importHash || null, + tenantId: data.tenant || currentUser.tenantId || null, + organizationsId: data.organizations || currentUser.organizationsId || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + return record; + } + + static async bulkImport(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const recordsData = data.map((item, index) => ({ + id: item.id || undefined, + name: item.name || null, + importHash: item.importHash || null, + tenantId: item.tenant || currentUser.tenantId || null, + organizationsId: item.organizations || currentUser.organizationsId || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + return await db.retailers.bulkCreate(recordsData, { transaction }); + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const record = await db.retailers.findByPk(id, {}, { transaction }); + const updatePayload = {}; + if (data.name !== undefined) updatePayload.name = data.name; + updatePayload.updatedById = currentUser.id; + + await record.update(updatePayload, { transaction }); + + if (data.tenant !== undefined) await record.setTenant(data.tenant, { transaction }); + if (data.organizations !== undefined) await record.setOrganizations(data.organizations, { transaction }); + + return record; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const records = await db.retailers.findAll({ + where: { id: { [Op.in]: ids } }, + transaction, + }); + + for (const record of records) { + await record.destroy({ transaction }); + } + return records; + } + + static async remove(id, options) { + const transaction = (options && options.transaction) || undefined; + const record = await db.retailers.findByPk(id, options); + await record.destroy({ transaction }); + return record; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + return await db.retailers.findOne({ where }, { transaction }); + } + + static async findAll(filter, globalAccess, options) { + const limit = filter.limit || 0; + const currentPage = +filter.page || 0; + const offset = currentPage * limit; + let where = {}; + + const user = (options && options.currentUser) || null; + if (!globalAccess && user?.organizationsId) { + where.organizationsId = user.organizationsId; + } + + if (filter) { + if (filter.id) where.id = Utils.uuid(filter.id); + if (filter.name) where[Op.and] = Utils.ilike('retailers', 'name', filter.name); + } + + const { rows, count } = await db.retailers.findAndCountAll({ + where, + include: [ + { model: db.tenants, as: 'tenant' }, + { model: db.organizations, as: 'organizations' } + ], + distinct: true, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + order: filter.field && filter.sort ? [[filter.field, filter.sort]] : [['createdAt', 'desc']], + transaction: options?.transaction, + }); + + return { rows, count }; + } + + static async findAllAutocomplete(query, limit, offset, globalAccess, organizationId) { + let where = {}; + if (!globalAccess && organizationId) where.organizationsId = organizationId; + + if (query) { + where[Op.or] = [ + { id: Utils.uuid(query) }, + Utils.ilike('retailers', 'name', query), + ]; + } + + const records = await db.retailers.findAll({ + attributes: ['id', 'name'], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + order: [['name', 'ASC']], + }); + + return records.map((record) => ({ id: record.id, label: record.name })); + } +}; diff --git a/backend/src/db/api/sites.js b/backend/src/db/api/sites.js new file mode 100644 index 0000000..619f5af --- /dev/null +++ b/backend/src/db/api/sites.js @@ -0,0 +1,141 @@ +const db = require('../models'); +const Utils = require('../utils'); +const Sequelize = db.Sequelize; +const Op = Sequelize.Op; + +module.exports = class sitesDBApi { + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const record = await db.sites.create( + { + id: data.id || undefined, + name: data.name || null, + importHash: data.importHash || null, + tenantId: data.tenant || currentUser.tenantId || null, + organizationsId: data.organizations || currentUser.organizationsId || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + return record; + } + + static async bulkImport(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const recordsData = data.map((item, index) => ({ + id: item.id || undefined, + name: item.name || null, + importHash: item.importHash || null, + tenantId: item.tenant || currentUser.tenantId || null, + organizationsId: item.organizations || currentUser.organizationsId || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + return await db.sites.bulkCreate(recordsData, { transaction }); + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const record = await db.sites.findByPk(id, {}, { transaction }); + const updatePayload = {}; + if (data.name !== undefined) updatePayload.name = data.name; + updatePayload.updatedById = currentUser.id; + + await record.update(updatePayload, { transaction }); + + if (data.tenant !== undefined) await record.setTenant(data.tenant, { transaction }); + if (data.organizations !== undefined) await record.setOrganizations(data.organizations, { transaction }); + + return record; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const records = await db.sites.findAll({ + where: { id: { [Op.in]: ids } }, + transaction, + }); + + for (const record of records) { + await record.destroy({ transaction }); + } + return records; + } + + static async remove(id, options) { + const transaction = (options && options.transaction) || undefined; + const record = await db.sites.findByPk(id, options); + await record.destroy({ transaction }); + return record; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + return await db.sites.findOne({ where }, { transaction }); + } + + static async findAll(filter, globalAccess, options) { + const limit = filter.limit || 0; + const currentPage = +filter.page || 0; + const offset = currentPage * limit; + let where = {}; + + const user = (options && options.currentUser) || null; + if (!globalAccess && user?.organizationsId) { + where.organizationsId = user.organizationsId; + } + + if (filter) { + if (filter.id) where.id = Utils.uuid(filter.id); + if (filter.name) where[Op.and] = Utils.ilike('sites', 'name', filter.name); + } + + const { rows, count } = await db.sites.findAndCountAll({ + where, + include: [ + { model: db.tenants, as: 'tenant' }, + { model: db.organizations, as: 'organizations' } + ], + distinct: true, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + order: filter.field && filter.sort ? [[filter.field, filter.sort]] : [['createdAt', 'desc']], + transaction: options?.transaction, + }); + + return { rows, count }; + } + + static async findAllAutocomplete(query, limit, offset, globalAccess, organizationId) { + let where = {}; + if (!globalAccess && organizationId) where.organizationsId = organizationId; + + if (query) { + where[Op.or] = [ + { id: Utils.uuid(query) }, + Utils.ilike('sites', 'name', query), + ]; + } + + const records = await db.sites.findAll({ + attributes: ['id', 'name'], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + order: [['name', 'ASC']], + }); + + return records.map((record) => ({ id: record.id, label: record.name })); + } +}; diff --git a/backend/src/db/api/trials.js b/backend/src/db/api/trials.js index 8f86e7b..5ac3648 100644 --- a/backend/src/db/api/trials.js +++ b/backend/src/db/api/trials.js @@ -1,18 +1,12 @@ - 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 TrialsDBApi { - - - static async create(data, options) { const currentUser = (options && options.currentUser) || { id: null }; const transaction = (options && options.transaction) || undefined; @@ -20,330 +14,144 @@ module.exports = class TrialsDBApi { const trials = await db.trials.create( { id: data.id || undefined, - - name: data.name - || - null - , - - variety_name: data.variety_name - || - null - , - - breeder: data.breeder - || - null - , - - batch_code: data.batch_code - || - null - , - - trial_type: data.trial_type - || - null - , - - status: data.status - || - null - , - - planted_at: data.planted_at - || - null - , - - harvested_at: data.harvested_at - || - null - , - - greenhouse: data.greenhouse - || - null - , - - zone: data.zone - || - null - , - - bench: data.bench - || - null - , - - plants_count: data.plants_count - || - null - , - - target_temperature_c: data.target_temperature_c - || - null - , - - target_humidity_percent: data.target_humidity_percent - || - null - , - - target_ec: data.target_ec - || - null - , - - target_ph: data.target_ph - || - null - , - - notes: data.notes - || - null - , - - importHash: data.importHash || null, - createdById: currentUser.id, - updatedById: currentUser.id, - }, - { transaction }, - ); + name: data.name || null, + trial_code: data.trial_code || null, + variety_name: data.variety_name || null, + genus: data.genus || null, + species: data.species || null, + colour: data.colour || null, + batch_code: data.batch_code || null, + trial_type: data.trial_type || null, + status: data.status || null, + planted_at: data.planted_at || null, + harvested_at: data.harvested_at || null, + greenhouse: data.greenhouse || null, + zone: data.zone || null, + bench: data.bench || null, + plants_count: data.plants_count || null, + target_temperature_c: data.target_temperature_c || null, + target_humidity_percent: data.target_humidity_percent || null, + target_ec: data.target_ec || null, + target_ph: data.target_ph || null, + notes: data.notes || null, + containerSize: data.containerSize || null, + finishedHeight: data.finishedHeight || null, + finishedSpread: data.finishedSpread || null, + description: data.description || null, + internalReferenceNumber: data.internalReferenceNumber || null, + programYear: data.programYear || null, + distributor: data.distributor || null, + quantity: data.quantity || null, + receiveWeek: data.receiveWeek || null, + finishWeek: data.finishWeek || null, + tableId: data.tableId || null, + breederId: data.breeder || null, + propagationTypeId: data.propagationType || null, + cropTypeId: data.cropType || null, + containerId: data.container || null, + cultureSheetId: data.cultureSheet && data.cultureSheet.length ? data.cultureSheet[0].id : (data.cultureSheet || null), + programId: data.program || null, + siteId: data.site || null, + locationId: data.location || null, + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + tenantId: data.tenant || currentUser.tenantId || null, + projectId: data.project || null, + organizationsId: data.organizations || currentUser.organizationsId || null, + trial_category: data.trial_category || 'PLANT', + attributes: data.attributes || {}, + }, + { transaction }, + ); - - await trials.setTenant( data.tenant || null, { - transaction, - }); - - await trials.setProject( data.project || null, { - transaction, - }); - - await trials.setOrganizations( data.organizations || null, { - transaction, - }); - + if (data.growthHabits && data.growthHabits.length) { + await trials.setGrowthHabits(data.growthHabits, { transaction }); + } - + if (data.retailers && data.retailers.length) { + await trials.setRetailers(data.retailers, { transaction }); + } - + if (data.groups && data.groups.length) { + await trials.setGroups(data.groups, { transaction }); + } return trials; } - - static async bulkImport(data, options) { + static async update(id, 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 trialsData = data.map((item, index) => ({ - id: item.id || undefined, - - name: item.name - || - null - , - - variety_name: item.variety_name - || - null - , - - breeder: item.breeder - || - null - , - - batch_code: item.batch_code - || - null - , - - trial_type: item.trial_type - || - null - , - - status: item.status - || - null - , - - planted_at: item.planted_at - || - null - , - - harvested_at: item.harvested_at - || - null - , - - greenhouse: item.greenhouse - || - null - , - - zone: item.zone - || - null - , - - bench: item.bench - || - null - , - - plants_count: item.plants_count - || - null - , - - target_temperature_c: item.target_temperature_c - || - null - , - - target_humidity_percent: item.target_humidity_percent - || - null - , - - target_ec: item.target_ec - || - null - , - - target_ph: item.target_ph - || - null - , - - notes: item.notes - || - null - , - - importHash: item.importHash || null, - createdById: currentUser.id, - updatedById: currentUser.id, - createdAt: new Date(Date.now() + index * 1000), - })); - - // Bulk create items - const trials = await db.trials.bulkCreate(trialsData, { transaction }); - - // For each item created, replace relation files - - - return trials; - } - - static async update(id, data, options) { - const currentUser = (options && options.currentUser) || {id: null}; - const transaction = (options && options.transaction) || undefined; - const globalAccess = currentUser.app_role?.globalAccess; - - const trials = await db.trials.findByPk(id, {}, {transaction}); - - - + const trials = await db.trials.findByPk(id, {}, { transaction }); const updatePayload = {}; - if (data.name !== undefined) updatePayload.name = data.name; - - + if (data.trial_code !== undefined) updatePayload.trial_code = data.trial_code; if (data.variety_name !== undefined) updatePayload.variety_name = data.variety_name; - - - if (data.breeder !== undefined) updatePayload.breeder = data.breeder; - - + if (data.genus !== undefined) updatePayload.genus = data.genus; + if (data.species !== undefined) updatePayload.species = data.species; + if (data.colour !== undefined) updatePayload.colour = data.colour; if (data.batch_code !== undefined) updatePayload.batch_code = data.batch_code; - - if (data.trial_type !== undefined) updatePayload.trial_type = data.trial_type; - - if (data.status !== undefined) updatePayload.status = data.status; - - if (data.planted_at !== undefined) updatePayload.planted_at = data.planted_at; - - if (data.harvested_at !== undefined) updatePayload.harvested_at = data.harvested_at; - - if (data.greenhouse !== undefined) updatePayload.greenhouse = data.greenhouse; - - if (data.zone !== undefined) updatePayload.zone = data.zone; - - if (data.bench !== undefined) updatePayload.bench = data.bench; - - if (data.plants_count !== undefined) updatePayload.plants_count = data.plants_count; - - if (data.target_temperature_c !== undefined) updatePayload.target_temperature_c = data.target_temperature_c; - - if (data.target_humidity_percent !== undefined) updatePayload.target_humidity_percent = data.target_humidity_percent; - - if (data.target_ec !== undefined) updatePayload.target_ec = data.target_ec; - - if (data.target_ph !== undefined) updatePayload.target_ph = data.target_ph; - - if (data.notes !== undefined) updatePayload.notes = data.notes; + if (data.containerSize !== undefined) updatePayload.containerSize = data.containerSize; + if (data.finishedHeight !== undefined) updatePayload.finishedHeight = data.finishedHeight; + if (data.finishedSpread !== undefined) updatePayload.finishedSpread = data.finishedSpread; + if (data.description !== undefined) updatePayload.description = data.description; + if (data.internalReferenceNumber !== undefined) updatePayload.internalReferenceNumber = data.internalReferenceNumber; + if (data.programYear !== undefined) updatePayload.programYear = data.programYear; + if (data.distributor !== undefined) updatePayload.distributor = data.distributor; + if (data.quantity !== undefined) updatePayload.quantity = data.quantity; + if (data.receiveWeek !== undefined) updatePayload.receiveWeek = data.receiveWeek; + if (data.finishWeek !== undefined) updatePayload.finishWeek = data.finishWeek; + if (data.tableId !== undefined) updatePayload.tableId = data.tableId; + + if (data.breeder !== undefined) updatePayload.breederId = data.breeder; + if (data.propagationType !== undefined) updatePayload.propagationTypeId = data.propagationType; + if (data.cropType !== undefined) updatePayload.cropTypeId = data.cropType; + if (data.container !== undefined) updatePayload.containerId = data.container; + if (data.cultureSheet !== undefined) { + updatePayload.cultureSheetId = data.cultureSheet && data.cultureSheet.length ? data.cultureSheet[0].id : (data.cultureSheet || null); + } + if (data.program !== undefined) updatePayload.programId = data.program; + if (data.site !== undefined) updatePayload.siteId = data.site; + if (data.location !== undefined) updatePayload.locationId = data.location; + + if (data.tenant !== undefined) updatePayload.tenantId = data.tenant; + if (data.project !== undefined) updatePayload.projectId = data.project; + if (data.organizations !== undefined) updatePayload.organizationsId = data.organizations; + if (data.trial_category !== undefined) updatePayload.trial_category = data.trial_category; + if (data.attributes !== undefined) updatePayload.attributes = data.attributes; updatePayload.updatedById = currentUser.id; - await trials.update(updatePayload, {transaction}); + await trials.update(updatePayload, { transaction }); - - - if (data.tenant !== undefined) { - await trials.setTenant( - - data.tenant, - - { transaction } - ); + if (data.growthHabits !== undefined) { + await trials.setGrowthHabits(data.growthHabits || [], { transaction }); } - - if (data.project !== undefined) { - await trials.setProject( - - data.project, - - { transaction } - ); - } - - if (data.organizations !== undefined) { - await trials.setOrganizations( - - data.organizations, - - { transaction } - ); - } - - - + if (data.retailers !== undefined) { + await trials.setRetailers(data.retailers || [], { transaction }); + } - + if (data.groups !== undefined) { + await trials.setGroups(data.groups || [], { transaction }); + } return trials; } @@ -353,45 +161,25 @@ module.exports = class TrialsDBApi { const transaction = (options && options.transaction) || undefined; const trials = await db.trials.findAll({ - where: { - id: { - [Op.in]: ids, - }, - }, + where: { id: { [Op.in]: ids } }, transaction, }); - await db.sequelize.transaction(async (transaction) => { - for (const record of trials) { - await record.update( - {deletedBy: currentUser.id}, - {transaction} - ); - } - for (const record of trials) { - await record.destroy({transaction}); - } - }); - + for (const record of trials) { + await record.update({ deletedBy: currentUser.id }, { transaction }); + await record.destroy({ transaction }); + } return trials; } static async remove(id, options) { - const currentUser = (options && options.currentUser) || {id: null}; + const currentUser = (options && options.currentUser) || { id: null }; const transaction = (options && options.transaction) || undefined; const trials = await db.trials.findByPk(id, options); - - await trials.update({ - deletedBy: currentUser.id - }, { - transaction, - }); - - await trials.destroy({ - transaction - }); + await trials.update({ deletedBy: currentUser.id }, { transaction }); + await trials.destroy({ transaction }); return trials; } @@ -400,558 +188,120 @@ module.exports = class TrialsDBApi { const transaction = (options && options.transaction) || undefined; const trials = await db.trials.findOne( - { where }, + { + where, + include: [ + { model: db.breeders, as: 'breeder' }, + { model: db.propagation_types, as: 'propagationType' }, + { model: db.crop_types, as: 'cropType' }, + { model: db.growth_habits, as: 'growthHabits' }, + { model: db.containers, as: 'container' }, + { model: db.file, as: 'cultureSheet' }, + { model: db.programs, as: 'program' }, + { model: db.sites, as: 'site' }, + { model: db.locations, as: 'location' }, + { model: db.retailers, as: 'retailers' }, + { model: db.groups, as: 'groups' }, + { model: db.tenants, as: 'tenant' }, + { model: db.projects, as: 'project' }, + { model: db.organizations, as: 'organizations' } + ] + }, { transaction }, ); - if (!trials) { - return trials; - } - - const output = trials.get({plain: true}); + if (!trials) return trials; + const output = trials.get({ plain: true }); - - - - - - - - - - output.tracking_activities_trial = await trials.getTracking_activities_trial({ - transaction - }); - - - - - - - - output.tenant = await trials.getTenant({ - transaction - }); - - - output.project = await trials.getProject({ - transaction - }); - - - output.organizations = await trials.getOrganizations({ - transaction - }); - - + output.tracking_activities_trial = await trials.getTracking_activities_trial({ transaction }); return output; } - static async findAll( - filter, - globalAccess, options - ) { + static async findAll(filter, globalAccess, options) { const limit = filter.limit || 0; - let offset = 0; + const currentPage = +filter.page || 0; + const offset = currentPage * limit; let where = {}; - const currentPage = +filter.page; - const user = (options && options.currentUser) || null; - const userOrganizations = (user && user.organizations?.id) || null; - - - - if (userOrganizations) { - if (options?.currentUser?.organizationsId) { - where.organizationsId = options.currentUser.organizationsId; - } + if (!globalAccess && user?.organizationsId) { + where.organizationsId = user.organizationsId; } - - - offset = currentPage * limit; - - const orderBy = null; - - const transaction = (options && options.transaction) || undefined; - - let include = [ - - { - model: db.tenants, - as: 'tenant', - - where: filter.tenant ? { - [Op.or]: [ - { id: { [Op.in]: filter.tenant.split('|').map(term => Utils.uuid(term)) } }, - { - name: { - [Op.or]: filter.tenant.split('|').map(term => ({ [Op.iLike]: `%${term}%` })) - } - }, - ] - } : {}, - - }, - - { - model: db.projects, - as: 'project', - - where: filter.project ? { - [Op.or]: [ - { id: { [Op.in]: filter.project.split('|').map(term => Utils.uuid(term)) } }, - { - name: { - [Op.or]: filter.project.split('|').map(term => ({ [Op.iLike]: `%${term}%` })) - } - }, - ] - } : {}, - - }, - - { - model: db.organizations, - as: 'organizations', - - }, - - - - ]; if (filter) { - if (filter.id) { - where = { - ...where, - ['id']: Utils.uuid(filter.id), - }; - } - + if (filter.id) where.id = Utils.uuid(filter.id); + if (filter.name) where[Op.and] = Utils.ilike('trials', 'name', filter.name); + if (filter.trial_code) where[Op.and] = Utils.ilike('trials', 'trial_code', filter.trial_code); + if (filter.variety_name) where[Op.and] = Utils.ilike('trials', 'variety_name', filter.variety_name); + if (filter.genus) where[Op.and] = Utils.ilike('trials', 'genus', filter.genus); + if (filter.species) where[Op.and] = Utils.ilike('trials', 'species', filter.species); + if (filter.colour) where[Op.and] = Utils.ilike('trials', 'colour', filter.colour); + if (filter.batch_code) where[Op.and] = Utils.ilike('trials', 'batch_code', filter.batch_code); + if (filter.status) where.status = filter.status; + if (filter.trial_type) where.trial_type = filter.trial_type; + if (filter.trial_category) where.trial_category = filter.trial_category; - if (filter.name) { - where = { - ...where, - [Op.and]: Utils.ilike( - 'trials', - 'name', - filter.name, - ), - }; - } - - if (filter.variety_name) { - where = { - ...where, - [Op.and]: Utils.ilike( - 'trials', - 'variety_name', - filter.variety_name, - ), - }; - } - - if (filter.breeder) { - where = { - ...where, - [Op.and]: Utils.ilike( - 'trials', - 'breeder', - filter.breeder, - ), - }; - } - - if (filter.batch_code) { - where = { - ...where, - [Op.and]: Utils.ilike( - 'trials', - 'batch_code', - filter.batch_code, - ), - }; - } - - if (filter.greenhouse) { - where = { - ...where, - [Op.and]: Utils.ilike( - 'trials', - 'greenhouse', - filter.greenhouse, - ), - }; - } - - if (filter.zone) { - where = { - ...where, - [Op.and]: Utils.ilike( - 'trials', - 'zone', - filter.zone, - ), - }; - } - - if (filter.bench) { - where = { - ...where, - [Op.and]: Utils.ilike( - 'trials', - 'bench', - filter.bench, - ), - }; - } - - if (filter.notes) { - where = { - ...where, - [Op.and]: Utils.ilike( - 'trials', - 'notes', - filter.notes, - ), - }; - } - - - - - if (filter.calendarStart && filter.calendarEnd) { - where = { - ...where, - [Op.or]: [ - { - planted_at: { - [Op.between]: [filter.calendarStart, filter.calendarEnd], - }, - }, - { - harvested_at: { - [Op.between]: [filter.calendarStart, filter.calendarEnd], - }, - }, - ], - }; - } - - - - if (filter.planted_atRange) { - const [start, end] = filter.planted_atRange; - - if (start !== undefined && start !== null && start !== '') { - where = { - ...where, - planted_at: { - ...where.planted_at, - [Op.gte]: start, - }, - }; - } - - if (end !== undefined && end !== null && end !== '') { - where = { - ...where, - planted_at: { - ...where.planted_at, - [Op.lte]: end, - }, - }; - } - } - - if (filter.harvested_atRange) { - const [start, end] = filter.harvested_atRange; - - if (start !== undefined && start !== null && start !== '') { - where = { - ...where, - harvested_at: { - ...where.harvested_at, - [Op.gte]: start, - }, - }; - } - - if (end !== undefined && end !== null && end !== '') { - where = { - ...where, - harvested_at: { - ...where.harvested_at, - [Op.lte]: end, - }, - }; - } - } - - if (filter.plants_countRange) { - const [start, end] = filter.plants_countRange; - - if (start !== undefined && start !== null && start !== '') { - where = { - ...where, - plants_count: { - ...where.plants_count, - [Op.gte]: start, - }, - }; - } - - if (end !== undefined && end !== null && end !== '') { - where = { - ...where, - plants_count: { - ...where.plants_count, - [Op.lte]: end, - }, - }; - } - } - - if (filter.target_temperature_cRange) { - const [start, end] = filter.target_temperature_cRange; - - if (start !== undefined && start !== null && start !== '') { - where = { - ...where, - target_temperature_c: { - ...where.target_temperature_c, - [Op.gte]: start, - }, - }; - } - - if (end !== undefined && end !== null && end !== '') { - where = { - ...where, - target_temperature_c: { - ...where.target_temperature_c, - [Op.lte]: end, - }, - }; - } - } - - if (filter.target_humidity_percentRange) { - const [start, end] = filter.target_humidity_percentRange; - - if (start !== undefined && start !== null && start !== '') { - where = { - ...where, - target_humidity_percent: { - ...where.target_humidity_percent, - [Op.gte]: start, - }, - }; - } - - if (end !== undefined && end !== null && end !== '') { - where = { - ...where, - target_humidity_percent: { - ...where.target_humidity_percent, - [Op.lte]: end, - }, - }; - } - } - - if (filter.target_ecRange) { - const [start, end] = filter.target_ecRange; - - if (start !== undefined && start !== null && start !== '') { - where = { - ...where, - target_ec: { - ...where.target_ec, - [Op.gte]: start, - }, - }; - } - - if (end !== undefined && end !== null && end !== '') { - where = { - ...where, - target_ec: { - ...where.target_ec, - [Op.lte]: end, - }, - }; - } - } - - if (filter.target_phRange) { - const [start, end] = filter.target_phRange; - - if (start !== undefined && start !== null && start !== '') { - where = { - ...where, - target_ph: { - ...where.target_ph, - [Op.gte]: start, - }, - }; - } - - if (end !== undefined && end !== null && end !== '') { - where = { - ...where, - target_ph: { - ...where.target_ph, - [Op.lte]: end, - }, - }; - } - } - - - if (filter.active !== undefined) { - where = { - ...where, - active: filter.active === true || filter.active === 'true' - }; - } - - - if (filter.trial_type) { - where = { - ...where, - trial_type: filter.trial_type, - }; - } - - if (filter.status) { - where = { - ...where, - status: filter.status, - }; - } - - - - - - - - - if (filter.organizations) { - const listItems = filter.organizations.split('|').map(item => { - return Utils.uuid(item) - }); - - where = { - ...where, - organizationsId: {[Op.or]: listItems} - }; - } - - - - - if (filter.createdAtRange) { - const [start, end] = filter.createdAtRange; - - if (start !== undefined && start !== null && start !== '') { - where = { - ...where, - ['createdAt']: { - ...where.createdAt, - [Op.gte]: start, - }, - }; - } - - if (end !== undefined && end !== null && end !== '') { - where = { - ...where, - ['createdAt']: { - ...where.createdAt, - [Op.lte]: end, - }, - }; - } - } + if (filter.breeder) where.breederId = filter.breeder; + if (filter.propagationType) where.propagationTypeId = filter.propagationType; + if (filter.cropType) where.cropTypeId = filter.cropType; + if (filter.project) where.projectId = filter.project; + if (filter.container) where.containerId = filter.container; + if (filter.program) where.programId = filter.program; + if (filter.site) where.siteId = filter.site; + if (filter.location) where.locationId = filter.location; } - - - - if (globalAccess) { - delete where.organizationsId; - } - const queryOptions = { where, - include, + include: [ + { model: db.breeders, as: 'breeder' }, + { model: db.propagation_types, as: 'propagationType' }, + { model: db.crop_types, as: 'cropType' }, + { model: db.growth_habits, as: 'growthHabits' }, + { model: db.containers, as: 'container' }, + { model: db.file, as: 'cultureSheet' }, + { model: db.programs, as: 'program' }, + { model: db.sites, as: 'site' }, + { model: db.locations, as: 'location' }, + { model: db.retailers, as: 'retailers' }, + { model: db.groups, as: 'groups' }, + { model: db.tenants, as: 'tenant' }, + { model: db.projects, as: 'project' }, + { model: db.organizations, as: 'organizations' } + ], distinct: true, - order: filter.field && filter.sort - ? [[filter.field, filter.sort]] - : [['createdAt', 'desc']], + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + 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; - } + const { rows, count } = await db.trials.findAndCountAll(queryOptions); - try { - const { rows, count } = await db.trials.findAndCountAll(queryOptions); - - return { - rows: options?.countOnly ? [] : rows, - count: count - }; - } catch (error) { - console.error('Error executing query:', error); - throw error; - } + return { rows, count }; } - static async findAllAutocomplete(query, limit, offset, globalAccess, organizationId,) { + static async findAllAutocomplete(query, limit, offset, globalAccess, organizationId) { let where = {}; - - - if (!globalAccess && organizationId) { - where.organizationId = organizationId; - } - + if (!globalAccess && organizationId) where.organizationsId = organizationId; if (query) { - where = { - [Op.or]: [ - { ['id']: Utils.uuid(query) }, - Utils.ilike( - 'trials', - 'name', - query, - ), - ], - }; + where[Op.or] = [ + { id: Utils.uuid(query) }, + Utils.ilike('trials', 'name', query), + ]; } const records = await db.trials.findAll({ - attributes: [ 'id', 'name' ], + attributes: ['id', 'name'], where, limit: limit ? Number(limit) : undefined, offset: offset ? Number(offset) : undefined, - orderBy: [['name', 'ASC']], + order: [['name', 'ASC']], }); - return records.map((record) => ({ - id: record.id, - label: record.name, - })); + return records.map((record) => ({ id: record.id, label: record.name })); } - - }; - diff --git a/backend/src/db/db.config.js b/backend/src/db/db.config.js index 9878f88..8f37ad8 100644 --- a/backend/src/db/db.config.js +++ b/backend/src/db/db.config.js @@ -1,4 +1,4 @@ - +require('dotenv').config(); module.exports = { production: { @@ -12,11 +12,12 @@ module.exports = { seederStorage: 'sequelize', }, development: { - username: 'postgres', dialect: 'postgres', - password: '', - database: 'db_trial_tracker', - host: process.env.DB_HOST || 'localhost', + username: process.env.DB_USER, + password: process.env.DB_PASS, + database: process.env.DB_NAME, + host: process.env.DB_HOST, + port: process.env.DB_PORT, logging: console.log, seederStorage: 'sequelize', }, @@ -30,4 +31,4 @@ module.exports = { logging: console.log, seederStorage: 'sequelize', } -}; +}; \ No newline at end of file diff --git a/backend/src/db/migrations/1771207082257.js b/backend/src/db/migrations/1771207082257.js new file mode 100644 index 0000000..22800b0 --- /dev/null +++ b/backend/src/db/migrations/1771207082257.js @@ -0,0 +1,93 @@ + +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + const transaction = await queryInterface.sequelize.transaction(); + try { + const tables = [ + 'propagation_types', + 'growth_habits', + 'crop_types', + 'containers', + 'retailer_programs' + ]; + + for (const tableName of tables) { + await queryInterface.createTable(tableName, { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.DataTypes.UUIDV4, + primaryKey: true, + }, + name: { + type: Sequelize.DataTypes.TEXT, + allowNull: false, + }, + tenantId: { + type: Sequelize.DataTypes.UUID, + references: { + model: 'tenants', + key: 'id', + }, + }, + organizationsId: { + type: Sequelize.DataTypes.UUID, + references: { + model: 'organizations', + key: 'id', + }, + }, + createdById: { + type: Sequelize.DataTypes.UUID, + references: { + model: 'users', + key: 'id', + }, + }, + updatedById: { + type: Sequelize.DataTypes.UUID, + references: { + model: 'users', + key: 'id', + }, + }, + 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; + } + }, + async down(queryInterface, Sequelize) { + const transaction = await queryInterface.sequelize.transaction(); + try { + const tables = [ + 'propagation_types', + 'growth_habits', + 'crop_types', + 'containers', + 'retailer_programs' + ]; + for (const tableName of tables) { + await queryInterface.dropTable(tableName, { transaction }); + } + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + } +}; diff --git a/backend/src/db/migrations/20260216000001-trials-update.js b/backend/src/db/migrations/20260216000001-trials-update.js new file mode 100644 index 0000000..51598b9 --- /dev/null +++ b/backend/src/db/migrations/20260216000001-trials-update.js @@ -0,0 +1,138 @@ +module.exports = { + async up(queryInterface, Sequelize) { + const transaction = await queryInterface.sequelize.transaction(); + try { + // Create breeders table + await queryInterface.createTable('breeders', { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.DataTypes.UUIDV4, + primaryKey: true, + }, + name: { + type: Sequelize.DataTypes.TEXT, + allowNull: false, + }, + tenantId: { + type: Sequelize.DataTypes.UUID, + references: { + model: 'tenants', + key: 'id', + }, + }, + organizationsId: { + type: Sequelize.DataTypes.UUID, + references: { + model: 'organizations', + key: 'id', + }, + }, + createdById: { + type: Sequelize.DataTypes.UUID, + references: { + model: 'users', + key: 'id', + }, + }, + updatedById: { + type: Sequelize.DataTypes.UUID, + references: { + model: 'users', + key: 'id', + }, + }, + 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 }); + + // Add columns to trials + await queryInterface.addColumn('trials', 'trial_code', { type: Sequelize.DataTypes.TEXT }, { transaction }); + await queryInterface.addColumn('trials', 'genus', { type: Sequelize.DataTypes.TEXT }, { transaction }); + await queryInterface.addColumn('trials', 'species', { type: Sequelize.DataTypes.TEXT }, { transaction }); + await queryInterface.addColumn('trials', 'colour', { type: Sequelize.DataTypes.TEXT }, { transaction }); + + await queryInterface.addColumn('trials', 'breederId', { + type: Sequelize.DataTypes.UUID, + references: { + model: 'breeders', + key: 'id', + }, + }, { transaction }); + + await queryInterface.addColumn('trials', 'propagationTypeId', { + type: Sequelize.DataTypes.UUID, + references: { + model: 'propagation_types', + key: 'id', + }, + }, { transaction }); + + await queryInterface.addColumn('trials', 'cropTypeId', { + type: Sequelize.DataTypes.UUID, + references: { + model: 'crop_types', + key: 'id', + }, + }, { transaction }); + + // Create join table for growth_habits (multi-select) + await queryInterface.createTable('trials_growth_habits', { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.DataTypes.UUIDV4, + primaryKey: true, + }, + trialId: { + type: Sequelize.DataTypes.UUID, + references: { + model: 'trials', + key: 'id', + }, + allowNull: false, + onDelete: 'CASCADE', + }, + growthHabitId: { + type: Sequelize.DataTypes.UUID, + references: { + model: 'growth_habits', + key: 'id', + }, + allowNull: false, + onDelete: 'CASCADE', + }, + createdAt: { type: Sequelize.DataTypes.DATE }, + updatedAt: { type: Sequelize.DataTypes.DATE }, + }, { transaction }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + + async down(queryInterface, Sequelize) { + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.dropTable('trials_growth_habits', { transaction }); + await queryInterface.removeColumn('trials', 'cropTypeId', { transaction }); + await queryInterface.removeColumn('trials', 'propagationTypeId', { transaction }); + await queryInterface.removeColumn('trials', 'breederId', { transaction }); + await queryInterface.removeColumn('trials', 'colour', { transaction }); + await queryInterface.removeColumn('trials', 'species', { transaction }); + await queryInterface.removeColumn('trials', 'genus', { transaction }); + await queryInterface.removeColumn('trials', 'trial_code', { transaction }); + await queryInterface.dropTable('breeders', { transaction }); + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + } +}; diff --git a/backend/src/db/migrations/20260216000002-trials-additional-fields.js b/backend/src/db/migrations/20260216000002-trials-additional-fields.js new file mode 100644 index 0000000..cceca3a --- /dev/null +++ b/backend/src/db/migrations/20260216000002-trials-additional-fields.js @@ -0,0 +1,200 @@ +module.exports = { + async up(queryInterface, Sequelize) { + const transaction = await queryInterface.sequelize.transaction(); + try { + const tablesToCreate = ['retailers', 'programs', 'groups', 'sites', 'locations']; + + for (const tableName of tablesToCreate) { + // Check if table exists + const tableExists = await queryInterface.describeTable(tableName).then(() => true).catch(() => false); + if (!tableExists) { + await queryInterface.createTable(tableName, { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.DataTypes.UUIDV4, + primaryKey: true, + }, + name: { + type: Sequelize.DataTypes.TEXT, + allowNull: false, + }, + tenantId: { + type: Sequelize.DataTypes.UUID, + references: { + model: 'tenants', + key: 'id', + }, + }, + organizationsId: { + type: Sequelize.DataTypes.UUID, + references: { + model: 'organizations', + key: 'id', + }, + }, + createdById: { + type: Sequelize.DataTypes.UUID, + references: { + model: 'users', + key: 'id', + }, + }, + updatedById: { + type: Sequelize.DataTypes.UUID, + references: { + model: 'users', + key: 'id', + }, + }, + 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 }); + } + } + + // Add columns to trials + const trialsColumns = await queryInterface.describeTable('trials'); + + const addColumnIfMissing = async (table, column, definition) => { + if (!trialsColumns[column]) { + await queryInterface.addColumn(table, column, definition, { transaction }); + } + }; + + await addColumnIfMissing('trials', 'containerSize', { type: Sequelize.DataTypes.TEXT }); + await addColumnIfMissing('trials', 'finishedHeight', { type: Sequelize.DataTypes.TEXT }); + await addColumnIfMissing('trials', 'finishedSpread', { type: Sequelize.DataTypes.TEXT }); + await addColumnIfMissing('trials', 'description', { type: Sequelize.DataTypes.TEXT }); + await addColumnIfMissing('trials', 'internalReferenceNumber', { type: Sequelize.DataTypes.TEXT }); + await addColumnIfMissing('trials', 'programYear', { type: Sequelize.DataTypes.INTEGER }); + await addColumnIfMissing('trials', 'distributor', { type: Sequelize.DataTypes.TEXT }); + await addColumnIfMissing('trials', 'quantity', { type: Sequelize.DataTypes.DECIMAL }); + await addColumnIfMissing('trials', 'receiveWeek', { type: Sequelize.DataTypes.INTEGER }); + await addColumnIfMissing('trials', 'finishWeek', { type: Sequelize.DataTypes.INTEGER }); + await addColumnIfMissing('trials', 'tableId', { type: Sequelize.DataTypes.TEXT }); + + await addColumnIfMissing('trials', 'containerId', { + type: Sequelize.DataTypes.UUID, + references: { model: 'containers', key: 'id' }, + }); + + await addColumnIfMissing('trials', 'cultureSheetId', { + type: Sequelize.DataTypes.UUID, + references: { model: 'files', key: 'id' }, + }); + + await addColumnIfMissing('trials', 'programId', { + type: Sequelize.DataTypes.UUID, + references: { model: 'programs', key: 'id' }, + }); + + await addColumnIfMissing('trials', 'siteId', { + type: Sequelize.DataTypes.UUID, + references: { model: 'sites', key: 'id' }, + }); + + await addColumnIfMissing('trials', 'locationId', { + type: Sequelize.DataTypes.UUID, + references: { model: 'locations', key: 'id' }, + }); + + // Create junction tables for multi-select + const retailersJunctionExists = await queryInterface.describeTable('trials_retailers').then(() => true).catch(() => false); + if (!retailersJunctionExists) { + await queryInterface.createTable('trials_retailers', { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.DataTypes.UUIDV4, + primaryKey: true, + }, + trialId: { + type: Sequelize.DataTypes.UUID, + references: { model: 'trials', key: 'id' }, + allowNull: false, + onDelete: 'CASCADE', + }, + retailerId: { + type: Sequelize.DataTypes.UUID, + references: { model: 'retailers', key: 'id' }, + allowNull: false, + onDelete: 'CASCADE', + }, + createdAt: { type: Sequelize.DataTypes.DATE }, + updatedAt: { type: Sequelize.DataTypes.DATE }, + }, { transaction }); + } + + const groupsJunctionExists = await queryInterface.describeTable('trials_groups').then(() => true).catch(() => false); + if (!groupsJunctionExists) { + await queryInterface.createTable('trials_groups', { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.DataTypes.UUIDV4, + primaryKey: true, + }, + trialId: { + type: Sequelize.DataTypes.UUID, + references: { model: 'trials', key: 'id' }, + allowNull: false, + onDelete: 'CASCADE', + }, + groupId: { + type: Sequelize.DataTypes.UUID, + references: { model: 'groups', key: 'id' }, + allowNull: false, + onDelete: 'CASCADE', + }, + createdAt: { type: Sequelize.DataTypes.DATE }, + updatedAt: { type: Sequelize.DataTypes.DATE }, + }, { transaction }); + } + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + + async down(queryInterface, Sequelize) { + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.dropTable('trials_groups', { transaction }); + await queryInterface.dropTable('trials_retailers', { transaction }); + + await queryInterface.removeColumn('trials', 'locationId', { transaction }); + await queryInterface.removeColumn('trials', 'siteId', { transaction }); + await queryInterface.removeColumn('trials', 'programId', { transaction }); + await queryInterface.removeColumn('trials', 'cultureSheetId', { transaction }); + await queryInterface.removeColumn('trials', 'containerId', { transaction }); + + await queryInterface.removeColumn('trials', 'tableId', { transaction }); + await queryInterface.removeColumn('trials', 'finishWeek', { transaction }); + await queryInterface.removeColumn('trials', 'receiveWeek', { transaction }); + await queryInterface.removeColumn('trials', 'quantity', { transaction }); + await queryInterface.removeColumn('trials', 'distributor', { transaction }); + await queryInterface.removeColumn('trials', 'programYear', { transaction }); + await queryInterface.removeColumn('trials', 'internalReferenceNumber', { transaction }); + await queryInterface.removeColumn('trials', 'description', { transaction }); + await queryInterface.removeColumn('trials', 'finishedSpread', { transaction }); + await queryInterface.removeColumn('trials', 'finishedHeight', { transaction }); + await queryInterface.removeColumn('trials', 'containerSize', { transaction }); + + const tablesToDrop = ['retailers', 'programs', 'groups', 'sites', 'locations']; + for (const tableName of tablesToDrop) { + await queryInterface.dropTable(tableName, { transaction }); + } + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + } +}; diff --git a/backend/src/db/migrations/20260216182021-add-attributes-to-trials.js b/backend/src/db/migrations/20260216182021-add-attributes-to-trials.js new file mode 100644 index 0000000..37bbac0 --- /dev/null +++ b/backend/src/db/migrations/20260216182021-add-attributes-to-trials.js @@ -0,0 +1,21 @@ +'use strict'; + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.addColumn('trials', 'attributes', { + type: Sequelize.JSONB, + allowNull: true, + defaultValue: {}, + }); + await queryInterface.addColumn('trials', 'trial_category', { + type: Sequelize.STRING, + allowNull: false, + defaultValue: 'PLANT', + }); + }, + + down: async (queryInterface) => { + await queryInterface.removeColumn('trials', 'attributes'); + await queryInterface.removeColumn('trials', 'trial_category'); + }, +}; diff --git a/backend/src/db/models/breeders.js b/backend/src/db/models/breeders.js new file mode 100644 index 0000000..bffbff7 --- /dev/null +++ b/backend/src/db/models/breeders.js @@ -0,0 +1,54 @@ +module.exports = function(sequelize, DataTypes) { + const breeders = sequelize.define( + 'breeders', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + name: { + type: DataTypes.TEXT, + allowNull: false, + }, + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + breeders.associate = (db) => { + db.breeders.belongsTo(db.tenants, { + as: 'tenant', + foreignKey: { + name: 'tenantId', + }, + constraints: false, + }); + + db.breeders.belongsTo(db.organizations, { + as: 'organizations', + foreignKey: { + name: 'organizationsId', + }, + constraints: false, + }); + + db.breeders.belongsTo(db.users, { + as: 'createdBy', + }); + + db.breeders.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return breeders; +}; diff --git a/backend/src/db/models/containers.js b/backend/src/db/models/containers.js new file mode 100644 index 0000000..b2964a4 --- /dev/null +++ b/backend/src/db/models/containers.js @@ -0,0 +1,55 @@ + +module.exports = function(sequelize, DataTypes) { + const containers = sequelize.define( + 'containers', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + name: { + type: DataTypes.TEXT, + allowNull: false, + }, + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + containers.associate = (db) => { + db.containers.belongsTo(db.tenants, { + as: 'tenant', + foreignKey: { + name: 'tenantId', + }, + constraints: false, + }); + + db.containers.belongsTo(db.organizations, { + as: 'organizations', + foreignKey: { + name: 'organizationsId', + }, + constraints: false, + }); + + db.containers.belongsTo(db.users, { + as: 'createdBy', + }); + + db.containers.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return containers; +}; diff --git a/backend/src/db/models/crop_types.js b/backend/src/db/models/crop_types.js new file mode 100644 index 0000000..ca29fa2 --- /dev/null +++ b/backend/src/db/models/crop_types.js @@ -0,0 +1,55 @@ + +module.exports = function(sequelize, DataTypes) { + const crop_types = sequelize.define( + 'crop_types', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + name: { + type: DataTypes.TEXT, + allowNull: false, + }, + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + crop_types.associate = (db) => { + db.crop_types.belongsTo(db.tenants, { + as: 'tenant', + foreignKey: { + name: 'tenantId', + }, + constraints: false, + }); + + db.crop_types.belongsTo(db.organizations, { + as: 'organizations', + foreignKey: { + name: 'organizationsId', + }, + constraints: false, + }); + + db.crop_types.belongsTo(db.users, { + as: 'createdBy', + }); + + db.crop_types.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return crop_types; +}; diff --git a/backend/src/db/models/groups.js b/backend/src/db/models/groups.js new file mode 100644 index 0000000..b6b1a2d --- /dev/null +++ b/backend/src/db/models/groups.js @@ -0,0 +1,54 @@ +module.exports = function(sequelize, DataTypes) { + const groups = sequelize.define( + 'groups', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + name: { + type: DataTypes.TEXT, + allowNull: false, + }, + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + groups.associate = (db) => { + db.groups.belongsTo(db.tenants, { + as: 'tenant', + foreignKey: { + name: 'tenantId', + }, + constraints: false, + }); + + db.groups.belongsTo(db.organizations, { + as: 'organizations', + foreignKey: { + name: 'organizationsId', + }, + constraints: false, + }); + + db.groups.belongsTo(db.users, { + as: 'createdBy', + }); + + db.groups.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return groups; +}; diff --git a/backend/src/db/models/growth_habits.js b/backend/src/db/models/growth_habits.js new file mode 100644 index 0000000..5948c54 --- /dev/null +++ b/backend/src/db/models/growth_habits.js @@ -0,0 +1,55 @@ + +module.exports = function(sequelize, DataTypes) { + const growth_habits = sequelize.define( + 'growth_habits', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + name: { + type: DataTypes.TEXT, + allowNull: false, + }, + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + growth_habits.associate = (db) => { + db.growth_habits.belongsTo(db.tenants, { + as: 'tenant', + foreignKey: { + name: 'tenantId', + }, + constraints: false, + }); + + db.growth_habits.belongsTo(db.organizations, { + as: 'organizations', + foreignKey: { + name: 'organizationsId', + }, + constraints: false, + }); + + db.growth_habits.belongsTo(db.users, { + as: 'createdBy', + }); + + db.growth_habits.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return growth_habits; +}; diff --git a/backend/src/db/models/locations.js b/backend/src/db/models/locations.js new file mode 100644 index 0000000..0ae2599 --- /dev/null +++ b/backend/src/db/models/locations.js @@ -0,0 +1,54 @@ +module.exports = function(sequelize, DataTypes) { + const locations = sequelize.define( + 'locations', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + name: { + type: DataTypes.TEXT, + allowNull: false, + }, + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + locations.associate = (db) => { + db.locations.belongsTo(db.tenants, { + as: 'tenant', + foreignKey: { + name: 'tenantId', + }, + constraints: false, + }); + + db.locations.belongsTo(db.organizations, { + as: 'organizations', + foreignKey: { + name: 'organizationsId', + }, + 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/programs.js b/backend/src/db/models/programs.js new file mode 100644 index 0000000..1e6001c --- /dev/null +++ b/backend/src/db/models/programs.js @@ -0,0 +1,54 @@ +module.exports = function(sequelize, DataTypes) { + const programs = sequelize.define( + 'programs', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + name: { + type: DataTypes.TEXT, + allowNull: false, + }, + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + programs.associate = (db) => { + db.programs.belongsTo(db.tenants, { + as: 'tenant', + foreignKey: { + name: 'tenantId', + }, + constraints: false, + }); + + db.programs.belongsTo(db.organizations, { + as: 'organizations', + foreignKey: { + name: 'organizationsId', + }, + constraints: false, + }); + + db.programs.belongsTo(db.users, { + as: 'createdBy', + }); + + db.programs.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return programs; +}; diff --git a/backend/src/db/models/propagation_types.js b/backend/src/db/models/propagation_types.js new file mode 100644 index 0000000..bd22c46 --- /dev/null +++ b/backend/src/db/models/propagation_types.js @@ -0,0 +1,55 @@ + +module.exports = function(sequelize, DataTypes) { + const propagation_types = sequelize.define( + 'propagation_types', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + name: { + type: DataTypes.TEXT, + allowNull: false, + }, + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + propagation_types.associate = (db) => { + db.propagation_types.belongsTo(db.tenants, { + as: 'tenant', + foreignKey: { + name: 'tenantId', + }, + constraints: false, + }); + + db.propagation_types.belongsTo(db.organizations, { + as: 'organizations', + foreignKey: { + name: 'organizationsId', + }, + constraints: false, + }); + + db.propagation_types.belongsTo(db.users, { + as: 'createdBy', + }); + + db.propagation_types.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return propagation_types; +}; diff --git a/backend/src/db/models/retailer_programs.js b/backend/src/db/models/retailer_programs.js new file mode 100644 index 0000000..d8fb711 --- /dev/null +++ b/backend/src/db/models/retailer_programs.js @@ -0,0 +1,55 @@ + +module.exports = function(sequelize, DataTypes) { + const retailer_programs = sequelize.define( + 'retailer_programs', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + name: { + type: DataTypes.TEXT, + allowNull: false, + }, + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + retailer_programs.associate = (db) => { + db.retailer_programs.belongsTo(db.tenants, { + as: 'tenant', + foreignKey: { + name: 'tenantId', + }, + constraints: false, + }); + + db.retailer_programs.belongsTo(db.organizations, { + as: 'organizations', + foreignKey: { + name: 'organizationsId', + }, + constraints: false, + }); + + db.retailer_programs.belongsTo(db.users, { + as: 'createdBy', + }); + + db.retailer_programs.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return retailer_programs; +}; diff --git a/backend/src/db/models/retailers.js b/backend/src/db/models/retailers.js new file mode 100644 index 0000000..4b17feb --- /dev/null +++ b/backend/src/db/models/retailers.js @@ -0,0 +1,54 @@ +module.exports = function(sequelize, DataTypes) { + const retailers = sequelize.define( + 'retailers', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + name: { + type: DataTypes.TEXT, + allowNull: false, + }, + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + retailers.associate = (db) => { + db.retailers.belongsTo(db.tenants, { + as: 'tenant', + foreignKey: { + name: 'tenantId', + }, + constraints: false, + }); + + db.retailers.belongsTo(db.organizations, { + as: 'organizations', + foreignKey: { + name: 'organizationsId', + }, + constraints: false, + }); + + db.retailers.belongsTo(db.users, { + as: 'createdBy', + }); + + db.retailers.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return retailers; +}; diff --git a/backend/src/db/models/sites.js b/backend/src/db/models/sites.js new file mode 100644 index 0000000..6b57dfc --- /dev/null +++ b/backend/src/db/models/sites.js @@ -0,0 +1,54 @@ +module.exports = function(sequelize, DataTypes) { + const sites = sequelize.define( + 'sites', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + name: { + type: DataTypes.TEXT, + allowNull: false, + }, + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + sites.associate = (db) => { + db.sites.belongsTo(db.tenants, { + as: 'tenant', + foreignKey: { + name: 'tenantId', + }, + constraints: false, + }); + + db.sites.belongsTo(db.organizations, { + as: 'organizations', + foreignKey: { + name: 'organizationsId', + }, + constraints: false, + }); + + db.sites.belongsTo(db.users, { + as: 'createdBy', + }); + + db.sites.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return sites; +}; diff --git a/backend/src/db/models/trials.js b/backend/src/db/models/trials.js index d01af58..8ee6ed3 100644 --- a/backend/src/db/models/trials.js +++ b/backend/src/db/models/trials.js @@ -1,8 +1,4 @@ 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 trials = sequelize.define( @@ -13,165 +9,122 @@ module.exports = function(sequelize, DataTypes) { defaultValue: DataTypes.UUIDV4, primaryKey: true, }, - -name: { + name: { type: DataTypes.TEXT, - - - }, - -variety_name: { + trial_code: { type: DataTypes.TEXT, - - - }, - -breeder: { + variety_name: { type: DataTypes.TEXT, - - - }, - -batch_code: { + genus: { type: DataTypes.TEXT, - - - }, - -trial_type: { + species: { + type: DataTypes.TEXT, + }, + colour: { + type: DataTypes.TEXT, + }, + batch_code: { + type: DataTypes.TEXT, + }, + trial_type: { type: DataTypes.ENUM, - - - values: [ - -"new_variety", - - -"performance", - - -"disease_resistance", - - -"yield", - - -"quality", - - -"other" - + "new_variety", + "performance", + "disease_resistance", + "yield", + "quality", + "other" ], - }, - -status: { + status: { type: DataTypes.ENUM, - - - values: [ - -"setup", - - -"in_progress", - - -"paused", - - -"completed", - - -"archived" - + "setup", + "in_progress", + "paused", + "completed", + "archived" ], - }, - -planted_at: { + planted_at: { type: DataTypes.DATE, - - - }, - -harvested_at: { + harvested_at: { type: DataTypes.DATE, - - - }, - -greenhouse: { + greenhouse: { type: DataTypes.TEXT, - - - }, - -zone: { + zone: { type: DataTypes.TEXT, - - - }, - -bench: { + bench: { type: DataTypes.TEXT, - - - }, - -plants_count: { + plants_count: { type: DataTypes.INTEGER, - - - }, - -target_temperature_c: { + target_temperature_c: { type: DataTypes.DECIMAL, - - - }, - -target_humidity_percent: { + target_humidity_percent: { type: DataTypes.DECIMAL, - - - }, - -target_ec: { + target_ec: { type: DataTypes.DECIMAL, - - - }, - -target_ph: { + target_ph: { type: DataTypes.DECIMAL, - - - }, - -notes: { + notes: { type: DataTypes.TEXT, - - - }, - + containerSize: { + type: DataTypes.TEXT, + }, + finishedHeight: { + type: DataTypes.TEXT, + }, + finishedSpread: { + type: DataTypes.TEXT, + }, + description: { + type: DataTypes.TEXT, + }, + internalReferenceNumber: { + type: DataTypes.TEXT, + }, + programYear: { + type: DataTypes.INTEGER, + }, + distributor: { + type: DataTypes.TEXT, + }, + quantity: { + type: DataTypes.DECIMAL, + }, + receiveWeek: { + type: DataTypes.INTEGER, + }, + finishWeek: { + type: DataTypes.INTEGER, + }, + tableId: { + type: DataTypes.TEXT, + }, + attributes: { + type: DataTypes.JSONB, + defaultValue: {}, + }, + trial_category: { + type: DataTypes.STRING, + defaultValue: 'PLANT', + }, importHash: { type: DataTypes.STRING(255), allowNull: true, @@ -186,19 +139,66 @@ notes: { ); trials.associate = (db) => { + db.trials.belongsTo(db.breeders, { + as: 'breeder', + foreignKey: 'breederId', + }); + db.trials.belongsTo(db.propagation_types, { + as: 'propagationType', + foreignKey: 'propagationTypeId', + }); -/// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity - - + db.trials.belongsTo(db.crop_types, { + as: 'cropType', + foreignKey: 'cropTypeId', + }); + db.trials.belongsToMany(db.growth_habits, { + as: 'growthHabits', + through: 'trials_growth_habits', + foreignKey: 'trialId', + otherKey: 'growthHabitId', + }); + db.trials.belongsTo(db.containers, { + as: 'container', + foreignKey: 'containerId', + }); + db.trials.belongsTo(db.file, { + as: 'cultureSheet', + foreignKey: 'cultureSheetId', + }); + db.trials.belongsTo(db.programs, { + as: 'program', + foreignKey: 'programId', + }); + db.trials.belongsTo(db.sites, { + as: 'site', + foreignKey: 'siteId', + }); + db.trials.belongsTo(db.locations, { + as: 'location', + foreignKey: 'locationId', + }); + db.trials.belongsToMany(db.retailers, { + as: 'retailers', + through: 'trials_retailers', + foreignKey: 'trialId', + otherKey: 'retailerId', + }); + db.trials.belongsToMany(db.groups, { + as: 'groups', + through: 'trials_groups', + foreignKey: 'trialId', + otherKey: 'groupId', + }); db.trials.hasMany(db.tracking_activities, { as: 'tracking_activities_trial', @@ -208,16 +208,6 @@ notes: { constraints: false, }); - - - - - - -//end loop - - - db.trials.belongsTo(db.tenants, { as: 'tenant', foreignKey: { @@ -242,9 +232,6 @@ notes: { constraints: false, }); - - - db.trials.belongsTo(db.users, { as: 'createdBy', }); @@ -254,9 +241,5 @@ notes: { }); }; - - return trials; }; - - diff --git a/backend/src/db/seeders/20260216000000-dataset-permissions.js b/backend/src/db/seeders/20260216000000-dataset-permissions.js new file mode 100644 index 0000000..6e33440 --- /dev/null +++ b/backend/src/db/seeders/20260216000000-dataset-permissions.js @@ -0,0 +1,67 @@ + +const { v4: uuid } = require("uuid"); + +module.exports = { + async up(queryInterface) { + const createdAt = new Date(); + const updatedAt = new Date(); + + const entities = [ + "propagation_types", + "growth_habits", + "crop_types", + "containers", + "retailer_programs" + ]; + + const permissions = []; + for (const entity of entities) { + const upperName = entity.toUpperCase(); + permissions.push( + { id: uuid(), name: `CREATE_${upperName}`, createdAt, updatedAt }, + { id: uuid(), name: `READ_${upperName}`, createdAt, updatedAt }, + { id: uuid(), name: `UPDATE_${upperName}`, createdAt, updatedAt }, + { id: uuid(), name: `DELETE_${upperName}`, createdAt, updatedAt } + ); + } + + await queryInterface.bulkInsert("permissions", permissions); + + const [roles] = await queryInterface.sequelize.query( + `SELECT id, name FROM roles WHERE name IN ('Super Administrator', 'Administrator', 'Platform Owner', 'Tenant Owner', 'Trial Manager');` + ); + + const [dbPermissions] = await queryInterface.sequelize.query( + `SELECT id, name FROM permissions WHERE name LIKE 'CREATE_%' OR name LIKE 'READ_%' OR name LIKE 'UPDATE_%' OR name LIKE 'DELETE_%';` + ); + + const rolesPermissions = []; + for (const role of roles) { + for (const permission of dbPermissions) { + // Only add if it's one of our new permissions + const isNewPermission = entities.some(e => permission.name.includes(e.toUpperCase())); + if (isNewPermission) { + rolesPermissions.push({ + createdAt, + updatedAt, + roles_permissionsId: role.id, + permissionId: permission.id + }); + } + } + } + + // Use a try-catch or IGNORE to avoid duplicates if seeder runs multiple times + for (const rp of rolesPermissions) { + try { + await queryInterface.bulkInsert("rolesPermissionsPermissions", [rp]); + } catch (e) { + // Likely duplicate key + } + } + }, + + async down(queryInterface) { + // Optional: cleanup + } +}; diff --git a/backend/src/db/seeders/20260216000001-breeder-permissions.js b/backend/src/db/seeders/20260216000001-breeder-permissions.js new file mode 100644 index 0000000..c67475b --- /dev/null +++ b/backend/src/db/seeders/20260216000001-breeder-permissions.js @@ -0,0 +1,51 @@ +const { v4: uuid } = require("uuid"); + +module.exports = { + async up(queryInterface) { + const createdAt = new Date(); + const updatedAt = new Date(); + + const entity = "breeders"; + const upperName = entity.toUpperCase(); + + const permissions = [ + { id: uuid(), name: `CREATE_${upperName}`, createdAt, updatedAt }, + { id: uuid(), name: `READ_${upperName}`, createdAt, updatedAt }, + { id: uuid(), name: `UPDATE_${upperName}`, createdAt, updatedAt }, + { id: uuid(), name: `DELETE_${upperName}`, createdAt, updatedAt } + ]; + + await queryInterface.bulkInsert("permissions", permissions); + + const [roles] = await queryInterface.sequelize.query( + `SELECT id, name FROM roles WHERE name IN ('Super Administrator', 'Administrator', 'Platform Owner', 'Tenant Owner', 'Trial Manager');` + ); + + const [dbPermissions] = await queryInterface.sequelize.query( + `SELECT id, name FROM permissions WHERE name LIKE '%BREEDERS%';` + ); + + const rolesPermissions = []; + for (const role of roles) { + for (const permission of dbPermissions) { + rolesPermissions.push({ + createdAt, + updatedAt, + roles_permissionsId: role.id, + permissionId: permission.id + }); + } + } + + for (const rp of rolesPermissions) { + try { + await queryInterface.bulkInsert("rolesPermissionsPermissions", [rp]); + } catch (e) { + // Likely duplicate key + } + } + }, + + async down(queryInterface) { + } +}; diff --git a/backend/src/db/seeders/20260216000002-additional-dataset-permissions.js b/backend/src/db/seeders/20260216000002-additional-dataset-permissions.js new file mode 100644 index 0000000..ebe80a0 --- /dev/null +++ b/backend/src/db/seeders/20260216000002-additional-dataset-permissions.js @@ -0,0 +1,54 @@ +const { v4: uuid } = require("uuid"); + +module.exports = { + async up(queryInterface) { + const createdAt = new Date(); + const updatedAt = new Date(); + + const entities = ["retailers", "programs", "groups", "sites", "locations"]; + + for (const entity of entities) { + const upperName = entity.toUpperCase(); + + const permissions = [ + { id: uuid(), name: `CREATE_${upperName}`, createdAt, updatedAt }, + { id: uuid(), name: `READ_${upperName}`, createdAt, updatedAt }, + { id: uuid(), name: `UPDATE_${upperName}`, createdAt, updatedAt }, + { id: uuid(), name: `DELETE_${upperName}`, createdAt, updatedAt } + ]; + + await queryInterface.bulkInsert("permissions", permissions); + + const [roles] = await queryInterface.sequelize.query( + `SELECT id, name FROM roles WHERE name IN ('Super Administrator', 'Administrator', 'Platform Owner', 'Tenant Owner', 'Trial Manager');` + ); + + const [dbPermissions] = await queryInterface.sequelize.query( + `SELECT id, name FROM permissions WHERE name LIKE '%${upperName}%';` + ); + + const rolesPermissions = []; + for (const role of roles) { + for (const permission of dbPermissions) { + rolesPermissions.push({ + createdAt, + updatedAt, + roles_permissionsId: role.id, + permissionId: permission.id + }); + } + } + + for (const rp of rolesPermissions) { + try { + await queryInterface.bulkInsert("rolesPermissionsPermissions", [rp]); + } catch (e) { + // Likely duplicate key + } + } + } + }, + + async down(queryInterface) { + } +}; diff --git a/backend/src/index.js b/backend/src/index.js index 507626d..7211874 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -1,4 +1,3 @@ - const express = require('express'); const cors = require('cors'); const app = express(); @@ -16,41 +15,36 @@ const fileRoutes = require('./routes/file'); const searchRoutes = require('./routes/search'); const sqlRoutes = require('./routes/sql'); const pexelsRoutes = require('./routes/pexels'); - const organizationForAuthRoutes = require('./routes/organizationLogin'); - const openaiRoutes = require('./routes/openai'); - - const usersRoutes = require('./routes/users'); - const rolesRoutes = require('./routes/roles'); - const permissionsRoutes = require('./routes/permissions'); - const organizationsRoutes = require('./routes/organizations'); - const tenantsRoutes = require('./routes/tenants'); - const impersonation_sessionsRoutes = require('./routes/impersonation_sessions'); - const projectsRoutes = require('./routes/projects'); - const trialsRoutes = require('./routes/trials'); - const tracking_activity_typesRoutes = require('./routes/tracking_activity_types'); - const tracking_activitiesRoutes = require('./routes/tracking_activities'); - const dashboardsRoutes = require('./routes/dashboards'); - const report_definitionsRoutes = require('./routes/report_definitions'); - const report_runsRoutes = require('./routes/report_runs'); - const tenant_settingsRoutes = require('./routes/tenant_settings'); +const propagation_typesRoutes = require('./routes/propagation_types'); +const growth_habitsRoutes = require('./routes/growth_habits'); +const crop_typesRoutes = require('./routes/crop_types'); +const containersRoutes = require('./routes/containers'); +const retailer_programsRoutes = require('./routes/retailer_programs'); +const breedersRoutes = require('./routes/breeders'); +const retailersRoutes = require('./routes/retailers'); +const programsRoutes = require('./routes/programs'); +const groupsRoutes = require('./routes/groups'); +const sitesRoutes = require('./routes/sites'); +const locationsRoutes = require('./routes/locations'); + const getBaseUrl = (url) => { if (!url) return ''; @@ -110,33 +104,32 @@ app.enable('trust proxy'); app.use('/api/users', passport.authenticate('jwt', {session: false}), usersRoutes); - app.use('/api/roles', passport.authenticate('jwt', {session: false}), rolesRoutes); - app.use('/api/permissions', passport.authenticate('jwt', {session: false}), permissionsRoutes); - app.use('/api/organizations', passport.authenticate('jwt', {session: false}), organizationsRoutes); - app.use('/api/tenants', passport.authenticate('jwt', {session: false}), tenantsRoutes); - app.use('/api/impersonation_sessions', passport.authenticate('jwt', {session: false}), impersonation_sessionsRoutes); - app.use('/api/projects', passport.authenticate('jwt', {session: false}), projectsRoutes); - app.use('/api/trials', passport.authenticate('jwt', {session: false}), trialsRoutes); - app.use('/api/tracking_activity_types', passport.authenticate('jwt', {session: false}), tracking_activity_typesRoutes); - app.use('/api/tracking_activities', passport.authenticate('jwt', {session: false}), tracking_activitiesRoutes); - app.use('/api/dashboards', passport.authenticate('jwt', {session: false}), dashboardsRoutes); - app.use('/api/report_definitions', passport.authenticate('jwt', {session: false}), report_definitionsRoutes); - app.use('/api/report_runs', passport.authenticate('jwt', {session: false}), report_runsRoutes); - app.use('/api/tenant_settings', passport.authenticate('jwt', {session: false}), tenant_settingsRoutes); +app.use('/api/propagation_types', passport.authenticate('jwt', {session: false}), propagation_typesRoutes); +app.use('/api/growth_habits', passport.authenticate('jwt', {session: false}), growth_habitsRoutes); +app.use('/api/crop_types', passport.authenticate('jwt', {session: false}), crop_typesRoutes); +app.use('/api/containers', passport.authenticate('jwt', {session: false}), containersRoutes); +app.use('/api/retailer_programs', passport.authenticate('jwt', {session: false}), retailer_programsRoutes); +app.use('/api/breeders', passport.authenticate('jwt', {session: false}), breedersRoutes); +app.use('/api/retailers', passport.authenticate('jwt', {session: false}), retailersRoutes); +app.use('/api/programs', passport.authenticate('jwt', {session: false}), programsRoutes); +app.use('/api/groups', passport.authenticate('jwt', {session: false}), groupsRoutes); +app.use('/api/sites', passport.authenticate('jwt', {session: false}), sitesRoutes); +app.use('/api/locations', passport.authenticate('jwt', {session: false}), locationsRoutes); + app.use( '/api/openai', passport.authenticate('jwt', { session: false }), @@ -186,4 +179,4 @@ db.sequelize.sync().then(function () { }); }); -module.exports = app; +module.exports = app; \ No newline at end of file diff --git a/backend/src/routes/breeders.js b/backend/src/routes/breeders.js new file mode 100644 index 0000000..4887cbb --- /dev/null +++ b/backend/src/routes/breeders.js @@ -0,0 +1,53 @@ +const express = require('express'); +const BreedersService = require('../services/breeders'); +const BreedersDBApi = require('../db/api/breeders'); +const wrapAsync = require('../helpers').wrapAsync; +const router = express.Router(); +const { checkCrudPermissions } = require('../middlewares/check-permissions'); + +router.use(checkCrudPermissions('breeders')); + +router.post('/', wrapAsync(async (req, res) => { + await BreedersService.create(req.body.data, req.currentUser); + res.status(200).send(true); +})); + +router.put('/:id', wrapAsync(async (req, res) => { + await BreedersService.update(req.body.data, req.body.id, req.currentUser); + res.status(200).send(true); +})); + +router.delete('/:id', wrapAsync(async (req, res) => { + await BreedersService.remove(req.params.id, req.currentUser); + res.status(200).send(true); +})); + +router.post('/deleteByIds', wrapAsync(async (req, res) => { + await BreedersService.deleteByIds(req.body.data, req.currentUser); + res.status(200).send(true); +})); + +router.get('/', wrapAsync(async (req, res) => { + const globalAccess = req.currentUser.app_role.globalAccess; + const currentUser = req.currentUser; + const payload = await BreedersDBApi.findAll(req.query, globalAccess, { currentUser }); + res.status(200).send(payload); +})); + +router.get('/autocomplete', wrapAsync(async (req, res) => { + const globalAccess = req.currentUser.app_role.globalAccess; + const organizationId = req.currentUser.organizationsId; + const payload = await BreedersDBApi.findAllAutocomplete( + req.query.query, req.query.limit, req.query.offset, globalAccess, organizationId + ); + res.status(200).send(payload); +})); + +router.get('/:id', wrapAsync(async (req, res) => { + const payload = await BreedersDBApi.findBy({ id: req.params.id }); + res.status(200).send(payload); +})); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/routes/containers.js b/backend/src/routes/containers.js new file mode 100644 index 0000000..991c3db --- /dev/null +++ b/backend/src/routes/containers.js @@ -0,0 +1,54 @@ + +const express = require('express'); +const ContainersService = require('../services/containers'); +const ContainersDBApi = require('../db/api/containers'); +const wrapAsync = require('../helpers').wrapAsync; +const router = express.Router(); +const { checkCrudPermissions } = require('../middlewares/check-permissions'); + +router.use(checkCrudPermissions('containers')); + +router.post('/', wrapAsync(async (req, res) => { + await ContainersService.create(req.body.data, req.currentUser); + res.status(200).send(true); +})); + +router.put('/:id', wrapAsync(async (req, res) => { + await ContainersService.update(req.body.data, req.body.id, req.currentUser); + res.status(200).send(true); +})); + +router.delete('/:id', wrapAsync(async (req, res) => { + await ContainersService.remove(req.params.id, req.currentUser); + res.status(200).send(true); +})); + +router.post('/deleteByIds', wrapAsync(async (req, res) => { + await ContainersService.deleteByIds(req.body.data, req.currentUser); + res.status(200).send(true); +})); + +router.get('/', wrapAsync(async (req, res) => { + const globalAccess = req.currentUser.app_role.globalAccess; + const currentUser = req.currentUser; + const payload = await ContainersDBApi.findAll(req.query, globalAccess, { currentUser }); + res.status(200).send(payload); +})); + +router.get('/autocomplete', wrapAsync(async (req, res) => { + const globalAccess = req.currentUser.app_role.globalAccess; + const organizationId = req.currentUser.organizationsId; + const payload = await ContainersDBApi.findAllAutocomplete( + req.query.query, req.query.limit, req.query.offset, globalAccess, organizationId + ); + res.status(200).send(payload); +})); + +router.get('/:id', wrapAsync(async (req, res) => { + const payload = await ContainersDBApi.findBy({ id: req.params.id }); + res.status(200).send(payload); +})); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/routes/crop_types.js b/backend/src/routes/crop_types.js new file mode 100644 index 0000000..96c7948 --- /dev/null +++ b/backend/src/routes/crop_types.js @@ -0,0 +1,54 @@ + +const express = require('express'); +const Crop_typesService = require('../services/crop_types'); +const Crop_typesDBApi = require('../db/api/crop_types'); +const wrapAsync = require('../helpers').wrapAsync; +const router = express.Router(); +const { checkCrudPermissions } = require('../middlewares/check-permissions'); + +router.use(checkCrudPermissions('crop_types')); + +router.post('/', wrapAsync(async (req, res) => { + await Crop_typesService.create(req.body.data, req.currentUser); + res.status(200).send(true); +})); + +router.put('/:id', wrapAsync(async (req, res) => { + await Crop_typesService.update(req.body.data, req.body.id, req.currentUser); + res.status(200).send(true); +})); + +router.delete('/:id', wrapAsync(async (req, res) => { + await Crop_typesService.remove(req.params.id, req.currentUser); + res.status(200).send(true); +})); + +router.post('/deleteByIds', wrapAsync(async (req, res) => { + await Crop_typesService.deleteByIds(req.body.data, req.currentUser); + res.status(200).send(true); +})); + +router.get('/', wrapAsync(async (req, res) => { + const globalAccess = req.currentUser.app_role.globalAccess; + const currentUser = req.currentUser; + const payload = await Crop_typesDBApi.findAll(req.query, globalAccess, { currentUser }); + res.status(200).send(payload); +})); + +router.get('/autocomplete', wrapAsync(async (req, res) => { + const globalAccess = req.currentUser.app_role.globalAccess; + const organizationId = req.currentUser.organizationsId; + const payload = await Crop_typesDBApi.findAllAutocomplete( + req.query.query, req.query.limit, req.query.offset, globalAccess, organizationId + ); + res.status(200).send(payload); +})); + +router.get('/:id', wrapAsync(async (req, res) => { + const payload = await Crop_typesDBApi.findBy({ id: req.params.id }); + res.status(200).send(payload); +})); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/routes/groups.js b/backend/src/routes/groups.js new file mode 100644 index 0000000..311e0e9 --- /dev/null +++ b/backend/src/routes/groups.js @@ -0,0 +1,53 @@ +const express = require('express'); +const groupsService = require('../services/groups'); +const groupsDBApi = require('../db/api/groups'); +const wrapAsync = require('../helpers').wrapAsync; +const router = express.Router(); +const { checkCrudPermissions } = require('../middlewares/check-permissions'); + +router.use(checkCrudPermissions('groups')); + +router.post('/', wrapAsync(async (req, res) => { + await groupsService.create(req.body.data, req.currentUser); + res.status(200).send(true); +})); + +router.put('/:id', wrapAsync(async (req, res) => { + await groupsService.update(req.body.data, req.body.id, req.currentUser); + res.status(200).send(true); +})); + +router.delete('/:id', wrapAsync(async (req, res) => { + await groupsService.remove(req.params.id, req.currentUser); + res.status(200).send(true); +})); + +router.post('/deleteByIds', wrapAsync(async (req, res) => { + await groupsService.deleteByIds(req.body.data, req.currentUser); + res.status(200).send(true); +})); + +router.get('/', wrapAsync(async (req, res) => { + const globalAccess = req.currentUser.app_role.globalAccess; + const currentUser = req.currentUser; + const payload = await groupsDBApi.findAll(req.query, globalAccess, { currentUser }); + res.status(200).send(payload); +})); + +router.get('/autocomplete', wrapAsync(async (req, res) => { + const globalAccess = req.currentUser.app_role.globalAccess; + const organizationId = req.currentUser.organizationsId; + const payload = await groupsDBApi.findAllAutocomplete( + req.query.query, req.query.limit, req.query.offset, globalAccess, organizationId + ); + res.status(200).send(payload); +})); + +router.get('/:id', wrapAsync(async (req, res) => { + const payload = await groupsDBApi.findBy({ id: req.params.id }); + res.status(200).send(payload); +})); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/routes/growth_habits.js b/backend/src/routes/growth_habits.js new file mode 100644 index 0000000..b0bc56d --- /dev/null +++ b/backend/src/routes/growth_habits.js @@ -0,0 +1,54 @@ + +const express = require('express'); +const Growth_habitsService = require('../services/growth_habits'); +const Growth_habitsDBApi = require('../db/api/growth_habits'); +const wrapAsync = require('../helpers').wrapAsync; +const router = express.Router(); +const { checkCrudPermissions } = require('../middlewares/check-permissions'); + +router.use(checkCrudPermissions('growth_habits')); + +router.post('/', wrapAsync(async (req, res) => { + await Growth_habitsService.create(req.body.data, req.currentUser); + res.status(200).send(true); +})); + +router.put('/:id', wrapAsync(async (req, res) => { + await Growth_habitsService.update(req.body.data, req.body.id, req.currentUser); + res.status(200).send(true); +})); + +router.delete('/:id', wrapAsync(async (req, res) => { + await Growth_habitsService.remove(req.params.id, req.currentUser); + res.status(200).send(true); +})); + +router.post('/deleteByIds', wrapAsync(async (req, res) => { + await Growth_habitsService.deleteByIds(req.body.data, req.currentUser); + res.status(200).send(true); +})); + +router.get('/', wrapAsync(async (req, res) => { + const globalAccess = req.currentUser.app_role.globalAccess; + const currentUser = req.currentUser; + const payload = await Growth_habitsDBApi.findAll(req.query, globalAccess, { currentUser }); + res.status(200).send(payload); +})); + +router.get('/autocomplete', wrapAsync(async (req, res) => { + const globalAccess = req.currentUser.app_role.globalAccess; + const organizationId = req.currentUser.organizationsId; + const payload = await Growth_habitsDBApi.findAllAutocomplete( + req.query.query, req.query.limit, req.query.offset, globalAccess, organizationId + ); + res.status(200).send(payload); +})); + +router.get('/:id', wrapAsync(async (req, res) => { + const payload = await Growth_habitsDBApi.findBy({ id: req.params.id }); + res.status(200).send(payload); +})); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/routes/locations.js b/backend/src/routes/locations.js new file mode 100644 index 0000000..32cede3 --- /dev/null +++ b/backend/src/routes/locations.js @@ -0,0 +1,53 @@ +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 { checkCrudPermissions } = require('../middlewares/check-permissions'); + +router.use(checkCrudPermissions('locations')); + +router.post('/', wrapAsync(async (req, res) => { + await locationsService.create(req.body.data, req.currentUser); + res.status(200).send(true); +})); + +router.put('/:id', wrapAsync(async (req, res) => { + await locationsService.update(req.body.data, req.body.id, req.currentUser); + res.status(200).send(true); +})); + +router.delete('/:id', wrapAsync(async (req, res) => { + await locationsService.remove(req.params.id, req.currentUser); + res.status(200).send(true); +})); + +router.post('/deleteByIds', wrapAsync(async (req, res) => { + await locationsService.deleteByIds(req.body.data, req.currentUser); + res.status(200).send(true); +})); + +router.get('/', wrapAsync(async (req, res) => { + const globalAccess = req.currentUser.app_role.globalAccess; + const currentUser = req.currentUser; + const payload = await locationsDBApi.findAll(req.query, globalAccess, { currentUser }); + res.status(200).send(payload); +})); + +router.get('/autocomplete', wrapAsync(async (req, res) => { + const globalAccess = req.currentUser.app_role.globalAccess; + const organizationId = req.currentUser.organizationsId; + const payload = await locationsDBApi.findAllAutocomplete( + req.query.query, req.query.limit, req.query.offset, globalAccess, organizationId + ); + res.status(200).send(payload); +})); + +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/programs.js b/backend/src/routes/programs.js new file mode 100644 index 0000000..eec322e --- /dev/null +++ b/backend/src/routes/programs.js @@ -0,0 +1,53 @@ +const express = require('express'); +const programsService = require('../services/programs'); +const programsDBApi = require('../db/api/programs'); +const wrapAsync = require('../helpers').wrapAsync; +const router = express.Router(); +const { checkCrudPermissions } = require('../middlewares/check-permissions'); + +router.use(checkCrudPermissions('programs')); + +router.post('/', wrapAsync(async (req, res) => { + await programsService.create(req.body.data, req.currentUser); + res.status(200).send(true); +})); + +router.put('/:id', wrapAsync(async (req, res) => { + await programsService.update(req.body.data, req.body.id, req.currentUser); + res.status(200).send(true); +})); + +router.delete('/:id', wrapAsync(async (req, res) => { + await programsService.remove(req.params.id, req.currentUser); + res.status(200).send(true); +})); + +router.post('/deleteByIds', wrapAsync(async (req, res) => { + await programsService.deleteByIds(req.body.data, req.currentUser); + res.status(200).send(true); +})); + +router.get('/', wrapAsync(async (req, res) => { + const globalAccess = req.currentUser.app_role.globalAccess; + const currentUser = req.currentUser; + const payload = await programsDBApi.findAll(req.query, globalAccess, { currentUser }); + res.status(200).send(payload); +})); + +router.get('/autocomplete', wrapAsync(async (req, res) => { + const globalAccess = req.currentUser.app_role.globalAccess; + const organizationId = req.currentUser.organizationsId; + const payload = await programsDBApi.findAllAutocomplete( + req.query.query, req.query.limit, req.query.offset, globalAccess, organizationId + ); + res.status(200).send(payload); +})); + +router.get('/:id', wrapAsync(async (req, res) => { + const payload = await programsDBApi.findBy({ id: req.params.id }); + res.status(200).send(payload); +})); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/routes/propagation_types.js b/backend/src/routes/propagation_types.js new file mode 100644 index 0000000..5994257 --- /dev/null +++ b/backend/src/routes/propagation_types.js @@ -0,0 +1,54 @@ + +const express = require('express'); +const Propagation_typesService = require('../services/propagation_types'); +const Propagation_typesDBApi = require('../db/api/propagation_types'); +const wrapAsync = require('../helpers').wrapAsync; +const router = express.Router(); +const { checkCrudPermissions } = require('../middlewares/check-permissions'); + +router.use(checkCrudPermissions('propagation_types')); + +router.post('/', wrapAsync(async (req, res) => { + await Propagation_typesService.create(req.body.data, req.currentUser); + res.status(200).send(true); +})); + +router.put('/:id', wrapAsync(async (req, res) => { + await Propagation_typesService.update(req.body.data, req.body.id, req.currentUser); + res.status(200).send(true); +})); + +router.delete('/:id', wrapAsync(async (req, res) => { + await Propagation_typesService.remove(req.params.id, req.currentUser); + res.status(200).send(true); +})); + +router.post('/deleteByIds', wrapAsync(async (req, res) => { + await Propagation_typesService.deleteByIds(req.body.data, req.currentUser); + res.status(200).send(true); +})); + +router.get('/', wrapAsync(async (req, res) => { + const globalAccess = req.currentUser.app_role.globalAccess; + const currentUser = req.currentUser; + const payload = await Propagation_typesDBApi.findAll(req.query, globalAccess, { currentUser }); + res.status(200).send(payload); +})); + +router.get('/autocomplete', wrapAsync(async (req, res) => { + const globalAccess = req.currentUser.app_role.globalAccess; + const organizationId = req.currentUser.organizationsId; + const payload = await Propagation_typesDBApi.findAllAutocomplete( + req.query.query, req.query.limit, req.query.offset, globalAccess, organizationId + ); + res.status(200).send(payload); +})); + +router.get('/:id', wrapAsync(async (req, res) => { + const payload = await Propagation_typesDBApi.findBy({ id: req.params.id }); + res.status(200).send(payload); +})); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/routes/retailer_programs.js b/backend/src/routes/retailer_programs.js new file mode 100644 index 0000000..54d3700 --- /dev/null +++ b/backend/src/routes/retailer_programs.js @@ -0,0 +1,54 @@ + +const express = require('express'); +const Retailer_programsService = require('../services/retailer_programs'); +const Retailer_programsDBApi = require('../db/api/retailer_programs'); +const wrapAsync = require('../helpers').wrapAsync; +const router = express.Router(); +const { checkCrudPermissions } = require('../middlewares/check-permissions'); + +router.use(checkCrudPermissions('retailer_programs')); + +router.post('/', wrapAsync(async (req, res) => { + await Retailer_programsService.create(req.body.data, req.currentUser); + res.status(200).send(true); +})); + +router.put('/:id', wrapAsync(async (req, res) => { + await Retailer_programsService.update(req.body.data, req.body.id, req.currentUser); + res.status(200).send(true); +})); + +router.delete('/:id', wrapAsync(async (req, res) => { + await Retailer_programsService.remove(req.params.id, req.currentUser); + res.status(200).send(true); +})); + +router.post('/deleteByIds', wrapAsync(async (req, res) => { + await Retailer_programsService.deleteByIds(req.body.data, req.currentUser); + res.status(200).send(true); +})); + +router.get('/', wrapAsync(async (req, res) => { + const globalAccess = req.currentUser.app_role.globalAccess; + const currentUser = req.currentUser; + const payload = await Retailer_programsDBApi.findAll(req.query, globalAccess, { currentUser }); + res.status(200).send(payload); +})); + +router.get('/autocomplete', wrapAsync(async (req, res) => { + const globalAccess = req.currentUser.app_role.globalAccess; + const organizationId = req.currentUser.organizationsId; + const payload = await Retailer_programsDBApi.findAllAutocomplete( + req.query.query, req.query.limit, req.query.offset, globalAccess, organizationId + ); + res.status(200).send(payload); +})); + +router.get('/:id', wrapAsync(async (req, res) => { + const payload = await Retailer_programsDBApi.findBy({ id: req.params.id }); + res.status(200).send(payload); +})); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/routes/retailers.js b/backend/src/routes/retailers.js new file mode 100644 index 0000000..17168b9 --- /dev/null +++ b/backend/src/routes/retailers.js @@ -0,0 +1,53 @@ +const express = require('express'); +const retailersService = require('../services/retailers'); +const retailersDBApi = require('../db/api/retailers'); +const wrapAsync = require('../helpers').wrapAsync; +const router = express.Router(); +const { checkCrudPermissions } = require('../middlewares/check-permissions'); + +router.use(checkCrudPermissions('retailers')); + +router.post('/', wrapAsync(async (req, res) => { + await retailersService.create(req.body.data, req.currentUser); + res.status(200).send(true); +})); + +router.put('/:id', wrapAsync(async (req, res) => { + await retailersService.update(req.body.data, req.body.id, req.currentUser); + res.status(200).send(true); +})); + +router.delete('/:id', wrapAsync(async (req, res) => { + await retailersService.remove(req.params.id, req.currentUser); + res.status(200).send(true); +})); + +router.post('/deleteByIds', wrapAsync(async (req, res) => { + await retailersService.deleteByIds(req.body.data, req.currentUser); + res.status(200).send(true); +})); + +router.get('/', wrapAsync(async (req, res) => { + const globalAccess = req.currentUser.app_role.globalAccess; + const currentUser = req.currentUser; + const payload = await retailersDBApi.findAll(req.query, globalAccess, { currentUser }); + res.status(200).send(payload); +})); + +router.get('/autocomplete', wrapAsync(async (req, res) => { + const globalAccess = req.currentUser.app_role.globalAccess; + const organizationId = req.currentUser.organizationsId; + const payload = await retailersDBApi.findAllAutocomplete( + req.query.query, req.query.limit, req.query.offset, globalAccess, organizationId + ); + res.status(200).send(payload); +})); + +router.get('/:id', wrapAsync(async (req, res) => { + const payload = await retailersDBApi.findBy({ id: req.params.id }); + res.status(200).send(payload); +})); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/routes/sites.js b/backend/src/routes/sites.js new file mode 100644 index 0000000..ae02333 --- /dev/null +++ b/backend/src/routes/sites.js @@ -0,0 +1,53 @@ +const express = require('express'); +const sitesService = require('../services/sites'); +const sitesDBApi = require('../db/api/sites'); +const wrapAsync = require('../helpers').wrapAsync; +const router = express.Router(); +const { checkCrudPermissions } = require('../middlewares/check-permissions'); + +router.use(checkCrudPermissions('sites')); + +router.post('/', wrapAsync(async (req, res) => { + await sitesService.create(req.body.data, req.currentUser); + res.status(200).send(true); +})); + +router.put('/:id', wrapAsync(async (req, res) => { + await sitesService.update(req.body.data, req.body.id, req.currentUser); + res.status(200).send(true); +})); + +router.delete('/:id', wrapAsync(async (req, res) => { + await sitesService.remove(req.params.id, req.currentUser); + res.status(200).send(true); +})); + +router.post('/deleteByIds', wrapAsync(async (req, res) => { + await sitesService.deleteByIds(req.body.data, req.currentUser); + res.status(200).send(true); +})); + +router.get('/', wrapAsync(async (req, res) => { + const globalAccess = req.currentUser.app_role.globalAccess; + const currentUser = req.currentUser; + const payload = await sitesDBApi.findAll(req.query, globalAccess, { currentUser }); + res.status(200).send(payload); +})); + +router.get('/autocomplete', wrapAsync(async (req, res) => { + const globalAccess = req.currentUser.app_role.globalAccess; + const organizationId = req.currentUser.organizationsId; + const payload = await sitesDBApi.findAllAutocomplete( + req.query.query, req.query.limit, req.query.offset, globalAccess, organizationId + ); + res.status(200).send(payload); +})); + +router.get('/:id', wrapAsync(async (req, res) => { + const payload = await sitesDBApi.findBy({ id: req.params.id }); + res.status(200).send(payload); +})); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/services/breeders.js b/backend/src/services/breeders.js new file mode 100644 index 0000000..330e4b6 --- /dev/null +++ b/backend/src/services/breeders.js @@ -0,0 +1,52 @@ +const db = require('../db/models'); +const BreedersDBApi = require('../db/api/breeders'); +const ValidationError = require('./notifications/errors/validation'); + +module.exports = class BreedersService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await BreedersDBApi.create(data, { currentUser, transaction }); + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async update(data, id, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + const record = await BreedersDBApi.findBy({ id }, { transaction }); + if (!record) throw new ValidationError('breedersNotFound'); + const updated = await BreedersDBApi.update(id, data, { currentUser, transaction }); + await transaction.commit(); + return updated; + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await BreedersDBApi.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 BreedersDBApi.remove(id, { currentUser, transaction }); + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; diff --git a/backend/src/services/containers.js b/backend/src/services/containers.js new file mode 100644 index 0000000..2084327 --- /dev/null +++ b/backend/src/services/containers.js @@ -0,0 +1,53 @@ + +const db = require('../db/models'); +const ContainersDBApi = require('../db/api/containers'); +const ValidationError = require('./notifications/errors/validation'); + +module.exports = class ContainersService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await ContainersDBApi.create(data, { currentUser, transaction }); + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async update(data, id, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + const record = await ContainersDBApi.findBy({ id }, { transaction }); + if (!record) throw new ValidationError('containersNotFound'); + const updated = await ContainersDBApi.update(id, data, { currentUser, transaction }); + await transaction.commit(); + return updated; + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await ContainersDBApi.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 ContainersDBApi.remove(id, { currentUser, transaction }); + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; diff --git a/backend/src/services/crop_types.js b/backend/src/services/crop_types.js new file mode 100644 index 0000000..19360db --- /dev/null +++ b/backend/src/services/crop_types.js @@ -0,0 +1,53 @@ + +const db = require('../db/models'); +const Crop_typesDBApi = require('../db/api/crop_types'); +const ValidationError = require('./notifications/errors/validation'); + +module.exports = class Crop_typesService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await Crop_typesDBApi.create(data, { currentUser, transaction }); + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async update(data, id, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + const record = await Crop_typesDBApi.findBy({ id }, { transaction }); + if (!record) throw new ValidationError('crop_typesNotFound'); + const updated = await Crop_typesDBApi.update(id, data, { currentUser, transaction }); + await transaction.commit(); + return updated; + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await Crop_typesDBApi.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 Crop_typesDBApi.remove(id, { currentUser, transaction }); + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; diff --git a/backend/src/services/groups.js b/backend/src/services/groups.js new file mode 100644 index 0000000..2eccd86 --- /dev/null +++ b/backend/src/services/groups.js @@ -0,0 +1,52 @@ +const db = require('../db/models'); +const groupsDBApi = require('../db/api/groups'); +const ValidationError = require('./notifications/errors/validation'); + +module.exports = class groupsService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await groupsDBApi.create(data, { currentUser, transaction }); + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async update(data, id, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + const record = await groupsDBApi.findBy({ id }, { transaction }); + if (!record) throw new ValidationError('groupsNotFound'); + const updated = await groupsDBApi.update(id, data, { currentUser, transaction }); + await transaction.commit(); + return updated; + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await groupsDBApi.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 groupsDBApi.remove(id, { currentUser, transaction }); + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; diff --git a/backend/src/services/growth_habits.js b/backend/src/services/growth_habits.js new file mode 100644 index 0000000..03c886e --- /dev/null +++ b/backend/src/services/growth_habits.js @@ -0,0 +1,53 @@ + +const db = require('../db/models'); +const Growth_habitsDBApi = require('../db/api/growth_habits'); +const ValidationError = require('./notifications/errors/validation'); + +module.exports = class Growth_habitsService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await Growth_habitsDBApi.create(data, { currentUser, transaction }); + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async update(data, id, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + const record = await Growth_habitsDBApi.findBy({ id }, { transaction }); + if (!record) throw new ValidationError('growth_habitsNotFound'); + const updated = await Growth_habitsDBApi.update(id, data, { currentUser, transaction }); + await transaction.commit(); + return updated; + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await Growth_habitsDBApi.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 Growth_habitsDBApi.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..82ec36d --- /dev/null +++ b/backend/src/services/locations.js @@ -0,0 +1,52 @@ +const db = require('../db/models'); +const locationsDBApi = require('../db/api/locations'); +const ValidationError = require('./notifications/errors/validation'); + +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 update(data, id, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + const record = await locationsDBApi.findBy({ id }, { transaction }); + if (!record) throw new ValidationError('locationsNotFound'); + const updated = await locationsDBApi.update(id, data, { currentUser, transaction }); + await transaction.commit(); + return updated; + } 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/programs.js b/backend/src/services/programs.js new file mode 100644 index 0000000..8c19ef0 --- /dev/null +++ b/backend/src/services/programs.js @@ -0,0 +1,52 @@ +const db = require('../db/models'); +const programsDBApi = require('../db/api/programs'); +const ValidationError = require('./notifications/errors/validation'); + +module.exports = class programsService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await programsDBApi.create(data, { currentUser, transaction }); + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async update(data, id, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + const record = await programsDBApi.findBy({ id }, { transaction }); + if (!record) throw new ValidationError('programsNotFound'); + const updated = await programsDBApi.update(id, data, { currentUser, transaction }); + await transaction.commit(); + return updated; + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await programsDBApi.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 programsDBApi.remove(id, { currentUser, transaction }); + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; diff --git a/backend/src/services/propagation_types.js b/backend/src/services/propagation_types.js new file mode 100644 index 0000000..95538d0 --- /dev/null +++ b/backend/src/services/propagation_types.js @@ -0,0 +1,53 @@ + +const db = require('../db/models'); +const Propagation_typesDBApi = require('../db/api/propagation_types'); +const ValidationError = require('./notifications/errors/validation'); + +module.exports = class Propagation_typesService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await Propagation_typesDBApi.create(data, { currentUser, transaction }); + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async update(data, id, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + const record = await Propagation_typesDBApi.findBy({ id }, { transaction }); + if (!record) throw new ValidationError('propagation_typesNotFound'); + const updated = await Propagation_typesDBApi.update(id, data, { currentUser, transaction }); + await transaction.commit(); + return updated; + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await Propagation_typesDBApi.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 Propagation_typesDBApi.remove(id, { currentUser, transaction }); + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; diff --git a/backend/src/services/retailer_programs.js b/backend/src/services/retailer_programs.js new file mode 100644 index 0000000..a49498d --- /dev/null +++ b/backend/src/services/retailer_programs.js @@ -0,0 +1,53 @@ + +const db = require('../db/models'); +const Retailer_programsDBApi = require('../db/api/retailer_programs'); +const ValidationError = require('./notifications/errors/validation'); + +module.exports = class Retailer_programsService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await Retailer_programsDBApi.create(data, { currentUser, transaction }); + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async update(data, id, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + const record = await Retailer_programsDBApi.findBy({ id }, { transaction }); + if (!record) throw new ValidationError('retailer_programsNotFound'); + const updated = await Retailer_programsDBApi.update(id, data, { currentUser, transaction }); + await transaction.commit(); + return updated; + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await Retailer_programsDBApi.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 Retailer_programsDBApi.remove(id, { currentUser, transaction }); + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; diff --git a/backend/src/services/retailers.js b/backend/src/services/retailers.js new file mode 100644 index 0000000..4a5ee2b --- /dev/null +++ b/backend/src/services/retailers.js @@ -0,0 +1,52 @@ +const db = require('../db/models'); +const retailersDBApi = require('../db/api/retailers'); +const ValidationError = require('./notifications/errors/validation'); + +module.exports = class retailersService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await retailersDBApi.create(data, { currentUser, transaction }); + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async update(data, id, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + const record = await retailersDBApi.findBy({ id }, { transaction }); + if (!record) throw new ValidationError('retailersNotFound'); + const updated = await retailersDBApi.update(id, data, { currentUser, transaction }); + await transaction.commit(); + return updated; + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await retailersDBApi.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 retailersDBApi.remove(id, { currentUser, transaction }); + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; diff --git a/backend/src/services/sites.js b/backend/src/services/sites.js new file mode 100644 index 0000000..9efcd55 --- /dev/null +++ b/backend/src/services/sites.js @@ -0,0 +1,52 @@ +const db = require('../db/models'); +const sitesDBApi = require('../db/api/sites'); +const ValidationError = require('./notifications/errors/validation'); + +module.exports = class sitesService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await sitesDBApi.create(data, { currentUser, transaction }); + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async update(data, id, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + const record = await sitesDBApi.findBy({ id }, { transaction }); + if (!record) throw new ValidationError('sitesNotFound'); + const updated = await sitesDBApi.update(id, data, { currentUser, transaction }); + await transaction.commit(); + return updated; + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await sitesDBApi.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 sitesDBApi.remove(id, { currentUser, transaction }); + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; diff --git a/frontend/src/components/AsideMenuLayer.tsx b/frontend/src/components/AsideMenuLayer.tsx index 8693dea..a669004 100644 --- a/frontend/src/components/AsideMenuLayer.tsx +++ b/frontend/src/components/AsideMenuLayer.tsx @@ -3,10 +3,9 @@ import { mdiLogout, mdiClose } from '@mdi/js' import BaseIcon from './BaseIcon' import AsideMenuList from './AsideMenuList' import { MenuAsideItem } from '../interfaces' -import { useAppSelector } from '../stores/hooks' +import { useAppSelector, useAppDispatch } from '../stores/hooks' import Link from 'next/link'; -import { useAppDispatch } from '../stores/hooks'; import { createAsyncThunk } from '@reduxjs/toolkit'; import axios from 'axios'; @@ -91,4 +90,4 @@ export default function AsideMenuLayer({ menu, className = '', ...props }: Props ) -} +} \ No newline at end of file diff --git a/frontend/src/components/Breeders/TableBreeders.tsx b/frontend/src/components/Breeders/TableBreeders.tsx new file mode 100644 index 0000000..cf3d226 --- /dev/null +++ b/frontend/src/components/Breeders/TableBreeders.tsx @@ -0,0 +1,78 @@ + +import React, { useEffect, useState } from 'react' +import { useAppDispatch, useAppSelector } from '../../stores/hooks' +import { fetch, deleteItem, setRefetch, deleteItemsByIds } from '../../stores/breeders/breedersSlice' +import { loadColumns } from "./configureBreedersCols"; +import { DataGrid, GridRowSelectionModel } from '@mui/x-data-grid'; +import BaseButton from '../BaseButton'; +import { mdiDelete } from '@mdi/js'; +import CardBoxModal from '../CardBoxModal'; +import { createPortal } from 'react-dom'; + +const TableBreeders = ({ filterItems, setFilterItems, filters, showGrid }) => { + const dispatch = useAppDispatch(); + const { breeders, loading, count, refetch } = useAppSelector((state) => state.breeders); + const { currentUser } = useAppSelector((state) => state.auth); + const [paginationModel, setPaginationModel] = useState({ pageSize: 10, page: 0 }); + const [selectionModel, setSelectionModel] = useState([]); + const [isDeleteModalActive, setIsDeleteModalActive] = useState(false); + + useEffect(() => { + const query = `?limit=${paginationModel.pageSize}&page=${paginationModel.page}`; + dispatch(fetch({ query })); + if (refetch) dispatch(setRefetch(false)); + }, [dispatch, paginationModel, refetch]); + + const deleteHandler = (id: string) => { + dispatch(deleteItem(id)).then(() => dispatch(setRefetch(true))); + }; + + const handleDeleteByIds = () => { + dispatch(deleteItemsByIds(selectionModel)).then(() => { + dispatch(setRefetch(true)); + setIsDeleteModalActive(false); + }); + }; + + const columns = loadColumns(currentUser, deleteHandler); + + return ( +
+ {selectionModel.length > 0 && typeof document !== 'undefined' && document.getElementById('delete-rows-button') && + createPortal( + setIsDeleteModalActive(true)} + />, + document.getElementById('delete-rows-button') as HTMLElement + ) + } + setSelectionModel(newSelectionModel)} + rowSelectionModel={selectionModel} + /> + setIsDeleteModalActive(false)} + > +

Are you sure you want to delete {selectionModel.length} items?

+
+
+ ); +}; + +export default TableBreeders; diff --git a/frontend/src/components/Breeders/configureBreedersCols.tsx b/frontend/src/components/Breeders/configureBreedersCols.tsx new file mode 100644 index 0000000..11f41d2 --- /dev/null +++ b/frontend/src/components/Breeders/configureBreedersCols.tsx @@ -0,0 +1,33 @@ +import { GridColDef } from '@mui/x-data-grid' +import React from 'react' +import { hasPermission } from "../../helpers/userPermissions"; +import ListActionsPopover from "../ListActionsPopover"; + +export const loadColumns = (user: any, deleteHandler: any) => { + const hasUpdatePermission = hasPermission(user, 'UPDATE_BREEDERS') + + const columns: GridColDef[] = [ + { + field: 'name', + headerName: 'Name', + flex: 1, + }, + { + field: 'actions', + headerName: 'Actions', + sortable: false, + width: 100, + renderCell: (params) => ( + + ), + }, + ] + + return columns +} \ No newline at end of file diff --git a/frontend/src/components/Containers/TableContainers.tsx b/frontend/src/components/Containers/TableContainers.tsx new file mode 100644 index 0000000..c2397a5 --- /dev/null +++ b/frontend/src/components/Containers/TableContainers.tsx @@ -0,0 +1,73 @@ + +import React, { useEffect, useState } from 'react' +import { useAppDispatch, useAppSelector } from '../../stores/hooks' +import { fetch, deleteItem, setRefetch, deleteItemsByIds } from '../../stores/containers/containersSlice' +import { loadColumns } from "./configureContainersCols"; +import { DataGrid, GridRowSelectionModel } from '@mui/x-data-grid'; +import BaseButton from '../BaseButton'; +import { mdiDelete } from '@mdi/js'; +import CardBoxModal from '../CardBoxModal'; +import { createPortal } from 'react-dom'; + +const TableContainers = ({ filterItems, setFilterItems, filters, showGrid }) => { + const dispatch = useAppDispatch(); + const { containers, loading, count, refetch } = useAppSelector((state) => state.containers); + const { currentUser } = useAppSelector((state) => state.auth); + const [paginationModel, setPaginationModel] = useState({ pageSize: 10, page: 0 }); + const [selectionModel, setSelectionModel] = useState([]); + const [isDeleteModalActive, setIsDeleteModalActive] = useState(false); + + useEffect(() => { + const query = `?limit=${paginationModel.pageSize}&page=${paginationModel.page}`; + dispatch(fetch({ query })); + if (refetch) dispatch(setRefetch(false)); + }, [dispatch, paginationModel, refetch]); + + const deleteHandler = (id: string) => { + dispatch(deleteItem(id)).then(() => dispatch(setRefetch(true))); + }; + + const handleDeleteByIds = () => { + dispatch(deleteItemsByIds(selectionModel)).then(() => { + dispatch(setRefetch(true)); + setIsDeleteModalActive(false); + }); + }; + + const columns = loadColumns(currentUser, deleteHandler); + + return ( +
+ {selectionModel.length > 0 && typeof document !== 'undefined' && document.getElementById('delete-rows-button') && + createPortal( + setIsDeleteModalActive(true)} />, + document.getElementById('delete-rows-button') as HTMLElement + ) + } + setSelectionModel(newSelectionModel)} + rowSelectionModel={selectionModel} + /> + setIsDeleteModalActive(false)} + > +

Are you sure you want to delete {selectionModel.length} items?

+
+
+ ); +}; + +export default TableContainers; diff --git a/frontend/src/components/Containers/configureContainersCols.tsx b/frontend/src/components/Containers/configureContainersCols.tsx new file mode 100644 index 0000000..096cd01 --- /dev/null +++ b/frontend/src/components/Containers/configureContainersCols.tsx @@ -0,0 +1,30 @@ + +import { GridColDef } from '@mui/x-data-grid' +import React from 'react' +import { hasPermission } from "../../helpers/userPermissions"; +import ListActionsPopover from "../ListActionsPopover"; + +export const loadColumns = (user: any, deleteHandler: any) => { + const hasUpdatePermission = hasPermission(user, 'UPDATE_CONTAINERS') + const hasDeletePermission = hasPermission(user, 'DELETE_CONTAINERS') + const columns: GridColDef[] = [ + { field: 'name', headerName: 'Name', flex: 1 }, + { + field: 'actions', + headerName: 'Actions', + sortable: false, + width: 100, + renderCell: (params) => ( + + ), + }, + ] + return columns +} diff --git a/frontend/src/components/Crop_types/TableCrop_types.tsx b/frontend/src/components/Crop_types/TableCrop_types.tsx new file mode 100644 index 0000000..abe49c1 --- /dev/null +++ b/frontend/src/components/Crop_types/TableCrop_types.tsx @@ -0,0 +1,73 @@ + +import React, { useEffect, useState } from 'react' +import { useAppDispatch, useAppSelector } from '../../stores/hooks' +import { fetch, deleteItem, setRefetch, deleteItemsByIds } from '../../stores/crop_types/crop_typesSlice' +import { loadColumns } from "./configureCrop_typesCols"; +import { DataGrid, GridRowSelectionModel } from '@mui/x-data-grid'; +import BaseButton from '../BaseButton'; +import { mdiDelete } from '@mdi/js'; +import CardBoxModal from '../CardBoxModal'; +import { createPortal } from 'react-dom'; + +const TableCrop_types = ({ filterItems, setFilterItems, filters, showGrid }) => { + const dispatch = useAppDispatch(); + const { crop_types, loading, count, refetch } = useAppSelector((state) => state.crop_types); + const { currentUser } = useAppSelector((state) => state.auth); + const [paginationModel, setPaginationModel] = useState({ pageSize: 10, page: 0 }); + const [selectionModel, setSelectionModel] = useState([]); + const [isDeleteModalActive, setIsDeleteModalActive] = useState(false); + + useEffect(() => { + const query = `?limit=${paginationModel.pageSize}&page=${paginationModel.page}`; + dispatch(fetch({ query })); + if (refetch) dispatch(setRefetch(false)); + }, [dispatch, paginationModel, refetch]); + + const deleteHandler = (id: string) => { + dispatch(deleteItem(id)).then(() => dispatch(setRefetch(true))); + }; + + const handleDeleteByIds = () => { + dispatch(deleteItemsByIds(selectionModel)).then(() => { + dispatch(setRefetch(true)); + setIsDeleteModalActive(false); + }); + }; + + const columns = loadColumns(currentUser, deleteHandler); + + return ( +
+ {selectionModel.length > 0 && typeof document !== 'undefined' && document.getElementById('delete-rows-button') && + createPortal( + setIsDeleteModalActive(true)} />, + document.getElementById('delete-rows-button') as HTMLElement + ) + } + setSelectionModel(newSelectionModel)} + rowSelectionModel={selectionModel} + /> + setIsDeleteModalActive(false)} + > +

Are you sure you want to delete {selectionModel.length} items?

+
+
+ ); +}; + +export default TableCrop_types; diff --git a/frontend/src/components/Crop_types/configureCrop_typesCols.tsx b/frontend/src/components/Crop_types/configureCrop_typesCols.tsx new file mode 100644 index 0000000..3db92b2 --- /dev/null +++ b/frontend/src/components/Crop_types/configureCrop_typesCols.tsx @@ -0,0 +1,30 @@ + +import { GridColDef } from '@mui/x-data-grid' +import React from 'react' +import { hasPermission } from "../../helpers/userPermissions"; +import ListActionsPopover from "../ListActionsPopover"; + +export const loadColumns = (user: any, deleteHandler: any) => { + const hasUpdatePermission = hasPermission(user, 'UPDATE_CROP_TYPES') + const hasDeletePermission = hasPermission(user, 'DELETE_CROP_TYPES') + const columns: GridColDef[] = [ + { field: 'name', headerName: 'Name', flex: 1 }, + { + field: 'actions', + headerName: 'Actions', + sortable: false, + width: 100, + renderCell: (params) => ( + + ), + }, + ] + return columns +} diff --git a/frontend/src/components/Groups/TableGroups.tsx b/frontend/src/components/Groups/TableGroups.tsx new file mode 100644 index 0000000..2837e25 --- /dev/null +++ b/frontend/src/components/Groups/TableGroups.tsx @@ -0,0 +1,78 @@ + +import React, { useEffect, useState } from 'react' +import { useAppDispatch, useAppSelector } from '../../stores/hooks' +import { fetch, deleteItem, setRefetch, deleteItemsByIds } from '../../stores/groups/groupsSlice' +import { loadColumns } from "./configureGroupsCols"; +import { DataGrid, GridRowSelectionModel } from '@mui/x-data-grid'; +import BaseButton from '../BaseButton'; +import { mdiDelete } from '@mdi/js'; +import CardBoxModal from '../CardBoxModal'; +import { createPortal } from 'react-dom'; + +const TableGroups = ({ filterItems, setFilterItems, filters, showGrid }) => { + const dispatch = useAppDispatch(); + const { groups, loading, count, refetch } = useAppSelector((state) => state.groups); + const { currentUser } = useAppSelector((state) => state.auth); + const [paginationModel, setPaginationModel] = useState({ pageSize: 10, page: 0 }); + const [selectionModel, setSelectionModel] = useState([]); + const [isDeleteModalActive, setIsDeleteModalActive] = useState(false); + + useEffect(() => { + const query = `?limit=${paginationModel.pageSize}&page=${paginationModel.page}`; + dispatch(fetch({ query })); + if (refetch) dispatch(setRefetch(false)); + }, [dispatch, paginationModel, refetch]); + + const deleteHandler = (id: string) => { + dispatch(deleteItem(id)).then(() => dispatch(setRefetch(true))); + }; + + const handleDeleteByIds = () => { + dispatch(deleteItemsByIds(selectionModel)).then(() => { + dispatch(setRefetch(true)); + setIsDeleteModalActive(false); + }); + }; + + const columns = loadColumns(currentUser, deleteHandler); + + return ( +
+ {selectionModel.length > 0 && typeof document !== 'undefined' && document.getElementById('delete-rows-button') && + createPortal( + setIsDeleteModalActive(true)} + />, + document.getElementById('delete-rows-button') as HTMLElement + ) + } + setSelectionModel(newSelectionModel)} + rowSelectionModel={selectionModel} + /> + setIsDeleteModalActive(false)} + > +

Are you sure you want to delete {selectionModel.length} items?

+
+
+ ); +}; + +export default TableGroups; diff --git a/frontend/src/components/Groups/configureGroupsCols.tsx b/frontend/src/components/Groups/configureGroupsCols.tsx new file mode 100644 index 0000000..46ba6fe --- /dev/null +++ b/frontend/src/components/Groups/configureGroupsCols.tsx @@ -0,0 +1,33 @@ +import { GridColDef } from '@mui/x-data-grid' +import React from 'react' +import { hasPermission } from "../../helpers/userPermissions"; +import ListActionsPopover from "../ListActionsPopover"; + +export const loadColumns = (user: any, deleteHandler: any) => { + const hasUpdatePermission = hasPermission(user, 'UPDATE_GROUPS') + + const columns: GridColDef[] = [ + { + field: 'name', + headerName: 'Name', + flex: 1, + }, + { + field: 'actions', + headerName: 'Actions', + sortable: false, + width: 100, + renderCell: (params) => ( + + ), + }, + ] + + return columns +} \ No newline at end of file diff --git a/frontend/src/components/Growth_habits/TableGrowth_habits.tsx b/frontend/src/components/Growth_habits/TableGrowth_habits.tsx new file mode 100644 index 0000000..342a73e --- /dev/null +++ b/frontend/src/components/Growth_habits/TableGrowth_habits.tsx @@ -0,0 +1,73 @@ + +import React, { useEffect, useState } from 'react' +import { useAppDispatch, useAppSelector } from '../../stores/hooks' +import { fetch, deleteItem, setRefetch, deleteItemsByIds } from '../../stores/growth_habits/growth_habitsSlice' +import { loadColumns } from "./configureGrowth_habitsCols"; +import { DataGrid, GridRowSelectionModel } from '@mui/x-data-grid'; +import BaseButton from '../BaseButton'; +import { mdiDelete } from '@mdi/js'; +import CardBoxModal from '../CardBoxModal'; +import { createPortal } from 'react-dom'; + +const TableGrowth_habits = ({ filterItems, setFilterItems, filters, showGrid }) => { + const dispatch = useAppDispatch(); + const { growth_habits, loading, count, refetch } = useAppSelector((state) => state.growth_habits); + const { currentUser } = useAppSelector((state) => state.auth); + const [paginationModel, setPaginationModel] = useState({ pageSize: 10, page: 0 }); + const [selectionModel, setSelectionModel] = useState([]); + const [isDeleteModalActive, setIsDeleteModalActive] = useState(false); + + useEffect(() => { + const query = `?limit=${paginationModel.pageSize}&page=${paginationModel.page}`; + dispatch(fetch({ query })); + if (refetch) dispatch(setRefetch(false)); + }, [dispatch, paginationModel, refetch]); + + const deleteHandler = (id: string) => { + dispatch(deleteItem(id)).then(() => dispatch(setRefetch(true))); + }; + + const handleDeleteByIds = () => { + dispatch(deleteItemsByIds(selectionModel)).then(() => { + dispatch(setRefetch(true)); + setIsDeleteModalActive(false); + }); + }; + + const columns = loadColumns(currentUser, deleteHandler); + + return ( +
+ {selectionModel.length > 0 && typeof document !== 'undefined' && document.getElementById('delete-rows-button') && + createPortal( + setIsDeleteModalActive(true)} />, + document.getElementById('delete-rows-button') as HTMLElement + ) + } + setSelectionModel(newSelectionModel)} + rowSelectionModel={selectionModel} + /> + setIsDeleteModalActive(false)} + > +

Are you sure you want to delete {selectionModel.length} items?

+
+
+ ); +}; + +export default TableGrowth_habits; diff --git a/frontend/src/components/Growth_habits/configureGrowth_habitsCols.tsx b/frontend/src/components/Growth_habits/configureGrowth_habitsCols.tsx new file mode 100644 index 0000000..3101713 --- /dev/null +++ b/frontend/src/components/Growth_habits/configureGrowth_habitsCols.tsx @@ -0,0 +1,31 @@ + +import { GridColDef } from '@mui/x-data-grid' +import React from 'react' +import { hasPermission } from "../../helpers/userPermissions"; +import ListActionsPopover from "../ListActionsPopover"; + +export const loadColumns = (user: any, deleteHandler: any) => { + const hasUpdatePermission = hasPermission(user, 'UPDATE_GROWTH_HABITS') + const hasDeletePermission = hasPermission(user, 'DELETE_GROWTH_HABITS') + + const columns: GridColDef[] = [ + { field: 'name', headerName: 'Name', flex: 1 }, + { + field: 'actions', + headerName: 'Actions', + sortable: false, + width: 100, + renderCell: (params) => ( + + ), + }, + ] + return columns +} diff --git a/frontend/src/components/Locations/TableLocations.tsx b/frontend/src/components/Locations/TableLocations.tsx new file mode 100644 index 0000000..30e3227 --- /dev/null +++ b/frontend/src/components/Locations/TableLocations.tsx @@ -0,0 +1,78 @@ + +import React, { useEffect, useState } from 'react' +import { useAppDispatch, useAppSelector } from '../../stores/hooks' +import { fetch, deleteItem, setRefetch, deleteItemsByIds } from '../../stores/locations/locationsSlice' +import { loadColumns } from "./configureLocationsCols"; +import { DataGrid, GridRowSelectionModel } from '@mui/x-data-grid'; +import BaseButton from '../BaseButton'; +import { mdiDelete } from '@mdi/js'; +import CardBoxModal from '../CardBoxModal'; +import { createPortal } from 'react-dom'; + +const TableLocations = ({ filterItems, setFilterItems, filters, showGrid }) => { + const dispatch = useAppDispatch(); + const { locations, loading, count, refetch } = useAppSelector((state) => state.locations); + const { currentUser } = useAppSelector((state) => state.auth); + const [paginationModel, setPaginationModel] = useState({ pageSize: 10, page: 0 }); + const [selectionModel, setSelectionModel] = useState([]); + const [isDeleteModalActive, setIsDeleteModalActive] = useState(false); + + useEffect(() => { + const query = `?limit=${paginationModel.pageSize}&page=${paginationModel.page}`; + dispatch(fetch({ query })); + if (refetch) dispatch(setRefetch(false)); + }, [dispatch, paginationModel, refetch]); + + const deleteHandler = (id: string) => { + dispatch(deleteItem(id)).then(() => dispatch(setRefetch(true))); + }; + + const handleDeleteByIds = () => { + dispatch(deleteItemsByIds(selectionModel)).then(() => { + dispatch(setRefetch(true)); + setIsDeleteModalActive(false); + }); + }; + + const columns = loadColumns(currentUser, deleteHandler); + + return ( +
+ {selectionModel.length > 0 && typeof document !== 'undefined' && document.getElementById('delete-rows-button') && + createPortal( + setIsDeleteModalActive(true)} + />, + document.getElementById('delete-rows-button') as HTMLElement + ) + } + setSelectionModel(newSelectionModel)} + rowSelectionModel={selectionModel} + /> + setIsDeleteModalActive(false)} + > +

Are you sure you want to delete {selectionModel.length} items?

+
+
+ ); +}; + +export default TableLocations; diff --git a/frontend/src/components/Locations/configureLocationsCols.tsx b/frontend/src/components/Locations/configureLocationsCols.tsx new file mode 100644 index 0000000..6be0885 --- /dev/null +++ b/frontend/src/components/Locations/configureLocationsCols.tsx @@ -0,0 +1,33 @@ +import { GridColDef } from '@mui/x-data-grid' +import React from 'react' +import { hasPermission } from "../../helpers/userPermissions"; +import ListActionsPopover from "../ListActionsPopover"; + +export const loadColumns = (user: any, deleteHandler: any) => { + const hasUpdatePermission = hasPermission(user, 'UPDATE_LOCATIONS') + + const columns: GridColDef[] = [ + { + field: 'name', + headerName: 'Name', + flex: 1, + }, + { + field: 'actions', + headerName: 'Actions', + sortable: false, + width: 100, + renderCell: (params) => ( + + ), + }, + ] + + return columns +} \ No newline at end of file diff --git a/frontend/src/components/NavBar.tsx b/frontend/src/components/NavBar.tsx index c270ae0..d28ffc4 100644 --- a/frontend/src/components/NavBar.tsx +++ b/frontend/src/components/NavBar.tsx @@ -9,11 +9,12 @@ import { useAppSelector } from '../stores/hooks'; type Props = { menu: MenuNavBarItem[] + leftMenu?: MenuNavBarItem[] className: string children: ReactNode } -export default function NavBar({ menu, className = '', children }: Props) { +export default function NavBar({ menu, leftMenu = [], className = '', children }: Props) { const [isMenuNavBarActive, setIsMenuNavBarActive] = useState(false) const [isScrolled, setIsScrolled] = useState(false); const bgColor = useAppSelector((state) => state.style.bgLayoutColor); @@ -38,7 +39,12 @@ export default function NavBar({ menu, className = '', children }: Props) { className={`${className} top-0 inset-x-0 fixed ${bgColor} h-14 z-30 transition-position w-screen lg:w-auto dark:bg-dark-800`} >
-
{children}
+
+
+ +
+ {children} +
@@ -49,9 +55,12 @@ export default function NavBar({ menu, className = '', children }: Props) { isMenuNavBarActive ? 'block' : 'hidden' } flex items-center max-h-screen-menu overflow-y-auto lg:overflow-visible absolute w-screen top-14 left-0 ${bgColor} shadow-lg lg:w-auto lg:flex lg:static lg:shadow-none dark:bg-dark-800`} > +
+ +
) -} +} \ No newline at end of file diff --git a/frontend/src/components/NavBarItem.tsx b/frontend/src/components/NavBarItem.tsx index 72935e6..4c43782 100644 --- a/frontend/src/components/NavBarItem.tsx +++ b/frontend/src/components/NavBarItem.tsx @@ -1,6 +1,5 @@ -import React, {useEffect, useRef} from 'react' +import React, {useEffect, useRef, useState} from 'react' import Link from 'next/link' -import { useState } from 'react' import { mdiChevronUp, mdiChevronDown } from '@mdi/js' import BaseDivider from './BaseDivider' import BaseIcon from './BaseIcon' @@ -39,17 +38,21 @@ export default function NavBarItem({ item }: Props) { }, [router.pathname]); const componentClass = [ - 'block lg:flex items-center relative cursor-pointer', + 'block lg:flex items-center relative', + item.isHeader ? 'cursor-default' : 'cursor-pointer', isDropdownActive ? `${navBarItemLabelActiveColorStyle} dark:text-slate-400` - : `${navBarItemLabelStyle} dark:text-white dark:hover:text-slate-400 ${navBarItemLabelHoverStyle}`, + : `${navBarItemLabelStyle} dark:text-white dark:hover:text-slate-400 ${item.isHeader ? '' : navBarItemLabelHoverStyle}`, item.menu ? 'lg:py-2 lg:px-3' : 'py-2 px-3', item.isDesktopNoLabel ? 'lg:w-16 lg:justify-center' : '', + item.isHeader ? 'text-xs uppercase font-bold text-gray-400 dark:text-slate-500 mt-2' : '' ].join(' ') const itemLabel = item.isCurrentUser ? userName : item.label const handleMenuClick = () => { + if (item.isHeader) return + if (item.menu) { setIsDropdownActive(!isDropdownActive) } @@ -86,9 +89,9 @@ export default function NavBarItem({ item }: Props) { }`} onClick={handleMenuClick} > - {item.icon && } + {item.icon && } @@ -120,7 +123,7 @@ export default function NavBarItem({ item }: Props) { return } - if (item.href) { + if (item.href && !item.isHeader) { return ( {NavBarItemComponentContents} @@ -129,4 +132,4 @@ export default function NavBarItem({ item }: Props) { } return
{NavBarItemComponentContents}
-} +} \ No newline at end of file diff --git a/frontend/src/components/NavBarMenuList.tsx b/frontend/src/components/NavBarMenuList.tsx index 0896428..1a3d526 100644 --- a/frontend/src/components/NavBarMenuList.tsx +++ b/frontend/src/components/NavBarMenuList.tsx @@ -1,19 +1,29 @@ import React from 'react' import { MenuNavBarItem } from '../interfaces' import NavBarItem from './NavBarItem' +import { useAppSelector } from '../stores/hooks' +import { hasPermission } from '../helpers/userPermissions' type Props = { menu: MenuNavBarItem[] } export default function NavBarMenuList({ menu }: Props) { + const { currentUser } = useAppSelector((state) => state.auth) + + if (!currentUser) return null + return ( <> - {menu.map((item, index) => ( + {menu.map((item, index) => { + if (!hasPermission(currentUser, item.permissions)) return null + + return (
- +
- ))} + ) + })} ) -} +} \ No newline at end of file diff --git a/frontend/src/components/Programs/TablePrograms.tsx b/frontend/src/components/Programs/TablePrograms.tsx new file mode 100644 index 0000000..2533f1e --- /dev/null +++ b/frontend/src/components/Programs/TablePrograms.tsx @@ -0,0 +1,78 @@ + +import React, { useEffect, useState } from 'react' +import { useAppDispatch, useAppSelector } from '../../stores/hooks' +import { fetch, deleteItem, setRefetch, deleteItemsByIds } from '../../stores/programs/programsSlice' +import { loadColumns } from "./configureProgramsCols"; +import { DataGrid, GridRowSelectionModel } from '@mui/x-data-grid'; +import BaseButton from '../BaseButton'; +import { mdiDelete } from '@mdi/js'; +import CardBoxModal from '../CardBoxModal'; +import { createPortal } from 'react-dom'; + +const TablePrograms = ({ filterItems, setFilterItems, filters, showGrid }) => { + const dispatch = useAppDispatch(); + const { programs, loading, count, refetch } = useAppSelector((state) => state.programs); + const { currentUser } = useAppSelector((state) => state.auth); + const [paginationModel, setPaginationModel] = useState({ pageSize: 10, page: 0 }); + const [selectionModel, setSelectionModel] = useState([]); + const [isDeleteModalActive, setIsDeleteModalActive] = useState(false); + + useEffect(() => { + const query = `?limit=${paginationModel.pageSize}&page=${paginationModel.page}`; + dispatch(fetch({ query })); + if (refetch) dispatch(setRefetch(false)); + }, [dispatch, paginationModel, refetch]); + + const deleteHandler = (id: string) => { + dispatch(deleteItem(id)).then(() => dispatch(setRefetch(true))); + }; + + const handleDeleteByIds = () => { + dispatch(deleteItemsByIds(selectionModel)).then(() => { + dispatch(setRefetch(true)); + setIsDeleteModalActive(false); + }); + }; + + const columns = loadColumns(currentUser, deleteHandler); + + return ( +
+ {selectionModel.length > 0 && typeof document !== 'undefined' && document.getElementById('delete-rows-button') && + createPortal( + setIsDeleteModalActive(true)} + />, + document.getElementById('delete-rows-button') as HTMLElement + ) + } + setSelectionModel(newSelectionModel)} + rowSelectionModel={selectionModel} + /> + setIsDeleteModalActive(false)} + > +

Are you sure you want to delete {selectionModel.length} items?

+
+
+ ); +}; + +export default TablePrograms; diff --git a/frontend/src/components/Programs/configureProgramsCols.tsx b/frontend/src/components/Programs/configureProgramsCols.tsx new file mode 100644 index 0000000..13ca2da --- /dev/null +++ b/frontend/src/components/Programs/configureProgramsCols.tsx @@ -0,0 +1,33 @@ +import { GridColDef } from '@mui/x-data-grid' +import React from 'react' +import { hasPermission } from "../../helpers/userPermissions"; +import ListActionsPopover from "../ListActionsPopover"; + +export const loadColumns = (user: any, deleteHandler: any) => { + const hasUpdatePermission = hasPermission(user, 'UPDATE_PROGRAMS') + + const columns: GridColDef[] = [ + { + field: 'name', + headerName: 'Name', + flex: 1, + }, + { + field: 'actions', + headerName: 'Actions', + sortable: false, + width: 100, + renderCell: (params) => ( + + ), + }, + ] + + return columns +} \ No newline at end of file diff --git a/frontend/src/components/Propagation_types/TablePropagation_types.tsx b/frontend/src/components/Propagation_types/TablePropagation_types.tsx new file mode 100644 index 0000000..dd199fc --- /dev/null +++ b/frontend/src/components/Propagation_types/TablePropagation_types.tsx @@ -0,0 +1,78 @@ + +import React, { useEffect, useState } from 'react' +import { useAppDispatch, useAppSelector } from '../../stores/hooks' +import { fetch, deleteItem, setRefetch, deleteItemsByIds } from '../../stores/propagation_types/propagation_typesSlice' +import { loadColumns } from "./configurePropagation_typesCols"; +import { DataGrid, GridRowSelectionModel } from '@mui/x-data-grid'; +import BaseButton from '../BaseButton'; +import { mdiDelete } from '@mdi/js'; +import CardBoxModal from '../CardBoxModal'; +import { createPortal } from 'react-dom'; + +const TablePropagation_types = ({ filterItems, setFilterItems, filters, showGrid }) => { + const dispatch = useAppDispatch(); + const { propagation_types, loading, count, refetch } = useAppSelector((state) => state.propagation_types); + const { currentUser } = useAppSelector((state) => state.auth); + const [paginationModel, setPaginationModel] = useState({ pageSize: 10, page: 0 }); + const [selectionModel, setSelectionModel] = useState([]); + const [isDeleteModalActive, setIsDeleteModalActive] = useState(false); + + useEffect(() => { + const query = `?limit=${paginationModel.pageSize}&page=${paginationModel.page}`; + dispatch(fetch({ query })); + if (refetch) dispatch(setRefetch(false)); + }, [dispatch, paginationModel, refetch]); + + const deleteHandler = (id: string) => { + dispatch(deleteItem(id)).then(() => dispatch(setRefetch(true))); + }; + + const handleDeleteByIds = () => { + dispatch(deleteItemsByIds(selectionModel)).then(() => { + dispatch(setRefetch(true)); + setIsDeleteModalActive(false); + }); + }; + + const columns = loadColumns(currentUser, deleteHandler); + + return ( +
+ {selectionModel.length > 0 && typeof document !== 'undefined' && document.getElementById('delete-rows-button') && + createPortal( + setIsDeleteModalActive(true)} + />, + document.getElementById('delete-rows-button') as HTMLElement + ) + } + setSelectionModel(newSelectionModel)} + rowSelectionModel={selectionModel} + /> + setIsDeleteModalActive(false)} + > +

Are you sure you want to delete {selectionModel.length} items?

+
+
+ ); +}; + +export default TablePropagation_types; diff --git a/frontend/src/components/Propagation_types/configurePropagation_typesCols.tsx b/frontend/src/components/Propagation_types/configurePropagation_typesCols.tsx new file mode 100644 index 0000000..f1061e0 --- /dev/null +++ b/frontend/src/components/Propagation_types/configurePropagation_typesCols.tsx @@ -0,0 +1,36 @@ + +import { GridColDef } from '@mui/x-data-grid' +import React from 'react' +import { hasPermission } from "../../helpers/userPermissions"; +import ListActionsPopover from "../ListActionsPopover"; + +export const loadColumns = (user: any, deleteHandler: any) => { + const hasUpdatePermission = hasPermission(user, 'UPDATE_PROPAGATION_TYPES') + const hasDeletePermission = hasPermission(user, 'DELETE_PROPAGATION_TYPES') + + const columns: GridColDef[] = [ + { + field: 'name', + headerName: 'Name', + flex: 1, + }, + { + field: 'actions', + headerName: 'Actions', + sortable: false, + width: 100, + renderCell: (params) => ( + + ), + }, + ] + + return columns +} diff --git a/frontend/src/components/Retailer_programs/TableRetailer_programs.tsx b/frontend/src/components/Retailer_programs/TableRetailer_programs.tsx new file mode 100644 index 0000000..eb864a0 --- /dev/null +++ b/frontend/src/components/Retailer_programs/TableRetailer_programs.tsx @@ -0,0 +1,73 @@ + +import React, { useEffect, useState } from 'react' +import { useAppDispatch, useAppSelector } from '../../stores/hooks' +import { fetch, deleteItem, setRefetch, deleteItemsByIds } from '../../stores/retailer_programs/retailer_programsSlice' +import { loadColumns } from "./configureRetailer_programsCols"; +import { DataGrid, GridRowSelectionModel } from '@mui/x-data-grid'; +import BaseButton from '../BaseButton'; +import { mdiDelete } from '@mdi/js'; +import CardBoxModal from '../CardBoxModal'; +import { createPortal } from 'react-dom'; + +const TableRetailer_programs = ({ filterItems, setFilterItems, filters, showGrid }) => { + const dispatch = useAppDispatch(); + const { retailer_programs, loading, count, refetch } = useAppSelector((state) => state.retailer_programs); + const { currentUser } = useAppSelector((state) => state.auth); + const [paginationModel, setPaginationModel] = useState({ pageSize: 10, page: 0 }); + const [selectionModel, setSelectionModel] = useState([]); + const [isDeleteModalActive, setIsDeleteModalActive] = useState(false); + + useEffect(() => { + const query = `?limit=${paginationModel.pageSize}&page=${paginationModel.page}`; + dispatch(fetch({ query })); + if (refetch) dispatch(setRefetch(false)); + }, [dispatch, paginationModel, refetch]); + + const deleteHandler = (id: string) => { + dispatch(deleteItem(id)).then(() => dispatch(setRefetch(true))); + }; + + const handleDeleteByIds = () => { + dispatch(deleteItemsByIds(selectionModel)).then(() => { + dispatch(setRefetch(true)); + setIsDeleteModalActive(false); + }); + }; + + const columns = loadColumns(currentUser, deleteHandler); + + return ( +
+ {selectionModel.length > 0 && typeof document !== 'undefined' && document.getElementById('delete-rows-button') && + createPortal( + setIsDeleteModalActive(true)} />, + document.getElementById('delete-rows-button') as HTMLElement + ) + } + setSelectionModel(newSelectionModel)} + rowSelectionModel={selectionModel} + /> + setIsDeleteModalActive(false)} + > +

Are you sure you want to delete {selectionModel.length} items?

+
+
+ ); +}; + +export default TableRetailer_programs; diff --git a/frontend/src/components/Retailer_programs/configureRetailer_programsCols.tsx b/frontend/src/components/Retailer_programs/configureRetailer_programsCols.tsx new file mode 100644 index 0000000..1266aa2 --- /dev/null +++ b/frontend/src/components/Retailer_programs/configureRetailer_programsCols.tsx @@ -0,0 +1,30 @@ + +import { GridColDef } from '@mui/x-data-grid' +import React from 'react' +import { hasPermission } from "../../helpers/userPermissions"; +import ListActionsPopover from "../ListActionsPopover"; + +export const loadColumns = (user: any, deleteHandler: any) => { + const hasUpdatePermission = hasPermission(user, 'UPDATE_RETAILER_PROGRAMS') + const hasDeletePermission = hasPermission(user, 'DELETE_RETAILER_PROGRAMS') + const columns: GridColDef[] = [ + { field: 'name', headerName: 'Name', flex: 1 }, + { + field: 'actions', + headerName: 'Actions', + sortable: false, + width: 100, + renderCell: (params) => ( + + ), + }, + ] + return columns +} diff --git a/frontend/src/components/Retailers/TableRetailers.tsx b/frontend/src/components/Retailers/TableRetailers.tsx new file mode 100644 index 0000000..90f3006 --- /dev/null +++ b/frontend/src/components/Retailers/TableRetailers.tsx @@ -0,0 +1,78 @@ + +import React, { useEffect, useState } from 'react' +import { useAppDispatch, useAppSelector } from '../../stores/hooks' +import { fetch, deleteItem, setRefetch, deleteItemsByIds } from '../../stores/retailers/retailersSlice' +import { loadColumns } from "./configureRetailersCols"; +import { DataGrid, GridRowSelectionModel } from '@mui/x-data-grid'; +import BaseButton from '../BaseButton'; +import { mdiDelete } from '@mdi/js'; +import CardBoxModal from '../CardBoxModal'; +import { createPortal } from 'react-dom'; + +const TableRetailers = ({ filterItems, setFilterItems, filters, showGrid }) => { + const dispatch = useAppDispatch(); + const { retailers, loading, count, refetch } = useAppSelector((state) => state.retailers); + const { currentUser } = useAppSelector((state) => state.auth); + const [paginationModel, setPaginationModel] = useState({ pageSize: 10, page: 0 }); + const [selectionModel, setSelectionModel] = useState([]); + const [isDeleteModalActive, setIsDeleteModalActive] = useState(false); + + useEffect(() => { + const query = `?limit=${paginationModel.pageSize}&page=${paginationModel.page}`; + dispatch(fetch({ query })); + if (refetch) dispatch(setRefetch(false)); + }, [dispatch, paginationModel, refetch]); + + const deleteHandler = (id: string) => { + dispatch(deleteItem(id)).then(() => dispatch(setRefetch(true))); + }; + + const handleDeleteByIds = () => { + dispatch(deleteItemsByIds(selectionModel)).then(() => { + dispatch(setRefetch(true)); + setIsDeleteModalActive(false); + }); + }; + + const columns = loadColumns(currentUser, deleteHandler); + + return ( +
+ {selectionModel.length > 0 && typeof document !== 'undefined' && document.getElementById('delete-rows-button') && + createPortal( + setIsDeleteModalActive(true)} + />, + document.getElementById('delete-rows-button') as HTMLElement + ) + } + setSelectionModel(newSelectionModel)} + rowSelectionModel={selectionModel} + /> + setIsDeleteModalActive(false)} + > +

Are you sure you want to delete {selectionModel.length} items?

+
+
+ ); +}; + +export default TableRetailers; diff --git a/frontend/src/components/Retailers/configureRetailersCols.tsx b/frontend/src/components/Retailers/configureRetailersCols.tsx new file mode 100644 index 0000000..f9dadd1 --- /dev/null +++ b/frontend/src/components/Retailers/configureRetailersCols.tsx @@ -0,0 +1,33 @@ +import { GridColDef } from '@mui/x-data-grid' +import React from 'react' +import { hasPermission } from "../../helpers/userPermissions"; +import ListActionsPopover from "../ListActionsPopover"; + +export const loadColumns = (user: any, deleteHandler: any) => { + const hasUpdatePermission = hasPermission(user, 'UPDATE_RETAILERS') + + const columns: GridColDef[] = [ + { + field: 'name', + headerName: 'Name', + flex: 1, + }, + { + field: 'actions', + headerName: 'Actions', + sortable: false, + width: 100, + renderCell: (params) => ( + + ), + }, + ] + + return columns +} \ No newline at end of file diff --git a/frontend/src/components/Sites/TableSites.tsx b/frontend/src/components/Sites/TableSites.tsx new file mode 100644 index 0000000..0dd517f --- /dev/null +++ b/frontend/src/components/Sites/TableSites.tsx @@ -0,0 +1,78 @@ + +import React, { useEffect, useState } from 'react' +import { useAppDispatch, useAppSelector } from '../../stores/hooks' +import { fetch, deleteItem, setRefetch, deleteItemsByIds } from '../../stores/sites/sitesSlice' +import { loadColumns } from "./configureSitesCols"; +import { DataGrid, GridRowSelectionModel } from '@mui/x-data-grid'; +import BaseButton from '../BaseButton'; +import { mdiDelete } from '@mdi/js'; +import CardBoxModal from '../CardBoxModal'; +import { createPortal } from 'react-dom'; + +const TableSites = ({ filterItems, setFilterItems, filters, showGrid }) => { + const dispatch = useAppDispatch(); + const { sites, loading, count, refetch } = useAppSelector((state) => state.sites); + const { currentUser } = useAppSelector((state) => state.auth); + const [paginationModel, setPaginationModel] = useState({ pageSize: 10, page: 0 }); + const [selectionModel, setSelectionModel] = useState([]); + const [isDeleteModalActive, setIsDeleteModalActive] = useState(false); + + useEffect(() => { + const query = `?limit=${paginationModel.pageSize}&page=${paginationModel.page}`; + dispatch(fetch({ query })); + if (refetch) dispatch(setRefetch(false)); + }, [dispatch, paginationModel, refetch]); + + const deleteHandler = (id: string) => { + dispatch(deleteItem(id)).then(() => dispatch(setRefetch(true))); + }; + + const handleDeleteByIds = () => { + dispatch(deleteItemsByIds(selectionModel)).then(() => { + dispatch(setRefetch(true)); + setIsDeleteModalActive(false); + }); + }; + + const columns = loadColumns(currentUser, deleteHandler); + + return ( +
+ {selectionModel.length > 0 && typeof document !== 'undefined' && document.getElementById('delete-rows-button') && + createPortal( + setIsDeleteModalActive(true)} + />, + document.getElementById('delete-rows-button') as HTMLElement + ) + } + setSelectionModel(newSelectionModel)} + rowSelectionModel={selectionModel} + /> + setIsDeleteModalActive(false)} + > +

Are you sure you want to delete {selectionModel.length} items?

+
+
+ ); +}; + +export default TableSites; diff --git a/frontend/src/components/Sites/configureSitesCols.tsx b/frontend/src/components/Sites/configureSitesCols.tsx new file mode 100644 index 0000000..bf4a9dc --- /dev/null +++ b/frontend/src/components/Sites/configureSitesCols.tsx @@ -0,0 +1,33 @@ +import { GridColDef } from '@mui/x-data-grid' +import React from 'react' +import { hasPermission } from "../../helpers/userPermissions"; +import ListActionsPopover from "../ListActionsPopover"; + +export const loadColumns = (user: any, deleteHandler: any) => { + const hasUpdatePermission = hasPermission(user, 'UPDATE_SITES') + + const columns: GridColDef[] = [ + { + field: 'name', + headerName: 'Name', + flex: 1, + }, + { + field: 'actions', + headerName: 'Actions', + sortable: false, + width: 100, + renderCell: (params) => ( + + ), + }, + ] + + return columns +} \ No newline at end of file diff --git a/frontend/src/components/Trials/TableTrials.tsx b/frontend/src/components/Trials/TableTrials.tsx index dca51e9..4fe05f2 100644 --- a/frontend/src/components/Trials/TableTrials.tsx +++ b/frontend/src/components/Trials/TableTrials.tsx @@ -11,6 +11,7 @@ import { Field, Form, Formik } from "formik"; import { DataGrid, GridColDef, + GridToolbar, } from '@mui/x-data-grid'; import {loadColumns} from "./configureTrialsCols"; import _ from 'lodash'; @@ -233,6 +234,9 @@ const TableSampleTrials = ({ filterItems, setFilterItems, filters, showGrid }) = }, }, }} + slots={{ + toolbar: GridToolbar, + }} disableRowSelectionOnClick onProcessRowUpdateError={(params) => { console.log('Error', params); @@ -486,4 +490,4 @@ const TableSampleTrials = ({ filterItems, setFilterItems, filters, showGrid }) = ) } -export default TableSampleTrials +export default TableSampleTrials \ No newline at end of file diff --git a/frontend/src/components/Trials/configureTrialsCols.tsx b/frontend/src/components/Trials/configureTrialsCols.tsx index c4fb3c0..4441421 100644 --- a/frontend/src/components/Trials/configureTrialsCols.tsx +++ b/frontend/src/components/Trials/configureTrialsCols.tsx @@ -1,18 +1,11 @@ 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; @@ -20,345 +13,160 @@ 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; + const data = await axios(`/${entityName}/autocomplete?limit=100`); + return data.data; } catch (error) { - console.log(error); - return []; + console.log(error); + return []; } } const hasUpdatePermission = hasPermission(user, 'UPDATE_TRIALS') + const weeksOptions = Array.from({ length: 53 }, (_, i) => ({ id: i + 1, label: `Week ${i + 1}` })); return [ - - { - field: 'tenant', - headerName: 'Tenant', - 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('tenants'), - valueGetter: (params: GridValueGetterParams) => - params?.value?.id ?? params?.value, - - }, - - { - field: 'project', - headerName: 'Project', - 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('projects'), - valueGetter: (params: GridValueGetterParams) => - params?.value?.id ?? params?.value, - - }, - { field: 'name', - headerName: 'TrialName', + headerName: 'Trial Name', flex: 1, - minWidth: 120, - filterable: false, + minWidth: 150, headerClassName: 'datagrid--header', cellClassName: 'datagrid--cell', - - editable: hasUpdatePermission, - - }, - { - field: 'variety_name', - headerName: 'VarietyName', + field: 'trial_code', + headerName: 'Trial Code', flex: 1, minWidth: 120, - filterable: false, headerClassName: 'datagrid--header', cellClassName: 'datagrid--cell', - - editable: hasUpdatePermission, - - }, - { field: 'breeder', headerName: 'Breeder', flex: 1, - minWidth: 120, - filterable: false, + minWidth: 150, headerClassName: 'datagrid--header', cellClassName: 'datagrid--cell', - - editable: hasUpdatePermission, - - + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('breeders'), + valueGetter: (params: GridValueGetterParams) => params?.row?.breeder?.name || params?.row?.breeder?.id || params?.value?.id || params?.value, }, - { - field: 'batch_code', - headerName: 'BatchCode', + field: 'variety_name', + headerName: 'Variety Name', + flex: 1, + minWidth: 150, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + editable: hasUpdatePermission, + }, + { + field: 'container', + headerName: 'Container', + flex: 1, + minWidth: 150, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + editable: hasUpdatePermission, + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('containers'), + valueGetter: (params: GridValueGetterParams) => params?.row?.container?.name || params?.row?.container?.id || params?.value?.id || params?.value, + }, + { + field: 'quantity', + headerName: 'Quantity', + flex: 1, + minWidth: 100, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + editable: hasUpdatePermission, + type: 'number', + }, + { + field: 'site', + headerName: 'Site', + flex: 1, + minWidth: 150, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + editable: hasUpdatePermission, + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('sites'), + valueGetter: (params: GridValueGetterParams) => params?.row?.site?.name || params?.row?.site?.id || params?.value?.id || params?.value, + }, + { + field: 'location', + headerName: 'Location', + flex: 1, + minWidth: 150, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + editable: hasUpdatePermission, + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('locations'), + valueGetter: (params: GridValueGetterParams) => params?.row?.location?.name || params?.row?.location?.id || params?.value?.id || params?.value, + }, + { + field: 'receiveWeek', + headerName: 'Receive Week', flex: 1, minWidth: 120, - filterable: false, headerClassName: 'datagrid--header', cellClassName: 'datagrid--cell', - - editable: hasUpdatePermission, - - + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: weeksOptions, + valueFormatter: (params) => params.value ? `Week ${params.value}` : '', }, - { - field: 'trial_type', - headerName: 'TrialType', + field: 'finishWeek', + headerName: 'Finish Week', flex: 1, minWidth: 120, - filterable: false, headerClassName: 'datagrid--header', cellClassName: 'datagrid--cell', - - editable: hasUpdatePermission, - - + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: weeksOptions, + valueFormatter: (params) => params.value ? `Week ${params.value}` : '', }, - { field: 'status', headerName: 'Status', flex: 1, minWidth: 120, - filterable: false, headerClassName: 'datagrid--header', cellClassName: 'datagrid--cell', - - editable: hasUpdatePermission, - - }, - - { - field: 'planted_at', - headerName: 'PlantedAt', - flex: 1, - minWidth: 120, - filterable: false, - headerClassName: 'datagrid--header', - cellClassName: 'datagrid--cell', - - - editable: hasUpdatePermission, - - type: 'dateTime', - valueGetter: (params: GridValueGetterParams) => - new Date(params.row.planted_at), - - }, - - { - field: 'harvested_at', - headerName: 'HarvestedAt', - flex: 1, - minWidth: 120, - filterable: false, - headerClassName: 'datagrid--header', - cellClassName: 'datagrid--cell', - - - editable: hasUpdatePermission, - - type: 'dateTime', - valueGetter: (params: GridValueGetterParams) => - new Date(params.row.harvested_at), - - }, - - { - field: 'greenhouse', - headerName: 'Greenhouse', - flex: 1, - minWidth: 120, - filterable: false, - headerClassName: 'datagrid--header', - cellClassName: 'datagrid--cell', - - - editable: hasUpdatePermission, - - - }, - - { - field: 'zone', - headerName: 'Zone', - flex: 1, - minWidth: 120, - filterable: false, - headerClassName: 'datagrid--header', - cellClassName: 'datagrid--cell', - - - editable: hasUpdatePermission, - - - }, - - { - field: 'bench', - headerName: 'Bench', - flex: 1, - minWidth: 120, - filterable: false, - headerClassName: 'datagrid--header', - cellClassName: 'datagrid--cell', - - - editable: hasUpdatePermission, - - - }, - - { - field: 'plants_count', - headerName: 'PlantsCount', - flex: 1, - minWidth: 120, - filterable: false, - headerClassName: 'datagrid--header', - cellClassName: 'datagrid--cell', - - - editable: hasUpdatePermission, - - type: 'number', - - }, - - { - field: 'target_temperature_c', - headerName: 'TargetTemperature(C)', - flex: 1, - minWidth: 120, - filterable: false, - headerClassName: 'datagrid--header', - cellClassName: 'datagrid--cell', - - - editable: hasUpdatePermission, - - type: 'number', - - }, - - { - field: 'target_humidity_percent', - headerName: 'TargetHumidity(%)', - flex: 1, - minWidth: 120, - filterable: false, - headerClassName: 'datagrid--header', - cellClassName: 'datagrid--cell', - - - editable: hasUpdatePermission, - - type: 'number', - - }, - - { - field: 'target_ec', - headerName: 'TargetEC', - flex: 1, - minWidth: 120, - filterable: false, - headerClassName: 'datagrid--header', - cellClassName: 'datagrid--cell', - - - editable: hasUpdatePermission, - - type: 'number', - - }, - - { - field: 'target_ph', - headerName: 'TargetpH', - 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: 'actions', type: 'actions', - minWidth: 30, + minWidth: 80, headerClassName: 'datagrid--header', cellClassName: 'datagrid--cell', getActions: (params: GridRowParams) => { - return [
, ] }, }, ]; -}; +}; \ No newline at end of file diff --git a/frontend/src/interfaces/index.ts b/frontend/src/interfaces/index.ts index 0c7dd74..87da37a 100644 --- a/frontend/src/interfaces/index.ts +++ b/frontend/src/interfaces/index.ts @@ -11,6 +11,7 @@ export type MenuAsideItem = { target?: string color?: ColorButtonKey isLogout?: boolean + isHeader?: boolean withDevider?: boolean; menu?: MenuAsideItem[] permissions?: string | string[] @@ -22,11 +23,13 @@ export type MenuNavBarItem = { href?: string target?: string isDivider?: boolean + isHeader?: boolean isLogout?: boolean isDesktopNoLabel?: boolean isToggleLightDark?: boolean isCurrentUser?: boolean menu?: MenuNavBarItem[] + permissions?: string | string[] } export type ColorKey = 'white' | 'light' | 'contrast' | 'success' | 'danger' | 'warning' | 'info' diff --git a/frontend/src/layouts/Authenticated.tsx b/frontend/src/layouts/Authenticated.tsx index 1b9907d..90c033d 100644 --- a/frontend/src/layouts/Authenticated.tsx +++ b/frontend/src/layouts/Authenticated.tsx @@ -1,13 +1,10 @@ import React, { ReactNode, useEffect } from 'react' -import { useState } from 'react' import jwt from 'jsonwebtoken'; -import { mdiForwardburger, mdiBackburger, mdiMenu } from '@mdi/js' import menuAside from '../menuAside' import menuNavBar from '../menuNavBar' -import BaseIcon from '../components/BaseIcon' import NavBar from '../components/NavBar' import NavBarItemPlain from '../components/NavBarItemPlain' -import AsideMenu from '../components/AsideMenu' +import NavBarMenuList from '../components/NavBarMenuList' import FooterBar from '../components/FooterBar' import { useAppDispatch, useAppSelector } from '../stores/hooks' import Search from '../components/Search'; @@ -67,63 +64,36 @@ export default function LayoutAuthenticated({ const darkMode = useAppSelector((state) => state.style.darkMode) - const [isAsideMobileExpanded, setIsAsideMobileExpanded] = useState(false) - const [isAsideLgActive, setIsAsideLgActive] = useState(false) - useEffect(() => { const handleRouteChangeStart = () => { - setIsAsideMobileExpanded(false) - setIsAsideLgActive(false) + // Logic for route change start } router.events.on('routeChangeStart', handleRouteChangeStart) - // If the component is unmounted, unsubscribe - // from the event with the `off` method: return () => { router.events.off('routeChangeStart', handleRouteChangeStart) } }, [router.events, dispatch]) - - const layoutAsidePadding = 'xl:pl-60' - return (
- setIsAsideMobileExpanded(!isAsideMobileExpanded)} - > - - - setIsAsideLgActive(true)} - > - - - setIsAsideLgActive(false)} - /> + {children} Hand-crafted & Made with ❤️
) -} +} \ No newline at end of file diff --git a/frontend/src/menuAside.ts b/frontend/src/menuAside.ts index dea03ca..cf539fa 100644 --- a/frontend/src/menuAside.ts +++ b/frontend/src/menuAside.ts @@ -7,61 +7,12 @@ const menuAside: MenuAsideItem[] = [ icon: icon.mdiViewDashboardOutline, label: 'Dashboard', }, - - { - href: '/users/users-list', - label: 'Users', - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - icon: icon.mdiAccountGroup ?? icon.mdiTable, - permissions: 'READ_USERS' - }, - { - href: '/roles/roles-list', - label: 'Roles', - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - icon: icon.mdiShieldAccountVariantOutline ?? icon.mdiTable, - permissions: 'READ_ROLES' - }, - { - href: '/permissions/permissions-list', - label: 'Permissions', - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - icon: icon.mdiShieldAccountOutline ?? icon.mdiTable, - permissions: 'READ_PERMISSIONS' - }, - { - href: '/organizations/organizations-list', - label: 'Organizations', - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - icon: icon.mdiTable ?? icon.mdiTable, - permissions: 'READ_ORGANIZATIONS' - }, - { - href: '/tenants/tenants-list', - label: 'Tenants', - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - icon: 'mdiDomain' in icon ? icon['mdiDomain' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, - permissions: 'READ_TENANTS' - }, - { - href: '/impersonation_sessions/impersonation_sessions-list', - label: 'Impersonation sessions', - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - icon: 'mdiAccountSwitch' in icon ? icon['mdiAccountSwitch' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, - permissions: 'READ_IMPERSONATION_SESSIONS' - }, { href: '/projects/projects-list', label: 'Projects', // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - icon: 'mdiBriefcaseOutline' in icon ? icon['mdiBriefcaseOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, + icon: 'mdiBriefcaseOutline' in icon ? icon['mdiBriefcaseOutline' as keyof typeof icon] : icon.mdiTable, permissions: 'READ_PROJECTS' }, { @@ -69,64 +20,187 @@ const menuAside: MenuAsideItem[] = [ label: 'Trials', // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - icon: 'mdiSprout' in icon ? icon['mdiSprout' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, + icon: 'mdiSprout' in icon ? icon['mdiSprout' as keyof typeof icon] : icon.mdiTable, permissions: 'READ_TRIALS' }, { - href: '/tracking_activity_types/tracking_activity_types-list', - label: 'Tracking activity types', - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - icon: 'mdiFormatListBulletedType' in icon ? icon['mdiFormatListBulletedType' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, - permissions: 'READ_TRACKING_ACTIVITY_TYPES' + label: 'Tracking', + icon: icon.mdiTimelineTextOutline, + menu: [ + { + href: '/tracking_activities/tracking_activities-list', + label: 'Activities', + icon: icon.mdiCircleSmall, + permissions: 'READ_TRACKING_ACTIVITIES' + }, + { + href: '/tracking_activity_types/tracking_activity_types-list', + label: 'Activity Types', + icon: icon.mdiCircleSmall, + permissions: 'READ_TRACKING_ACTIVITY_TYPES' + } + ] }, { - href: '/tracking_activities/tracking_activities-list', - label: 'Tracking activities', - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - icon: 'mdiTimelineTextOutline' in icon ? icon['mdiTimelineTextOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, - permissions: 'READ_TRACKING_ACTIVITIES' + label: 'Reports', + icon: icon.mdiFileChartOutline, + menu: [ + { + href: '/report_definitions/report_definitions-list', + label: 'Definitions', + icon: icon.mdiCircleSmall, + permissions: 'READ_REPORT_DEFINITIONS' + }, + { + href: '/report_runs/report_runs-list', + label: 'Runs', + icon: icon.mdiCircleSmall, + permissions: 'READ_REPORT_RUNS' + } + ] }, { - href: '/dashboards/dashboards-list', - label: 'Dashboards', - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - icon: 'mdiViewDashboardOutline' in icon ? icon['mdiViewDashboardOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, - permissions: 'READ_DASHBOARDS' + label: 'Settings', + icon: icon.mdiCogOutline, + menu: [ + { + label: 'Data Sets', + icon: icon.mdiDatabaseOutline, + menu: [ + { + label: 'Trials', + isHeader: true, + icon: icon.mdiSprout + }, + { + href: '/breeders/breeders-list', + label: 'Breeder', + icon: icon.mdiCircleSmall, + permissions: 'READ_BREEDERS' + }, + { + href: '/propagation_types/propagation_types-list', + label: 'Propagation Type', + icon: icon.mdiCircleSmall, + permissions: 'READ_PROPAGATION_TYPES' + }, + { + href: '/growth_habits/growth_habits-list', + label: 'Growth Habit', + icon: icon.mdiCircleSmall, + permissions: 'READ_GROWTH_HABITS' + }, + { + href: '/crop_types/crop_types-list', + label: 'Crop Type', + icon: icon.mdiCircleSmall, + permissions: 'READ_CROP_TYPES' + }, + { + href: '/containers/containers-list', + label: 'Container', + icon: icon.mdiCircleSmall, + permissions: 'READ_CONTAINERS' + }, + { + href: '/retailer_programs/retailer_programs-list', + label: 'Retailer Program', + icon: icon.mdiCircleSmall, + permissions: 'READ_RETAILER_PROGRAMS' + }, + { + href: '/retailers/retailers-list', + label: 'Retailer', + icon: icon.mdiCircleSmall, + permissions: 'READ_RETAILERS' + }, + { + href: '/programs/programs-list', + label: 'Program', + icon: icon.mdiCircleSmall, + permissions: 'READ_PROGRAMS' + }, + { + href: '/groups/groups-list', + label: 'Group', + icon: icon.mdiCircleSmall, + permissions: 'READ_GROUPS' + }, + { + href: '/sites/sites-list', + label: 'Site', + icon: icon.mdiCircleSmall, + permissions: 'READ_SITES' + }, + { + href: '/locations/locations-list', + label: 'Default Location', + icon: icon.mdiCircleSmall, + permissions: 'READ_LOCATIONS' + } + ] + } + ] }, { - href: '/report_definitions/report_definitions-list', - label: 'Report definitions', - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - icon: 'mdiFileChartOutline' in icon ? icon['mdiFileChartOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, - permissions: 'READ_REPORT_DEFINITIONS' - }, - { - href: '/report_runs/report_runs-list', - label: 'Report runs', - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - icon: 'mdiProgressDownload' in icon ? icon['mdiProgressDownload' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, - permissions: 'READ_REPORT_RUNS' - }, - { - href: '/tenant_settings/tenant_settings-list', - label: 'Tenant settings', - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - icon: 'mdiCogOutline' in icon ? icon['mdiCogOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, - permissions: 'READ_TENANT_SETTINGS' + label: 'System', + icon: icon.mdiCogOutline, + menu: [ + { + href: '/users/users-list', + label: 'Users', + icon: icon.mdiCircleSmall, + permissions: 'READ_USERS' + }, + { + href: '/roles/roles-list', + label: 'Roles', + icon: icon.mdiCircleSmall, + permissions: 'READ_ROLES' + }, + { + href: '/permissions/permissions-list', + label: 'Permissions', + icon: icon.mdiCircleSmall, + permissions: 'READ_PERMISSIONS' + }, + { + href: '/organizations/organizations-list', + label: 'Organizations', + icon: icon.mdiCircleSmall, + permissions: 'READ_ORGANIZATIONS' + }, + { + href: '/tenants/tenants-list', + label: 'Tenants', + icon: icon.mdiCircleSmall, + permissions: 'READ_TENANTS' + }, + { + href: '/impersonation_sessions/impersonation_sessions-list', + label: 'Impersonation Sessions', + icon: icon.mdiCircleSmall, + permissions: 'READ_IMPERSONATION_SESSIONS' + }, + { + href: '/tenant_settings/tenant_settings-list', + label: 'Tenant Settings', + icon: icon.mdiCircleSmall, + permissions: 'READ_TENANT_SETTINGS' + }, + { + href: '/dashboards/dashboards-list', + label: 'Dashboard Layouts', + icon: icon.mdiCircleSmall, + permissions: 'READ_DASHBOARDS' + } + ] }, { href: '/profile', label: 'Profile', icon: icon.mdiAccountCircle, }, - - { href: '/api-docs', target: '_blank', @@ -136,4 +210,4 @@ const menuAside: MenuAsideItem[] = [ }, ] -export default menuAside +export default menuAside \ No newline at end of file diff --git a/frontend/src/pages/breeders/breeders-edit.tsx b/frontend/src/pages/breeders/breeders-edit.tsx new file mode 100644 index 0000000..6c94ab6 --- /dev/null +++ b/frontend/src/pages/breeders/breeders-edit.tsx @@ -0,0 +1,72 @@ +import { mdiChartTimelineVariant } from '@mdi/js' +import Head from 'next/head' +import React, { ReactElement, useEffect, useState } from 'react' +import { useRouter } from 'next/router' +import { Formik, Form, Field } from 'formik' +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 FormField from '../../components/FormField' +import BaseButton from '../../components/BaseButton' +import BaseButtons from '../../components/BaseButtons' +import { useAppDispatch, useAppSelector } from "../../stores/hooks" +import { update, fetch } from '../../stores/breeders/breedersSlice' + +const BreedersEdit = () => { + const dispatch = useAppDispatch(); + const router = useRouter(); + const { id } = router.query; + const { breeders } = useAppSelector(state => state.breeders); + const [initialValues, setInitialValues] = useState({ name: '' }); + + useEffect(() => { + if (id) { + dispatch(fetch({ id })); + } + }, [dispatch, id]); + + useEffect(() => { + if (breeders && !Array.isArray(breeders)) { + setInitialValues({ name: breeders.name || '' }); + } + }, [breeders]); + + const handleSubmit = async (values: any) => { + await dispatch(update({ id, data: values })); + router.push('/breeders/breeders-list'); + }; + + return ( + <> + + {getPageTitle('Edit Propagation Type')} + + + + {''} + + + +
+ + + + + + router.push('/breeders/breeders-list')} /> + +
+
+
+
+ + ) +} + +BreedersEdit.getLayout = function getLayout(page: ReactElement) { + return {page} +} + +export default BreedersEdit \ No newline at end of file diff --git a/frontend/src/pages/breeders/breeders-list.tsx b/frontend/src/pages/breeders/breeders-list.tsx new file mode 100644 index 0000000..1bd65b5 --- /dev/null +++ b/frontend/src/pages/breeders/breeders-list.tsx @@ -0,0 +1,44 @@ +import { mdiChartTimelineVariant } from '@mdi/js' +import Head from 'next/head' +import React, { ReactElement } from 'react' +import LayoutAuthenticated from '../../layouts/Authenticated' +import SectionMain from '../../components/SectionMain' +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton' +import { getPageTitle } from '../../config' +import TableBreeders from '../../components/Breeders/TableBreeders' +import BaseButton from '../../components/BaseButton' +import { useAppSelector } from "../../stores/hooks"; +import { hasPermission } from "../../helpers/userPermissions"; + +const BreedersListPage = () => { + const { currentUser } = useAppSelector((state) => state.auth); + const hasCreatePermission = currentUser && hasPermission(currentUser, 'CREATE_PROPAGATION_TYPES'); + + return ( + <> + + {getPageTitle('Propagation Types')} + + + + {''} + +
+ {hasCreatePermission && } +
+
+ undefined} filters={[]} showGrid={false} /> +
+ + ) +} + +BreedersListPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ) +} + +export default BreedersListPage \ No newline at end of file diff --git a/frontend/src/pages/breeders/breeders-new.tsx b/frontend/src/pages/breeders/breeders-new.tsx new file mode 100644 index 0000000..0a8eba7 --- /dev/null +++ b/frontend/src/pages/breeders/breeders-new.tsx @@ -0,0 +1,59 @@ +import { mdiChartTimelineVariant } from '@mdi/js' +import Head from 'next/head' +import React, { ReactElement } from 'react' +import { useRouter } from 'next/router' +import { Formik, Form, Field } from 'formik' +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 FormField from '../../components/FormField' +import BaseButton from '../../components/BaseButton' +import BaseButtons from '../../components/BaseButtons' +import { useAppDispatch } from "../../stores/hooks" +import { create } from '../../stores/breeders/breedersSlice' + +const BreedersNew = () => { + const dispatch = useAppDispatch(); + const router = useRouter(); + + const initialValues = { name: '' }; + + const handleSubmit = async (values: any) => { + await dispatch(create(values)); + router.push('/breeders/breeders-list'); + }; + + return ( + <> + + {getPageTitle('New Propagation Type')} + + + + {''} + + + +
+ + + + + + router.push('/breeders/breeders-list')} /> + +
+
+
+
+ + ) +} + +BreedersNew.getLayout = function getLayout(page: ReactElement) { + return {page} +} + +export default BreedersNew \ No newline at end of file diff --git a/frontend/src/pages/breeders/breeders-view.tsx b/frontend/src/pages/breeders/breeders-view.tsx new file mode 100644 index 0000000..7d55a3b --- /dev/null +++ b/frontend/src/pages/breeders/breeders-view.tsx @@ -0,0 +1,52 @@ + +import { mdiChartTimelineVariant } from '@mdi/js' +import Head from 'next/head' +import React, { ReactElement, useEffect } from 'react' +import { useRouter } from 'next/router' +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 BaseButton from '../../components/BaseButton' +import { useAppDispatch, useAppSelector } from "../../stores/hooks" +import { fetch } from '../../stores/breeders/breedersSlice' + +const BreedersView = () => { + const dispatch = useAppDispatch(); + const router = useRouter(); + const { id } = router.query; + const { breeders } = useAppSelector(state => state.breeders); + + useEffect(() => { + if (id) { + dispatch(fetch({ id })); + } + }, [dispatch, id]); + + return ( + <> + + {getPageTitle('View Propagation Type')} + + + + {''} + + +
+ +

{breeders?.name || 'N/A'}

+
+ router.push('/breeders/breeders-list')} /> +
+
+ + ) +} + +BreedersView.getLayout = function getLayout(page: ReactElement) { + return {page} +} + +export default BreedersView diff --git a/frontend/src/pages/containers/containers-edit.tsx b/frontend/src/pages/containers/containers-edit.tsx new file mode 100644 index 0000000..380eb2a --- /dev/null +++ b/frontend/src/pages/containers/containers-edit.tsx @@ -0,0 +1,72 @@ +import { mdiChartTimelineVariant } from '@mdi/js' +import Head from 'next/head' +import React, { ReactElement, useEffect, useState } from 'react' +import { useRouter } from 'next/router' +import { Formik, Form, Field } from 'formik' +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 FormField from '../../components/FormField' +import BaseButton from '../../components/BaseButton' +import BaseButtons from '../../components/BaseButtons' +import { useAppDispatch, useAppSelector } from "../../stores/hooks" +import { update, fetch } from '../../stores/containers/containersSlice' + +const ContainersEdit = () => { + const dispatch = useAppDispatch(); + const router = useRouter(); + const { id } = router.query; + const { containers } = useAppSelector(state => state.containers); + const [initialValues, setInitialValues] = useState({ name: '' }); + + useEffect(() => { + if (id) { + dispatch(fetch({ id })); + } + }, [dispatch, id]); + + useEffect(() => { + if (containers && !Array.isArray(containers)) { + setInitialValues({ name: containers.name || '' }); + } + }, [containers]); + + const handleSubmit = async (values: any) => { + await dispatch(update({ id, data: values })); + router.push('/containers/containers-list'); + }; + + return ( + <> + + {getPageTitle('Edit Container')} + + + + {''} + + + +
+ + + + + + router.push('/containers/containers-list')} /> + +
+
+
+
+ + ) +} + +ContainersEdit.getLayout = function getLayout(page: ReactElement) { + return {page} +} + +export default ContainersEdit \ No newline at end of file diff --git a/frontend/src/pages/containers/containers-list.tsx b/frontend/src/pages/containers/containers-list.tsx new file mode 100644 index 0000000..985dfd6 --- /dev/null +++ b/frontend/src/pages/containers/containers-list.tsx @@ -0,0 +1,44 @@ +import { mdiChartTimelineVariant } from '@mdi/js' +import Head from 'next/head' +import React, { ReactElement } from 'react' +import LayoutAuthenticated from '../../layouts/Authenticated' +import SectionMain from '../../components/SectionMain' +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton' +import { getPageTitle } from '../../config' +import TableContainers from '../../components/Containers/TableContainers' +import BaseButton from '../../components/BaseButton' +import { useAppSelector } from "../../stores/hooks"; +import { hasPermission } from "../../helpers/userPermissions"; + +const ContainersListPage = () => { + const { currentUser } = useAppSelector((state) => state.auth); + const hasCreatePermission = currentUser && hasPermission(currentUser, 'CREATE_CONTAINERS'); + + return ( + <> + + {getPageTitle('Containers')} + + + + {''} + +
+ {hasCreatePermission && } +
+
+ undefined} filters={[]} showGrid={false} /> +
+ + ) +} + +ContainersListPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ) +} + +export default ContainersListPage \ No newline at end of file diff --git a/frontend/src/pages/containers/containers-new.tsx b/frontend/src/pages/containers/containers-new.tsx new file mode 100644 index 0000000..727f39f --- /dev/null +++ b/frontend/src/pages/containers/containers-new.tsx @@ -0,0 +1,59 @@ +import { mdiChartTimelineVariant } from '@mdi/js' +import Head from 'next/head' +import React, { ReactElement } from 'react' +import { useRouter } from 'next/router' +import { Formik, Form, Field } from 'formik' +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 FormField from '../../components/FormField' +import BaseButton from '../../components/BaseButton' +import BaseButtons from '../../components/BaseButtons' +import { useAppDispatch } from "../../stores/hooks" +import { create } from '../../stores/containers/containersSlice' + +const ContainersNew = () => { + const dispatch = useAppDispatch(); + const router = useRouter(); + + const initialValues = { name: '' }; + + const handleSubmit = async (values: any) => { + await dispatch(create(values)); + router.push('/containers/containers-list'); + }; + + return ( + <> + + {getPageTitle('New Container')} + + + + {''} + + + +
+ + + + + + router.push('/containers/containers-list')} /> + +
+
+
+
+ + ) +} + +ContainersNew.getLayout = function getLayout(page: ReactElement) { + return {page} +} + +export default ContainersNew \ No newline at end of file diff --git a/frontend/src/pages/containers/containers-view.tsx b/frontend/src/pages/containers/containers-view.tsx new file mode 100644 index 0000000..9b1e883 --- /dev/null +++ b/frontend/src/pages/containers/containers-view.tsx @@ -0,0 +1,37 @@ + +import { mdiChartTimelineVariant } from '@mdi/js' +import Head from 'next/head' +import React, { ReactElement, useEffect } from 'react' +import { useRouter } from 'next/router' +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 BaseButton from '../../components/BaseButton' +import { useAppDispatch, useAppSelector } from "../../stores/hooks" +import { fetch } from '../../stores/containers/containersSlice' + +const ContainersView = () => { + const dispatch = useAppDispatch(); + const router = useRouter(); + const { id } = router.query; + const { containers } = useAppSelector(state => state.containers); + useEffect(() => { if (id) dispatch(fetch({ id })); }, [dispatch, id]); + return ( + <> + {getPageTitle('View Container')} + + {''} + +

{containers?.name || 'N/A'}

+ router.push('/containers/containers-list')} /> +
+
+ + ) +} +ContainersView.getLayout = function getLayout(page: ReactElement) { + return {page} +} +export default ContainersView diff --git a/frontend/src/pages/crop_types/crop_types-edit.tsx b/frontend/src/pages/crop_types/crop_types-edit.tsx new file mode 100644 index 0000000..74edf74 --- /dev/null +++ b/frontend/src/pages/crop_types/crop_types-edit.tsx @@ -0,0 +1,72 @@ +import { mdiChartTimelineVariant } from '@mdi/js' +import Head from 'next/head' +import React, { ReactElement, useEffect, useState } from 'react' +import { useRouter } from 'next/router' +import { Formik, Form, Field } from 'formik' +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 FormField from '../../components/FormField' +import BaseButton from '../../components/BaseButton' +import BaseButtons from '../../components/BaseButtons' +import { useAppDispatch, useAppSelector } from "../../stores/hooks" +import { update, fetch } from '../../stores/crop_types/crop_typesSlice' + +const Crop_typesEdit = () => { + const dispatch = useAppDispatch(); + const router = useRouter(); + const { id } = router.query; + const { crop_types } = useAppSelector(state => state.crop_types); + const [initialValues, setInitialValues] = useState({ name: '' }); + + useEffect(() => { + if (id) { + dispatch(fetch({ id })); + } + }, [dispatch, id]); + + useEffect(() => { + if (crop_types && !Array.isArray(crop_types)) { + setInitialValues({ name: crop_types.name || '' }); + } + }, [crop_types]); + + const handleSubmit = async (values: any) => { + await dispatch(update({ id, data: values })); + router.push('/crop_types/crop_types-list'); + }; + + return ( + <> + + {getPageTitle('Edit Crop Type')} + + + + {''} + + + +
+ + + + + + router.push('/crop_types/crop_types-list')} /> + +
+
+
+
+ + ) +} + +Crop_typesEdit.getLayout = function getLayout(page: ReactElement) { + return {page} +} + +export default Crop_typesEdit \ No newline at end of file diff --git a/frontend/src/pages/crop_types/crop_types-list.tsx b/frontend/src/pages/crop_types/crop_types-list.tsx new file mode 100644 index 0000000..2481b51 --- /dev/null +++ b/frontend/src/pages/crop_types/crop_types-list.tsx @@ -0,0 +1,44 @@ +import { mdiChartTimelineVariant } from '@mdi/js' +import Head from 'next/head' +import React, { ReactElement } from 'react' +import LayoutAuthenticated from '../../layouts/Authenticated' +import SectionMain from '../../components/SectionMain' +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton' +import { getPageTitle } from '../../config' +import TableCrop_types from '../../components/Crop_types/TableCrop_types' +import BaseButton from '../../components/BaseButton' +import { useAppSelector } from "../../stores/hooks"; +import { hasPermission } from "../../helpers/userPermissions"; + +const Crop_typesListPage = () => { + const { currentUser } = useAppSelector((state) => state.auth); + const hasCreatePermission = currentUser && hasPermission(currentUser, 'CREATE_CROP_TYPES'); + + return ( + <> + + {getPageTitle('Crop Types')} + + + + {''} + +
+ {hasCreatePermission && } +
+
+ undefined} filters={[]} showGrid={false} /> +
+ + ) +} + +Crop_typesListPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ) +} + +export default Crop_typesListPage \ No newline at end of file diff --git a/frontend/src/pages/crop_types/crop_types-new.tsx b/frontend/src/pages/crop_types/crop_types-new.tsx new file mode 100644 index 0000000..4a059e7 --- /dev/null +++ b/frontend/src/pages/crop_types/crop_types-new.tsx @@ -0,0 +1,59 @@ +import { mdiChartTimelineVariant } from '@mdi/js' +import Head from 'next/head' +import React, { ReactElement } from 'react' +import { useRouter } from 'next/router' +import { Formik, Form, Field } from 'formik' +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 FormField from '../../components/FormField' +import BaseButton from '../../components/BaseButton' +import BaseButtons from '../../components/BaseButtons' +import { useAppDispatch } from "../../stores/hooks" +import { create } from '../../stores/crop_types/crop_typesSlice' + +const Crop_typesNew = () => { + const dispatch = useAppDispatch(); + const router = useRouter(); + + const initialValues = { name: '' }; + + const handleSubmit = async (values: any) => { + await dispatch(create(values)); + router.push('/crop_types/crop_types-list'); + }; + + return ( + <> + + {getPageTitle('New Crop Type')} + + + + {''} + + + +
+ + + + + + router.push('/crop_types/crop_types-list')} /> + +
+
+
+
+ + ) +} + +Crop_typesNew.getLayout = function getLayout(page: ReactElement) { + return {page} +} + +export default Crop_typesNew \ No newline at end of file diff --git a/frontend/src/pages/crop_types/crop_types-view.tsx b/frontend/src/pages/crop_types/crop_types-view.tsx new file mode 100644 index 0000000..bb0f883 --- /dev/null +++ b/frontend/src/pages/crop_types/crop_types-view.tsx @@ -0,0 +1,37 @@ + +import { mdiChartTimelineVariant } from '@mdi/js' +import Head from 'next/head' +import React, { ReactElement, useEffect } from 'react' +import { useRouter } from 'next/router' +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 BaseButton from '../../components/BaseButton' +import { useAppDispatch, useAppSelector } from "../../stores/hooks" +import { fetch } from '../../stores/crop_types/crop_typesSlice' + +const Crop_typesView = () => { + const dispatch = useAppDispatch(); + const router = useRouter(); + const { id } = router.query; + const { crop_types } = useAppSelector(state => state.crop_types); + useEffect(() => { if (id) dispatch(fetch({ id })); }, [dispatch, id]); + return ( + <> + {getPageTitle('View Crop Type')} + + {''} + +

{crop_types?.name || 'N/A'}

+ router.push('/crop_types/crop_types-list')} /> +
+
+ + ) +} +Crop_typesView.getLayout = function getLayout(page: ReactElement) { + return {page} +} +export default Crop_typesView diff --git a/frontend/src/pages/dashboard.tsx b/frontend/src/pages/dashboard.tsx index 0d4002b..ad77042 100644 --- a/frontend/src/pages/dashboard.tsx +++ b/frontend/src/pages/dashboard.tsx @@ -9,13 +9,18 @@ import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton import BaseIcon from "../components/BaseIcon"; import { getPageTitle } from '../config' import Link from "next/link"; +import moment from 'moment'; import { hasPermission } from "../helpers/userPermissions"; import { fetchWidgets } from '../stores/roles/rolesSlice'; import { WidgetCreator } from '../components/WidgetCreator/WidgetCreator'; import { SmartWidget } from '../components/SmartWidget/SmartWidget'; +import { fetch as fetchActivities } from '../stores/tracking_activities/tracking_activitiesSlice'; import { useAppDispatch, useAppSelector } from '../stores/hooks'; +import CardBox from '../components/CardBox'; +import UserAvatar from '../components/UserAvatar'; + const Dashboard = () => { const dispatch = useAppDispatch(); const iconsColor = useAppSelector((state) => state.style.iconsColor); @@ -24,534 +29,214 @@ const Dashboard = () => { const loadingMessage = 'Loading...'; - const [users, setUsers] = React.useState(loadingMessage); - const [roles, setRoles] = React.useState(loadingMessage); - const [permissions, setPermissions] = React.useState(loadingMessage); - const [organizations, setOrganizations] = React.useState(loadingMessage); - const [tenants, setTenants] = React.useState(loadingMessage); - const [impersonation_sessions, setImpersonation_sessions] = React.useState(loadingMessage); - const [projects, setProjects] = React.useState(loadingMessage); - const [trials, setTrials] = React.useState(loadingMessage); - const [tracking_activity_types, setTracking_activity_types] = React.useState(loadingMessage); - const [tracking_activities, setTracking_activities] = React.useState(loadingMessage); - const [dashboards, setDashboards] = React.useState(loadingMessage); - const [report_definitions, setReport_definitions] = React.useState(loadingMessage); - const [report_runs, setReport_runs] = React.useState(loadingMessage); - const [tenant_settings, setTenant_settings] = React.useState(loadingMessage); + const [projectsCount, setProjectsCount] = React.useState(loadingMessage); + const [trialsCount, setTrialsCount] = React.useState(loadingMessage); + const [activitiesCount, setActivitiesCount] = React.useState(loadingMessage); - - const [widgetsRole, setWidgetsRole] = React.useState({ - role: { value: '', label: '' }, - }); const { currentUser } = useAppSelector((state) => state.auth); const { isFetchingQuery } = useAppSelector((state) => state.openAi); - - const { rolesWidgets, loading } = useAppSelector((state) => state.roles); - - - const organizationId = currentUser?.organizations?.id; - - async function loadData() { - const entities = ['users','roles','permissions','organizations','tenants','impersonation_sessions','projects','trials','tracking_activity_types','tracking_activities','dashboards','report_definitions','report_runs','tenant_settings',]; - const fns = [setUsers,setRoles,setPermissions,setOrganizations,setTenants,setImpersonation_sessions,setProjects,setTrials,setTracking_activity_types,setTracking_activities,setDashboards,setReport_definitions,setReport_runs,setTenant_settings,]; + const { rolesWidgets, loading: widgetsLoading } = useAppSelector((state) => state.roles); + const { tracking_activities, loading: activitiesLoading } = useAppSelector((state) => state.tracking_activities); + + async function loadCounts() { + const entities = ['users', 'projects', 'trials', 'tracking_activities']; + const setters = [setUsers, setProjectsCount, setTrialsCount, setActivitiesCount]; const requests = entities.map((entity, index) => { - - if(hasPermission(currentUser, `READ_${entity.toUpperCase()}`)) { - return axios.get(`/${entity.toLowerCase()}/count`); - } else { - fns[index](null); - return Promise.resolve({data: {count: null}}); - } - + if (hasPermission(currentUser, `READ_${entity.toUpperCase()}`)) { + return axios.get(`/${entity.toLowerCase()}/count`); + } else { + setters[index](null); + return Promise.resolve({ data: { count: null } }); + } }); Promise.allSettled(requests).then((results) => { results.forEach((result, i) => { if (result.status === 'fulfilled') { - fns[i](result.value.data.count); + setters[i](result.value.data.count); } else { - fns[i](result.reason.message); + setters[i](result.reason.message); } }); }); } - - async function getWidgets(roleId) { - await dispatch(fetchWidgets(roleId)); - } + React.useEffect(() => { if (!currentUser) return; - loadData().then(); - setWidgetsRole({ role: { value: currentUser?.app_role?.id, label: currentUser?.app_role?.name } }); - }, [currentUser]); + loadCounts().then(); + if (hasPermission(currentUser, 'READ_TRACKING_ACTIVITIES')) { + dispatch(fetchActivities({ query: '?limit=10' })); + } + dispatch(fetchWidgets(currentUser?.app_role?.id || '')).then(); + }, [currentUser, dispatch]); - React.useEffect(() => { - if (!currentUser || !widgetsRole?.role?.value) return; - getWidgets(widgetsRole?.role?.value || '').then(); - }, [widgetsRole?.role?.value]); - - return ( - <> - - - {getPageTitle('Overview')} - - - - - {''} - - - {hasPermission(currentUser, 'CREATE_ROLES') && } - {!!rolesWidgets.length && - hasPermission(currentUser, 'CREATE_ROLES') && ( -

- {`${widgetsRole?.role?.label || 'Users'}'s widgets`} -

- )} + const getSeverityColor = (severity: string) => { + switch (severity) { + case 'critical': return 'text-red-600'; + case 'high': return 'text-orange-600'; + case 'medium': return 'text-yellow-600'; + case 'low': return 'text-blue-600'; + default: return 'text-gray-600'; + } + }; -
- {(isFetchingQuery || loading) && ( -
- {' '} - Loading widgets... + return ( + <> + + {getPageTitle('Dashboard')} + + + + {''} + + + {/* Quick Stats */} +
+ {hasPermission(currentUser, 'READ_PROJECTS') && ( + +
+
+

Projects

+

{projectsCount}

+
+ +
+
+ )} + {hasPermission(currentUser, 'READ_TRIALS') && ( + +
+
+

Active Trials

+

{trialsCount}

+
+ +
+
+ )} + {hasPermission(currentUser, 'READ_TRACKING_ACTIVITIES') && ( + +
+
+

Total Activities

+

{activitiesCount}

+
+ +
+
+ )} + {hasPermission(currentUser, 'READ_USERS') && ( + +
+
+

Team Members

+

{users}

+
+ +
+
+ )}
- )} - { rolesWidgets && - rolesWidgets.map((widget) => ( - { + // No-op for dashboard feed view + }} + widgetsRole={{ role: { value: currentUser?.app_role?.id, label: currentUser?.app_role?.name } }} /> - ))} -
+ )} - {!!rolesWidgets.length &&
} - -
- - - {hasPermission(currentUser, 'READ_USERS') && -
-
-
-
- Users -
-
- {users} -
-
-
- +
+ {/* Activity Feed */} +
+ + + {activitiesLoading ? ( + + + Loading activities... + + ) : tracking_activities && tracking_activities.length > 0 ? ( + tracking_activities.map((activity: any) => ( + +
+ +
+
+
+ {activity.recorded_by?.firstName} {activity.recorded_by?.lastName} + logged an activity for + + {activity.trial?.name} + +
+ + {moment(activity.occurred_at || activity.createdAt).fromNow()} + +
+
+
+ + {activity.activity_type?.name} + + {activity.severity && activity.severity !== 'none' && ( + + {activity.severity} + + )} +
+

+ {activity.summary} +

+ {activity.details && ( +

+ {activity.details} +

+ )} +
+
+
+
+ )) + ) : ( + + No recent activities found. Start tracking to see them here! + + )} +
+ + {/* Right Column: Widgets or Quick Info */} +
+ +
+ {rolesWidgets && rolesWidgets.map((widget) => ( + + ))}
- } - - {hasPermission(currentUser, 'READ_ROLES') && -
-
-
-
- Roles -
-
- {roles} -
-
-
- -
-
-
- } - - {hasPermission(currentUser, 'READ_PERMISSIONS') && -
-
-
-
- Permissions -
-
- {permissions} -
-
-
- -
-
-
- } - - {hasPermission(currentUser, 'READ_ORGANIZATIONS') && -
-
-
-
- Organizations -
-
- {organizations} -
-
-
- -
-
-
- } - - {hasPermission(currentUser, 'READ_TENANTS') && -
-
-
-
- Tenants -
-
- {tenants} -
-
-
- -
-
-
- } - - {hasPermission(currentUser, 'READ_IMPERSONATION_SESSIONS') && -
-
-
-
- Impersonation sessions -
-
- {impersonation_sessions} -
-
-
- -
-
-
- } - - {hasPermission(currentUser, 'READ_PROJECTS') && -
-
-
-
- Projects -
-
- {projects} -
-
-
- -
-
-
- } - - {hasPermission(currentUser, 'READ_TRIALS') && -
-
-
-
- Trials -
-
- {trials} -
-
-
- -
-
-
- } - - {hasPermission(currentUser, 'READ_TRACKING_ACTIVITY_TYPES') && -
-
-
-
- Tracking activity types -
-
- {tracking_activity_types} -
-
-
- -
-
-
- } - - {hasPermission(currentUser, 'READ_TRACKING_ACTIVITIES') && -
-
-
-
- Tracking activities -
-
- {tracking_activities} -
-
-
- -
-
-
- } - - {hasPermission(currentUser, 'READ_DASHBOARDS') && -
-
-
-
- Dashboards -
-
- {dashboards} -
-
-
- -
-
-
- } - - {hasPermission(currentUser, 'READ_REPORT_DEFINITIONS') && -
-
-
-
- Report definitions -
-
- {report_definitions} -
-
-
- -
-
-
- } - - {hasPermission(currentUser, 'READ_REPORT_RUNS') && -
-
-
-
- Report runs -
-
- {report_runs} -
-
-
- -
-
-
- } - - {hasPermission(currentUser, 'READ_TENANT_SETTINGS') && -
-
-
-
- Tenant settings -
-
- {tenant_settings} -
-
-
- -
-
-
- } - - -
- - - ) + + + ) } Dashboard.getLayout = function getLayout(page: ReactElement) { - return {page} + return {page} } export default Dashboard diff --git a/frontend/src/pages/groups/groups-edit.tsx b/frontend/src/pages/groups/groups-edit.tsx new file mode 100644 index 0000000..35b061d --- /dev/null +++ b/frontend/src/pages/groups/groups-edit.tsx @@ -0,0 +1,72 @@ +import { mdiChartTimelineVariant } from '@mdi/js' +import Head from 'next/head' +import React, { ReactElement, useEffect, useState } from 'react' +import { useRouter } from 'next/router' +import { Formik, Form, Field } from 'formik' +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 FormField from '../../components/FormField' +import BaseButton from '../../components/BaseButton' +import BaseButtons from '../../components/BaseButtons' +import { useAppDispatch, useAppSelector } from "../../stores/hooks" +import { update, fetch } from '../../stores/groups/groupsSlice' + +const GroupsEdit = () => { + const dispatch = useAppDispatch(); + const router = useRouter(); + const { id } = router.query; + const { groups } = useAppSelector(state => state.groups); + const [initialValues, setInitialValues] = useState({ name: '' }); + + useEffect(() => { + if (id) { + dispatch(fetch({ id })); + } + }, [dispatch, id]); + + useEffect(() => { + if (groups && !Array.isArray(groups)) { + setInitialValues({ name: groups.name || '' }); + } + }, [groups]); + + const handleSubmit = async (values: any) => { + await dispatch(update({ id, data: values })); + router.push('/groups/groups-list'); + }; + + return ( + <> + + {getPageTitle('Edit Propagation Type')} + + + + {''} + + + +
+ + + + + + router.push('/groups/groups-list')} /> + +
+
+
+
+ + ) +} + +GroupsEdit.getLayout = function getLayout(page: ReactElement) { + return {page} +} + +export default GroupsEdit \ No newline at end of file diff --git a/frontend/src/pages/groups/groups-list.tsx b/frontend/src/pages/groups/groups-list.tsx new file mode 100644 index 0000000..13df3d6 --- /dev/null +++ b/frontend/src/pages/groups/groups-list.tsx @@ -0,0 +1,44 @@ +import { mdiChartTimelineVariant } from '@mdi/js' +import Head from 'next/head' +import React, { ReactElement } from 'react' +import LayoutAuthenticated from '../../layouts/Authenticated' +import SectionMain from '../../components/SectionMain' +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton' +import { getPageTitle } from '../../config' +import TableGroups from '../../components/Groups/TableGroups' +import BaseButton from '../../components/BaseButton' +import { useAppSelector } from "../../stores/hooks"; +import { hasPermission } from "../../helpers/userPermissions"; + +const GroupsListPage = () => { + const { currentUser } = useAppSelector((state) => state.auth); + const hasCreatePermission = currentUser && hasPermission(currentUser, 'CREATE_PROPAGATION_TYPES'); + + return ( + <> + + {getPageTitle('Propagation Types')} + + + + {''} + +
+ {hasCreatePermission && } +
+
+ undefined} filters={[]} showGrid={false} /> +
+ + ) +} + +GroupsListPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ) +} + +export default GroupsListPage \ No newline at end of file diff --git a/frontend/src/pages/groups/groups-new.tsx b/frontend/src/pages/groups/groups-new.tsx new file mode 100644 index 0000000..0ae9868 --- /dev/null +++ b/frontend/src/pages/groups/groups-new.tsx @@ -0,0 +1,59 @@ +import { mdiChartTimelineVariant } from '@mdi/js' +import Head from 'next/head' +import React, { ReactElement } from 'react' +import { useRouter } from 'next/router' +import { Formik, Form, Field } from 'formik' +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 FormField from '../../components/FormField' +import BaseButton from '../../components/BaseButton' +import BaseButtons from '../../components/BaseButtons' +import { useAppDispatch } from "../../stores/hooks" +import { create } from '../../stores/groups/groupsSlice' + +const GroupsNew = () => { + const dispatch = useAppDispatch(); + const router = useRouter(); + + const initialValues = { name: '' }; + + const handleSubmit = async (values: any) => { + await dispatch(create(values)); + router.push('/groups/groups-list'); + }; + + return ( + <> + + {getPageTitle('New Propagation Type')} + + + + {''} + + + +
+ + + + + + router.push('/groups/groups-list')} /> + +
+
+
+
+ + ) +} + +GroupsNew.getLayout = function getLayout(page: ReactElement) { + return {page} +} + +export default GroupsNew \ No newline at end of file diff --git a/frontend/src/pages/groups/groups-view.tsx b/frontend/src/pages/groups/groups-view.tsx new file mode 100644 index 0000000..ea8071b --- /dev/null +++ b/frontend/src/pages/groups/groups-view.tsx @@ -0,0 +1,52 @@ + +import { mdiChartTimelineVariant } from '@mdi/js' +import Head from 'next/head' +import React, { ReactElement, useEffect } from 'react' +import { useRouter } from 'next/router' +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 BaseButton from '../../components/BaseButton' +import { useAppDispatch, useAppSelector } from "../../stores/hooks" +import { fetch } from '../../stores/groups/groupsSlice' + +const GroupsView = () => { + const dispatch = useAppDispatch(); + const router = useRouter(); + const { id } = router.query; + const { groups } = useAppSelector(state => state.groups); + + useEffect(() => { + if (id) { + dispatch(fetch({ id })); + } + }, [dispatch, id]); + + return ( + <> + + {getPageTitle('View Propagation Type')} + + + + {''} + + +
+ +

{groups?.name || 'N/A'}

+
+ router.push('/groups/groups-list')} /> +
+
+ + ) +} + +GroupsView.getLayout = function getLayout(page: ReactElement) { + return {page} +} + +export default GroupsView diff --git a/frontend/src/pages/growth_habits/growth_habits-edit.tsx b/frontend/src/pages/growth_habits/growth_habits-edit.tsx new file mode 100644 index 0000000..0c7c522 --- /dev/null +++ b/frontend/src/pages/growth_habits/growth_habits-edit.tsx @@ -0,0 +1,72 @@ +import { mdiChartTimelineVariant } from '@mdi/js' +import Head from 'next/head' +import React, { ReactElement, useEffect, useState } from 'react' +import { useRouter } from 'next/router' +import { Formik, Form, Field } from 'formik' +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 FormField from '../../components/FormField' +import BaseButton from '../../components/BaseButton' +import BaseButtons from '../../components/BaseButtons' +import { useAppDispatch, useAppSelector } from "../../stores/hooks" +import { update, fetch } from '../../stores/growth_habits/growth_habitsSlice' + +const Growth_habitsEdit = () => { + const dispatch = useAppDispatch(); + const router = useRouter(); + const { id } = router.query; + const { growth_habits } = useAppSelector(state => state.growth_habits); + const [initialValues, setInitialValues] = useState({ name: '' }); + + useEffect(() => { + if (id) { + dispatch(fetch({ id })); + } + }, [dispatch, id]); + + useEffect(() => { + if (growth_habits && !Array.isArray(growth_habits)) { + setInitialValues({ name: growth_habits.name || '' }); + } + }, [growth_habits]); + + const handleSubmit = async (values: any) => { + await dispatch(update({ id, data: values })); + router.push('/growth_habits/growth_habits-list'); + }; + + return ( + <> + + {getPageTitle('Edit Growth Habit')} + + + + {''} + + + +
+ + + + + + router.push('/growth_habits/growth_habits-list')} /> + +
+
+
+
+ + ) +} + +Growth_habitsEdit.getLayout = function getLayout(page: ReactElement) { + return {page} +} + +export default Growth_habitsEdit \ No newline at end of file diff --git a/frontend/src/pages/growth_habits/growth_habits-list.tsx b/frontend/src/pages/growth_habits/growth_habits-list.tsx new file mode 100644 index 0000000..68207ff --- /dev/null +++ b/frontend/src/pages/growth_habits/growth_habits-list.tsx @@ -0,0 +1,44 @@ +import { mdiChartTimelineVariant } from '@mdi/js' +import Head from 'next/head' +import React, { ReactElement } from 'react' +import LayoutAuthenticated from '../../layouts/Authenticated' +import SectionMain from '../../components/SectionMain' +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton' +import { getPageTitle } from '../../config' +import TableGrowth_habits from '../../components/Growth_habits/TableGrowth_habits' +import BaseButton from '../../components/BaseButton' +import { useAppSelector } from "../../stores/hooks"; +import { hasPermission } from "../../helpers/userPermissions"; + +const Growth_habitsListPage = () => { + const { currentUser } = useAppSelector((state) => state.auth); + const hasCreatePermission = currentUser && hasPermission(currentUser, 'CREATE_GROWTH_HABITS'); + + return ( + <> + + {getPageTitle('Growth Habits')} + + + + {''} + +
+ {hasCreatePermission && } +
+
+ undefined} filters={[]} showGrid={false} /> +
+ + ) +} + +Growth_habitsListPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ) +} + +export default Growth_habitsListPage \ No newline at end of file diff --git a/frontend/src/pages/growth_habits/growth_habits-new.tsx b/frontend/src/pages/growth_habits/growth_habits-new.tsx new file mode 100644 index 0000000..e81a551 --- /dev/null +++ b/frontend/src/pages/growth_habits/growth_habits-new.tsx @@ -0,0 +1,59 @@ +import { mdiChartTimelineVariant } from '@mdi/js' +import Head from 'next/head' +import React, { ReactElement } from 'react' +import { useRouter } from 'next/router' +import { Formik, Form, Field } from 'formik' +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 FormField from '../../components/FormField' +import BaseButton from '../../components/BaseButton' +import BaseButtons from '../../components/BaseButtons' +import { useAppDispatch } from "../../stores/hooks" +import { create } from '../../stores/growth_habits/growth_habitsSlice' + +const Growth_habitsNew = () => { + const dispatch = useAppDispatch(); + const router = useRouter(); + + const initialValues = { name: '' }; + + const handleSubmit = async (values: any) => { + await dispatch(create(values)); + router.push('/growth_habits/growth_habits-list'); + }; + + return ( + <> + + {getPageTitle('New Growth Habit')} + + + + {''} + + + +
+ + + + + + router.push('/growth_habits/growth_habits-list')} /> + +
+
+
+
+ + ) +} + +Growth_habitsNew.getLayout = function getLayout(page: ReactElement) { + return {page} +} + +export default Growth_habitsNew \ No newline at end of file diff --git a/frontend/src/pages/growth_habits/growth_habits-view.tsx b/frontend/src/pages/growth_habits/growth_habits-view.tsx new file mode 100644 index 0000000..25b33ab --- /dev/null +++ b/frontend/src/pages/growth_habits/growth_habits-view.tsx @@ -0,0 +1,37 @@ + +import { mdiChartTimelineVariant } from '@mdi/js' +import Head from 'next/head' +import React, { ReactElement, useEffect } from 'react' +import { useRouter } from 'next/router' +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 BaseButton from '../../components/BaseButton' +import { useAppDispatch, useAppSelector } from "../../stores/hooks" +import { fetch } from '../../stores/growth_habits/growth_habitsSlice' + +const Growth_habitsView = () => { + const dispatch = useAppDispatch(); + const router = useRouter(); + const { id } = router.query; + const { growth_habits } = useAppSelector(state => state.growth_habits); + useEffect(() => { if (id) dispatch(fetch({ id })); }, [dispatch, id]); + return ( + <> + {getPageTitle('View Growth Habit')} + + {''} + +

{growth_habits?.name || 'N/A'}

+ router.push('/growth_habits/growth_habits-list')} /> +
+
+ + ) +} +Growth_habitsView.getLayout = function getLayout(page: ReactElement) { + return {page} +} +export default Growth_habitsView diff --git a/frontend/src/pages/locations/locations-edit.tsx b/frontend/src/pages/locations/locations-edit.tsx new file mode 100644 index 0000000..7473271 --- /dev/null +++ b/frontend/src/pages/locations/locations-edit.tsx @@ -0,0 +1,72 @@ +import { mdiChartTimelineVariant } from '@mdi/js' +import Head from 'next/head' +import React, { ReactElement, useEffect, useState } from 'react' +import { useRouter } from 'next/router' +import { Formik, Form, Field } from 'formik' +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 FormField from '../../components/FormField' +import BaseButton from '../../components/BaseButton' +import BaseButtons from '../../components/BaseButtons' +import { useAppDispatch, useAppSelector } from "../../stores/hooks" +import { update, fetch } from '../../stores/locations/locationsSlice' + +const LocationsEdit = () => { + const dispatch = useAppDispatch(); + const router = useRouter(); + const { id } = router.query; + const { locations } = useAppSelector(state => state.locations); + const [initialValues, setInitialValues] = useState({ name: '' }); + + useEffect(() => { + if (id) { + dispatch(fetch({ id })); + } + }, [dispatch, id]); + + useEffect(() => { + if (locations && !Array.isArray(locations)) { + setInitialValues({ name: locations.name || '' }); + } + }, [locations]); + + const handleSubmit = async (values: any) => { + await dispatch(update({ id, data: values })); + router.push('/locations/locations-list'); + }; + + return ( + <> + + {getPageTitle('Edit Propagation Type')} + + + + {''} + + + +
+ + + + + + router.push('/locations/locations-list')} /> + +
+
+
+
+ + ) +} + +LocationsEdit.getLayout = function getLayout(page: ReactElement) { + return {page} +} + +export default LocationsEdit \ No newline at end of file diff --git a/frontend/src/pages/locations/locations-list.tsx b/frontend/src/pages/locations/locations-list.tsx new file mode 100644 index 0000000..bedfb31 --- /dev/null +++ b/frontend/src/pages/locations/locations-list.tsx @@ -0,0 +1,44 @@ +import { mdiChartTimelineVariant } from '@mdi/js' +import Head from 'next/head' +import React, { ReactElement } from 'react' +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 { useAppSelector } from "../../stores/hooks"; +import { hasPermission } from "../../helpers/userPermissions"; + +const LocationsListPage = () => { + const { currentUser } = useAppSelector((state) => state.auth); + const hasCreatePermission = currentUser && hasPermission(currentUser, 'CREATE_PROPAGATION_TYPES'); + + return ( + <> + + {getPageTitle('Propagation Types')} + + + + {''} + +
+ {hasCreatePermission && } +
+
+ undefined} filters={[]} showGrid={false} /> +
+ + ) +} + +LocationsListPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ) +} + +export default LocationsListPage \ No newline at end of file diff --git a/frontend/src/pages/locations/locations-new.tsx b/frontend/src/pages/locations/locations-new.tsx new file mode 100644 index 0000000..8155a9f --- /dev/null +++ b/frontend/src/pages/locations/locations-new.tsx @@ -0,0 +1,59 @@ +import { mdiChartTimelineVariant } from '@mdi/js' +import Head from 'next/head' +import React, { ReactElement } from 'react' +import { useRouter } from 'next/router' +import { Formik, Form, Field } from 'formik' +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 FormField from '../../components/FormField' +import BaseButton from '../../components/BaseButton' +import BaseButtons from '../../components/BaseButtons' +import { useAppDispatch } from "../../stores/hooks" +import { create } from '../../stores/locations/locationsSlice' + +const LocationsNew = () => { + const dispatch = useAppDispatch(); + const router = useRouter(); + + const initialValues = { name: '' }; + + const handleSubmit = async (values: any) => { + await dispatch(create(values)); + router.push('/locations/locations-list'); + }; + + return ( + <> + + {getPageTitle('New Propagation Type')} + + + + {''} + + + +
+ + + + + + router.push('/locations/locations-list')} /> + +
+
+
+
+ + ) +} + +LocationsNew.getLayout = function getLayout(page: ReactElement) { + return {page} +} + +export default LocationsNew \ No newline at end of file diff --git a/frontend/src/pages/locations/locations-view.tsx b/frontend/src/pages/locations/locations-view.tsx new file mode 100644 index 0000000..663278d --- /dev/null +++ b/frontend/src/pages/locations/locations-view.tsx @@ -0,0 +1,52 @@ + +import { mdiChartTimelineVariant } from '@mdi/js' +import Head from 'next/head' +import React, { ReactElement, useEffect } from 'react' +import { useRouter } from 'next/router' +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 BaseButton from '../../components/BaseButton' +import { useAppDispatch, useAppSelector } from "../../stores/hooks" +import { fetch } from '../../stores/locations/locationsSlice' + +const LocationsView = () => { + const dispatch = useAppDispatch(); + const router = useRouter(); + const { id } = router.query; + const { locations } = useAppSelector(state => state.locations); + + useEffect(() => { + if (id) { + dispatch(fetch({ id })); + } + }, [dispatch, id]); + + return ( + <> + + {getPageTitle('View Propagation Type')} + + + + {''} + + +
+ +

{locations?.name || 'N/A'}

+
+ router.push('/locations/locations-list')} /> +
+
+ + ) +} + +LocationsView.getLayout = function getLayout(page: ReactElement) { + return {page} +} + +export default LocationsView diff --git a/frontend/src/pages/programs/programs-edit.tsx b/frontend/src/pages/programs/programs-edit.tsx new file mode 100644 index 0000000..41cf4dc --- /dev/null +++ b/frontend/src/pages/programs/programs-edit.tsx @@ -0,0 +1,72 @@ +import { mdiChartTimelineVariant } from '@mdi/js' +import Head from 'next/head' +import React, { ReactElement, useEffect, useState } from 'react' +import { useRouter } from 'next/router' +import { Formik, Form, Field } from 'formik' +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 FormField from '../../components/FormField' +import BaseButton from '../../components/BaseButton' +import BaseButtons from '../../components/BaseButtons' +import { useAppDispatch, useAppSelector } from "../../stores/hooks" +import { update, fetch } from '../../stores/programs/programsSlice' + +const ProgramsEdit = () => { + const dispatch = useAppDispatch(); + const router = useRouter(); + const { id } = router.query; + const { programs } = useAppSelector(state => state.programs); + const [initialValues, setInitialValues] = useState({ name: '' }); + + useEffect(() => { + if (id) { + dispatch(fetch({ id })); + } + }, [dispatch, id]); + + useEffect(() => { + if (programs && !Array.isArray(programs)) { + setInitialValues({ name: programs.name || '' }); + } + }, [programs]); + + const handleSubmit = async (values: any) => { + await dispatch(update({ id, data: values })); + router.push('/programs/programs-list'); + }; + + return ( + <> + + {getPageTitle('Edit Propagation Type')} + + + + {''} + + + +
+ + + + + + router.push('/programs/programs-list')} /> + +
+
+
+
+ + ) +} + +ProgramsEdit.getLayout = function getLayout(page: ReactElement) { + return {page} +} + +export default ProgramsEdit \ No newline at end of file diff --git a/frontend/src/pages/programs/programs-list.tsx b/frontend/src/pages/programs/programs-list.tsx new file mode 100644 index 0000000..f31d44c --- /dev/null +++ b/frontend/src/pages/programs/programs-list.tsx @@ -0,0 +1,44 @@ +import { mdiChartTimelineVariant } from '@mdi/js' +import Head from 'next/head' +import React, { ReactElement } from 'react' +import LayoutAuthenticated from '../../layouts/Authenticated' +import SectionMain from '../../components/SectionMain' +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton' +import { getPageTitle } from '../../config' +import TablePrograms from '../../components/Programs/TablePrograms' +import BaseButton from '../../components/BaseButton' +import { useAppSelector } from "../../stores/hooks"; +import { hasPermission } from "../../helpers/userPermissions"; + +const ProgramsListPage = () => { + const { currentUser } = useAppSelector((state) => state.auth); + const hasCreatePermission = currentUser && hasPermission(currentUser, 'CREATE_PROPAGATION_TYPES'); + + return ( + <> + + {getPageTitle('Propagation Types')} + + + + {''} + +
+ {hasCreatePermission && } +
+
+ undefined} filters={[]} showGrid={false} /> +
+ + ) +} + +ProgramsListPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ) +} + +export default ProgramsListPage \ No newline at end of file diff --git a/frontend/src/pages/programs/programs-new.tsx b/frontend/src/pages/programs/programs-new.tsx new file mode 100644 index 0000000..5ffebcc --- /dev/null +++ b/frontend/src/pages/programs/programs-new.tsx @@ -0,0 +1,59 @@ +import { mdiChartTimelineVariant } from '@mdi/js' +import Head from 'next/head' +import React, { ReactElement } from 'react' +import { useRouter } from 'next/router' +import { Formik, Form, Field } from 'formik' +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 FormField from '../../components/FormField' +import BaseButton from '../../components/BaseButton' +import BaseButtons from '../../components/BaseButtons' +import { useAppDispatch } from "../../stores/hooks" +import { create } from '../../stores/programs/programsSlice' + +const ProgramsNew = () => { + const dispatch = useAppDispatch(); + const router = useRouter(); + + const initialValues = { name: '' }; + + const handleSubmit = async (values: any) => { + await dispatch(create(values)); + router.push('/programs/programs-list'); + }; + + return ( + <> + + {getPageTitle('New Propagation Type')} + + + + {''} + + + +
+ + + + + + router.push('/programs/programs-list')} /> + +
+
+
+
+ + ) +} + +ProgramsNew.getLayout = function getLayout(page: ReactElement) { + return {page} +} + +export default ProgramsNew \ No newline at end of file diff --git a/frontend/src/pages/programs/programs-view.tsx b/frontend/src/pages/programs/programs-view.tsx new file mode 100644 index 0000000..46d36b4 --- /dev/null +++ b/frontend/src/pages/programs/programs-view.tsx @@ -0,0 +1,52 @@ + +import { mdiChartTimelineVariant } from '@mdi/js' +import Head from 'next/head' +import React, { ReactElement, useEffect } from 'react' +import { useRouter } from 'next/router' +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 BaseButton from '../../components/BaseButton' +import { useAppDispatch, useAppSelector } from "../../stores/hooks" +import { fetch } from '../../stores/programs/programsSlice' + +const ProgramsView = () => { + const dispatch = useAppDispatch(); + const router = useRouter(); + const { id } = router.query; + const { programs } = useAppSelector(state => state.programs); + + useEffect(() => { + if (id) { + dispatch(fetch({ id })); + } + }, [dispatch, id]); + + return ( + <> + + {getPageTitle('View Propagation Type')} + + + + {''} + + +
+ +

{programs?.name || 'N/A'}

+
+ router.push('/programs/programs-list')} /> +
+
+ + ) +} + +ProgramsView.getLayout = function getLayout(page: ReactElement) { + return {page} +} + +export default ProgramsView diff --git a/frontend/src/pages/projects/projects-edit.tsx b/frontend/src/pages/projects/projects-edit.tsx index 65d0ce6..a32f611 100644 --- a/frontend/src/pages/projects/projects-edit.tsx +++ b/frontend/src/pages/projects/projects-edit.tsx @@ -1,4 +1,4 @@ -import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js' +import { mdiChartTimelineVariant } from '@mdi/js' import Head from 'next/head' import React, { ReactElement, useEffect, useState } from 'react' import DatePicker from "react-datepicker"; @@ -16,264 +16,29 @@ 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/projects/projectsSlice' import { useAppDispatch, useAppSelector } from '../../stores/hooks' import { useRouter } from 'next/router' -import {saveFile} from "../../helpers/fileSaver"; -import dataFormatter from '../../helpers/dataFormatter'; -import ImageField from "../../components/ImageField"; - -import {hasPermission} from "../../helpers/userPermissions"; - +const initVals = { + tenant: null, + name: '', + code: '', + description: '', + status: '', + start_date: new Date(), + end_date: new Date(), + organizations: null, +} const EditProjectsPage = () => { const router = useRouter() const dispatch = useAppDispatch() - const initVals = { - - - - - - - - - - - - - - - - - - - - - - - - - tenant: null, - - - - - - 'name': '', - - - - - - - - - - - - - - - - - - - - - - - - - - - - 'code': '', - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - description: '', - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - status: '', - - - - - - - - - - - - - - - - - - - - - - start_date: new Date(), - - - - - - - - - - - - - - - - - - - - - - - - - - - - end_date: new Date(), - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - organizations: null, - - - - - - } const [initialValues, setInitialValues] = useState(initVals) const { projects } = useAppSelector((state) => state.projects) - - const { currentUser } = useAppSelector((state) => state.auth); - - const { id } = router.query useEffect(() => { @@ -281,15 +46,11 @@ const EditProjectsPage = () => { }, [id]) useEffect(() => { - if (typeof projects === 'object') { - setInitialValues(projects) - } - }, [projects]) - - useEffect(() => { - if (typeof projects === 'object') { - const newInitialVal = {...initVals}; - Object.keys(initVals).forEach(el => newInitialVal[el] = (projects)[el]) + if (typeof projects === 'object' && projects !== null && !Array.isArray(projects)) { + const newInitialVal = { + ...initVals, + ...projects, + }; setInitialValues(newInitialVal); } }, [projects]) @@ -315,399 +76,54 @@ const EditProjectsPage = () => { onSubmit={(values) => handleSubmit(values)} >
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - setInitialValues({...initialValues, 'start_date': date})} - /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - setInitialValues({...initialValues, 'end_date': date})} - /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - router.push('/projects/projects-list')}/> - + + + + + + + + + + + + + + + + + + + + + + + + setInitialValues({...initialValues, 'start_date': date})} + className="w-full py-2 px-2 my-2 rounded border border-gray-300 dark:bg-slate-800 dark:border-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500" + /> + + + + setInitialValues({...initialValues, 'end_date': date})} + className="w-full py-2 px-2 my-2 rounded border border-gray-300 dark:bg-slate-800 dark:border-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500" + /> + + + + + + + router.push('/projects/projects-list')}/> + @@ -718,14 +134,10 @@ const EditProjectsPage = () => { EditProjectsPage.getLayout = function getLayout(page: ReactElement) { return ( - + {page} ) } -export default EditProjectsPage +export default EditProjectsPage \ No newline at end of file diff --git a/frontend/src/pages/projects/projects-new.tsx b/frontend/src/pages/projects/projects-new.tsx index e1796bc..a7e2f05 100644 --- a/frontend/src/pages/projects/projects-new.tsx +++ b/frontend/src/pages/projects/projects-new.tsx @@ -1,4 +1,4 @@ -import { mdiAccount, mdiChartTimelineVariant, mdiMail, mdiUpload } from '@mdi/js' +import { mdiChartTimelineVariant } from '@mdi/js' import Head from 'next/head' import React, { ReactElement } from 'react' import CardBox from '../../components/CardBox' @@ -12,166 +12,38 @@ 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/projects/projectsSlice' -import { useAppDispatch } from '../../stores/hooks' +import { useAppDispatch, useAppSelector } from '../../stores/hooks' import { useRouter } from 'next/router' -import moment from 'moment'; const initialValues = { - - - - - - - - - - - - - tenant: '', - - - - name: '', - - - - - - - - - - - - - - - code: '', - - - - - - - - - - - - - - - - - description: '', - - - - - - - - - - - - - - - - - - - - - - status: 'planned', - - - - - - - - - - - - start_date: '', - - - - - - - - - - - - - - - end_date: '', - - - - - - - - - - - - - - - - - - - - - organizations: '', - - - } const ProjectsNew = () => { const router = useRouter() const dispatch = useAppDispatch() - - - + const { currentUser } = useAppSelector((state) => state.auth) const handleSubmit = async (data) => { - await dispatch(create(data)) + const submitData = { ...data } + if (currentUser?.organizationId) { + submitData.organizations = currentUser.organizationId + } + await dispatch(create(submitData)) await router.push('/projects/projects-list') } + return ( <> @@ -183,299 +55,49 @@ const ProjectsNew = () => { handleSubmit(values)} >
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - router.push('/projects/projects-list')}/> - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + router.push('/projects/projects-list')}/> +
@@ -486,14 +108,10 @@ const ProjectsNew = () => { ProjectsNew.getLayout = function getLayout(page: ReactElement) { return ( - + {page} ) } -export default ProjectsNew +export default ProjectsNew \ No newline at end of file diff --git a/frontend/src/pages/propagation_types/propagation_types-edit.tsx b/frontend/src/pages/propagation_types/propagation_types-edit.tsx new file mode 100644 index 0000000..a46c152 --- /dev/null +++ b/frontend/src/pages/propagation_types/propagation_types-edit.tsx @@ -0,0 +1,72 @@ +import { mdiChartTimelineVariant } from '@mdi/js' +import Head from 'next/head' +import React, { ReactElement, useEffect, useState } from 'react' +import { useRouter } from 'next/router' +import { Formik, Form, Field } from 'formik' +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 FormField from '../../components/FormField' +import BaseButton from '../../components/BaseButton' +import BaseButtons from '../../components/BaseButtons' +import { useAppDispatch, useAppSelector } from "../../stores/hooks" +import { update, fetch } from '../../stores/propagation_types/propagation_typesSlice' + +const Propagation_typesEdit = () => { + const dispatch = useAppDispatch(); + const router = useRouter(); + const { id } = router.query; + const { propagation_types } = useAppSelector(state => state.propagation_types); + const [initialValues, setInitialValues] = useState({ name: '' }); + + useEffect(() => { + if (id) { + dispatch(fetch({ id })); + } + }, [dispatch, id]); + + useEffect(() => { + if (propagation_types && !Array.isArray(propagation_types)) { + setInitialValues({ name: propagation_types.name || '' }); + } + }, [propagation_types]); + + const handleSubmit = async (values: any) => { + await dispatch(update({ id, data: values })); + router.push('/propagation_types/propagation_types-list'); + }; + + return ( + <> + + {getPageTitle('Edit Propagation Type')} + + + + {''} + + + +
+ + + + + + router.push('/propagation_types/propagation_types-list')} /> + +
+
+
+
+ + ) +} + +Propagation_typesEdit.getLayout = function getLayout(page: ReactElement) { + return {page} +} + +export default Propagation_typesEdit \ No newline at end of file diff --git a/frontend/src/pages/propagation_types/propagation_types-list.tsx b/frontend/src/pages/propagation_types/propagation_types-list.tsx new file mode 100644 index 0000000..5a8d230 --- /dev/null +++ b/frontend/src/pages/propagation_types/propagation_types-list.tsx @@ -0,0 +1,44 @@ +import { mdiChartTimelineVariant } from '@mdi/js' +import Head from 'next/head' +import React, { ReactElement } from 'react' +import LayoutAuthenticated from '../../layouts/Authenticated' +import SectionMain from '../../components/SectionMain' +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton' +import { getPageTitle } from '../../config' +import TablePropagation_types from '../../components/Propagation_types/TablePropagation_types' +import BaseButton from '../../components/BaseButton' +import { useAppSelector } from "../../stores/hooks"; +import { hasPermission } from "../../helpers/userPermissions"; + +const Propagation_typesListPage = () => { + const { currentUser } = useAppSelector((state) => state.auth); + const hasCreatePermission = currentUser && hasPermission(currentUser, 'CREATE_PROPAGATION_TYPES'); + + return ( + <> + + {getPageTitle('Propagation Types')} + + + + {''} + +
+ {hasCreatePermission && } +
+
+ undefined} filters={[]} showGrid={false} /> +
+ + ) +} + +Propagation_typesListPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ) +} + +export default Propagation_typesListPage \ No newline at end of file diff --git a/frontend/src/pages/propagation_types/propagation_types-new.tsx b/frontend/src/pages/propagation_types/propagation_types-new.tsx new file mode 100644 index 0000000..69e70e3 --- /dev/null +++ b/frontend/src/pages/propagation_types/propagation_types-new.tsx @@ -0,0 +1,59 @@ +import { mdiChartTimelineVariant } from '@mdi/js' +import Head from 'next/head' +import React, { ReactElement } from 'react' +import { useRouter } from 'next/router' +import { Formik, Form, Field } from 'formik' +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 FormField from '../../components/FormField' +import BaseButton from '../../components/BaseButton' +import BaseButtons from '../../components/BaseButtons' +import { useAppDispatch } from "../../stores/hooks" +import { create } from '../../stores/propagation_types/propagation_typesSlice' + +const Propagation_typesNew = () => { + const dispatch = useAppDispatch(); + const router = useRouter(); + + const initialValues = { name: '' }; + + const handleSubmit = async (values: any) => { + await dispatch(create(values)); + router.push('/propagation_types/propagation_types-list'); + }; + + return ( + <> + + {getPageTitle('New Propagation Type')} + + + + {''} + + + +
+ + + + + + router.push('/propagation_types/propagation_types-list')} /> + +
+
+
+
+ + ) +} + +Propagation_typesNew.getLayout = function getLayout(page: ReactElement) { + return {page} +} + +export default Propagation_typesNew \ No newline at end of file diff --git a/frontend/src/pages/propagation_types/propagation_types-view.tsx b/frontend/src/pages/propagation_types/propagation_types-view.tsx new file mode 100644 index 0000000..30612c8 --- /dev/null +++ b/frontend/src/pages/propagation_types/propagation_types-view.tsx @@ -0,0 +1,52 @@ + +import { mdiChartTimelineVariant } from '@mdi/js' +import Head from 'next/head' +import React, { ReactElement, useEffect } from 'react' +import { useRouter } from 'next/router' +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 BaseButton from '../../components/BaseButton' +import { useAppDispatch, useAppSelector } from "../../stores/hooks" +import { fetch } from '../../stores/propagation_types/propagation_typesSlice' + +const Propagation_typesView = () => { + const dispatch = useAppDispatch(); + const router = useRouter(); + const { id } = router.query; + const { propagation_types } = useAppSelector(state => state.propagation_types); + + useEffect(() => { + if (id) { + dispatch(fetch({ id })); + } + }, [dispatch, id]); + + return ( + <> + + {getPageTitle('View Propagation Type')} + + + + {''} + + +
+ +

{propagation_types?.name || 'N/A'}

+
+ router.push('/propagation_types/propagation_types-list')} /> +
+
+ + ) +} + +Propagation_typesView.getLayout = function getLayout(page: ReactElement) { + return {page} +} + +export default Propagation_typesView diff --git a/frontend/src/pages/retailer_programs/retailer_programs-edit.tsx b/frontend/src/pages/retailer_programs/retailer_programs-edit.tsx new file mode 100644 index 0000000..943fe30 --- /dev/null +++ b/frontend/src/pages/retailer_programs/retailer_programs-edit.tsx @@ -0,0 +1,72 @@ +import { mdiChartTimelineVariant } from '@mdi/js' +import Head from 'next/head' +import React, { ReactElement, useEffect, useState } from 'react' +import { useRouter } from 'next/router' +import { Formik, Form, Field } from 'formik' +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 FormField from '../../components/FormField' +import BaseButton from '../../components/BaseButton' +import BaseButtons from '../../components/BaseButtons' +import { useAppDispatch, useAppSelector } from "../../stores/hooks" +import { update, fetch } from '../../stores/retailer_programs/retailer_programsSlice' + +const Retailer_programsEdit = () => { + const dispatch = useAppDispatch(); + const router = useRouter(); + const { id } = router.query; + const { retailer_programs } = useAppSelector(state => state.retailer_programs); + const [initialValues, setInitialValues] = useState({ name: '' }); + + useEffect(() => { + if (id) { + dispatch(fetch({ id })); + } + }, [dispatch, id]); + + useEffect(() => { + if (retailer_programs && !Array.isArray(retailer_programs)) { + setInitialValues({ name: retailer_programs.name || '' }); + } + }, [retailer_programs]); + + const handleSubmit = async (values: any) => { + await dispatch(update({ id, data: values })); + router.push('/retailer_programs/retailer_programs-list'); + }; + + return ( + <> + + {getPageTitle('Edit Retailer Program')} + + + + {''} + + + +
+ + + + + + router.push('/retailer_programs/retailer_programs-list')} /> + +
+
+
+
+ + ) +} + +Retailer_programsEdit.getLayout = function getLayout(page: ReactElement) { + return {page} +} + +export default Retailer_programsEdit \ No newline at end of file diff --git a/frontend/src/pages/retailer_programs/retailer_programs-list.tsx b/frontend/src/pages/retailer_programs/retailer_programs-list.tsx new file mode 100644 index 0000000..7f60311 --- /dev/null +++ b/frontend/src/pages/retailer_programs/retailer_programs-list.tsx @@ -0,0 +1,44 @@ +import { mdiChartTimelineVariant } from '@mdi/js' +import Head from 'next/head' +import React, { ReactElement } from 'react' +import LayoutAuthenticated from '../../layouts/Authenticated' +import SectionMain from '../../components/SectionMain' +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton' +import { getPageTitle } from '../../config' +import TableRetailer_programs from '../../components/Retailer_programs/TableRetailer_programs' +import BaseButton from '../../components/BaseButton' +import { useAppSelector } from "../../stores/hooks"; +import { hasPermission } from "../../helpers/userPermissions"; + +const Retailer_programsListPage = () => { + const { currentUser } = useAppSelector((state) => state.auth); + const hasCreatePermission = currentUser && hasPermission(currentUser, 'CREATE_RETAILER_PROGRAMS'); + + return ( + <> + + {getPageTitle('Retailer Programs')} + + + + {''} + +
+ {hasCreatePermission && } +
+
+ undefined} filters={[]} showGrid={false} /> +
+ + ) +} + +Retailer_programsListPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ) +} + +export default Retailer_programsListPage \ No newline at end of file diff --git a/frontend/src/pages/retailer_programs/retailer_programs-new.tsx b/frontend/src/pages/retailer_programs/retailer_programs-new.tsx new file mode 100644 index 0000000..31e1a31 --- /dev/null +++ b/frontend/src/pages/retailer_programs/retailer_programs-new.tsx @@ -0,0 +1,59 @@ +import { mdiChartTimelineVariant } from '@mdi/js' +import Head from 'next/head' +import React, { ReactElement } from 'react' +import { useRouter } from 'next/router' +import { Formik, Form, Field } from 'formik' +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 FormField from '../../components/FormField' +import BaseButton from '../../components/BaseButton' +import BaseButtons from '../../components/BaseButtons' +import { useAppDispatch } from "../../stores/hooks" +import { create } from '../../stores/retailer_programs/retailer_programsSlice' + +const Retailer_programsNew = () => { + const dispatch = useAppDispatch(); + const router = useRouter(); + + const initialValues = { name: '' }; + + const handleSubmit = async (values: any) => { + await dispatch(create(values)); + router.push('/retailer_programs/retailer_programs-list'); + }; + + return ( + <> + + {getPageTitle('New Retailer Program')} + + + + {''} + + + +
+ + + + + + router.push('/retailer_programs/retailer_programs-list')} /> + +
+
+
+
+ + ) +} + +Retailer_programsNew.getLayout = function getLayout(page: ReactElement) { + return {page} +} + +export default Retailer_programsNew \ No newline at end of file diff --git a/frontend/src/pages/retailer_programs/retailer_programs-view.tsx b/frontend/src/pages/retailer_programs/retailer_programs-view.tsx new file mode 100644 index 0000000..a8e652e --- /dev/null +++ b/frontend/src/pages/retailer_programs/retailer_programs-view.tsx @@ -0,0 +1,36 @@ +import { mdiChartTimelineVariant } from '@mdi/js' +import Head from 'next/head' +import React, { ReactElement, useEffect } from 'react' +import { useRouter } from 'next/router' +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 BaseButton from '../../components/BaseButton' +import { useAppDispatch, useAppSelector } from "../../stores/hooks" +import { fetch } from '../../stores/retailer_programs/retailer_programsSlice' + +const Retailer_programsView = () => { + const dispatch = useAppDispatch(); + const router = useRouter(); + const { id } = router.query; + const { retailer_programs } = useAppSelector(state => state.retailer_programs); + useEffect(() => { if (id) dispatch(fetch({ id })); }, [dispatch, id]); + return ( + <> + {getPageTitle('View Retailer Program')} + + {''} + +

{retailer_programs?.name || 'N/A'}

+ router.push('/retailer_programs/retailer_programs-list')} /> +
+
+ + ) +} +Retailer_programsView.getLayout = function getLayout(page: ReactElement) { + return {page} +} +export default Retailer_programsView \ No newline at end of file diff --git a/frontend/src/pages/retailers/retailers-edit.tsx b/frontend/src/pages/retailers/retailers-edit.tsx new file mode 100644 index 0000000..e1eb701 --- /dev/null +++ b/frontend/src/pages/retailers/retailers-edit.tsx @@ -0,0 +1,72 @@ +import { mdiChartTimelineVariant } from '@mdi/js' +import Head from 'next/head' +import React, { ReactElement, useEffect, useState } from 'react' +import { useRouter } from 'next/router' +import { Formik, Form, Field } from 'formik' +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 FormField from '../../components/FormField' +import BaseButton from '../../components/BaseButton' +import BaseButtons from '../../components/BaseButtons' +import { useAppDispatch, useAppSelector } from "../../stores/hooks" +import { update, fetch } from '../../stores/retailers/retailersSlice' + +const RetailersEdit = () => { + const dispatch = useAppDispatch(); + const router = useRouter(); + const { id } = router.query; + const { retailers } = useAppSelector(state => state.retailers); + const [initialValues, setInitialValues] = useState({ name: '' }); + + useEffect(() => { + if (id) { + dispatch(fetch({ id })); + } + }, [dispatch, id]); + + useEffect(() => { + if (retailers && !Array.isArray(retailers)) { + setInitialValues({ name: retailers.name || '' }); + } + }, [retailers]); + + const handleSubmit = async (values: any) => { + await dispatch(update({ id, data: values })); + router.push('/retailers/retailers-list'); + }; + + return ( + <> + + {getPageTitle('Edit Propagation Type')} + + + + {''} + + + +
+ + + + + + router.push('/retailers/retailers-list')} /> + +
+
+
+
+ + ) +} + +RetailersEdit.getLayout = function getLayout(page: ReactElement) { + return {page} +} + +export default RetailersEdit \ No newline at end of file diff --git a/frontend/src/pages/retailers/retailers-list.tsx b/frontend/src/pages/retailers/retailers-list.tsx new file mode 100644 index 0000000..f63db95 --- /dev/null +++ b/frontend/src/pages/retailers/retailers-list.tsx @@ -0,0 +1,44 @@ +import { mdiChartTimelineVariant } from '@mdi/js' +import Head from 'next/head' +import React, { ReactElement } from 'react' +import LayoutAuthenticated from '../../layouts/Authenticated' +import SectionMain from '../../components/SectionMain' +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton' +import { getPageTitle } from '../../config' +import TableRetailers from '../../components/Retailers/TableRetailers' +import BaseButton from '../../components/BaseButton' +import { useAppSelector } from "../../stores/hooks"; +import { hasPermission } from "../../helpers/userPermissions"; + +const RetailersListPage = () => { + const { currentUser } = useAppSelector((state) => state.auth); + const hasCreatePermission = currentUser && hasPermission(currentUser, 'CREATE_PROPAGATION_TYPES'); + + return ( + <> + + {getPageTitle('Propagation Types')} + + + + {''} + +
+ {hasCreatePermission && } +
+
+ undefined} filters={[]} showGrid={false} /> +
+ + ) +} + +RetailersListPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ) +} + +export default RetailersListPage \ No newline at end of file diff --git a/frontend/src/pages/retailers/retailers-new.tsx b/frontend/src/pages/retailers/retailers-new.tsx new file mode 100644 index 0000000..ba50e7c --- /dev/null +++ b/frontend/src/pages/retailers/retailers-new.tsx @@ -0,0 +1,59 @@ +import { mdiChartTimelineVariant } from '@mdi/js' +import Head from 'next/head' +import React, { ReactElement } from 'react' +import { useRouter } from 'next/router' +import { Formik, Form, Field } from 'formik' +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 FormField from '../../components/FormField' +import BaseButton from '../../components/BaseButton' +import BaseButtons from '../../components/BaseButtons' +import { useAppDispatch } from "../../stores/hooks" +import { create } from '../../stores/retailers/retailersSlice' + +const RetailersNew = () => { + const dispatch = useAppDispatch(); + const router = useRouter(); + + const initialValues = { name: '' }; + + const handleSubmit = async (values: any) => { + await dispatch(create(values)); + router.push('/retailers/retailers-list'); + }; + + return ( + <> + + {getPageTitle('New Propagation Type')} + + + + {''} + + + +
+ + + + + + router.push('/retailers/retailers-list')} /> + +
+
+
+
+ + ) +} + +RetailersNew.getLayout = function getLayout(page: ReactElement) { + return {page} +} + +export default RetailersNew \ No newline at end of file diff --git a/frontend/src/pages/retailers/retailers-view.tsx b/frontend/src/pages/retailers/retailers-view.tsx new file mode 100644 index 0000000..731df37 --- /dev/null +++ b/frontend/src/pages/retailers/retailers-view.tsx @@ -0,0 +1,52 @@ + +import { mdiChartTimelineVariant } from '@mdi/js' +import Head from 'next/head' +import React, { ReactElement, useEffect } from 'react' +import { useRouter } from 'next/router' +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 BaseButton from '../../components/BaseButton' +import { useAppDispatch, useAppSelector } from "../../stores/hooks" +import { fetch } from '../../stores/retailers/retailersSlice' + +const RetailersView = () => { + const dispatch = useAppDispatch(); + const router = useRouter(); + const { id } = router.query; + const { retailers } = useAppSelector(state => state.retailers); + + useEffect(() => { + if (id) { + dispatch(fetch({ id })); + } + }, [dispatch, id]); + + return ( + <> + + {getPageTitle('View Propagation Type')} + + + + {''} + + +
+ +

{retailers?.name || 'N/A'}

+
+ router.push('/retailers/retailers-list')} /> +
+
+ + ) +} + +RetailersView.getLayout = function getLayout(page: ReactElement) { + return {page} +} + +export default RetailersView diff --git a/frontend/src/pages/search.tsx b/frontend/src/pages/search.tsx index 00f5168..64bf121 100644 --- a/frontend/src/pages/search.tsx +++ b/frontend/src/pages/search.tsx @@ -1,9 +1,7 @@ import React, { ReactElement, useEffect, useState } from 'react'; import Head from 'next/head'; import 'react-datepicker/dist/react-datepicker.css'; -import { useAppDispatch } from '../stores/hooks'; - -import { useAppSelector } from '../stores/hooks'; +import { useAppDispatch, useAppSelector } from '../stores/hooks'; import { useRouter } from 'next/router'; import LayoutAuthenticated from '../layouts/Authenticated'; @@ -93,4 +91,4 @@ SearchView.getLayout = function getLayout(page: ReactElement) { ); }; -export default SearchView; +export default SearchView; \ No newline at end of file diff --git a/frontend/src/pages/sites/sites-edit.tsx b/frontend/src/pages/sites/sites-edit.tsx new file mode 100644 index 0000000..99ba816 --- /dev/null +++ b/frontend/src/pages/sites/sites-edit.tsx @@ -0,0 +1,72 @@ +import { mdiChartTimelineVariant } from '@mdi/js' +import Head from 'next/head' +import React, { ReactElement, useEffect, useState } from 'react' +import { useRouter } from 'next/router' +import { Formik, Form, Field } from 'formik' +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 FormField from '../../components/FormField' +import BaseButton from '../../components/BaseButton' +import BaseButtons from '../../components/BaseButtons' +import { useAppDispatch, useAppSelector } from "../../stores/hooks" +import { update, fetch } from '../../stores/sites/sitesSlice' + +const SitesEdit = () => { + const dispatch = useAppDispatch(); + const router = useRouter(); + const { id } = router.query; + const { sites } = useAppSelector(state => state.sites); + const [initialValues, setInitialValues] = useState({ name: '' }); + + useEffect(() => { + if (id) { + dispatch(fetch({ id })); + } + }, [dispatch, id]); + + useEffect(() => { + if (sites && !Array.isArray(sites)) { + setInitialValues({ name: sites.name || '' }); + } + }, [sites]); + + const handleSubmit = async (values: any) => { + await dispatch(update({ id, data: values })); + router.push('/sites/sites-list'); + }; + + return ( + <> + + {getPageTitle('Edit Propagation Type')} + + + + {''} + + + +
+ + + + + + router.push('/sites/sites-list')} /> + +
+
+
+
+ + ) +} + +SitesEdit.getLayout = function getLayout(page: ReactElement) { + return {page} +} + +export default SitesEdit \ No newline at end of file diff --git a/frontend/src/pages/sites/sites-list.tsx b/frontend/src/pages/sites/sites-list.tsx new file mode 100644 index 0000000..2527960 --- /dev/null +++ b/frontend/src/pages/sites/sites-list.tsx @@ -0,0 +1,44 @@ +import { mdiChartTimelineVariant } from '@mdi/js' +import Head from 'next/head' +import React, { ReactElement } from 'react' +import LayoutAuthenticated from '../../layouts/Authenticated' +import SectionMain from '../../components/SectionMain' +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton' +import { getPageTitle } from '../../config' +import TableSites from '../../components/Sites/TableSites' +import BaseButton from '../../components/BaseButton' +import { useAppSelector } from "../../stores/hooks"; +import { hasPermission } from "../../helpers/userPermissions"; + +const SitesListPage = () => { + const { currentUser } = useAppSelector((state) => state.auth); + const hasCreatePermission = currentUser && hasPermission(currentUser, 'CREATE_PROPAGATION_TYPES'); + + return ( + <> + + {getPageTitle('Propagation Types')} + + + + {''} + +
+ {hasCreatePermission && } +
+
+ undefined} filters={[]} showGrid={false} /> +
+ + ) +} + +SitesListPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ) +} + +export default SitesListPage \ No newline at end of file diff --git a/frontend/src/pages/sites/sites-new.tsx b/frontend/src/pages/sites/sites-new.tsx new file mode 100644 index 0000000..e3d496c --- /dev/null +++ b/frontend/src/pages/sites/sites-new.tsx @@ -0,0 +1,59 @@ +import { mdiChartTimelineVariant } from '@mdi/js' +import Head from 'next/head' +import React, { ReactElement } from 'react' +import { useRouter } from 'next/router' +import { Formik, Form, Field } from 'formik' +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 FormField from '../../components/FormField' +import BaseButton from '../../components/BaseButton' +import BaseButtons from '../../components/BaseButtons' +import { useAppDispatch } from "../../stores/hooks" +import { create } from '../../stores/sites/sitesSlice' + +const SitesNew = () => { + const dispatch = useAppDispatch(); + const router = useRouter(); + + const initialValues = { name: '' }; + + const handleSubmit = async (values: any) => { + await dispatch(create(values)); + router.push('/sites/sites-list'); + }; + + return ( + <> + + {getPageTitle('New Propagation Type')} + + + + {''} + + + +
+ + + + + + router.push('/sites/sites-list')} /> + +
+
+
+
+ + ) +} + +SitesNew.getLayout = function getLayout(page: ReactElement) { + return {page} +} + +export default SitesNew \ No newline at end of file diff --git a/frontend/src/pages/sites/sites-view.tsx b/frontend/src/pages/sites/sites-view.tsx new file mode 100644 index 0000000..4835e4a --- /dev/null +++ b/frontend/src/pages/sites/sites-view.tsx @@ -0,0 +1,52 @@ + +import { mdiChartTimelineVariant } from '@mdi/js' +import Head from 'next/head' +import React, { ReactElement, useEffect } from 'react' +import { useRouter } from 'next/router' +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 BaseButton from '../../components/BaseButton' +import { useAppDispatch, useAppSelector } from "../../stores/hooks" +import { fetch } from '../../stores/sites/sitesSlice' + +const SitesView = () => { + const dispatch = useAppDispatch(); + const router = useRouter(); + const { id } = router.query; + const { sites } = useAppSelector(state => state.sites); + + useEffect(() => { + if (id) { + dispatch(fetch({ id })); + } + }, [dispatch, id]); + + return ( + <> + + {getPageTitle('View Propagation Type')} + + + + {''} + + +
+ +

{sites?.name || 'N/A'}

+
+ router.push('/sites/sites-list')} /> +
+
+ + ) +} + +SitesView.getLayout = function getLayout(page: ReactElement) { + return {page} +} + +export default SitesView diff --git a/frontend/src/pages/tracking_activities/tracking_activities-edit.tsx b/frontend/src/pages/tracking_activities/tracking_activities-edit.tsx index f8e50df..05e7345 100644 --- a/frontend/src/pages/tracking_activities/tracking_activities-edit.tsx +++ b/frontend/src/pages/tracking_activities/tracking_activities-edit.tsx @@ -16,488 +16,41 @@ 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/tracking_activities/tracking_activitiesSlice' import { useAppDispatch, useAppSelector } from '../../stores/hooks' import { useRouter } from 'next/router' -import {saveFile} from "../../helpers/fileSaver"; -import dataFormatter from '../../helpers/dataFormatter'; -import ImageField from "../../components/ImageField"; - -import {hasPermission} from "../../helpers/userPermissions"; - +const initVals = { + tenant: null, + trial: null, + activity_type: null, + recorded_by: null, + occurred_at: new Date(), + visibility: '', + summary: '', + details: '', + value_decimal: '', + value_int: '', + value_boolean: false, + value_text: '', + severity: '', + photos: [], + attachments: [], + organizations: null, +} const EditTracking_activitiesPage = () => { const router = useRouter() const dispatch = useAppDispatch() - const initVals = { - - - - - - - - - - - - - - - - - - - - - - - - - tenant: null, - - - - - - - - - - - - - - - - - - - - - - - - - - - - trial: null, - - - - - - - - - - - - - - - - - - - - - - - - - - - - activity_type: null, - - - - - - - - - - - - - - - - - - - - - - - - - - - - recorded_by: null, - - - - - - - - - - - - - - - - occurred_at: new Date(), - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - visibility: '', - - - - - - - - - - - - 'summary': '', - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - details: '', - - - - - - - - - - - - - - - - - - - - - - - - 'value_decimal': '', - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - value_int: '', - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - value_boolean: false, - - - - - - - - - - - - - - - - - - value_text: '', - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - severity: '', - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - photos: [], - - - - - - - - - - - - - - - - - - - - - - - - - - attachments: [], - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - organizations: null, - - - - - - } const [initialValues, setInitialValues] = useState(initVals) const { tracking_activities } = useAppSelector((state) => state.tracking_activities) - - const { currentUser } = useAppSelector((state) => state.auth); - - const { id } = router.query useEffect(() => { @@ -505,15 +58,11 @@ const EditTracking_activitiesPage = () => { }, [id]) useEffect(() => { - if (typeof tracking_activities === 'object') { - setInitialValues(tracking_activities) - } - }, [tracking_activities]) - - useEffect(() => { - if (typeof tracking_activities === 'object') { - const newInitialVal = {...initVals}; - Object.keys(initVals).forEach(el => newInitialVal[el] = (tracking_activities)[el]) + if (typeof tracking_activities === 'object' && tracking_activities !== null && !Array.isArray(tracking_activities)) { + const newInitialVal = { + ...initVals, + ...tracking_activities, + }; setInitialValues(newInitialVal); } }, [tracking_activities]) @@ -539,798 +88,90 @@ const EditTracking_activitiesPage = () => { onSubmit={(values) => handleSubmit(values)} >
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - setInitialValues({...initialValues, 'occurred_at': date})} - /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + setInitialValues({...initialValues, 'occurred_at': date})} + className="w-full py-2 px-2 my-2 rounded border border-gray-300 dark:bg-slate-800 dark:border-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500" + /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1348,14 +189,10 @@ const EditTracking_activitiesPage = () => { EditTracking_activitiesPage.getLayout = function getLayout(page: ReactElement) { return ( - + {page} ) } -export default EditTracking_activitiesPage +export default EditTracking_activitiesPage \ No newline at end of file diff --git a/frontend/src/pages/tracking_activities/tracking_activities-new.tsx b/frontend/src/pages/tracking_activities/tracking_activities-new.tsx index a7b07c8..9f25164 100644 --- a/frontend/src/pages/tracking_activities/tracking_activities-new.tsx +++ b/frontend/src/pages/tracking_activities/tracking_activities-new.tsx @@ -1,4 +1,4 @@ -import { mdiAccount, mdiChartTimelineVariant, mdiMail, mdiUpload } from '@mdi/js' +import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js' import Head from 'next/head' import React, { ReactElement } from 'react' import CardBox from '../../components/CardBox' @@ -12,295 +12,51 @@ 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/tracking_activities/tracking_activitiesSlice' -import { useAppDispatch } from '../../stores/hooks' +import { useAppDispatch, useAppSelector } from '../../stores/hooks' import { useRouter } from 'next/router' -import moment from 'moment'; const initialValues = { - - - - - - - - - - - - - tenant: '', - - - - - - - - - - - - - - - trial: '', - - - - - - - - - - - - - - - activity_type: '', - - - - - - - - - - - - - - - recorded_by: '', - - - - - - - - - occurred_at: '', - - - - - - - - - - - - - - - - - - - visibility: 'internal', - - - - - - - summary: '', - - - - - - - - - - - - - - - - - details: '', - - - - - - - - - - - - - value_decimal: '', - - - - - - - - - - - - - - - - - - value_int: '', - - - - - - - - - - - - - - - - - - value_boolean: false, - - - - - - - - - - value_text: '', - - - - - - - - - - - - - - - - - - - - - - - severity: 'none', - - - - - - - - - - - - - - - - - photos: [], - - - - - - - - - - - - - - attachments: [], - - - - - - - - - - - - - - - - - organizations: '', - - - } const Tracking_activitiesNew = () => { const router = useRouter() const dispatch = useAppDispatch() - - - + const { currentUser } = useAppSelector((state) => state.auth) const handleSubmit = async (data) => { - await dispatch(create(data)) + const submitData = { ...data } + if (currentUser?.organizationId) { + submitData.organizations = currentUser.organizationId + } + await dispatch(create(submitData)) await router.push('/tracking_activities/tracking_activities-list') } + return ( <> @@ -312,567 +68,91 @@ const Tracking_activitiesNew = () => { handleSubmit(values)} > - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -890,14 +170,10 @@ const Tracking_activitiesNew = () => { Tracking_activitiesNew.getLayout = function getLayout(page: ReactElement) { return ( - + {page} ) } -export default Tracking_activitiesNew +export default Tracking_activitiesNew \ No newline at end of file diff --git a/frontend/src/pages/trials/trials-edit.tsx b/frontend/src/pages/trials/trials-edit.tsx index a739f09..16e3197 100644 --- a/frontend/src/pages/trials/trials-edit.tsx +++ b/frontend/src/pages/trials/trials-edit.tsx @@ -1,4 +1,4 @@ -import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js' +import { mdiChartTimelineVariant } from '@mdi/js' import Head from 'next/head' import React, { ReactElement, useEffect, useState } from 'react' import DatePicker from "react-datepicker"; @@ -16,625 +16,130 @@ 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 FormFilePicker from "../../components/FormFilePicker"; import { update, fetch } from '../../stores/trials/trialsSlice' import { useAppDispatch, useAppSelector } from '../../stores/hooks' import { useRouter } from 'next/router' -import {saveFile} from "../../helpers/fileSaver"; -import dataFormatter from '../../helpers/dataFormatter'; -import ImageField from "../../components/ImageField"; - -import {hasPermission} from "../../helpers/userPermissions"; - +const initVals = { + name: '', + trial_code: '', + variety_name: '', + breeder: null, + genus: '', + species: '', + propagationType: null, + growthHabits: [], + cropType: null, + colour: '', + batch_code: '', + trial_type: '', + status: '', + planted_at: null, + harvested_at: null, + greenhouse: '', + zone: '', + bench: '', + plants_count: '', + target_temperature_c: '', + target_humidity_percent: '', + target_ec: '', + target_ph: '', + notes: '', + container: null, + containerSize: '', + finishedHeight: '', + finishedSpread: '', + cultureSheet: [], + description: '', + internalReferenceNumber: '', + retailers: [], + program: null, + programYear: '', + groups: [], + distributor: '', + quantity: '', + site: null, + location: null, + receiveWeek: '', + finishWeek: '', + tableId: '', + tenant: null, + project: null, + organizations: null, + trial_category: 'PLANT', + attributes: '', +} const EditTrialsPage = () => { const router = useRouter() const dispatch = useAppDispatch() - const initVals = { - - - - - - - - - - - - - - - - - - - - - - - - - tenant: null, - - - - - - - - - - - - - - - - - - - - - - - - - - - - project: null, - - - - - - 'name': '', - - - - - - - - - - - - - - - - - - - - - - - - - - - - 'variety_name': '', - - - - - - - - - - - - - - - - - - - - - - - - - - - - 'breeder': '', - - - - - - - - - - - - - - - - - - - - - - - - - - - - 'batch_code': '', - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - trial_type: '', - - - - - - - - - - - - - - - - - - - - - - - - - - - - status: '', - - - - - - - - - - - - - - - - - - - - - - planted_at: new Date(), - - - - - - - - - - - - - - - - - - - - - - - - - - - - harvested_at: new Date(), - - - - - - - - - - - - - - - - - - 'greenhouse': '', - - - - - - - - - - - - - - - - - - - - - - - - - - - - 'zone': '', - - - - - - - - - - - - - - - - - - - - - - - - - - - - 'bench': '', - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - plants_count: '', - - - - - - - - - - - - - - - - - - - - - - 'target_temperature_c': '', - - - - - - - - - - - - - - - - - - - - - - - - - - - - 'target_humidity_percent': '', - - - - - - - - - - - - - - - - - - - - - - - - - - - - 'target_ec': '', - - - - - - - - - - - - - - - - - - - - - - - - - - - - 'target_ph': '', - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - notes: '', - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - organizations: null, - - - - - - } const [initialValues, setInitialValues] = useState(initVals) + const [activeTab, setActiveTab] = useState(0) const { trials } = useAppSelector((state) => state.trials) - - const { currentUser } = useAppSelector((state) => state.auth); - - const { id } = router.query useEffect(() => { - dispatch(fetch({ id: id })) + if (id) { + dispatch(fetch({ id: id })) + } }, [id]) useEffect(() => { - if (typeof trials === 'object') { - setInitialValues(trials) - } - }, [trials]) - - useEffect(() => { - if (typeof trials === 'object') { - const newInitialVal = {...initVals}; - Object.keys(initVals).forEach(el => newInitialVal[el] = (trials)[el]) + if (trials && typeof trials === 'object' && !Array.isArray(trials)) { + const newInitialVal = { + ...initVals, + ...trials, + breeder: trials.breeder, + propagationType: trials.propagationType, + cropType: trials.cropType, + growthHabits: trials.growthHabits ? trials.growthHabits.map(gh => gh.id) : [], + container: trials.container, + cultureSheet: trials.cultureSheet ? [trials.cultureSheet] : [], + program: trials.program, + site: trials.site, + location: trials.location, + retailers: trials.retailers ? trials.retailers.map(r => r.id) : [], + groups: trials.groups ? trials.groups.map(g => g.id) : [], + tenant: trials.tenant, + project: trials.project, + organizations: trials.organizations, + trial_category: trials.trial_category || 'PLANT', + attributes: trials.attributes ? JSON.stringify(trials.attributes, null, 2) : '', + }; setInitialValues(newInitialVal); } }, [trials]) const handleSubmit = async (data) => { - await dispatch(update({ id: id, data })) + const submitData = { ...data }; + + // Parse attributes if it's a string and category is GENERIC (or even PLANT if we decide to use it there too) + if (typeof submitData.attributes === 'string') { + try { + if (submitData.attributes.trim().startsWith('{')) { + submitData.attributes = JSON.parse(submitData.attributes); + } else if (submitData.attributes.trim().length > 0) { + submitData.attributes = { notes: submitData.attributes }; + } else { + submitData.attributes = {}; + } + } catch (e) { + submitData.attributes = { raw_content: submitData.attributes }; + } + } + + await dispatch(update({ id: id, data: submitData })) await router.push('/trials/trials-list') } + const weeksOptions = Array.from({ length: 53 }, (_, i) => ({ id: i + 1, label: `Week ${i + 1}` })); + return ( <> @@ -650,894 +155,292 @@ const EditTrialsPage = () => { initialValues={initialValues} onSubmit={(values) => handleSubmit(values)} > - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - setInitialValues({...initialValues, 'planted_at': date})} - /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - setInitialValues({...initialValues, 'harvested_at': date})} - /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - router.push('/trials/trials-list')}/> - - + {({ values, setFieldValue }) => { + const isPlant = values.trial_category === 'PLANT'; + + const tabs = isPlant + ? [ + { label: 'Basic Info' }, + { label: 'Botanical & Logistics' }, + { label: 'Environment & Specs' }, + { label: 'Notes & Media' }, + ] + : [ + { label: 'Basic Info' }, + { label: 'Dataset Attributes' }, + { label: 'Notes & Media' }, + ]; + + return ( +
+
+ {tabs.map((tab, index) => ( + + ))} +
+ + {activeTab === 0 && ( + <> + + { + setFieldValue('trial_category', e.target.value); + setActiveTab(0); + }} className="px-3 py-2 max-w-full focus:ring focus:outline-none border-gray-700 rounded w-full dark:placeholder-gray-400 h-12 border bg-white dark:bg-slate-800"> + + + + + + + + + + + + + + {isPlant && ( + <> + + + + + + + + + )} + + + + + + + + + + + + + + + + + + + + + + + + + + + {isPlant && ( + <> + + setInitialValues({...initialValues, 'planted_at': date})} + className="w-full py-2 px-2 my-2 rounded border border-gray-300 dark:bg-slate-800 dark:border-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500" + /> + + + + setInitialValues({...initialValues, 'harvested_at': date})} + className="w-full py-2 px-2 my-2 rounded border border-gray-300 dark:bg-slate-800 dark:border-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500" + /> + + + )} + + )} + + {/* PLANT: Tab 1 (Botanical) */} + {isPlant && activeTab === 1 && ( + <> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + )} + + {/* PLANT: Tab 2 (Environment) */} + {isPlant && activeTab === 2 && ( + <> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + )} + + {/* GENERIC: Tab 1 (Dataset Attributes) */} + {!isPlant && activeTab === 1 && ( + + +
+ Enter attributes as JSON (e.g. {`{"speed": "100km/h", "weight": "500kg"}`}) or plain text. +
+
+ )} + + {/* Common: Notes (Tab 3 for Plant, Tab 2 for Generic) */} + {((isPlant && activeTab === 3) || (!isPlant && activeTab === 2)) && ( + <> + + + + + + + + + + + + + + + + + )} + + + + + + router.push('/trials/trials-list')}/> + + + ) + }}
@@ -1547,14 +450,10 @@ const EditTrialsPage = () => { EditTrialsPage.getLayout = function getLayout(page: ReactElement) { return ( - + {page} ) } -export default EditTrialsPage +export default EditTrialsPage \ No newline at end of file diff --git a/frontend/src/pages/trials/trials-list.tsx b/frontend/src/pages/trials/trials-list.tsx index d7feeb1..deab356 100644 --- a/frontend/src/pages/trials/trials-list.tsx +++ b/frontend/src/pages/trials/trials-list.tsx @@ -25,8 +25,6 @@ const TrialsTablesPage = () => { 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); @@ -125,7 +123,7 @@ const TrialsTablesPage = () => {
- Switch to Table + Switch to Calendar
@@ -135,7 +133,7 @@ const TrialsTablesPage = () => { filterItems={filterItems} setFilterItems={setFilterItems} filters={filters} - showGrid={false} + showGrid={true} /> @@ -171,4 +169,4 @@ TrialsTablesPage.getLayout = function getLayout(page: ReactElement) { ) } -export default TrialsTablesPage +export default TrialsTablesPage \ No newline at end of file diff --git a/frontend/src/pages/trials/trials-new.tsx b/frontend/src/pages/trials/trials-new.tsx index 736bdaa..ee6ba92 100644 --- a/frontend/src/pages/trials/trials-new.tsx +++ b/frontend/src/pages/trials/trials-new.tsx @@ -1,6 +1,6 @@ -import { mdiAccount, mdiChartTimelineVariant, mdiMail, mdiUpload } from '@mdi/js' +import { mdiChartTimelineVariant } from '@mdi/js' import Head from 'next/head' -import React, { ReactElement } from 'react' +import React, { ReactElement, useState } from 'react' import CardBox from '../../components/CardBox' import LayoutAuthenticated from '../../layouts/Authenticated' import SectionMain from '../../components/SectionMain' @@ -12,362 +12,102 @@ 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 FormFilePicker from "../../components/FormFilePicker"; import { create } from '../../stores/trials/trialsSlice' -import { useAppDispatch } from '../../stores/hooks' +import { useAppDispatch, useAppSelector } from '../../stores/hooks' import { useRouter } from 'next/router' import moment from 'moment'; const initialValues = { - - - - - - - - - - - - - - tenant: '', - - - - - - - - - - - - - - - - project: '', - - - - name: '', - - - - - - - - - - - - - - - + trial_code: '', variety_name: '', - - - - - - - - - - - - - - - breeder: '', - - - - - - - - - - - - - - - + genus: '', + species: '', + propagationType: '', + growthHabits: [], + cropType: '', + colour: '', batch_code: '', - - - - - - - - - - - - - - - - - - - - - - - - trial_type: 'new_variety', - - - - - - - - - - - - - - - - status: 'setup', - - - - - - - - - - - - planted_at: '', - - - - - - - - - - - - - - - harvested_at: '', - - - - - - - - - - greenhouse: '', - - - - - - - - - - - - - - - zone: '', - - - - - - - - - - - - - - - bench: '', - - - - - - - - - - - - - - - - - - plants_count: '', - - - - - - - - - - - - target_temperature_c: '', - - - - - - - - - - - - - - - target_humidity_percent: '', - - - - - - - - - - - - - - - target_ec: '', - - - - - - - - - - - - - - - target_ph: '', - - - - - - - - - - - - - - - - - notes: '', - - - - - - - - - - - - - - - - - - - - - - - - + container: '', + containerSize: '', + finishedHeight: '', + finishedSpread: '', + cultureSheet: [], + description: '', + internalReferenceNumber: '', + retailers: [], + program: '', + programYear: '', + groups: [], + distributor: '', + quantity: '', + site: '', + location: '', + receiveWeek: '', + finishWeek: '', + tableId: '', + tenant: '', + project: '', organizations: '', - - - + trial_category: 'PLANT', + attributes: '', // Using string for simple textarea input for now } - const TrialsNew = () => { const router = useRouter() const dispatch = useAppDispatch() + const [activeTab, setActiveTab] = useState(0) + const { currentUser } = useAppSelector((state) => state.auth) - - - // get from url params - const { dateRangeStart, dateRangeEnd } = router.query - + const { dateRangeStart, dateRangeEnd } = router.query const handleSubmit = async (data) => { - await dispatch(create(data)) + const submitData = { ...data } + if (currentUser?.organizationId) { + submitData.organizations = currentUser.organizationId + } + // Parse attributes if it's a string and category is GENERIC + if (submitData.trial_category !== 'PLANT' && typeof submitData.attributes === 'string') { + try { + // If it looks like JSON, parse it. Otherwise wrap in object. + if (submitData.attributes.trim().startsWith('{')) { + submitData.attributes = JSON.parse(submitData.attributes); + } else { + submitData.attributes = { notes: submitData.attributes }; + } + } catch (e) { + // Keep as string if parsing fails, backend might handle or we accept it as is? + // Postgres JSONB requires valid JSON. + submitData.attributes = { raw_content: submitData.attributes }; + } + } + + await dispatch(create(submitData)) await router.push('/trials/trials-list') } + + const weeksOptions = Array.from({ length: 53 }, (_, i) => ({ id: i + 1, label: `Week ${i + 1}` })); + return ( <> @@ -380,735 +120,293 @@ const TrialsNew = () => { handleSubmit(values)} > -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - router.push('/trials/trials-list')}/> - - + {({ values, setFieldValue }) => { + const isPlant = values.trial_category === 'PLANT'; + + const tabs = isPlant + ? [ + { label: 'Basic Info' }, + { label: 'Botanical & Logistics' }, + { label: 'Environment & Specs' }, + { label: 'Notes & Media' }, + ] + : [ + { label: 'Basic Info' }, + { label: 'Dataset Attributes' }, + { label: 'Notes & Media' }, + ]; + + return ( +
+
+ {tabs.map((tab, index) => ( + + ))} +
+ + {activeTab === 0 && ( + <> + + { + setFieldValue('trial_category', e.target.value); + setActiveTab(0); // Reset tab when changing category + }} className="px-3 py-2 max-w-full focus:ring focus:outline-none border-gray-700 rounded w-full dark:placeholder-gray-400 h-12 border bg-white dark:bg-slate-800"> + + + + + + + + + + + + + + {isPlant && ( + <> + + + + + + + + + )} + + + + + + + + + + + + + + + + + + + + + + + + + + + {isPlant && ( + <> + + + + + + + + + )} + + )} + + {/* PLANT: Tab 1 (Botanical) */} + {isPlant && activeTab === 1 && ( + <> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + )} + + {/* PLANT: Tab 2 (Environment) */} + {isPlant && activeTab === 2 && ( + <> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + )} + + {/* GENERIC: Tab 1 (Dataset Attributes) */} + {!isPlant && activeTab === 1 && ( + + +
+ Enter attributes as JSON (e.g. {`{"speed": "100km/h", "weight": "500kg"}`}) or plain text. +
+
+ )} + + {/* Common: Notes (Tab 3 for Plant, Tab 2 for Generic) */} + {((isPlant && activeTab === 3) || (!isPlant && activeTab === 2)) && ( + <> + + + + + + + + + + + + + + + + + )} + + + + + + router.push('/trials/trials-list')}/> + + + ) + }}
@@ -1118,14 +416,10 @@ const TrialsNew = () => { TrialsNew.getLayout = function getLayout(page: ReactElement) { return ( - + {page} ) } -export default TrialsNew +export default TrialsNew \ No newline at end of file diff --git a/frontend/src/pages/trials/trials-table.tsx b/frontend/src/pages/trials/trials-table.tsx index a8672e5..e50abf0 100644 --- a/frontend/src/pages/trials/trials-table.tsx +++ b/frontend/src/pages/trials/trials-table.tsx @@ -25,7 +25,6 @@ const TrialsTablesPage = () => { 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); @@ -124,7 +123,7 @@ const TrialsTablesPage = () => {
- Back to calendar + Back to list
@@ -134,7 +133,7 @@ const TrialsTablesPage = () => { filterItems={filterItems} setFilterItems={setFilterItems} filters={filters} - showGrid={true} + showGrid={false} /> @@ -169,4 +168,4 @@ TrialsTablesPage.getLayout = function getLayout(page: ReactElement) { ) } -export default TrialsTablesPage +export default TrialsTablesPage \ No newline at end of file diff --git a/frontend/src/pages/trials/trials-view.tsx b/frontend/src/pages/trials/trials-view.tsx index 051a224..9881e73 100644 --- a/frontend/src/pages/trials/trials-view.tsx +++ b/frontend/src/pages/trials/trials-view.tsx @@ -6,9 +6,6 @@ import dayjs from "dayjs"; import {useAppDispatch, useAppSelector} from "../../stores/hooks"; import {useRouter} from "next/router"; import { fetch } from '../../stores/trials/trialsSlice' -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"; @@ -17,39 +14,26 @@ import CardBox from "../../components/CardBox"; import BaseButton from "../../components/BaseButton"; import BaseDivider from "../../components/BaseDivider"; import {mdiChartTimelineVariant} from "@mdi/js"; -import {SwitchField} from "../../components/SwitchField"; import FormField from "../../components/FormField"; - -import {hasPermission} from "../../helpers/userPermissions"; - +import dataFormatter from '../../helpers/dataFormatter'; const TrialsView = () => { const router = useRouter() const dispatch = useAppDispatch() const { trials } = useAppSelector((state) => state.trials) - - const { currentUser } = useAppSelector((state) => state.auth); - - const { id } = router.query; - - function removeLastCharacter(str) { - console.log(str,`str`) - return str.slice(0, -1); - } useEffect(() => { dispatch(fetch({ id })); }, [dispatch, id]); - return ( <> {getPageTitle('View trials')} - + { /> - - - - - - - - - - - - - - - - - - - - - -
-

Tenant

- - - - - - - - - - -

{trials?.tenant?.name ?? 'No data'}

- - - - - - - - - - - - - - - - - - - - +

Trial Name

+

{trials?.name || 'No data'}

- - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-

Project

- - - - - - - - - - - - - - -

{trials?.project?.name ?? 'No data'}

- - - - - - - - - - - - - - - - +

Trial Code

+

{trials?.trial_code || 'No data'}

- - - - - - - - - - -
-

TrialName

-

{trials?.name}

-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-

VarietyName

-

{trials?.variety_name}

-
- - - - - - - - - - - - - - - - - - - - - - - - - - - -

Breeder

-

{trials?.breeder}

+

{trials?.breeder?.name || 'No data'}

- - - - - - - - - - - - - - - - - - - - - - - - - - -
-

BatchCode

-

{trials?.batch_code}

+

Variety Name

+

{trials?.variety_name || 'No data'}

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-

TrialType

-

{trials?.trial_type ?? 'No data'}

+

Genus

+

{trials?.genus || 'No data'}

- - +
+

Species

+

{trials?.species || 'No data'}

+
- +
+

Propagation Type

+

{trials?.propagationType?.name || 'No data'}

+
- +
+

Growth Habit

+

{trials?.growthHabits?.map(gh => gh.name).join(', ') || 'No data'}

+
- +
+

Crop Type

+

{trials?.cropType?.name || 'No data'}

+
- - +
+

Colour

+

{trials?.colour || 'No data'}

+
- + - +
+

Container

+

{trials?.container?.name || 'No data'}

+
- +
+

Container Size

+

{trials?.containerSize || 'No data'}

+
- +
+

Finished Height

+

{trials?.finishedHeight || 'No data'}

+
- +
+

Finished Spread

+

{trials?.finishedSpread || 'No data'}

+
- +
+

Culture Sheet

+ {trials?.cultureSheet ? ( + + {trials.cultureSheet.name || 'Download File'} + + ) : ( +

No data

+ )} +
- +
+

Description/Grower Notes & PGR Application

+ {trials?.description + ?

+ :

No data

+ } +
+ +
+

Internal Reference Number

+

{trials?.internalReferenceNumber || 'No data'}

+
+ +
+

Retailer

+

{trials?.retailers?.map(r => r.name).join(', ') || 'No data'}

+
+ +
+

Program

+

{trials?.program?.name || 'No data'}

+
+ +
+

Program Year

+

{trials?.programYear || 'No data'}

+
+ +
+

Group

+

{trials?.groups?.map(g => g.name).join(', ') || 'No data'}

+
+ +
+

Distributor

+

{trials?.distributor || 'No data'}

+
+ +
+

Quantity

+

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

+
+ +
+

Site

+

{trials?.site?.name || 'No data'}

+
+ +
+

Default Location

+

{trials?.location?.name || 'No data'}

+
+ +
+

Receive Week

+

{trials?.receiveWeek ? `Week ${trials.receiveWeek}` : 'No data'}

+
+ +
+

Finish Week

+

{trials?.finishWeek ? `Week ${trials.finishWeek}` : 'No data'}

+
+ +
+

Table ID

+

{trials?.tableId || 'No data'}

+
+ + + +
+

Trial Type

+

{trials?.trial_type || 'No data'}

+
-

Status

-

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

+

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

- - - - - - - - - - - - - - - - - - - - - - - - - {trials.planted_at ? + {trials?.planted_at ? :

No PlantedAt

} + /> :

No data

}
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - {trials.harvested_at ? + {trials?.harvested_at ? :

No HarvestedAt

} + /> :

No data

}
- - - - - - - - - - - - - - - - -
-

Greenhouse

-

{trials?.greenhouse}

+

Notes

+ {trials?.notes + ?

+ :

No data

+ }
- - + - - - - - - - - - - - - - - - - - - - - - - - -
-

Zone

-

{trials?.zone}

+

Associated Project

+

{trials?.project?.name || 'No data'}

- - - - - - - - - - - - - - - - - - - - - - - - - - -
-

Bench

-

{trials?.bench}

+

Organization

+

{trials?.organizations?.name || 'No data'}

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-

PlantsCount

-

{trials?.plants_count || 'No data'}

-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-

TargetTemperature(C)

-

{trials?.target_temperature_c || 'No data'}

-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-

TargetHumidity(%)

-

{trials?.target_humidity_percent || 'No data'}

-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-

TargetEC

-

{trials?.target_ec || 'No data'}

-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-

TargetpH

-

{trials?.target_ph || 'No data'}

-
- - - - - - - - - - - - - - - - - - - - - - - - - - -
-

Notes

- {trials.notes - ?

- :

No data

- } -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-

organizations

- - - - - - - - -

{trials?.organizations?.name ?? 'No data'}

- - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - <> -

Tracking_activities Trial

- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {trials.tracking_activities_trial && Array.isArray(trials.tracking_activities_trial) && - trials.tracking_activities_trial.map((item: any) => ( - router.push(`/tracking_activities/tracking_activities-view/?id=${item.id}`)}> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ))} - -
OccurredAtVisibilitySummaryValue(Decimal)Value(Integer)Value(Yes/No)Value(Text)Severity
- { dataFormatter.dateTimeFormatter(item.occurred_at) } - - { item.visibility } - - { item.summary } - - { item.value_decimal } - - { item.value_int } - - { dataFormatter.booleanFormatter(item.value_boolean) } - - { item.value_text } - - { item.severity } -
-
- {!trials?.tracking_activities_trial?.length &&
No data
} -
- - - - - - - @@ -980,11 +257,7 @@ const TrialsView = () => { TrialsView.getLayout = function getLayout(page: ReactElement) { return ( - + {page} ) diff --git a/frontend/src/stores/breeders/breedersSlice.ts b/frontend/src/stores/breeders/breedersSlice.ts new file mode 100644 index 0000000..1921adc --- /dev/null +++ b/frontend/src/stores/breeders/breedersSlice.ts @@ -0,0 +1,85 @@ +import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit' +import axios from 'axios' +import {fulfilledNotify, rejectNotify, resetNotify} from "../../helpers/notifyStateHandler"; + +interface MainState { + breeders: any + loading: boolean + count: number + refetch: boolean; + notify: { + showNotification: boolean + textNotification: string + typeNotification: string + } +} + +const initialState: MainState = { + breeders: [], + loading: false, + count: 0, + refetch: false, + notify: { + showNotification: false, + textNotification: '', + typeNotification: 'warn', + }, +} + +export const fetch = createAsyncThunk('breeders/fetch', async (data: any) => { + const { id, query } = data + const result = await axios.get(`breeders${query || (id ? `/${id}` : '')}`) + return id ? result.data : {rows: result.data.rows, count: result.data.count}; +}) + +export const deleteItemsByIds = createAsyncThunk('breeders/deleteByIds', async (data: any, { rejectWithValue }) => { + try { await axios.post('breeders/deleteByIds', { data }); } + catch (error) { return rejectWithValue(error.response?.data); } +}); + +export const deleteItem = createAsyncThunk('breeders/deleteBreeders', async (id: string, { rejectWithValue }) => { + try { await axios.delete(`breeders/${id}`) } + catch (error) { return rejectWithValue(error.response?.data); } +}) + +export const create = createAsyncThunk('breeders/createBreeders', async (data: any, { rejectWithValue }) => { + try { + const result = await axios.post('breeders', { data }) + return result.data + } catch (error) { return rejectWithValue(error.response?.data); } +}) + +export const update = createAsyncThunk('breeders/updateBreeders', async (payload: any, { rejectWithValue }) => { + try { + const result = await axios.put(`breeders/${payload.id}`, { id: payload.id, data: payload.data }) + return result.data + } catch (error) { return rejectWithValue(error.response?.data); } +}) + +export const breedersSlice = createSlice({ + name: 'breeders', + 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.breeders = action.payload.rows; + state.count = action.payload.count; + } else { state.breeders = action.payload; } + state.loading = false + }) + builder.addCase(deleteItemsByIds.fulfilled, (state) => { state.loading = false; fulfilledNotify(state, 'Breeders has been deleted'); }); + builder.addCase(deleteItem.fulfilled, (state) => { state.loading = false; fulfilledNotify(state, 'Breeder has been deleted'); }) + builder.addCase(create.fulfilled, (state) => { state.loading = false; fulfilledNotify(state, 'Breeder has been created'); }) + builder.addCase(update.fulfilled, (state) => { state.loading = false; fulfilledNotify(state, 'Breeder has been updated'); }) + builder.addMatcher((action) => action.type.endsWith('/pending'), (state) => { state.loading = true; resetNotify(state); }); + builder.addMatcher((action) => action.type.endsWith('/rejected'), (state, action) => { state.loading = false; rejectNotify(state, action); }); + }, +}) + +export const { setRefetch } = breedersSlice.actions +export default breedersSlice.reducer diff --git a/frontend/src/stores/containers/containersSlice.ts b/frontend/src/stores/containers/containersSlice.ts new file mode 100644 index 0000000..d614f55 --- /dev/null +++ b/frontend/src/stores/containers/containersSlice.ts @@ -0,0 +1,86 @@ + +import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit' +import axios from 'axios' +import {fulfilledNotify, rejectNotify, resetNotify} from "../../helpers/notifyStateHandler"; + +interface MainState { + containers: any + loading: boolean + count: number + refetch: boolean; + notify: { + showNotification: boolean + textNotification: string + typeNotification: string + } +} + +const initialState: MainState = { + containers: [], + loading: false, + count: 0, + refetch: false, + notify: { + showNotification: false, + textNotification: '', + typeNotification: 'warn', + }, +} + +export const fetch = createAsyncThunk('containers/fetch', async (data: any) => { + const { id, query } = data + const result = await axios.get(`containers${query || (id ? `/${id}` : '')}`) + return id ? result.data : {rows: result.data.rows, count: result.data.count}; +}) + +export const deleteItemsByIds = createAsyncThunk('containers/deleteByIds', async (data: any, { rejectWithValue }) => { + try { await axios.post('containers/deleteByIds', { data }); } + catch (error) { return rejectWithValue(error.response?.data); } +}); + +export const deleteItem = createAsyncThunk('containers/deleteContainers', async (id: string, { rejectWithValue }) => { + try { await axios.delete(`containers/${id}`) } + catch (error) { return rejectWithValue(error.response?.data); } +}) + +export const create = createAsyncThunk('containers/createContainers', async (data: any, { rejectWithValue }) => { + try { + const result = await axios.post('containers', { data }) + return result.data + } catch (error) { return rejectWithValue(error.response?.data); } +}) + +export const update = createAsyncThunk('containers/updateContainers', async (payload: any, { rejectWithValue }) => { + try { + const result = await axios.put(`containers/${payload.id}`, { id: payload.id, data: payload.data }) + return result.data + } catch (error) { return rejectWithValue(error.response?.data); } +}) + +export const containersSlice = createSlice({ + name: 'containers', + 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.containers = action.payload.rows; + state.count = action.payload.count; + } else { state.containers = action.payload; } + state.loading = false + }) + builder.addCase(deleteItemsByIds.fulfilled, (state) => { state.loading = false; fulfilledNotify(state, 'Containers has been deleted'); }); + builder.addCase(deleteItem.fulfilled, (state) => { state.loading = false; fulfilledNotify(state, 'Container has been deleted'); }) + builder.addCase(create.fulfilled, (state) => { state.loading = false; fulfilledNotify(state, 'Container has been created'); }) + builder.addCase(update.fulfilled, (state) => { state.loading = false; fulfilledNotify(state, 'Container has been updated'); }) + builder.addMatcher((action) => action.type.endsWith('/pending'), (state) => { state.loading = true; resetNotify(state); }); + builder.addMatcher((action) => action.type.endsWith('/rejected'), (state, action) => { state.loading = false; rejectNotify(state, action); }); + }, +}) + +export const { setRefetch } = containersSlice.actions +export default containersSlice.reducer diff --git a/frontend/src/stores/crop_types/crop_typesSlice.ts b/frontend/src/stores/crop_types/crop_typesSlice.ts new file mode 100644 index 0000000..4ac8bc9 --- /dev/null +++ b/frontend/src/stores/crop_types/crop_typesSlice.ts @@ -0,0 +1,86 @@ + +import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit' +import axios from 'axios' +import {fulfilledNotify, rejectNotify, resetNotify} from "../../helpers/notifyStateHandler"; + +interface MainState { + crop_types: any + loading: boolean + count: number + refetch: boolean; + notify: { + showNotification: boolean + textNotification: string + typeNotification: string + } +} + +const initialState: MainState = { + crop_types: [], + loading: false, + count: 0, + refetch: false, + notify: { + showNotification: false, + textNotification: '', + typeNotification: 'warn', + }, +} + +export const fetch = createAsyncThunk('crop_types/fetch', async (data: any) => { + const { id, query } = data + const result = await axios.get(`crop_types${query || (id ? `/${id}` : '')}`) + return id ? result.data : {rows: result.data.rows, count: result.data.count}; +}) + +export const deleteItemsByIds = createAsyncThunk('crop_types/deleteByIds', async (data: any, { rejectWithValue }) => { + try { await axios.post('crop_types/deleteByIds', { data }); } + catch (error) { return rejectWithValue(error.response?.data); } +}); + +export const deleteItem = createAsyncThunk('crop_types/deleteCrop_types', async (id: string, { rejectWithValue }) => { + try { await axios.delete(`crop_types/${id}`) } + catch (error) { return rejectWithValue(error.response?.data); } +}) + +export const create = createAsyncThunk('crop_types/createCrop_types', async (data: any, { rejectWithValue }) => { + try { + const result = await axios.post('crop_types', { data }) + return result.data + } catch (error) { return rejectWithValue(error.response?.data); } +}) + +export const update = createAsyncThunk('crop_types/updateCrop_types', async (payload: any, { rejectWithValue }) => { + try { + const result = await axios.put(`crop_types/${payload.id}`, { id: payload.id, data: payload.data }) + return result.data + } catch (error) { return rejectWithValue(error.response?.data); } +}) + +export const crop_typesSlice = createSlice({ + name: 'crop_types', + 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.crop_types = action.payload.rows; + state.count = action.payload.count; + } else { state.crop_types = action.payload; } + state.loading = false + }) + builder.addCase(deleteItemsByIds.fulfilled, (state) => { state.loading = false; fulfilledNotify(state, 'Crop_types has been deleted'); }); + builder.addCase(deleteItem.fulfilled, (state) => { state.loading = false; fulfilledNotify(state, 'Crop_type has been deleted'); }) + builder.addCase(create.fulfilled, (state) => { state.loading = false; fulfilledNotify(state, 'Crop_type has been created'); }) + builder.addCase(update.fulfilled, (state) => { state.loading = false; fulfilledNotify(state, 'Crop_type has been updated'); }) + builder.addMatcher((action) => action.type.endsWith('/pending'), (state) => { state.loading = true; resetNotify(state); }); + builder.addMatcher((action) => action.type.endsWith('/rejected'), (state, action) => { state.loading = false; rejectNotify(state, action); }); + }, +}) + +export const { setRefetch } = crop_typesSlice.actions +export default crop_typesSlice.reducer diff --git a/frontend/src/stores/groups/groupsSlice.ts b/frontend/src/stores/groups/groupsSlice.ts new file mode 100644 index 0000000..98e56f3 --- /dev/null +++ b/frontend/src/stores/groups/groupsSlice.ts @@ -0,0 +1,85 @@ +import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit' +import axios from 'axios' +import {fulfilledNotify, rejectNotify, resetNotify} from "../../helpers/notifyStateHandler"; + +interface MainState { + groups: any + loading: boolean + count: number + refetch: boolean; + notify: { + showNotification: boolean + textNotification: string + typeNotification: string + } +} + +const initialState: MainState = { + groups: [], + loading: false, + count: 0, + refetch: false, + notify: { + showNotification: false, + textNotification: '', + typeNotification: 'warn', + }, +} + +export const fetch = createAsyncThunk('groups/fetch', async (data: any) => { + const { id, query } = data + const result = await axios.get(`groups${query || (id ? `/${id}` : '')}`) + return id ? result.data : {rows: result.data.rows, count: result.data.count}; +}) + +export const deleteItemsByIds = createAsyncThunk('groups/deleteByIds', async (data: any, { rejectWithValue }) => { + try { await axios.post('groups/deleteByIds', { data }); } + catch (error) { return rejectWithValue(error.response?.data); } +}); + +export const deleteItem = createAsyncThunk('groups/deleteGroups', async (id: string, { rejectWithValue }) => { + try { await axios.delete(`groups/${id}`) } + catch (error) { return rejectWithValue(error.response?.data); } +}) + +export const create = createAsyncThunk('groups/createGroups', async (data: any, { rejectWithValue }) => { + try { + const result = await axios.post('groups', { data }) + return result.data + } catch (error) { return rejectWithValue(error.response?.data); } +}) + +export const update = createAsyncThunk('groups/updateGroups', async (payload: any, { rejectWithValue }) => { + try { + const result = await axios.put(`groups/${payload.id}`, { id: payload.id, data: payload.data }) + return result.data + } catch (error) { return rejectWithValue(error.response?.data); } +}) + +export const groupsSlice = createSlice({ + name: 'groups', + 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.groups = action.payload.rows; + state.count = action.payload.count; + } else { state.groups = action.payload; } + state.loading = false + }) + builder.addCase(deleteItemsByIds.fulfilled, (state) => { state.loading = false; fulfilledNotify(state, 'Groups has been deleted'); }); + builder.addCase(deleteItem.fulfilled, (state) => { state.loading = false; fulfilledNotify(state, 'Groups has been deleted'); }) + builder.addCase(create.fulfilled, (state) => { state.loading = false; fulfilledNotify(state, 'Groups has been created'); }) + builder.addCase(update.fulfilled, (state) => { state.loading = false; fulfilledNotify(state, 'Groups has been updated'); }) + builder.addMatcher((action) => action.type.endsWith('/pending'), (state) => { state.loading = true; resetNotify(state); }); + builder.addMatcher((action) => action.type.endsWith('/rejected'), (state, action) => { state.loading = false; rejectNotify(state, action); }); + }, +}) + +export const { setRefetch } = groupsSlice.actions +export default groupsSlice.reducer diff --git a/frontend/src/stores/growth_habits/growth_habitsSlice.ts b/frontend/src/stores/growth_habits/growth_habitsSlice.ts new file mode 100644 index 0000000..5e99d40 --- /dev/null +++ b/frontend/src/stores/growth_habits/growth_habitsSlice.ts @@ -0,0 +1,86 @@ + +import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit' +import axios from 'axios' +import {fulfilledNotify, rejectNotify, resetNotify} from "../../helpers/notifyStateHandler"; + +interface MainState { + growth_habits: any + loading: boolean + count: number + refetch: boolean; + notify: { + showNotification: boolean + textNotification: string + typeNotification: string + } +} + +const initialState: MainState = { + growth_habits: [], + loading: false, + count: 0, + refetch: false, + notify: { + showNotification: false, + textNotification: '', + typeNotification: 'warn', + }, +} + +export const fetch = createAsyncThunk('growth_habits/fetch', async (data: any) => { + const { id, query } = data + const result = await axios.get(`growth_habits${query || (id ? `/${id}` : '')}`) + return id ? result.data : {rows: result.data.rows, count: result.data.count}; +}) + +export const deleteItemsByIds = createAsyncThunk('growth_habits/deleteByIds', async (data: any, { rejectWithValue }) => { + try { await axios.post('growth_habits/deleteByIds', { data }); } + catch (error) { return rejectWithValue(error.response?.data); } +}); + +export const deleteItem = createAsyncThunk('growth_habits/deleteGrowth_habits', async (id: string, { rejectWithValue }) => { + try { await axios.delete(`growth_habits/${id}`) } + catch (error) { return rejectWithValue(error.response?.data); } +}) + +export const create = createAsyncThunk('growth_habits/createGrowth_habits', async (data: any, { rejectWithValue }) => { + try { + const result = await axios.post('growth_habits', { data }) + return result.data + } catch (error) { return rejectWithValue(error.response?.data); } +}) + +export const update = createAsyncThunk('growth_habits/updateGrowth_habits', async (payload: any, { rejectWithValue }) => { + try { + const result = await axios.put(`growth_habits/${payload.id}`, { id: payload.id, data: payload.data }) + return result.data + } catch (error) { return rejectWithValue(error.response?.data); } +}) + +export const growth_habitsSlice = createSlice({ + name: 'growth_habits', + 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.growth_habits = action.payload.rows; + state.count = action.payload.count; + } else { state.growth_habits = action.payload; } + state.loading = false + }) + builder.addCase(deleteItemsByIds.fulfilled, (state) => { state.loading = false; fulfilledNotify(state, 'Growth_habits has been deleted'); }); + builder.addCase(deleteItem.fulfilled, (state) => { state.loading = false; fulfilledNotify(state, 'Growth_habit has been deleted'); }) + builder.addCase(create.fulfilled, (state) => { state.loading = false; fulfilledNotify(state, 'Growth_habit has been created'); }) + builder.addCase(update.fulfilled, (state) => { state.loading = false; fulfilledNotify(state, 'Growth_habit has been updated'); }) + builder.addMatcher((action) => action.type.endsWith('/pending'), (state) => { state.loading = true; resetNotify(state); }); + builder.addMatcher((action) => action.type.endsWith('/rejected'), (state, action) => { state.loading = false; rejectNotify(state, action); }); + }, +}) + +export const { setRefetch } = growth_habitsSlice.actions +export default growth_habitsSlice.reducer diff --git a/frontend/src/stores/locations/locationsSlice.ts b/frontend/src/stores/locations/locationsSlice.ts new file mode 100644 index 0000000..cbeb224 --- /dev/null +++ b/frontend/src/stores/locations/locationsSlice.ts @@ -0,0 +1,85 @@ +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; + notify: { + showNotification: boolean + textNotification: string + typeNotification: string + } +} + +const initialState: MainState = { + locations: [], + loading: false, + count: 0, + refetch: false, + 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) { return rejectWithValue(error.response?.data); } +}); + +export const deleteItem = createAsyncThunk('locations/deleteLocations', async (id: string, { rejectWithValue }) => { + try { await axios.delete(`locations/${id}`) } + catch (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) { 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) { 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.fulfilled, (state) => { state.loading = false; fulfilledNotify(state, 'Locations has been deleted'); }); + builder.addCase(deleteItem.fulfilled, (state) => { state.loading = false; fulfilledNotify(state, 'Locations has been deleted'); }) + builder.addCase(create.fulfilled, (state) => { state.loading = false; fulfilledNotify(state, 'Locations has been created'); }) + builder.addCase(update.fulfilled, (state) => { state.loading = false; fulfilledNotify(state, 'Locations has been updated'); }) + builder.addMatcher((action) => action.type.endsWith('/pending'), (state) => { state.loading = true; resetNotify(state); }); + builder.addMatcher((action) => action.type.endsWith('/rejected'), (state, action) => { state.loading = false; rejectNotify(state, action); }); + }, +}) + +export const { setRefetch } = locationsSlice.actions +export default locationsSlice.reducer diff --git a/frontend/src/stores/programs/programsSlice.ts b/frontend/src/stores/programs/programsSlice.ts new file mode 100644 index 0000000..f54d1bb --- /dev/null +++ b/frontend/src/stores/programs/programsSlice.ts @@ -0,0 +1,85 @@ +import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit' +import axios from 'axios' +import {fulfilledNotify, rejectNotify, resetNotify} from "../../helpers/notifyStateHandler"; + +interface MainState { + programs: any + loading: boolean + count: number + refetch: boolean; + notify: { + showNotification: boolean + textNotification: string + typeNotification: string + } +} + +const initialState: MainState = { + programs: [], + loading: false, + count: 0, + refetch: false, + notify: { + showNotification: false, + textNotification: '', + typeNotification: 'warn', + }, +} + +export const fetch = createAsyncThunk('programs/fetch', async (data: any) => { + const { id, query } = data + const result = await axios.get(`programs${query || (id ? `/${id}` : '')}`) + return id ? result.data : {rows: result.data.rows, count: result.data.count}; +}) + +export const deleteItemsByIds = createAsyncThunk('programs/deleteByIds', async (data: any, { rejectWithValue }) => { + try { await axios.post('programs/deleteByIds', { data }); } + catch (error) { return rejectWithValue(error.response?.data); } +}); + +export const deleteItem = createAsyncThunk('programs/deletePrograms', async (id: string, { rejectWithValue }) => { + try { await axios.delete(`programs/${id}`) } + catch (error) { return rejectWithValue(error.response?.data); } +}) + +export const create = createAsyncThunk('programs/createPrograms', async (data: any, { rejectWithValue }) => { + try { + const result = await axios.post('programs', { data }) + return result.data + } catch (error) { return rejectWithValue(error.response?.data); } +}) + +export const update = createAsyncThunk('programs/updatePrograms', async (payload: any, { rejectWithValue }) => { + try { + const result = await axios.put(`programs/${payload.id}`, { id: payload.id, data: payload.data }) + return result.data + } catch (error) { return rejectWithValue(error.response?.data); } +}) + +export const programsSlice = createSlice({ + name: 'programs', + 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.programs = action.payload.rows; + state.count = action.payload.count; + } else { state.programs = action.payload; } + state.loading = false + }) + builder.addCase(deleteItemsByIds.fulfilled, (state) => { state.loading = false; fulfilledNotify(state, 'Programs has been deleted'); }); + builder.addCase(deleteItem.fulfilled, (state) => { state.loading = false; fulfilledNotify(state, 'Programs has been deleted'); }) + builder.addCase(create.fulfilled, (state) => { state.loading = false; fulfilledNotify(state, 'Programs has been created'); }) + builder.addCase(update.fulfilled, (state) => { state.loading = false; fulfilledNotify(state, 'Programs has been updated'); }) + builder.addMatcher((action) => action.type.endsWith('/pending'), (state) => { state.loading = true; resetNotify(state); }); + builder.addMatcher((action) => action.type.endsWith('/rejected'), (state, action) => { state.loading = false; rejectNotify(state, action); }); + }, +}) + +export const { setRefetch } = programsSlice.actions +export default programsSlice.reducer diff --git a/frontend/src/stores/propagation_types/propagation_typesSlice.ts b/frontend/src/stores/propagation_types/propagation_typesSlice.ts new file mode 100644 index 0000000..38b5927 --- /dev/null +++ b/frontend/src/stores/propagation_types/propagation_typesSlice.ts @@ -0,0 +1,86 @@ + +import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit' +import axios from 'axios' +import {fulfilledNotify, rejectNotify, resetNotify} from "../../helpers/notifyStateHandler"; + +interface MainState { + propagation_types: any + loading: boolean + count: number + refetch: boolean; + notify: { + showNotification: boolean + textNotification: string + typeNotification: string + } +} + +const initialState: MainState = { + propagation_types: [], + loading: false, + count: 0, + refetch: false, + notify: { + showNotification: false, + textNotification: '', + typeNotification: 'warn', + }, +} + +export const fetch = createAsyncThunk('propagation_types/fetch', async (data: any) => { + const { id, query } = data + const result = await axios.get(`propagation_types${query || (id ? `/${id}` : '')}`) + return id ? result.data : {rows: result.data.rows, count: result.data.count}; +}) + +export const deleteItemsByIds = createAsyncThunk('propagation_types/deleteByIds', async (data: any, { rejectWithValue }) => { + try { await axios.post('propagation_types/deleteByIds', { data }); } + catch (error) { return rejectWithValue(error.response?.data); } +}); + +export const deleteItem = createAsyncThunk('propagation_types/deletePropagation_types', async (id: string, { rejectWithValue }) => { + try { await axios.delete(`propagation_types/${id}`) } + catch (error) { return rejectWithValue(error.response?.data); } +}) + +export const create = createAsyncThunk('propagation_types/createPropagation_types', async (data: any, { rejectWithValue }) => { + try { + const result = await axios.post('propagation_types', { data }) + return result.data + } catch (error) { return rejectWithValue(error.response?.data); } +}) + +export const update = createAsyncThunk('propagation_types/updatePropagation_types', async (payload: any, { rejectWithValue }) => { + try { + const result = await axios.put(`propagation_types/${payload.id}`, { id: payload.id, data: payload.data }) + return result.data + } catch (error) { return rejectWithValue(error.response?.data); } +}) + +export const propagation_typesSlice = createSlice({ + name: 'propagation_types', + 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.propagation_types = action.payload.rows; + state.count = action.payload.count; + } else { state.propagation_types = action.payload; } + state.loading = false + }) + builder.addCase(deleteItemsByIds.fulfilled, (state) => { state.loading = false; fulfilledNotify(state, 'Propagation_types has been deleted'); }); + builder.addCase(deleteItem.fulfilled, (state) => { state.loading = false; fulfilledNotify(state, 'Propagation_type has been deleted'); }) + builder.addCase(create.fulfilled, (state) => { state.loading = false; fulfilledNotify(state, 'Propagation_type has been created'); }) + builder.addCase(update.fulfilled, (state) => { state.loading = false; fulfilledNotify(state, 'Propagation_type has been updated'); }) + builder.addMatcher((action) => action.type.endsWith('/pending'), (state) => { state.loading = true; resetNotify(state); }); + builder.addMatcher((action) => action.type.endsWith('/rejected'), (state, action) => { state.loading = false; rejectNotify(state, action); }); + }, +}) + +export const { setRefetch } = propagation_typesSlice.actions +export default propagation_typesSlice.reducer diff --git a/frontend/src/stores/retailer_programs/retailer_programsSlice.ts b/frontend/src/stores/retailer_programs/retailer_programsSlice.ts new file mode 100644 index 0000000..0e496e6 --- /dev/null +++ b/frontend/src/stores/retailer_programs/retailer_programsSlice.ts @@ -0,0 +1,86 @@ + +import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit' +import axios from 'axios' +import {fulfilledNotify, rejectNotify, resetNotify} from "../../helpers/notifyStateHandler"; + +interface MainState { + retailer_programs: any + loading: boolean + count: number + refetch: boolean; + notify: { + showNotification: boolean + textNotification: string + typeNotification: string + } +} + +const initialState: MainState = { + retailer_programs: [], + loading: false, + count: 0, + refetch: false, + notify: { + showNotification: false, + textNotification: '', + typeNotification: 'warn', + }, +} + +export const fetch = createAsyncThunk('retailer_programs/fetch', async (data: any) => { + const { id, query } = data + const result = await axios.get(`retailer_programs${query || (id ? `/${id}` : '')}`) + return id ? result.data : {rows: result.data.rows, count: result.data.count}; +}) + +export const deleteItemsByIds = createAsyncThunk('retailer_programs/deleteByIds', async (data: any, { rejectWithValue }) => { + try { await axios.post('retailer_programs/deleteByIds', { data }); } + catch (error) { return rejectWithValue(error.response?.data); } +}); + +export const deleteItem = createAsyncThunk('retailer_programs/deleteRetailer_programs', async (id: string, { rejectWithValue }) => { + try { await axios.delete(`retailer_programs/${id}`) } + catch (error) { return rejectWithValue(error.response?.data); } +}) + +export const create = createAsyncThunk('retailer_programs/createRetailer_programs', async (data: any, { rejectWithValue }) => { + try { + const result = await axios.post('retailer_programs', { data }) + return result.data + } catch (error) { return rejectWithValue(error.response?.data); } +}) + +export const update = createAsyncThunk('retailer_programs/updateRetailer_programs', async (payload: any, { rejectWithValue }) => { + try { + const result = await axios.put(`retailer_programs/${payload.id}`, { id: payload.id, data: payload.data }) + return result.data + } catch (error) { return rejectWithValue(error.response?.data); } +}) + +export const retailer_programsSlice = createSlice({ + name: 'retailer_programs', + 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.retailer_programs = action.payload.rows; + state.count = action.payload.count; + } else { state.retailer_programs = action.payload; } + state.loading = false + }) + builder.addCase(deleteItemsByIds.fulfilled, (state) => { state.loading = false; fulfilledNotify(state, 'Retailer_programs has been deleted'); }); + builder.addCase(deleteItem.fulfilled, (state) => { state.loading = false; fulfilledNotify(state, 'Retailer_program has been deleted'); }) + builder.addCase(create.fulfilled, (state) => { state.loading = false; fulfilledNotify(state, 'Retailer_program has been created'); }) + builder.addCase(update.fulfilled, (state) => { state.loading = false; fulfilledNotify(state, 'Retailer_program has been updated'); }) + builder.addMatcher((action) => action.type.endsWith('/pending'), (state) => { state.loading = true; resetNotify(state); }); + builder.addMatcher((action) => action.type.endsWith('/rejected'), (state, action) => { state.loading = false; rejectNotify(state, action); }); + }, +}) + +export const { setRefetch } = retailer_programsSlice.actions +export default retailer_programsSlice.reducer diff --git a/frontend/src/stores/retailers/retailersSlice.ts b/frontend/src/stores/retailers/retailersSlice.ts new file mode 100644 index 0000000..855c19f --- /dev/null +++ b/frontend/src/stores/retailers/retailersSlice.ts @@ -0,0 +1,85 @@ +import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit' +import axios from 'axios' +import {fulfilledNotify, rejectNotify, resetNotify} from "../../helpers/notifyStateHandler"; + +interface MainState { + retailers: any + loading: boolean + count: number + refetch: boolean; + notify: { + showNotification: boolean + textNotification: string + typeNotification: string + } +} + +const initialState: MainState = { + retailers: [], + loading: false, + count: 0, + refetch: false, + notify: { + showNotification: false, + textNotification: '', + typeNotification: 'warn', + }, +} + +export const fetch = createAsyncThunk('retailers/fetch', async (data: any) => { + const { id, query } = data + const result = await axios.get(`retailers${query || (id ? `/${id}` : '')}`) + return id ? result.data : {rows: result.data.rows, count: result.data.count}; +}) + +export const deleteItemsByIds = createAsyncThunk('retailers/deleteByIds', async (data: any, { rejectWithValue }) => { + try { await axios.post('retailers/deleteByIds', { data }); } + catch (error) { return rejectWithValue(error.response?.data); } +}); + +export const deleteItem = createAsyncThunk('retailers/deleteRetailers', async (id: string, { rejectWithValue }) => { + try { await axios.delete(`retailers/${id}`) } + catch (error) { return rejectWithValue(error.response?.data); } +}) + +export const create = createAsyncThunk('retailers/createRetailers', async (data: any, { rejectWithValue }) => { + try { + const result = await axios.post('retailers', { data }) + return result.data + } catch (error) { return rejectWithValue(error.response?.data); } +}) + +export const update = createAsyncThunk('retailers/updateRetailers', async (payload: any, { rejectWithValue }) => { + try { + const result = await axios.put(`retailers/${payload.id}`, { id: payload.id, data: payload.data }) + return result.data + } catch (error) { return rejectWithValue(error.response?.data); } +}) + +export const retailersSlice = createSlice({ + name: 'retailers', + 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.retailers = action.payload.rows; + state.count = action.payload.count; + } else { state.retailers = action.payload; } + state.loading = false + }) + builder.addCase(deleteItemsByIds.fulfilled, (state) => { state.loading = false; fulfilledNotify(state, 'Retailers has been deleted'); }); + builder.addCase(deleteItem.fulfilled, (state) => { state.loading = false; fulfilledNotify(state, 'Retailers has been deleted'); }) + builder.addCase(create.fulfilled, (state) => { state.loading = false; fulfilledNotify(state, 'Retailers has been created'); }) + builder.addCase(update.fulfilled, (state) => { state.loading = false; fulfilledNotify(state, 'Retailers has been updated'); }) + builder.addMatcher((action) => action.type.endsWith('/pending'), (state) => { state.loading = true; resetNotify(state); }); + builder.addMatcher((action) => action.type.endsWith('/rejected'), (state, action) => { state.loading = false; rejectNotify(state, action); }); + }, +}) + +export const { setRefetch } = retailersSlice.actions +export default retailersSlice.reducer diff --git a/frontend/src/stores/sites/sitesSlice.ts b/frontend/src/stores/sites/sitesSlice.ts new file mode 100644 index 0000000..2ef8e78 --- /dev/null +++ b/frontend/src/stores/sites/sitesSlice.ts @@ -0,0 +1,85 @@ +import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit' +import axios from 'axios' +import {fulfilledNotify, rejectNotify, resetNotify} from "../../helpers/notifyStateHandler"; + +interface MainState { + sites: any + loading: boolean + count: number + refetch: boolean; + notify: { + showNotification: boolean + textNotification: string + typeNotification: string + } +} + +const initialState: MainState = { + sites: [], + loading: false, + count: 0, + refetch: false, + notify: { + showNotification: false, + textNotification: '', + typeNotification: 'warn', + }, +} + +export const fetch = createAsyncThunk('sites/fetch', async (data: any) => { + const { id, query } = data + const result = await axios.get(`sites${query || (id ? `/${id}` : '')}`) + return id ? result.data : {rows: result.data.rows, count: result.data.count}; +}) + +export const deleteItemsByIds = createAsyncThunk('sites/deleteByIds', async (data: any, { rejectWithValue }) => { + try { await axios.post('sites/deleteByIds', { data }); } + catch (error) { return rejectWithValue(error.response?.data); } +}); + +export const deleteItem = createAsyncThunk('sites/deleteSites', async (id: string, { rejectWithValue }) => { + try { await axios.delete(`sites/${id}`) } + catch (error) { return rejectWithValue(error.response?.data); } +}) + +export const create = createAsyncThunk('sites/createSites', async (data: any, { rejectWithValue }) => { + try { + const result = await axios.post('sites', { data }) + return result.data + } catch (error) { return rejectWithValue(error.response?.data); } +}) + +export const update = createAsyncThunk('sites/updateSites', async (payload: any, { rejectWithValue }) => { + try { + const result = await axios.put(`sites/${payload.id}`, { id: payload.id, data: payload.data }) + return result.data + } catch (error) { return rejectWithValue(error.response?.data); } +}) + +export const sitesSlice = createSlice({ + name: 'sites', + 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.sites = action.payload.rows; + state.count = action.payload.count; + } else { state.sites = action.payload; } + state.loading = false + }) + builder.addCase(deleteItemsByIds.fulfilled, (state) => { state.loading = false; fulfilledNotify(state, 'Sites has been deleted'); }); + builder.addCase(deleteItem.fulfilled, (state) => { state.loading = false; fulfilledNotify(state, 'Sites has been deleted'); }) + builder.addCase(create.fulfilled, (state) => { state.loading = false; fulfilledNotify(state, 'Sites has been created'); }) + builder.addCase(update.fulfilled, (state) => { state.loading = false; fulfilledNotify(state, 'Sites has been updated'); }) + builder.addMatcher((action) => action.type.endsWith('/pending'), (state) => { state.loading = true; resetNotify(state); }); + builder.addMatcher((action) => action.type.endsWith('/rejected'), (state, action) => { state.loading = false; rejectNotify(state, action); }); + }, +}) + +export const { setRefetch } = sitesSlice.actions +export default sitesSlice.reducer diff --git a/frontend/src/stores/store.ts b/frontend/src/stores/store.ts index 5b07add..5332d0f 100644 --- a/frontend/src/stores/store.ts +++ b/frontend/src/stores/store.ts @@ -19,6 +19,18 @@ import report_definitionsSlice from "./report_definitions/report_definitionsSlic import report_runsSlice from "./report_runs/report_runsSlice"; import tenant_settingsSlice from "./tenant_settings/tenant_settingsSlice"; +import propagation_typesSlice from "./propagation_types/propagation_typesSlice"; +import growth_habitsSlice from "./growth_habits/growth_habitsSlice"; +import crop_typesSlice from "./crop_types/crop_typesSlice"; +import containersSlice from "./containers/containersSlice"; +import retailer_programsSlice from "./retailer_programs/retailer_programsSlice"; +import breedersSlice from "./breeders/breedersSlice"; +import retailersSlice from "./retailers/retailersSlice"; +import programsSlice from "./programs/programsSlice"; +import groupsSlice from "./groups/groupsSlice"; +import sitesSlice from "./sites/sitesSlice"; +import locationsSlice from "./locations/locationsSlice"; + export const store = configureStore({ reducer: { style: styleReducer, @@ -40,10 +52,22 @@ dashboards: dashboardsSlice, report_definitions: report_definitionsSlice, report_runs: report_runsSlice, tenant_settings: tenant_settingsSlice, + +propagation_types: propagation_typesSlice, +growth_habits: growth_habitsSlice, +crop_types: crop_typesSlice, +containers: containersSlice, +retailer_programs: retailer_programsSlice, +breeders: breedersSlice, +retailers: retailersSlice, +programs: programsSlice, +groups: groupsSlice, +sites: sitesSlice, +locations: locationsSlice, }, }) // Infer the `RootState` and `AppDispatch` types from the store itself export type RootState = ReturnType // Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState} -export type AppDispatch = typeof store.dispatch +export type AppDispatch = typeof store.getState