diff --git a/app-shell/src/_schema.json b/app-shell/src/_schema.json index 7d5f0ac..8a90a5c 100644 --- a/app-shell/src/_schema.json +++ b/app-shell/src/_schema.json @@ -1,4 +1,5 @@ { "Initial version": "{\"iv\":\"7neQDSScE/2vFA5B\",\"encryptedData\":\"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\"}", - "tuctucV1": "{\"iv\":\"wraJjvuRqDd/DCaI\",\"encryptedData\":\"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\"}" + "tuctucV1": "{\"iv\":\"wraJjvuRqDd/DCaI\",\"encryptedData\":\"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\"}", + "testscout": "{\"iv\":\"+X+LbEOSTseBZOQn\",\"encryptedData\":\"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\"}" } \ No newline at end of file diff --git a/backend/src/db/api/scouting.js b/backend/src/db/api/scouting.js new file mode 100644 index 0000000..2629c23 --- /dev/null +++ b/backend/src/db/api/scouting.js @@ -0,0 +1,435 @@ +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 ScoutingDBApi { + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const scouting = await db.scouting.create( + { + id: data.id || undefined, + + team_number: data.team_number || null, + coral_l1: data.coral_l1 || null, + coral_l2: data.coral_l2 || null, + coral_l3: data.coral_l3 || null, + coral_l4: data.coral_l4 || null, + algae_processor: data.algae_processor || null, + algae_net: data.algae_net || null, + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + return scouting; + } + + static async bulkImport(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + // Prepare data - wrapping individual data transformations in a map() method + const scoutingData = data.map((item, index) => ({ + id: item.id || undefined, + + team_number: item.team_number || null, + coral_l1: item.coral_l1 || null, + coral_l2: item.coral_l2 || null, + coral_l3: item.coral_l3 || null, + coral_l4: item.coral_l4 || null, + algae_processor: item.algae_processor || null, + algae_net: item.algae_net || null, + importHash: item.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + // Bulk create items + const scouting = await db.scouting.bulkCreate(scoutingData, { + transaction, + }); + + // For each item created, replace relation files + + return scouting; + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const scouting = await db.scouting.findByPk(id, {}, { transaction }); + + const updatePayload = {}; + + if (data.team_number !== undefined) + updatePayload.team_number = data.team_number; + + if (data.coral_l1 !== undefined) updatePayload.coral_l1 = data.coral_l1; + + if (data.coral_l2 !== undefined) updatePayload.coral_l2 = data.coral_l2; + + if (data.coral_l3 !== undefined) updatePayload.coral_l3 = data.coral_l3; + + if (data.coral_l4 !== undefined) updatePayload.coral_l4 = data.coral_l4; + + if (data.algae_processor !== undefined) + updatePayload.algae_processor = data.algae_processor; + + if (data.algae_net !== undefined) updatePayload.algae_net = data.algae_net; + + updatePayload.updatedById = currentUser.id; + + await scouting.update(updatePayload, { transaction }); + + return scouting; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const scouting = await db.scouting.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of scouting) { + await record.update({ deletedBy: currentUser.id }, { transaction }); + } + for (const record of scouting) { + await record.destroy({ transaction }); + } + }); + + return scouting; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const scouting = await db.scouting.findByPk(id, options); + + await scouting.update( + { + deletedBy: currentUser.id, + }, + { + transaction, + }, + ); + + await scouting.destroy({ + transaction, + }); + + return scouting; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const scouting = await db.scouting.findOne({ where }, { transaction }); + + if (!scouting) { + return scouting; + } + + const output = scouting.get({ plain: true }); + + return output; + } + + static async findAll(filter, options) { + const limit = filter.limit || 0; + let offset = 0; + let where = {}; + const currentPage = +filter.page; + + offset = currentPage * limit; + + const orderBy = null; + + const transaction = (options && options.transaction) || undefined; + + let include = []; + + if (filter) { + if (filter.id) { + where = { + ...where, + ['id']: Utils.uuid(filter.id), + }; + } + + if (filter.team_numberRange) { + const [start, end] = filter.team_numberRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + team_number: { + ...where.team_number, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + team_number: { + ...where.team_number, + [Op.lte]: end, + }, + }; + } + } + + if (filter.coral_l1Range) { + const [start, end] = filter.coral_l1Range; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + coral_l1: { + ...where.coral_l1, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + coral_l1: { + ...where.coral_l1, + [Op.lte]: end, + }, + }; + } + } + + if (filter.coral_l2Range) { + const [start, end] = filter.coral_l2Range; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + coral_l2: { + ...where.coral_l2, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + coral_l2: { + ...where.coral_l2, + [Op.lte]: end, + }, + }; + } + } + + if (filter.coral_l3Range) { + const [start, end] = filter.coral_l3Range; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + coral_l3: { + ...where.coral_l3, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + coral_l3: { + ...where.coral_l3, + [Op.lte]: end, + }, + }; + } + } + + if (filter.coral_l4Range) { + const [start, end] = filter.coral_l4Range; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + coral_l4: { + ...where.coral_l4, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + coral_l4: { + ...where.coral_l4, + [Op.lte]: end, + }, + }; + } + } + + if (filter.algae_processorRange) { + const [start, end] = filter.algae_processorRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + algae_processor: { + ...where.algae_processor, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + algae_processor: { + ...where.algae_processor, + [Op.lte]: end, + }, + }; + } + } + + if (filter.algae_netRange) { + const [start, end] = filter.algae_netRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + algae_net: { + ...where.algae_net, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + algae_net: { + ...where.algae_net, + [Op.lte]: end, + }, + }; + } + } + + if (filter.active !== undefined) { + where = { + ...where, + active: filter.active === true || filter.active === 'true', + }; + } + + if (filter.createdAtRange) { + const [start, end] = filter.createdAtRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.lte]: end, + }, + }; + } + } + } + + const queryOptions = { + where, + include, + distinct: true, + order: + filter.field && filter.sort + ? [[filter.field, filter.sort]] + : [['createdAt', 'desc']], + transaction: options?.transaction, + logging: console.log, + }; + + if (!options?.countOnly) { + queryOptions.limit = limit ? Number(limit) : undefined; + queryOptions.offset = offset ? Number(offset) : undefined; + } + + try { + const { rows, count } = await db.scouting.findAndCountAll(queryOptions); + + return { + rows: options?.countOnly ? [] : rows, + count: count, + }; + } catch (error) { + console.error('Error executing query:', error); + throw error; + } + } + + static async findAllAutocomplete(query, limit, offset) { + let where = {}; + + if (query) { + where = { + [Op.or]: [ + { ['id']: Utils.uuid(query) }, + Utils.ilike('scouting', 'team_number', query), + ], + }; + } + + const records = await db.scouting.findAll({ + attributes: ['id', 'team_number'], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + orderBy: [['team_number', 'ASC']], + }); + + return records.map((record) => ({ + id: record.id, + label: record.team_number, + })); + } +}; diff --git a/backend/src/db/migrations/1748036037805.js b/backend/src/db/migrations/1748036037805.js new file mode 100644 index 0000000..a3d0ec4 --- /dev/null +++ b/backend/src/db/migrations/1748036037805.js @@ -0,0 +1,72 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.createTable( + 'scouting', + { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.DataTypes.UUIDV4, + primaryKey: true, + }, + createdById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + updatedById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + createdAt: { type: Sequelize.DataTypes.DATE }, + updatedAt: { type: Sequelize.DataTypes.DATE }, + deletedAt: { type: Sequelize.DataTypes.DATE }, + importHash: { + type: Sequelize.DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { transaction }, + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async down(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.dropTable('scouting', { transaction }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1748036097691.js b/backend/src/db/migrations/1748036097691.js new file mode 100644 index 0000000..a024fa7 --- /dev/null +++ b/backend/src/db/migrations/1748036097691.js @@ -0,0 +1,49 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.addColumn( + 'scouting', + 'coral_l1', + { + type: Sequelize.DataTypes.INTEGER, + }, + { transaction }, + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async down(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.removeColumn('scouting', 'coral_l1', { + transaction, + }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1748036137612.js b/backend/src/db/migrations/1748036137612.js new file mode 100644 index 0000000..95a39cf --- /dev/null +++ b/backend/src/db/migrations/1748036137612.js @@ -0,0 +1,49 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.addColumn( + 'scouting', + 'coral_l2', + { + type: Sequelize.DataTypes.INTEGER, + }, + { transaction }, + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async down(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.removeColumn('scouting', 'coral_l2', { + transaction, + }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1748036164210.js b/backend/src/db/migrations/1748036164210.js new file mode 100644 index 0000000..0ff779d --- /dev/null +++ b/backend/src/db/migrations/1748036164210.js @@ -0,0 +1,49 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.addColumn( + 'scouting', + 'coral_l3', + { + type: Sequelize.DataTypes.INTEGER, + }, + { transaction }, + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async down(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.removeColumn('scouting', 'coral_l3', { + transaction, + }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1748036189280.js b/backend/src/db/migrations/1748036189280.js new file mode 100644 index 0000000..387ac9f --- /dev/null +++ b/backend/src/db/migrations/1748036189280.js @@ -0,0 +1,49 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.addColumn( + 'scouting', + 'coral_l4', + { + type: Sequelize.DataTypes.INTEGER, + }, + { transaction }, + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async down(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.removeColumn('scouting', 'coral_l4', { + transaction, + }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1748036213890.js b/backend/src/db/migrations/1748036213890.js new file mode 100644 index 0000000..d934826 --- /dev/null +++ b/backend/src/db/migrations/1748036213890.js @@ -0,0 +1,49 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.addColumn( + 'scouting', + 'algae_processor', + { + type: Sequelize.DataTypes.INTEGER, + }, + { transaction }, + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async down(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.removeColumn('scouting', 'algae_processor', { + transaction, + }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1748036239756.js b/backend/src/db/migrations/1748036239756.js new file mode 100644 index 0000000..6de0ee5 --- /dev/null +++ b/backend/src/db/migrations/1748036239756.js @@ -0,0 +1,49 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.addColumn( + 'scouting', + 'algae_net', + { + type: Sequelize.DataTypes.INTEGER, + }, + { transaction }, + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async down(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.removeColumn('scouting', 'algae_net', { + transaction, + }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/models/scouting.js b/backend/src/db/models/scouting.js new file mode 100644 index 0000000..dba5068 --- /dev/null +++ b/backend/src/db/models/scouting.js @@ -0,0 +1,73 @@ +const config = require('../../config'); +const providers = config.providers; +const crypto = require('crypto'); +const bcrypt = require('bcrypt'); +const moment = require('moment'); + +module.exports = function (sequelize, DataTypes) { + const scouting = sequelize.define( + 'scouting', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + + team_number: { + type: DataTypes.INTEGER, + }, + + coral_l1: { + type: DataTypes.INTEGER, + }, + + coral_l2: { + type: DataTypes.INTEGER, + }, + + coral_l3: { + type: DataTypes.INTEGER, + }, + + coral_l4: { + type: DataTypes.INTEGER, + }, + + algae_processor: { + type: DataTypes.INTEGER, + }, + + algae_net: { + type: DataTypes.INTEGER, + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + scouting.associate = (db) => { + /// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity + + //end loop + + db.scouting.belongsTo(db.users, { + as: 'createdBy', + }); + + db.scouting.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return scouting; +}; diff --git a/backend/src/db/seeders/20200430130760-user-roles.js b/backend/src/db/seeders/20200430130760-user-roles.js index 4d49cfc..093d8c1 100644 --- a/backend/src/db/seeders/20200430130760-user-roles.js +++ b/backend/src/db/seeders/20200430130760-user-roles.js @@ -106,6 +106,7 @@ module.exports = { 'tasks', 'roles', 'permissions', + 'scouting', , ]; await queryInterface.bulkInsert( @@ -686,6 +687,31 @@ primary key ("roles_permissionsId", "permissionId") permissionId: getId('DELETE_PERMISSIONS'), }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('CREATE_SCOUTING'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('READ_SCOUTING'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('UPDATE_SCOUTING'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('DELETE_SCOUTING'), + }, + { createdAt, updatedAt, diff --git a/backend/src/db/seeders/20231127130745-sample-data.js b/backend/src/db/seeders/20231127130745-sample-data.js index bc15e08..e48afc4 100644 --- a/backend/src/db/seeders/20231127130745-sample-data.js +++ b/backend/src/db/seeders/20231127130745-sample-data.js @@ -9,6 +9,8 @@ const ScoutingData = db.scouting_data; const Tasks = db.tasks; +const Scouting = db.scouting; + const EventsData = [ { name: 'Regional Qualifiers', @@ -39,6 +41,16 @@ const EventsData = [ // type code here for "relation_many" field }, + + { + name: 'International Expo', + + start_date: new Date('2024-03-25T11:00:00Z'), + + end_date: new Date('2024-03-27T20:00:00Z'), + + // type code here for "relation_many" field + }, ]; const RobotsData = [ @@ -65,6 +77,14 @@ const RobotsData = [ // type code here for "relation_many" field }, + + { + name: 'RoboEagle', + + description: 'A versatile robot capable of performing complex tasks.', + + // type code here for "relation_many" field + }, ]; const ScoutingDataData = [ @@ -97,6 +117,16 @@ const ScoutingDataData = [ // type code here for "relation_one" field }, + + { + match_number: '004', + + team_name: 'CircuitBreakers', + + score: 88, + + // type code here for "relation_one" field + }, ]; const TasksData = [ @@ -109,7 +139,7 @@ const TasksData = [ // type code here for "relation_one" field - status: 'in_progress', + status: 'completed', }, { @@ -121,7 +151,7 @@ const TasksData = [ // type code here for "relation_one" field - status: 'pending', + status: 'completed', }, { @@ -133,7 +163,85 @@ const TasksData = [ // type code here for "relation_one" field - status: 'pending', + status: 'in_progress', + }, + + { + title: 'Assemble Chassis', + + description: 'Assemble the main chassis of the robot.', + + due_date: new Date('2023-11-30T14:00:00Z'), + + // type code here for "relation_one" field + + status: 'completed', + }, +]; + +const ScoutingData = [ + { + team_number: 1, + + coral_l1: 8, + + coral_l2: 9, + + coral_l3: 3, + + coral_l4: 8, + + algae_processor: 1, + + algae_net: 3, + }, + + { + team_number: 2, + + coral_l1: 1, + + coral_l2: 3, + + coral_l3: 5, + + coral_l4: 2, + + algae_processor: 9, + + algae_net: 8, + }, + + { + team_number: 7, + + coral_l1: 9, + + coral_l2: 4, + + coral_l3: 2, + + coral_l4: 3, + + algae_processor: 5, + + algae_net: 6, + }, + + { + team_number: 6, + + coral_l1: 4, + + coral_l2: 2, + + coral_l3: 1, + + coral_l4: 6, + + algae_processor: 2, + + algae_net: 3, }, ]; @@ -176,6 +284,17 @@ async function associateScoutingDatumWithSubmitted_by() { if (ScoutingDatum2?.setSubmitted_by) { await ScoutingDatum2.setSubmitted_by(relatedSubmitted_by2); } + + const relatedSubmitted_by3 = await Users.findOne({ + offset: Math.floor(Math.random() * (await Users.count())), + }); + const ScoutingDatum3 = await ScoutingData.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (ScoutingDatum3?.setSubmitted_by) { + await ScoutingDatum3.setSubmitted_by(relatedSubmitted_by3); + } } async function associateTaskWithAssigned_to() { @@ -211,6 +330,17 @@ async function associateTaskWithAssigned_to() { if (Task2?.setAssigned_to) { await Task2.setAssigned_to(relatedAssigned_to2); } + + const relatedAssigned_to3 = await Users.findOne({ + offset: Math.floor(Math.random() * (await Users.count())), + }); + const Task3 = await Tasks.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (Task3?.setAssigned_to) { + await Task3.setAssigned_to(relatedAssigned_to3); + } } module.exports = { @@ -223,6 +353,8 @@ module.exports = { await Tasks.bulkCreate(TasksData); + await Scouting.bulkCreate(ScoutingData); + await Promise.all([ // Similar logic for "relation_many" @@ -244,5 +376,7 @@ module.exports = { await queryInterface.bulkDelete('scouting_data', null, {}); await queryInterface.bulkDelete('tasks', null, {}); + + await queryInterface.bulkDelete('scouting', null, {}); }, }; diff --git a/backend/src/db/seeders/20250523213357.js b/backend/src/db/seeders/20250523213357.js new file mode 100644 index 0000000..afd9922 --- /dev/null +++ b/backend/src/db/seeders/20250523213357.js @@ -0,0 +1,87 @@ +const { v4: uuid } = require('uuid'); +const db = require('../models'); +const Sequelize = require('sequelize'); +const config = require('../../config'); + +module.exports = { + /** + * @param{import("sequelize").QueryInterface} queryInterface + * @return {Promise} + */ + async up(queryInterface) { + const createdAt = new Date(); + const updatedAt = new Date(); + + /** @type {Map} */ + const idMap = new Map(); + + /** + * @param {string} key + * @return {string} + */ + function getId(key) { + if (idMap.has(key)) { + return idMap.get(key); + } + const id = uuid(); + idMap.set(key, id); + return id; + } + + /** + * @param {string} name + */ + function createPermissions(name) { + return [ + { + id: getId(`CREATE_${name.toUpperCase()}`), + createdAt, + updatedAt, + name: `CREATE_${name.toUpperCase()}`, + }, + { + id: getId(`READ_${name.toUpperCase()}`), + createdAt, + updatedAt, + name: `READ_${name.toUpperCase()}`, + }, + { + id: getId(`UPDATE_${name.toUpperCase()}`), + createdAt, + updatedAt, + name: `UPDATE_${name.toUpperCase()}`, + }, + { + id: getId(`DELETE_${name.toUpperCase()}`), + createdAt, + updatedAt, + name: `DELETE_${name.toUpperCase()}`, + }, + ]; + } + + const entities = ['scouting']; + + const createdPermissions = entities.flatMap(createPermissions); + + // Add permissions to database + await queryInterface.bulkInsert('permissions', createdPermissions); + // Get permissions ids + const permissionsIds = createdPermissions.map((p) => p.id); + // Get admin role + const adminRole = await db.roles.findOne({ + where: { name: config.roles.admin }, + }); + + if (adminRole) { + // Add permissions to admin role if it exists + await adminRole.addPermissions(permissionsIds); + } + }, + down: async (queryInterface, Sequelize) => { + await queryInterface.bulkDelete( + 'permissions', + entities.flatMap(createPermissions), + ); + }, +}; diff --git a/backend/src/index.js b/backend/src/index.js index b5c25fd..a15e35b 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -33,6 +33,8 @@ const rolesRoutes = require('./routes/roles'); const permissionsRoutes = require('./routes/permissions'); +const scoutingRoutes = require('./routes/scouting'); + const getBaseUrl = (url) => { if (!url) return ''; return url.endsWith('/api') ? url.slice(0, -4) : url; @@ -140,6 +142,12 @@ app.use( permissionsRoutes, ); +app.use( + '/api/scouting', + passport.authenticate('jwt', { session: false }), + scoutingRoutes, +); + app.use( '/api/openai', passport.authenticate('jwt', { session: false }), diff --git a/backend/src/routes/scouting.js b/backend/src/routes/scouting.js new file mode 100644 index 0000000..9850d3b --- /dev/null +++ b/backend/src/routes/scouting.js @@ -0,0 +1,465 @@ +const express = require('express'); + +const ScoutingService = require('../services/scouting'); +const ScoutingDBApi = require('../db/api/scouting'); +const wrapAsync = require('../helpers').wrapAsync; + +const router = express.Router(); + +const { parse } = require('json2csv'); + +const { checkCrudPermissions } = require('../middlewares/check-permissions'); + +router.use(checkCrudPermissions('scouting')); + +/** + * @swagger + * components: + * schemas: + * Scouting: + * type: object + * properties: + + * team_number: + * type: integer + * format: int64 + * coral_l1: + * type: integer + * format: int64 + * coral_l2: + * type: integer + * format: int64 + * coral_l3: + * type: integer + * format: int64 + * coral_l4: + * type: integer + * format: int64 + * algae_processor: + * type: integer + * format: int64 + * algae_net: + * type: integer + * format: int64 + + */ + +/** + * @swagger + * tags: + * name: Scouting + * description: The Scouting managing API + */ + +/** + * @swagger + * /api/scouting: + * post: + * security: + * - bearerAuth: [] + * tags: [Scouting] + * summary: Add new item + * description: Add new item + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Scouting" + * responses: + * 200: + * description: The item was successfully added + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Scouting" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + */ +router.post( + '/', + wrapAsync(async (req, res) => { + const referer = + req.headers.referer || + `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await ScoutingService.create( + req.body.data, + req.currentUser, + true, + link.host, + ); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/budgets/bulk-import: + * post: + * security: + * - bearerAuth: [] + * tags: [Scouting] + * summary: Bulk import items + * description: Bulk import items + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated items + * type: array + * items: + * $ref: "#/components/schemas/Scouting" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Scouting" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + * + */ +router.post( + '/bulk-import', + wrapAsync(async (req, res) => { + const referer = + req.headers.referer || + `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await ScoutingService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/scouting/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Scouting] + * summary: Update the data of the selected item + * description: Update the data of the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to update + * required: true + * schema: + * type: string + * requestBody: + * description: Set new item data + * required: true + * content: + * application/json: + * schema: + * properties: + * id: + * description: ID of the updated item + * type: string + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Scouting" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Scouting" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.put( + '/:id', + wrapAsync(async (req, res) => { + await ScoutingService.update(req.body.data, req.body.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/scouting/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Scouting] + * summary: Delete the selected item + * description: Delete the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to delete + * required: true + * schema: + * type: string + * responses: + * 200: + * description: The item was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Scouting" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.delete( + '/:id', + wrapAsync(async (req, res) => { + await ScoutingService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/scouting/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Scouting] + * summary: Delete the selected item list + * description: Delete the selected item list + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * ids: + * description: IDs of the updated items + * type: array + * responses: + * 200: + * description: The items was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Scouting" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post( + '/deleteByIds', + wrapAsync(async (req, res) => { + await ScoutingService.deleteByIds(req.body.data, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/scouting: + * get: + * security: + * - bearerAuth: [] + * tags: [Scouting] + * summary: Get all scouting + * description: Get all scouting + * responses: + * 200: + * description: Scouting list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Scouting" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get( + '/', + wrapAsync(async (req, res) => { + const filetype = req.query.filetype; + + const currentUser = req.currentUser; + const payload = await ScoutingDBApi.findAll(req.query, { currentUser }); + if (filetype && filetype === 'csv') { + const fields = [ + 'id', + 'team_number', + 'coral_l1', + 'coral_l2', + 'coral_l3', + 'coral_l4', + 'algae_processor', + 'algae_net', + ]; + const opts = { fields }; + try { + const csv = parse(payload.rows, opts); + res.status(200).attachment(csv); + res.send(csv); + } catch (err) { + console.error(err); + } + } else { + res.status(200).send(payload); + } + }), +); + +/** + * @swagger + * /api/scouting/count: + * get: + * security: + * - bearerAuth: [] + * tags: [Scouting] + * summary: Count all scouting + * description: Count all scouting + * responses: + * 200: + * description: Scouting count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Scouting" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get( + '/count', + wrapAsync(async (req, res) => { + const currentUser = req.currentUser; + const payload = await ScoutingDBApi.findAll(req.query, null, { + countOnly: true, + currentUser, + }); + + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/scouting/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [Scouting] + * summary: Find all scouting that match search criteria + * description: Find all scouting that match search criteria + * responses: + * 200: + * description: Scouting list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Scouting" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/autocomplete', async (req, res) => { + const payload = await ScoutingDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/scouting/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Scouting] + * summary: Get selected item + * description: Get selected item + * parameters: + * - in: path + * name: id + * description: ID of item to get + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Selected item successfully received + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Scouting" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.get( + '/:id', + wrapAsync(async (req, res) => { + const payload = await ScoutingDBApi.findBy({ id: req.params.id }); + + res.status(200).send(payload); + }), +); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/services/scouting.js b/backend/src/services/scouting.js new file mode 100644 index 0000000..4dcf7f5 --- /dev/null +++ b/backend/src/services/scouting.js @@ -0,0 +1,114 @@ +const db = require('../db/models'); +const ScoutingDBApi = require('../db/api/scouting'); +const processFile = require('../middlewares/upload'); +const ValidationError = require('./notifications/errors/validation'); +const csv = require('csv-parser'); +const axios = require('axios'); +const config = require('../config'); +const stream = require('stream'); + +module.exports = class ScoutingService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await ScoutingDBApi.create(data, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async bulkImport(req, res, sendInvitationEmails = true, host) { + const transaction = await db.sequelize.transaction(); + + try { + await processFile(req, res); + const bufferStream = new stream.PassThrough(); + const results = []; + + await bufferStream.end(Buffer.from(req.file.buffer, 'utf-8')); // convert Buffer to Stream + + await new Promise((resolve, reject) => { + bufferStream + .pipe(csv()) + .on('data', (data) => results.push(data)) + .on('end', async () => { + console.log('CSV results', results); + resolve(); + }) + .on('error', (error) => reject(error)); + }); + + await ScoutingDBApi.bulkImport(results, { + transaction, + ignoreDuplicates: true, + validate: true, + currentUser: req.currentUser, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async update(data, id, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + let scouting = await ScoutingDBApi.findBy({ id }, { transaction }); + + if (!scouting) { + throw new ValidationError('scoutingNotFound'); + } + + const updatedScouting = await ScoutingDBApi.update(id, data, { + currentUser, + transaction, + }); + + await transaction.commit(); + return updatedScouting; + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await ScoutingDBApi.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 ScoutingDBApi.remove(id, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; diff --git a/backend/src/services/search.js b/backend/src/services/search.js index a2e60a0..a062627 100644 --- a/backend/src/services/search.js +++ b/backend/src/services/search.js @@ -53,6 +53,22 @@ module.exports = class SearchService { }; const columnsInt = { scouting_data: ['score'], + + scouting: [ + 'team_number', + + 'coral_l1', + + 'coral_l2', + + 'coral_l3', + + 'coral_l4', + + 'algae_processor', + + 'algae_net', + ], }; let allFoundRecords = []; diff --git a/frontend/src/components/Scouting/CardScouting.tsx b/frontend/src/components/Scouting/CardScouting.tsx new file mode 100644 index 0000000..521f460 --- /dev/null +++ b/frontend/src/components/Scouting/CardScouting.tsx @@ -0,0 +1,175 @@ +import React from 'react'; +import ImageField from '../ImageField'; +import ListActionsPopover from '../ListActionsPopover'; +import { useAppSelector } from '../../stores/hooks'; +import dataFormatter from '../../helpers/dataFormatter'; +import { Pagination } from '../Pagination'; +import { saveFile } from '../../helpers/fileSaver'; +import LoadingSpinner from '../LoadingSpinner'; +import Link from 'next/link'; + +import { hasPermission } from '../../helpers/userPermissions'; + +type Props = { + scouting: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const CardScouting = ({ + scouting, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const asideScrollbarsStyle = useAppSelector( + (state) => state.style.asideScrollbarsStyle, + ); + const bgColor = useAppSelector((state) => state.style.cardsColor); + const darkMode = useAppSelector((state) => state.style.darkMode); + const corners = useAppSelector((state) => state.style.corners); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + + const currentUser = useAppSelector((state) => state.auth.currentUser); + const hasUpdatePermission = hasPermission(currentUser, 'UPDATE_SCOUTING'); + + return ( +
+ {loading && } +
    + {!loading && + scouting.map((item, index) => ( +
  • +
    + + {item.team_number} + + +
    + +
    +
    +
    +
    +
    + Team number +
    +
    +
    + {item.team_number} +
    +
    +
    + +
    +
    + Coral l1 +
    +
    +
    + {item.coral_l1} +
    +
    +
    + +
    +
    + Coral l2 +
    +
    +
    + {item.coral_l2} +
    +
    +
    + +
    +
    + Coral l3 +
    +
    +
    + {item.coral_l3} +
    +
    +
    + +
    +
    + Coral l4 +
    +
    +
    + {item.coral_l4} +
    +
    +
    + +
    +
    + Algae processor +
    +
    +
    + {item.algae_processor} +
    +
    +
    + +
    +
    + Algae net +
    +
    +
    + {item.algae_net} +
    +
    +
    +
    +
  • + ))} + {!loading && scouting.length === 0 && ( +
    +

    No data to display

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

Team number

+

{item.team_number}

+
+ +
+

Coral l1

+

{item.coral_l1}

+
+ +
+

Coral l2

+

{item.coral_l2}

+
+ +
+

Coral l3

+

{item.coral_l3}

+
+ +
+

Coral l4

+

{item.coral_l4}

+
+ +
+

+ Algae processor +

+

{item.algae_processor}

+
+ +
+

Algae net

+

{item.algae_net}

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

No data to display

+
+ )} +
+
+ +
+ + ); +}; + +export default ListScouting; diff --git a/frontend/src/components/Scouting/TableScouting.tsx b/frontend/src/components/Scouting/TableScouting.tsx new file mode 100644 index 0000000..79f99c3 --- /dev/null +++ b/frontend/src/components/Scouting/TableScouting.tsx @@ -0,0 +1,484 @@ +import React, { useEffect, useState, useMemo } from 'react'; +import { createPortal } from 'react-dom'; +import { ToastContainer, toast } from 'react-toastify'; +import BaseButton from '../BaseButton'; +import CardBoxModal from '../CardBoxModal'; +import CardBox from '../CardBox'; +import { + fetch, + update, + deleteItem, + setRefetch, + deleteItemsByIds, +} from '../../stores/scouting/scoutingSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { Field, Form, Formik } from 'formik'; +import { DataGrid, GridColDef } from '@mui/x-data-grid'; +import { loadColumns } from './configureScoutingCols'; +import _ from 'lodash'; +import dataFormatter from '../../helpers/dataFormatter'; +import { dataGridStyles } from '../../styles'; + +const perPage = 10; + +const TableSampleScouting = ({ + filterItems, + setFilterItems, + filters, + showGrid, +}) => { + const notify = (type, msg) => toast(msg, { type, position: 'bottom-center' }); + + const dispatch = useAppDispatch(); + const router = useRouter(); + + const pagesList = []; + const [id, setId] = useState(null); + const [currentPage, setCurrentPage] = useState(0); + const [filterRequest, setFilterRequest] = React.useState(''); + const [columns, setColumns] = useState([]); + const [selectedRows, setSelectedRows] = useState([]); + const [sortModel, setSortModel] = useState([ + { + field: '', + sort: 'desc', + }, + ]); + + const { + scouting, + loading, + count, + notify: scoutingNotify, + refetch, + } = useAppSelector((state) => state.scouting); + const { currentUser } = useAppSelector((state) => state.auth); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + const bgColor = useAppSelector((state) => state.style.bgLayoutColor); + const corners = useAppSelector((state) => state.style.corners); + const numPages = + Math.floor(count / perPage) === 0 ? 1 : Math.ceil(count / perPage); + for (let i = 0; i < numPages; i++) { + pagesList.push(i); + } + + const loadData = async (page = currentPage, request = filterRequest) => { + if (page !== currentPage) setCurrentPage(page); + if (request !== filterRequest) setFilterRequest(request); + const { sort, field } = sortModel[0]; + + const query = `?page=${page}&limit=${perPage}${request}&sort=${sort}&field=${field}`; + dispatch(fetch({ limit: perPage, page, query })); + }; + + useEffect(() => { + if (scoutingNotify.showNotification) { + notify(scoutingNotify.typeNotification, scoutingNotify.textNotification); + } + }, [scoutingNotify.showNotification]); + + useEffect(() => { + if (!currentUser) return; + loadData(); + }, [sortModel, currentUser]); + + useEffect(() => { + if (refetch) { + loadData(0); + dispatch(setRefetch(false)); + } + }, [refetch, dispatch]); + + const [isModalInfoActive, setIsModalInfoActive] = useState(false); + const [isModalTrashActive, setIsModalTrashActive] = useState(false); + + const handleModalAction = () => { + setIsModalInfoActive(false); + setIsModalTrashActive(false); + }; + + const handleDeleteModalAction = (id: string) => { + setId(id); + setIsModalTrashActive(true); + }; + const handleDeleteAction = async () => { + if (id) { + await dispatch(deleteItem(id)); + await loadData(0); + setIsModalTrashActive(false); + } + }; + + const generateFilterRequests = useMemo(() => { + let request = '&'; + filterItems.forEach((item) => { + const isRangeFilter = filters.find( + (filter) => + filter.title === item.fields.selectedField && + (filter.number || filter.date), + ); + + if (isRangeFilter) { + const from = item.fields.filterValueFrom; + const to = item.fields.filterValueTo; + if (from) { + request += `${item.fields.selectedField}Range=${from}&`; + } + if (to) { + request += `${item.fields.selectedField}Range=${to}&`; + } + } else { + const value = item.fields.filterValue; + if (value) { + request += `${item.fields.selectedField}=${value}&`; + } + } + }); + return request; + }, [filterItems, filters]); + + const deleteFilter = (value) => { + const newItems = filterItems.filter((item) => item.id !== value); + + if (newItems.length) { + setFilterItems(newItems); + } else { + loadData(0, ''); + + setFilterItems(newItems); + } + }; + + const handleSubmit = () => { + loadData(0, generateFilterRequests); + }; + + const handleChange = (id) => (e) => { + const value = e.target.value; + const name = e.target.name; + + setFilterItems( + filterItems.map((item) => { + if (item.id !== id) return item; + if (name === 'selectedField') return { id, fields: { [name]: value } }; + + return { id, fields: { ...item.fields, [name]: value } }; + }), + ); + }; + + const handleReset = () => { + setFilterItems([]); + loadData(0, ''); + }; + + const onPageChange = (page: number) => { + loadData(page); + setCurrentPage(page); + }; + + useEffect(() => { + if (!currentUser) return; + + loadColumns(handleDeleteModalAction, `scouting`, currentUser).then( + (newCols) => setColumns(newCols), + ); + }, [currentUser]); + + const handleTableSubmit = async (id: string, data) => { + if (!_.isEmpty(data)) { + await dispatch(update({ id, data })) + .unwrap() + .then((res) => res) + .catch((err) => { + throw new Error(err); + }); + } + }; + + const onDeleteRows = async (selectedRows) => { + await dispatch(deleteItemsByIds(selectedRows)); + await loadData(0); + }; + + const controlClasses = + 'w-full py-2 px-2 my-2 rounded dark:placeholder-gray-400 ' + + ` ${bgColor} ${focusRing} ${corners} ` + + 'dark:bg-slate-800 border'; + + const dataGrid = ( +
+ `datagrid--row`} + rows={scouting ?? []} + columns={columns} + initialState={{ + pagination: { + paginationModel: { + pageSize: 10, + }, + }, + }} + disableRowSelectionOnClick + onProcessRowUpdateError={(params) => { + console.log('Error', params); + }} + processRowUpdate={async (newRow, oldRow) => { + const data = dataFormatter.dataGridEditFormatter(newRow); + + try { + await handleTableSubmit(newRow.id, data); + return newRow; + } catch { + return oldRow; + } + }} + sortingMode={'server'} + checkboxSelection + onRowSelectionModelChange={(ids) => { + setSelectedRows(ids); + }} + onSortModelChange={(params) => { + params.length + ? setSortModel(params) + : setSortModel([{ field: '', sort: 'desc' }]); + }} + rowCount={count} + pageSizeOptions={[10]} + paginationMode={'server'} + loading={loading} + onPaginationModelChange={(params) => { + onPageChange(params.page); + }} + /> +
+ ); + + return ( + <> + {filterItems && Array.isArray(filterItems) && filterItems.length ? ( + + null} + > +
+ <> + {filterItems && + filterItems.map((filterItem) => { + return ( +
+
+
+ Filter +
+ + {filters.map((selectOption) => ( + + ))} + +
+ {filters.find( + (filter) => + filter.title === filterItem?.fields?.selectedField, + )?.type === 'enum' ? ( +
+
Value
+ + + {filters + .find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + ) + ?.options?.map((option) => ( + + ))} + +
+ ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + )?.number ? ( +
+
+
+ From +
+ +
+
+
+ To +
+ +
+
+ ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + )?.date ? ( +
+
+
+ From +
+ +
+
+
+ To +
+ +
+
+ ) : ( +
+
+ Contains +
+ +
+ )} +
+
+ Action +
+ { + deleteFilter(filterItem.id); + }} + /> +
+
+ ); + })} +
+ + +
+ +
+
+
+ ) : null} + +

Are you sure you want to delete this item?

+
+ + {dataGrid} + + {selectedRows.length > 0 && + createPortal( + onDeleteRows(selectedRows)} + />, + document.getElementById('delete-rows-button'), + )} + + + ); +}; + +export default TableSampleScouting; diff --git a/frontend/src/components/Scouting/configureScoutingCols.tsx b/frontend/src/components/Scouting/configureScoutingCols.tsx new file mode 100644 index 0000000..0b69f64 --- /dev/null +++ b/frontend/src/components/Scouting/configureScoutingCols.tsx @@ -0,0 +1,160 @@ +import React from 'react'; +import BaseIcon from '../BaseIcon'; +import { mdiEye, mdiTrashCan, mdiPencilOutline } from '@mdi/js'; +import axios from 'axios'; +import { + GridActionsCellItem, + GridRowParams, + GridValueGetterParams, +} from '@mui/x-data-grid'; +import ImageField from '../ImageField'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import DataGridMultiSelect from '../DataGridMultiSelect'; +import ListActionsPopover from '../ListActionsPopover'; + +import { hasPermission } from '../../helpers/userPermissions'; + +type Params = (id: string) => void; + +export const loadColumns = async ( + onDelete: Params, + entityName: string, + + user, +) => { + async function callOptionsApi(entityName: string) { + if (!hasPermission(user, 'READ_' + entityName.toUpperCase())) return []; + + try { + const data = await axios(`/${entityName}/autocomplete?limit=100`); + return data.data; + } catch (error) { + console.log(error); + return []; + } + } + + const hasUpdatePermission = hasPermission(user, 'UPDATE_SCOUTING'); + + return [ + { + field: 'team_number', + headerName: 'Team number', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + type: 'number', + }, + + { + field: 'coral_l1', + headerName: 'Coral l1', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + type: 'number', + }, + + { + field: 'coral_l2', + headerName: 'Coral l2', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + type: 'number', + }, + + { + field: 'coral_l3', + headerName: 'Coral l3', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + type: 'number', + }, + + { + field: 'coral_l4', + headerName: 'Coral l4', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + type: 'number', + }, + + { + field: 'algae_processor', + headerName: 'Algae processor', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + type: 'number', + }, + + { + field: 'algae_net', + headerName: 'Algae net', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + type: 'number', + }, + + { + field: 'actions', + type: 'actions', + minWidth: 30, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + getActions: (params: GridRowParams) => { + return [ +
+ +
, + ]; + }, + }, + ]; +}; diff --git a/frontend/src/components/WebPageComponents/Footer.tsx b/frontend/src/components/WebPageComponents/Footer.tsx index bd9c5a1..b1ac8c2 100644 --- a/frontend/src/components/WebPageComponents/Footer.tsx +++ b/frontend/src/components/WebPageComponents/Footer.tsx @@ -17,9 +17,9 @@ export default function WebSiteFooter({ projectName }: WebSiteFooterProps) { const borders = useAppSelector((state) => state.style.borders); const websiteHeder = useAppSelector((state) => state.style.websiteHeder); - const style = FooterStyle.WITH_PROJECT_NAME; + const style = FooterStyle.WITH_PAGES; - const design = FooterDesigns.DEFAULT_DESIGN; + const design = FooterDesigns.DESIGN_DIVERSITY; return (
state.style.websiteHeder); const borders = useAppSelector((state) => state.style.borders); - const style = HeaderStyle.PAGES_LEFT; + const style = HeaderStyle.PAGES_RIGHT; - const design = HeaderDesigns.DEFAULT_DESIGN; + const design = HeaderDesigns.DESIGN_DIVERSITY; return (
{ const [tasks, setTasks] = React.useState(loadingMessage); const [roles, setRoles] = React.useState(loadingMessage); const [permissions, setPermissions] = React.useState(loadingMessage); + const [scouting, setScouting] = React.useState(loadingMessage); const [widgetsRole, setWidgetsRole] = React.useState({ role: { value: '', label: '' }, @@ -53,6 +54,7 @@ const Dashboard = () => { 'tasks', 'roles', 'permissions', + 'scouting', ]; const fns = [ setUsers, @@ -62,6 +64,7 @@ const Dashboard = () => { setTasks, setRoles, setPermissions, + setScouting, ]; const requests = entities.map((entity, index) => { @@ -419,6 +422,38 @@ const Dashboard = () => {
)} + + {hasPermission(currentUser, 'READ_SCOUTING') && ( + +
+
+
+
+ Scouting +
+
+ {scouting} +
+
+
+ +
+
+
+ + )}
diff --git a/frontend/src/pages/index.tsx b/frontend/src/pages/index.tsx index 717999f..abf8e61 100644 --- a/frontend/src/pages/index.tsx +++ b/frontend/src/pages/index.tsx @@ -129,7 +129,7 @@ export default function WebSite() { { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + team_number: '', + + coral_l1: '', + + coral_l2: '', + + coral_l3: '', + + coral_l4: '', + + algae_processor: '', + + algae_net: '', + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { scouting } = useAppSelector((state) => state.scouting); + + const { scoutingId } = router.query; + + useEffect(() => { + dispatch(fetch({ id: scoutingId })); + }, [scoutingId]); + + useEffect(() => { + if (typeof scouting === 'object') { + setInitialValues(scouting); + } + }, [scouting]); + + useEffect(() => { + if (typeof scouting === 'object') { + const newInitialVal = { ...initVals }; + + Object.keys(initVals).forEach((el) => (newInitialVal[el] = scouting[el])); + + setInitialValues(newInitialVal); + } + }, [scouting]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: scoutingId, data })); + await router.push('/scouting/scouting-list'); + }; + + return ( + <> + + {getPageTitle('Edit scouting')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + router.push('/scouting/scouting-list')} + /> + + +
+
+
+ + ); +}; + +EditScouting.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditScouting; diff --git a/frontend/src/pages/scouting/index.tsx b/frontend/src/pages/scouting/index.tsx new file mode 100644 index 0000000..b9eb60b --- /dev/null +++ b/frontend/src/pages/scouting/index.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import Link from 'next/link'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import CardBox from '../../components/CardBox'; + +const ScoutingIndex = () => ( + + + + + + +); + +ScoutingIndex.getLayout = page => {page}; + +export default ScoutingIndex; diff --git a/frontend/src/pages/scouting/scouting-edit.tsx b/frontend/src/pages/scouting/scouting-edit.tsx new file mode 100644 index 0000000..fbc1642 --- /dev/null +++ b/frontend/src/pages/scouting/scouting-edit.tsx @@ -0,0 +1,166 @@ +import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement, useEffect, useState } from 'react'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; + +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { SwitchField } from '../../components/SwitchField'; +import { RichTextField } from '../../components/RichTextField'; + +import { update, fetch } from '../../stores/scouting/scoutingSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; + +const EditScoutingPage = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + team_number: '', + + coral_l1: '', + + coral_l2: '', + + coral_l3: '', + + coral_l4: '', + + algae_processor: '', + + algae_net: '', + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { scouting } = useAppSelector((state) => state.scouting); + + const { id } = router.query; + + useEffect(() => { + dispatch(fetch({ id: id })); + }, [id]); + + useEffect(() => { + if (typeof scouting === 'object') { + setInitialValues(scouting); + } + }, [scouting]); + + useEffect(() => { + if (typeof scouting === 'object') { + const newInitialVal = { ...initVals }; + Object.keys(initVals).forEach((el) => (newInitialVal[el] = scouting[el])); + setInitialValues(newInitialVal); + } + }, [scouting]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: id, data })); + await router.push('/scouting/scouting-list'); + }; + + return ( + <> + + {getPageTitle('Edit scouting')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + router.push('/scouting/scouting-list')} + /> + + +
+
+
+ + ); +}; + +EditScoutingPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditScoutingPage; diff --git a/frontend/src/pages/scouting/scouting-list.tsx b/frontend/src/pages/scouting/scouting-list.tsx new file mode 100644 index 0000000..d41e594 --- /dev/null +++ b/frontend/src/pages/scouting/scouting-list.tsx @@ -0,0 +1,170 @@ +import { mdiChartTimelineVariant } from '@mdi/js'; +import Head from 'next/head'; +import { uniqueId } from 'lodash'; +import React, { ReactElement, useState } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; +import TableScouting from '../../components/Scouting/TableScouting'; +import BaseButton from '../../components/BaseButton'; +import axios from 'axios'; +import Link from 'next/link'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import CardBoxModal from '../../components/CardBoxModal'; +import DragDropFilePicker from '../../components/DragDropFilePicker'; +import { setRefetch, uploadCsv } from '../../stores/scouting/scoutingSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const ScoutingTablesPage = () => { + const [filterItems, setFilterItems] = useState([]); + const [csvFile, setCsvFile] = useState(null); + const [isModalActive, setIsModalActive] = useState(false); + const [showTableView, setShowTableView] = useState(false); + + const { currentUser } = useAppSelector((state) => state.auth); + + const dispatch = useAppDispatch(); + + const [filters] = useState([ + { label: 'Team number', title: 'team_number', number: 'true' }, + { label: 'Coral l1', title: 'coral_l1', number: 'true' }, + { label: 'Coral l2', title: 'coral_l2', number: 'true' }, + { label: 'Coral l3', title: 'coral_l3', number: 'true' }, + { label: 'Coral l4', title: 'coral_l4', number: 'true' }, + { label: 'Algae processor', title: 'algae_processor', number: 'true' }, + { label: 'Algae net', title: 'algae_net', number: 'true' }, + ]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_SCOUTING'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getScoutingCSV = async () => { + const response = await axios({ + url: '/scouting?filetype=csv', + method: 'GET', + responseType: 'blob', + }); + const type = response.headers['content-type']; + const blob = new Blob([response.data], { type: type }); + const link = document.createElement('a'); + link.href = window.URL.createObjectURL(blob); + link.download = 'scoutingCSV.csv'; + link.click(); + }; + + const onModalConfirm = async () => { + if (!csvFile) return; + await dispatch(uploadCsv(csvFile)); + dispatch(setRefetch(true)); + setCsvFile(null); + setIsModalActive(false); + }; + + const onModalCancel = () => { + setCsvFile(null); + setIsModalActive(false); + }; + + return ( + <> + + {getPageTitle('Scouting')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
+
+
+
+ + + + +
+ + + + + ); +}; + +ScoutingTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default ScoutingTablesPage; diff --git a/frontend/src/pages/scouting/scouting-new.tsx b/frontend/src/pages/scouting/scouting-new.tsx new file mode 100644 index 0000000..ee5ed35 --- /dev/null +++ b/frontend/src/pages/scouting/scouting-new.tsx @@ -0,0 +1,142 @@ +import { + mdiAccount, + mdiChartTimelineVariant, + mdiMail, + mdiUpload, +} from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SwitchField } from '../../components/SwitchField'; + +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { RichTextField } from '../../components/RichTextField'; + +import { create } from '../../stores/scouting/scoutingSlice'; +import { useAppDispatch } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import moment from 'moment'; + +const initialValues = { + team_number: '', + + coral_l1: '', + + coral_l2: '', + + coral_l3: '', + + coral_l4: '', + + algae_processor: '', + + algae_net: '', +}; + +const ScoutingNew = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + + const handleSubmit = async (data) => { + await dispatch(create(data)); + await router.push('/scouting/scouting-list'); + }; + return ( + <> + + {getPageTitle('New Item')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + router.push('/scouting/scouting-list')} + /> + + +
+
+
+ + ); +}; + +ScoutingNew.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default ScoutingNew; diff --git a/frontend/src/pages/scouting/scouting-table.tsx b/frontend/src/pages/scouting/scouting-table.tsx new file mode 100644 index 0000000..4e933fe --- /dev/null +++ b/frontend/src/pages/scouting/scouting-table.tsx @@ -0,0 +1,169 @@ +import { mdiChartTimelineVariant } from '@mdi/js'; +import Head from 'next/head'; +import { uniqueId } from 'lodash'; +import React, { ReactElement, useState } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; +import TableScouting from '../../components/Scouting/TableScouting'; +import BaseButton from '../../components/BaseButton'; +import axios from 'axios'; +import Link from 'next/link'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import CardBoxModal from '../../components/CardBoxModal'; +import DragDropFilePicker from '../../components/DragDropFilePicker'; +import { setRefetch, uploadCsv } from '../../stores/scouting/scoutingSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const ScoutingTablesPage = () => { + const [filterItems, setFilterItems] = useState([]); + const [csvFile, setCsvFile] = useState(null); + const [isModalActive, setIsModalActive] = useState(false); + const [showTableView, setShowTableView] = useState(false); + + const { currentUser } = useAppSelector((state) => state.auth); + + const dispatch = useAppDispatch(); + + const [filters] = useState([ + { label: 'Team number', title: 'team_number', number: 'true' }, + { label: 'Coral l1', title: 'coral_l1', number: 'true' }, + { label: 'Coral l2', title: 'coral_l2', number: 'true' }, + { label: 'Coral l3', title: 'coral_l3', number: 'true' }, + { label: 'Coral l4', title: 'coral_l4', number: 'true' }, + { label: 'Algae processor', title: 'algae_processor', number: 'true' }, + { label: 'Algae net', title: 'algae_net', number: 'true' }, + ]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_SCOUTING'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getScoutingCSV = async () => { + const response = await axios({ + url: '/scouting?filetype=csv', + method: 'GET', + responseType: 'blob', + }); + const type = response.headers['content-type']; + const blob = new Blob([response.data], { type: type }); + const link = document.createElement('a'); + link.href = window.URL.createObjectURL(blob); + link.download = 'scoutingCSV.csv'; + link.click(); + }; + + const onModalConfirm = async () => { + if (!csvFile) return; + await dispatch(uploadCsv(csvFile)); + dispatch(setRefetch(true)); + setCsvFile(null); + setIsModalActive(false); + }; + + const onModalCancel = () => { + setCsvFile(null); + setIsModalActive(false); + }; + + return ( + <> + + {getPageTitle('Scouting')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
+
+
+
+ + + +
+ + + + + ); +}; + +ScoutingTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default ScoutingTablesPage; diff --git a/frontend/src/pages/scouting/scouting-view.tsx b/frontend/src/pages/scouting/scouting-view.tsx new file mode 100644 index 0000000..7098018 --- /dev/null +++ b/frontend/src/pages/scouting/scouting-view.tsx @@ -0,0 +1,113 @@ +import React, { ReactElement, useEffect } from 'react'; +import Head from 'next/head'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { fetch } from '../../stores/scouting/scoutingSlice'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import { getPageTitle } from '../../config'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import SectionMain from '../../components/SectionMain'; +import CardBox from '../../components/CardBox'; +import BaseButton from '../../components/BaseButton'; +import BaseDivider from '../../components/BaseDivider'; +import { mdiChartTimelineVariant } from '@mdi/js'; +import { SwitchField } from '../../components/SwitchField'; +import FormField from '../../components/FormField'; + +const ScoutingView = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const { scouting } = useAppSelector((state) => state.scouting); + + 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 scouting')} + + + + + + +
+

Team number

+

{scouting?.team_number || 'No data'}

+
+ +
+

Coral l1

+

{scouting?.coral_l1 || 'No data'}

+
+ +
+

Coral l2

+

{scouting?.coral_l2 || 'No data'}

+
+ +
+

Coral l3

+

{scouting?.coral_l3 || 'No data'}

+
+ +
+

Coral l4

+

{scouting?.coral_l4 || 'No data'}

+
+ +
+

Algae processor

+

{scouting?.algae_processor || 'No data'}

+
+ +
+

Algae net

+

{scouting?.algae_net || 'No data'}

+
+ + + + router.push('/scouting/scouting-list')} + /> +
+
+ + ); +}; + +ScoutingView.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default ScoutingView; diff --git a/frontend/src/stores/scouting/scoutingSlice.ts b/frontend/src/stores/scouting/scoutingSlice.ts new file mode 100644 index 0000000..98a17f0 --- /dev/null +++ b/frontend/src/stores/scouting/scoutingSlice.ts @@ -0,0 +1,236 @@ +import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit'; +import axios from 'axios'; +import { + fulfilledNotify, + rejectNotify, + resetNotify, +} from '../../helpers/notifyStateHandler'; + +interface MainState { + scouting: any; + loading: boolean; + count: number; + refetch: boolean; + rolesWidgets: any[]; + notify: { + showNotification: boolean; + textNotification: string; + typeNotification: string; + }; +} + +const initialState: MainState = { + scouting: [], + loading: false, + count: 0, + refetch: false, + rolesWidgets: [], + notify: { + showNotification: false, + textNotification: '', + typeNotification: 'warn', + }, +}; + +export const fetch = createAsyncThunk('scouting/fetch', async (data: any) => { + const { id, query } = data; + const result = await axios.get(`scouting${query || (id ? `/${id}` : '')}`); + return id + ? result.data + : { rows: result.data.rows, count: result.data.count }; +}); + +export const deleteItemsByIds = createAsyncThunk( + 'scouting/deleteByIds', + async (data: any, { rejectWithValue }) => { + try { + await axios.post('scouting/deleteByIds', { data }); + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const deleteItem = createAsyncThunk( + 'scouting/deleteScouting', + async (id: string, { rejectWithValue }) => { + try { + await axios.delete(`scouting/${id}`); + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const create = createAsyncThunk( + 'scouting/createScouting', + async (data: any, { rejectWithValue }) => { + try { + const result = await axios.post('scouting', { data }); + return result.data; + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const uploadCsv = createAsyncThunk( + 'scouting/uploadCsv', + async (file: File, { rejectWithValue }) => { + try { + const data = new FormData(); + data.append('file', file); + data.append('filename', file.name); + + const result = await axios.post('scouting/bulk-import', data, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }); + + return result.data; + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const update = createAsyncThunk( + 'scouting/updateScouting', + async (payload: any, { rejectWithValue }) => { + try { + const result = await axios.put(`scouting/${payload.id}`, { + id: payload.id, + data: payload.data, + }); + return result.data; + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const scoutingSlice = createSlice({ + name: 'scouting', + 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.scouting = action.payload.rows; + state.count = action.payload.count; + } else { + state.scouting = action.payload; + } + state.loading = false; + }); + + builder.addCase(deleteItemsByIds.pending, (state) => { + state.loading = true; + resetNotify(state); + }); + + builder.addCase(deleteItemsByIds.fulfilled, (state) => { + state.loading = false; + fulfilledNotify(state, 'Scouting has been deleted'); + }); + + builder.addCase(deleteItemsByIds.rejected, (state, action) => { + state.loading = false; + rejectNotify(state, action); + }); + + builder.addCase(deleteItem.pending, (state) => { + state.loading = true; + resetNotify(state); + }); + + builder.addCase(deleteItem.fulfilled, (state) => { + state.loading = false; + fulfilledNotify(state, `${'Scouting'.slice(0, -1)} has been deleted`); + }); + + builder.addCase(deleteItem.rejected, (state, action) => { + state.loading = false; + rejectNotify(state, action); + }); + + builder.addCase(create.pending, (state) => { + state.loading = true; + resetNotify(state); + }); + builder.addCase(create.rejected, (state, action) => { + state.loading = false; + rejectNotify(state, action); + }); + + builder.addCase(create.fulfilled, (state) => { + state.loading = false; + fulfilledNotify(state, `${'Scouting'.slice(0, -1)} has been created`); + }); + + builder.addCase(update.pending, (state) => { + state.loading = true; + resetNotify(state); + }); + builder.addCase(update.fulfilled, (state) => { + state.loading = false; + fulfilledNotify(state, `${'Scouting'.slice(0, -1)} has been updated`); + }); + builder.addCase(update.rejected, (state, action) => { + state.loading = false; + rejectNotify(state, action); + }); + + builder.addCase(uploadCsv.pending, (state) => { + state.loading = true; + resetNotify(state); + }); + builder.addCase(uploadCsv.fulfilled, (state) => { + state.loading = false; + fulfilledNotify(state, 'Scouting has been uploaded'); + }); + builder.addCase(uploadCsv.rejected, (state, action) => { + state.loading = false; + rejectNotify(state, action); + }); + }, +}); + +// Action creators are generated for each case reducer function +export const { setRefetch } = scoutingSlice.actions; + +export default scoutingSlice.reducer; diff --git a/frontend/src/stores/store.ts b/frontend/src/stores/store.ts index 3ba209f..093e86c 100644 --- a/frontend/src/stores/store.ts +++ b/frontend/src/stores/store.ts @@ -11,6 +11,7 @@ import scouting_dataSlice from './scouting_data/scouting_dataSlice'; import tasksSlice from './tasks/tasksSlice'; import rolesSlice from './roles/rolesSlice'; import permissionsSlice from './permissions/permissionsSlice'; +import scoutingSlice from './scouting/scoutingSlice'; export const store = configureStore({ reducer: { @@ -26,6 +27,7 @@ export const store = configureStore({ tasks: tasksSlice, roles: rolesSlice, permissions: permissionsSlice, + scouting: scoutingSlice, }, });