diff --git a/app-shell/src/_schema.json b/app-shell/src/_schema.json index 63f4c21..c027bb2 100644 --- a/app-shell/src/_schema.json +++ b/app-shell/src/_schema.json @@ -1,4 +1,5 @@ { "Initial version": "{\"iv\":\"qoPs598ArSL9Odd+\",\"encryptedData\":\"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\"}", - "Updated via schema editor on 2025-09-02 18:42": "{\"iv\":\"j7+aKdr1zrD1YMqX\",\"encryptedData\":\"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\"}" + "Updated via schema editor on 2025-09-02 18:42": "{\"iv\":\"j7+aKdr1zrD1YMqX\",\"encryptedData\":\"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\"}", + "Updated via schema editor on 2025-09-02 19:34": "{\"iv\":\"i8p/I+IpflO86TrB\",\"encryptedData\":\"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\"}" } \ No newline at end of file diff --git a/backend/src/db/api/games.js b/backend/src/db/api/games.js index 4af8d17..1fc0580 100644 --- a/backend/src/db/api/games.js +++ b/backend/src/db/api/games.js @@ -37,6 +37,10 @@ module.exports = class GamesDBApi { transaction, }); + await games.setSeason(data.season || null, { + transaction, + }); + return games; } @@ -110,6 +114,14 @@ module.exports = class GamesDBApi { ); } + if (data.season !== undefined) { + await games.setSeason( + data.season, + + { transaction }, + ); + } + return games; } @@ -171,6 +183,10 @@ module.exports = class GamesDBApi { const output = games.get({ plain: true }); + output.player_game_scores_game = await games.getPlayer_game_scores_game({ + transaction, + }); + output.league = await games.getLeague({ transaction, }); @@ -183,6 +199,10 @@ module.exports = class GamesDBApi { transaction, }); + output.season = await games.getSeason({ + transaction, + }); + return output; } @@ -243,6 +263,32 @@ module.exports = class GamesDBApi { model: db.leagues, as: 'leagues', }, + + { + model: db.seasons, + as: 'season', + + where: filter.season + ? { + [Op.or]: [ + { + id: { + [Op.in]: filter.season + .split('|') + .map((term) => Utils.uuid(term)), + }, + }, + { + name: { + [Op.or]: filter.season + .split('|') + .map((term) => ({ [Op.iLike]: `%${term}%` })), + }, + }, + ], + } + : {}, + }, ]; if (filter) { diff --git a/backend/src/db/api/leagues.js b/backend/src/db/api/leagues.js index 8e158b6..20c49a3 100644 --- a/backend/src/db/api/leagues.js +++ b/backend/src/db/api/leagues.js @@ -16,6 +16,7 @@ module.exports = class LeaguesDBApi { id: data.id || undefined, name: data.name || null, + handicapformula: data.handicapformula || null, importHash: data.importHash || null, createdById: currentUser.id, updatedById: currentUser.id, @@ -35,6 +36,7 @@ module.exports = class LeaguesDBApi { id: item.id || undefined, name: item.name || null, + handicapformula: item.handicapformula || null, importHash: item.importHash || null, createdById: currentUser.id, updatedById: currentUser.id, @@ -60,6 +62,9 @@ module.exports = class LeaguesDBApi { if (data.name !== undefined) updatePayload.name = data.name; + if (data.handicapformula !== undefined) + updatePayload.handicapformula = data.handicapformula; + updatePayload.updatedById = currentUser.id; await leagues.update(updatePayload, { transaction }); @@ -149,6 +154,29 @@ module.exports = class LeaguesDBApi { transaction, }); + output.seasons_leagues = await leagues.getSeasons_leagues({ + transaction, + }); + + output.seasons_league = await leagues.getSeasons_league({ + transaction, + }); + + output.player_game_scores_leagues = + await leagues.getPlayer_game_scores_leagues({ + transaction, + }); + + output.player_season_stats_leagues = + await leagues.getPlayer_season_stats_leagues({ + transaction, + }); + + output.team_season_stats_leagues = + await leagues.getTeam_season_stats_leagues({ + transaction, + }); + return output; } @@ -190,6 +218,17 @@ module.exports = class LeaguesDBApi { }; } + if (filter.handicapformula) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'leagues', + 'handicapformula', + filter.handicapformula, + ), + }; + } + if (filter.active !== undefined) { where = { ...where, diff --git a/backend/src/db/api/player_game_scores.js b/backend/src/db/api/player_game_scores.js new file mode 100644 index 0000000..54e1f90 --- /dev/null +++ b/backend/src/db/api/player_game_scores.js @@ -0,0 +1,432 @@ +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 Player_game_scoresDBApi { + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const player_game_scores = await db.player_game_scores.create( + { + id: data.id || undefined, + + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + await player_game_scores.setLeagues(data.leagues || null, { + transaction, + }); + + await player_game_scores.setGame(data.game || null, { + transaction, + }); + + await player_game_scores.setPlayer(data.player || null, { + transaction, + }); + + await player_game_scores.setTeam(data.team || null, { + transaction, + }); + + return player_game_scores; + } + + 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 player_game_scoresData = data.map((item, index) => ({ + id: item.id || undefined, + + importHash: item.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + // Bulk create items + const player_game_scores = await db.player_game_scores.bulkCreate( + player_game_scoresData, + { transaction }, + ); + + // For each item created, replace relation files + + return player_game_scores; + } + + 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 player_game_scores = await db.player_game_scores.findByPk( + id, + {}, + { transaction }, + ); + + const updatePayload = {}; + + updatePayload.updatedById = currentUser.id; + + await player_game_scores.update(updatePayload, { transaction }); + + if (data.leagues !== undefined) { + await player_game_scores.setLeagues( + data.leagues, + + { transaction }, + ); + } + + if (data.game !== undefined) { + await player_game_scores.setGame( + data.game, + + { transaction }, + ); + } + + if (data.player !== undefined) { + await player_game_scores.setPlayer( + data.player, + + { transaction }, + ); + } + + if (data.team !== undefined) { + await player_game_scores.setTeam( + data.team, + + { transaction }, + ); + } + + return player_game_scores; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const player_game_scores = await db.player_game_scores.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of player_game_scores) { + await record.update({ deletedBy: currentUser.id }, { transaction }); + } + for (const record of player_game_scores) { + await record.destroy({ transaction }); + } + }); + + return player_game_scores; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const player_game_scores = await db.player_game_scores.findByPk( + id, + options, + ); + + await player_game_scores.update( + { + deletedBy: currentUser.id, + }, + { + transaction, + }, + ); + + await player_game_scores.destroy({ + transaction, + }); + + return player_game_scores; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const player_game_scores = await db.player_game_scores.findOne( + { where }, + { transaction }, + ); + + if (!player_game_scores) { + return player_game_scores; + } + + const output = player_game_scores.get({ plain: true }); + + output.leagues = await player_game_scores.getLeagues({ + transaction, + }); + + output.game = await player_game_scores.getGame({ + transaction, + }); + + output.player = await player_game_scores.getPlayer({ + transaction, + }); + + output.team = await player_game_scores.getTeam({ + transaction, + }); + + return output; + } + + static async findAll(filter, globalAccess, options) { + const limit = filter.limit || 0; + let offset = 0; + let where = {}; + const currentPage = +filter.page; + + const user = (options && options.currentUser) || null; + const userLeagues = (user && user.leagues?.id) || null; + + if (userLeagues) { + if (options?.currentUser?.leaguesId) { + where.leaguesId = options.currentUser.leaguesId; + } + } + + offset = currentPage * limit; + + const orderBy = null; + + const transaction = (options && options.transaction) || undefined; + + let include = [ + { + model: db.leagues, + as: 'leagues', + }, + + { + model: db.games, + as: 'game', + + where: filter.game + ? { + [Op.or]: [ + { + id: { + [Op.in]: filter.game + .split('|') + .map((term) => Utils.uuid(term)), + }, + }, + { + date: { + [Op.or]: filter.game + .split('|') + .map((term) => ({ [Op.iLike]: `%${term}%` })), + }, + }, + ], + } + : {}, + }, + + { + model: db.players, + as: 'player', + + where: filter.player + ? { + [Op.or]: [ + { + id: { + [Op.in]: filter.player + .split('|') + .map((term) => Utils.uuid(term)), + }, + }, + { + first_name: { + [Op.or]: filter.player + .split('|') + .map((term) => ({ [Op.iLike]: `%${term}%` })), + }, + }, + ], + } + : {}, + }, + + { + model: db.teams, + as: 'team', + + where: filter.team + ? { + [Op.or]: [ + { + id: { + [Op.in]: filter.team + .split('|') + .map((term) => Utils.uuid(term)), + }, + }, + { + name: { + [Op.or]: filter.team + .split('|') + .map((term) => ({ [Op.iLike]: `%${term}%` })), + }, + }, + ], + } + : {}, + }, + ]; + + if (filter) { + if (filter.id) { + where = { + ...where, + ['id']: Utils.uuid(filter.id), + }; + } + + if (filter.active !== undefined) { + where = { + ...where, + active: filter.active === true || filter.active === 'true', + }; + } + + if (filter.leagues) { + const listItems = filter.leagues.split('|').map((item) => { + return Utils.uuid(item); + }); + + where = { + ...where, + leaguesId: { [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 (globalAccess) { + delete where.leaguesId; + } + + 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.player_game_scores.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, + globalAccess, + organizationId, + ) { + let where = {}; + + if (!globalAccess && organizationId) { + where.organizationId = organizationId; + } + + if (query) { + where = { + [Op.or]: [ + { ['id']: Utils.uuid(query) }, + Utils.ilike('player_game_scores', 'id', query), + ], + }; + } + + const records = await db.player_game_scores.findAll({ + attributes: ['id', 'id'], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + orderBy: [['id', 'ASC']], + }); + + return records.map((record) => ({ + id: record.id, + label: record.id, + })); + } +}; diff --git a/backend/src/db/api/player_season_stats.js b/backend/src/db/api/player_season_stats.js new file mode 100644 index 0000000..863a6f8 --- /dev/null +++ b/backend/src/db/api/player_season_stats.js @@ -0,0 +1,535 @@ +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 Player_season_statsDBApi { + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const player_season_stats = await db.player_season_stats.create( + { + id: data.id || undefined, + + totalpoints: data.totalpoints || null, + gamesplayed: data.gamesplayed || null, + eightballruns: data.eightballruns || null, + eightballbreaks: data.eightballbreaks || null, + nineballruns: data.nineballruns || null, + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + await player_season_stats.setLeagues(data.leagues || null, { + transaction, + }); + + await player_season_stats.setPlayer(data.player || null, { + transaction, + }); + + await player_season_stats.setSeason(data.season || null, { + transaction, + }); + + return player_season_stats; + } + + 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 player_season_statsData = data.map((item, index) => ({ + id: item.id || undefined, + + totalpoints: item.totalpoints || null, + gamesplayed: item.gamesplayed || null, + eightballruns: item.eightballruns || null, + eightballbreaks: item.eightballbreaks || null, + nineballruns: item.nineballruns || null, + importHash: item.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + // Bulk create items + const player_season_stats = await db.player_season_stats.bulkCreate( + player_season_statsData, + { transaction }, + ); + + // For each item created, replace relation files + + return player_season_stats; + } + + 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 player_season_stats = await db.player_season_stats.findByPk( + id, + {}, + { transaction }, + ); + + const updatePayload = {}; + + if (data.totalpoints !== undefined) + updatePayload.totalpoints = data.totalpoints; + + if (data.gamesplayed !== undefined) + updatePayload.gamesplayed = data.gamesplayed; + + if (data.eightballruns !== undefined) + updatePayload.eightballruns = data.eightballruns; + + if (data.eightballbreaks !== undefined) + updatePayload.eightballbreaks = data.eightballbreaks; + + if (data.nineballruns !== undefined) + updatePayload.nineballruns = data.nineballruns; + + updatePayload.updatedById = currentUser.id; + + await player_season_stats.update(updatePayload, { transaction }); + + if (data.leagues !== undefined) { + await player_season_stats.setLeagues( + data.leagues, + + { transaction }, + ); + } + + if (data.player !== undefined) { + await player_season_stats.setPlayer( + data.player, + + { transaction }, + ); + } + + if (data.season !== undefined) { + await player_season_stats.setSeason( + data.season, + + { transaction }, + ); + } + + return player_season_stats; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const player_season_stats = await db.player_season_stats.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of player_season_stats) { + await record.update({ deletedBy: currentUser.id }, { transaction }); + } + for (const record of player_season_stats) { + await record.destroy({ transaction }); + } + }); + + return player_season_stats; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const player_season_stats = await db.player_season_stats.findByPk( + id, + options, + ); + + await player_season_stats.update( + { + deletedBy: currentUser.id, + }, + { + transaction, + }, + ); + + await player_season_stats.destroy({ + transaction, + }); + + return player_season_stats; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const player_season_stats = await db.player_season_stats.findOne( + { where }, + { transaction }, + ); + + if (!player_season_stats) { + return player_season_stats; + } + + const output = player_season_stats.get({ plain: true }); + + output.leagues = await player_season_stats.getLeagues({ + transaction, + }); + + output.player = await player_season_stats.getPlayer({ + transaction, + }); + + output.season = await player_season_stats.getSeason({ + transaction, + }); + + return output; + } + + static async findAll(filter, globalAccess, options) { + const limit = filter.limit || 0; + let offset = 0; + let where = {}; + const currentPage = +filter.page; + + const user = (options && options.currentUser) || null; + const userLeagues = (user && user.leagues?.id) || null; + + if (userLeagues) { + if (options?.currentUser?.leaguesId) { + where.leaguesId = options.currentUser.leaguesId; + } + } + + offset = currentPage * limit; + + const orderBy = null; + + const transaction = (options && options.transaction) || undefined; + + let include = [ + { + model: db.leagues, + as: 'leagues', + }, + + { + model: db.players, + as: 'player', + + where: filter.player + ? { + [Op.or]: [ + { + id: { + [Op.in]: filter.player + .split('|') + .map((term) => Utils.uuid(term)), + }, + }, + { + first_name: { + [Op.or]: filter.player + .split('|') + .map((term) => ({ [Op.iLike]: `%${term}%` })), + }, + }, + ], + } + : {}, + }, + + { + model: db.seasons, + as: 'season', + + where: filter.season + ? { + [Op.or]: [ + { + id: { + [Op.in]: filter.season + .split('|') + .map((term) => Utils.uuid(term)), + }, + }, + { + name: { + [Op.or]: filter.season + .split('|') + .map((term) => ({ [Op.iLike]: `%${term}%` })), + }, + }, + ], + } + : {}, + }, + ]; + + if (filter) { + if (filter.id) { + where = { + ...where, + ['id']: Utils.uuid(filter.id), + }; + } + + if (filter.totalpointsRange) { + const [start, end] = filter.totalpointsRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + totalpoints: { + ...where.totalpoints, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + totalpoints: { + ...where.totalpoints, + [Op.lte]: end, + }, + }; + } + } + + if (filter.gamesplayedRange) { + const [start, end] = filter.gamesplayedRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + gamesplayed: { + ...where.gamesplayed, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + gamesplayed: { + ...where.gamesplayed, + [Op.lte]: end, + }, + }; + } + } + + if (filter.eightballrunsRange) { + const [start, end] = filter.eightballrunsRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + eightballruns: { + ...where.eightballruns, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + eightballruns: { + ...where.eightballruns, + [Op.lte]: end, + }, + }; + } + } + + if (filter.eightballbreaksRange) { + const [start, end] = filter.eightballbreaksRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + eightballbreaks: { + ...where.eightballbreaks, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + eightballbreaks: { + ...where.eightballbreaks, + [Op.lte]: end, + }, + }; + } + } + + if (filter.nineballrunsRange) { + const [start, end] = filter.nineballrunsRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + nineballruns: { + ...where.nineballruns, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + nineballruns: { + ...where.nineballruns, + [Op.lte]: end, + }, + }; + } + } + + if (filter.active !== undefined) { + where = { + ...where, + active: filter.active === true || filter.active === 'true', + }; + } + + if (filter.leagues) { + const listItems = filter.leagues.split('|').map((item) => { + return Utils.uuid(item); + }); + + where = { + ...where, + leaguesId: { [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 (globalAccess) { + delete where.leaguesId; + } + + 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.player_season_stats.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, + globalAccess, + organizationId, + ) { + let where = {}; + + if (!globalAccess && organizationId) { + where.organizationId = organizationId; + } + + if (query) { + where = { + [Op.or]: [ + { ['id']: Utils.uuid(query) }, + Utils.ilike('player_season_stats', 'gamesplayed', query), + ], + }; + } + + const records = await db.player_season_stats.findAll({ + attributes: ['id', 'gamesplayed'], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + orderBy: [['gamesplayed', 'ASC']], + }); + + return records.map((record) => ({ + id: record.id, + label: record.gamesplayed, + })); + } +}; diff --git a/backend/src/db/api/players.js b/backend/src/db/api/players.js index fbfbd00..cafc5dc 100644 --- a/backend/src/db/api/players.js +++ b/backend/src/db/api/players.js @@ -164,6 +164,16 @@ module.exports = class PlayersDBApi { const output = players.get({ plain: true }); + output.player_game_scores_player = + await players.getPlayer_game_scores_player({ + transaction, + }); + + output.player_season_stats_player = + await players.getPlayer_season_stats_player({ + transaction, + }); + output.team = await players.getTeam({ transaction, }); diff --git a/backend/src/db/api/seasons.js b/backend/src/db/api/seasons.js new file mode 100644 index 0000000..c6d2d64 --- /dev/null +++ b/backend/src/db/api/seasons.js @@ -0,0 +1,405 @@ +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 SeasonsDBApi { + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const seasons = await db.seasons.create( + { + id: data.id || undefined, + + name: data.name || null, + startdate: data.startdate || null, + enddate: data.enddate || null, + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + await seasons.setLeagues(data.leagues || null, { + transaction, + }); + + await seasons.setLeague(data.league || null, { + transaction, + }); + + return seasons; + } + + 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 seasonsData = data.map((item, index) => ({ + id: item.id || undefined, + + name: item.name || null, + startdate: item.startdate || null, + enddate: item.enddate || null, + importHash: item.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + // Bulk create items + const seasons = await db.seasons.bulkCreate(seasonsData, { transaction }); + + // For each item created, replace relation files + + return seasons; + } + + 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 seasons = await db.seasons.findByPk(id, {}, { transaction }); + + const updatePayload = {}; + + if (data.name !== undefined) updatePayload.name = data.name; + + if (data.startdate !== undefined) updatePayload.startdate = data.startdate; + + if (data.enddate !== undefined) updatePayload.enddate = data.enddate; + + updatePayload.updatedById = currentUser.id; + + await seasons.update(updatePayload, { transaction }); + + if (data.leagues !== undefined) { + await seasons.setLeagues( + data.leagues, + + { transaction }, + ); + } + + if (data.league !== undefined) { + await seasons.setLeague( + data.league, + + { transaction }, + ); + } + + return seasons; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const seasons = await db.seasons.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of seasons) { + await record.update({ deletedBy: currentUser.id }, { transaction }); + } + for (const record of seasons) { + await record.destroy({ transaction }); + } + }); + + return seasons; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const seasons = await db.seasons.findByPk(id, options); + + await seasons.update( + { + deletedBy: currentUser.id, + }, + { + transaction, + }, + ); + + await seasons.destroy({ + transaction, + }); + + return seasons; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const seasons = await db.seasons.findOne({ where }, { transaction }); + + if (!seasons) { + return seasons; + } + + const output = seasons.get({ plain: true }); + + output.games_season = await seasons.getGames_season({ + transaction, + }); + + output.player_season_stats_season = + await seasons.getPlayer_season_stats_season({ + transaction, + }); + + output.team_season_stats_season = await seasons.getTeam_season_stats_season( + { + transaction, + }, + ); + + output.leagues = await seasons.getLeagues({ + transaction, + }); + + output.league = await seasons.getLeague({ + transaction, + }); + + return output; + } + + static async findAll(filter, globalAccess, options) { + const limit = filter.limit || 0; + let offset = 0; + let where = {}; + const currentPage = +filter.page; + + const user = (options && options.currentUser) || null; + const userLeagues = (user && user.leagues?.id) || null; + + if (userLeagues) { + if (options?.currentUser?.leaguesId) { + where.leaguesId = options.currentUser.leaguesId; + } + } + + offset = currentPage * limit; + + const orderBy = null; + + const transaction = (options && options.transaction) || undefined; + + let include = [ + { + model: db.leagues, + as: 'leagues', + }, + + { + model: db.leagues, + as: 'league', + }, + ]; + + if (filter) { + if (filter.id) { + where = { + ...where, + ['id']: Utils.uuid(filter.id), + }; + } + + if (filter.name) { + where = { + ...where, + [Op.and]: Utils.ilike('seasons', 'name', filter.name), + }; + } + + if (filter.startdateRange) { + const [start, end] = filter.startdateRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + startdate: { + ...where.startdate, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + startdate: { + ...where.startdate, + [Op.lte]: end, + }, + }; + } + } + + if (filter.enddateRange) { + const [start, end] = filter.enddateRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + enddate: { + ...where.enddate, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + enddate: { + ...where.enddate, + [Op.lte]: end, + }, + }; + } + } + + if (filter.active !== undefined) { + where = { + ...where, + active: filter.active === true || filter.active === 'true', + }; + } + + if (filter.leagues) { + const listItems = filter.leagues.split('|').map((item) => { + return Utils.uuid(item); + }); + + where = { + ...where, + leaguesId: { [Op.or]: listItems }, + }; + } + + if (filter.league) { + const listItems = filter.league.split('|').map((item) => { + return Utils.uuid(item); + }); + + where = { + ...where, + leagueId: { [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 (globalAccess) { + delete where.leaguesId; + } + + 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.seasons.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, + globalAccess, + organizationId, + ) { + let where = {}; + + if (!globalAccess && organizationId) { + where.organizationId = organizationId; + } + + if (query) { + where = { + [Op.or]: [ + { ['id']: Utils.uuid(query) }, + Utils.ilike('seasons', 'name', query), + ], + }; + } + + const records = await db.seasons.findAll({ + attributes: ['id', 'name'], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + orderBy: [['name', 'ASC']], + }); + + return records.map((record) => ({ + id: record.id, + label: record.name, + })); + } +}; diff --git a/backend/src/db/api/team_season_stats.js b/backend/src/db/api/team_season_stats.js new file mode 100644 index 0000000..8c78c45 --- /dev/null +++ b/backend/src/db/api/team_season_stats.js @@ -0,0 +1,387 @@ +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 Team_season_statsDBApi { + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const team_season_stats = await db.team_season_stats.create( + { + id: data.id || undefined, + + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + await team_season_stats.setLeagues(data.leagues || null, { + transaction, + }); + + await team_season_stats.setTeam(data.team || null, { + transaction, + }); + + await team_season_stats.setSeason(data.season || null, { + transaction, + }); + + return team_season_stats; + } + + 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 team_season_statsData = data.map((item, index) => ({ + id: item.id || undefined, + + importHash: item.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + // Bulk create items + const team_season_stats = await db.team_season_stats.bulkCreate( + team_season_statsData, + { transaction }, + ); + + // For each item created, replace relation files + + return team_season_stats; + } + + 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 team_season_stats = await db.team_season_stats.findByPk( + id, + {}, + { transaction }, + ); + + const updatePayload = {}; + + updatePayload.updatedById = currentUser.id; + + await team_season_stats.update(updatePayload, { transaction }); + + if (data.leagues !== undefined) { + await team_season_stats.setLeagues( + data.leagues, + + { transaction }, + ); + } + + if (data.team !== undefined) { + await team_season_stats.setTeam( + data.team, + + { transaction }, + ); + } + + if (data.season !== undefined) { + await team_season_stats.setSeason( + data.season, + + { transaction }, + ); + } + + return team_season_stats; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const team_season_stats = await db.team_season_stats.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of team_season_stats) { + await record.update({ deletedBy: currentUser.id }, { transaction }); + } + for (const record of team_season_stats) { + await record.destroy({ transaction }); + } + }); + + return team_season_stats; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const team_season_stats = await db.team_season_stats.findByPk(id, options); + + await team_season_stats.update( + { + deletedBy: currentUser.id, + }, + { + transaction, + }, + ); + + await team_season_stats.destroy({ + transaction, + }); + + return team_season_stats; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const team_season_stats = await db.team_season_stats.findOne( + { where }, + { transaction }, + ); + + if (!team_season_stats) { + return team_season_stats; + } + + const output = team_season_stats.get({ plain: true }); + + output.leagues = await team_season_stats.getLeagues({ + transaction, + }); + + output.team = await team_season_stats.getTeam({ + transaction, + }); + + output.season = await team_season_stats.getSeason({ + transaction, + }); + + return output; + } + + static async findAll(filter, globalAccess, options) { + const limit = filter.limit || 0; + let offset = 0; + let where = {}; + const currentPage = +filter.page; + + const user = (options && options.currentUser) || null; + const userLeagues = (user && user.leagues?.id) || null; + + if (userLeagues) { + if (options?.currentUser?.leaguesId) { + where.leaguesId = options.currentUser.leaguesId; + } + } + + offset = currentPage * limit; + + const orderBy = null; + + const transaction = (options && options.transaction) || undefined; + + let include = [ + { + model: db.leagues, + as: 'leagues', + }, + + { + model: db.teams, + as: 'team', + + where: filter.team + ? { + [Op.or]: [ + { + id: { + [Op.in]: filter.team + .split('|') + .map((term) => Utils.uuid(term)), + }, + }, + { + name: { + [Op.or]: filter.team + .split('|') + .map((term) => ({ [Op.iLike]: `%${term}%` })), + }, + }, + ], + } + : {}, + }, + + { + model: db.seasons, + as: 'season', + + where: filter.season + ? { + [Op.or]: [ + { + id: { + [Op.in]: filter.season + .split('|') + .map((term) => Utils.uuid(term)), + }, + }, + { + name: { + [Op.or]: filter.season + .split('|') + .map((term) => ({ [Op.iLike]: `%${term}%` })), + }, + }, + ], + } + : {}, + }, + ]; + + if (filter) { + if (filter.id) { + where = { + ...where, + ['id']: Utils.uuid(filter.id), + }; + } + + if (filter.active !== undefined) { + where = { + ...where, + active: filter.active === true || filter.active === 'true', + }; + } + + if (filter.leagues) { + const listItems = filter.leagues.split('|').map((item) => { + return Utils.uuid(item); + }); + + where = { + ...where, + leaguesId: { [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 (globalAccess) { + delete where.leaguesId; + } + + 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.team_season_stats.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, + globalAccess, + organizationId, + ) { + let where = {}; + + if (!globalAccess && organizationId) { + where.organizationId = organizationId; + } + + if (query) { + where = { + [Op.or]: [ + { ['id']: Utils.uuid(query) }, + Utils.ilike('team_season_stats', 'id', query), + ], + }; + } + + const records = await db.team_season_stats.findAll({ + attributes: ['id', 'id'], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + orderBy: [['id', 'ASC']], + }); + + return records.map((record) => ({ + id: record.id, + label: record.id, + })); + } +}; diff --git a/backend/src/db/api/teams.js b/backend/src/db/api/teams.js index ef2e788..94151aa 100644 --- a/backend/src/db/api/teams.js +++ b/backend/src/db/api/teams.js @@ -177,6 +177,14 @@ module.exports = class TeamsDBApi { transaction, }); + output.player_game_scores_team = await teams.getPlayer_game_scores_team({ + transaction, + }); + + output.team_season_stats_team = await teams.getTeam_season_stats_team({ + transaction, + }); + output.league = await teams.getLeague({ transaction, }); diff --git a/backend/src/db/migrations/1756838714724.js b/backend/src/db/migrations/1756838714724.js new file mode 100644 index 0000000..0b386bd --- /dev/null +++ b/backend/src/db/migrations/1756838714724.js @@ -0,0 +1,90 @@ +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( + 'seasons', + { + 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 queryInterface.addColumn( + 'seasons', + 'leaguesId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'leagues', + key: 'id', + }, + }, + { transaction }, + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async down(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.removeColumn('seasons', 'leaguesId', { + transaction, + }); + + await queryInterface.dropTable('seasons', { transaction }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1756838754108.js b/backend/src/db/migrations/1756838754108.js new file mode 100644 index 0000000..eea0eb1 --- /dev/null +++ b/backend/src/db/migrations/1756838754108.js @@ -0,0 +1,47 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.addColumn( + 'seasons', + 'name', + { + type: Sequelize.DataTypes.TEXT, + }, + { transaction }, + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async down(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.removeColumn('seasons', 'name', { transaction }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1756838786759.js b/backend/src/db/migrations/1756838786759.js new file mode 100644 index 0000000..0c80050 --- /dev/null +++ b/backend/src/db/migrations/1756838786759.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( + 'seasons', + 'startdate', + { + type: Sequelize.DataTypes.DATEONLY, + }, + { transaction }, + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async down(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.removeColumn('seasons', 'startdate', { + transaction, + }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1756838811807.js b/backend/src/db/migrations/1756838811807.js new file mode 100644 index 0000000..6e37dd1 --- /dev/null +++ b/backend/src/db/migrations/1756838811807.js @@ -0,0 +1,47 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.addColumn( + 'seasons', + 'enddate', + { + type: Sequelize.DataTypes.DATEONLY, + }, + { transaction }, + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async down(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.removeColumn('seasons', 'enddate', { transaction }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1756838843347.js b/backend/src/db/migrations/1756838843347.js new file mode 100644 index 0000000..ca9e128 --- /dev/null +++ b/backend/src/db/migrations/1756838843347.js @@ -0,0 +1,52 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.addColumn( + 'seasons', + 'leagueId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'leagues', + key: 'id', + }, + }, + { transaction }, + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async down(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.removeColumn('seasons', 'leagueId', { transaction }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1756838870227.js b/backend/src/db/migrations/1756838870227.js new file mode 100644 index 0000000..6e5a4d8 --- /dev/null +++ b/backend/src/db/migrations/1756838870227.js @@ -0,0 +1,52 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.addColumn( + 'games', + 'seasonId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'seasons', + key: 'id', + }, + }, + { transaction }, + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async down(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.removeColumn('games', 'seasonId', { transaction }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1756838906955.js b/backend/src/db/migrations/1756838906955.js new file mode 100644 index 0000000..999188f --- /dev/null +++ b/backend/src/db/migrations/1756838906955.js @@ -0,0 +1,90 @@ +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( + 'player_game_scores', + { + 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 queryInterface.addColumn( + 'player_game_scores', + 'leaguesId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'leagues', + key: 'id', + }, + }, + { transaction }, + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async down(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.removeColumn('player_game_scores', 'leaguesId', { + transaction, + }); + + await queryInterface.dropTable('player_game_scores', { transaction }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1756838945828.js b/backend/src/db/migrations/1756838945828.js new file mode 100644 index 0000000..be0cde0 --- /dev/null +++ b/backend/src/db/migrations/1756838945828.js @@ -0,0 +1,54 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.addColumn( + 'player_game_scores', + 'gameId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'games', + key: 'id', + }, + }, + { transaction }, + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async down(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.removeColumn('player_game_scores', 'gameId', { + transaction, + }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1756838975793.js b/backend/src/db/migrations/1756838975793.js new file mode 100644 index 0000000..22fb63c --- /dev/null +++ b/backend/src/db/migrations/1756838975793.js @@ -0,0 +1,54 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.addColumn( + 'player_game_scores', + 'playerId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'players', + key: 'id', + }, + }, + { transaction }, + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async down(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.removeColumn('player_game_scores', 'playerId', { + transaction, + }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1756839010904.js b/backend/src/db/migrations/1756839010904.js new file mode 100644 index 0000000..add2e8d --- /dev/null +++ b/backend/src/db/migrations/1756839010904.js @@ -0,0 +1,54 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.addColumn( + 'player_game_scores', + 'teamId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'teams', + key: 'id', + }, + }, + { transaction }, + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async down(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.removeColumn('player_game_scores', 'teamId', { + transaction, + }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1756839033615.js b/backend/src/db/migrations/1756839033615.js new file mode 100644 index 0000000..20c4715 --- /dev/null +++ b/backend/src/db/migrations/1756839033615.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( + 'leagues', + 'handicapformula', + { + type: Sequelize.DataTypes.TEXT, + }, + { transaction }, + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async down(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.removeColumn('leagues', 'handicapformula', { + transaction, + }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1756839064708.js b/backend/src/db/migrations/1756839064708.js new file mode 100644 index 0000000..8955648 --- /dev/null +++ b/backend/src/db/migrations/1756839064708.js @@ -0,0 +1,90 @@ +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( + 'player_season_stats', + { + 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 queryInterface.addColumn( + 'player_season_stats', + 'leaguesId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'leagues', + key: 'id', + }, + }, + { transaction }, + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async down(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.removeColumn('player_season_stats', 'leaguesId', { + transaction, + }); + + await queryInterface.dropTable('player_season_stats', { transaction }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1756839091072.js b/backend/src/db/migrations/1756839091072.js new file mode 100644 index 0000000..7174a40 --- /dev/null +++ b/backend/src/db/migrations/1756839091072.js @@ -0,0 +1,54 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.addColumn( + 'player_season_stats', + 'playerId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'players', + key: 'id', + }, + }, + { transaction }, + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async down(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.removeColumn('player_season_stats', 'playerId', { + transaction, + }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1756839123590.js b/backend/src/db/migrations/1756839123590.js new file mode 100644 index 0000000..c9ba7bc --- /dev/null +++ b/backend/src/db/migrations/1756839123590.js @@ -0,0 +1,54 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.addColumn( + 'player_season_stats', + 'seasonId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'seasons', + key: 'id', + }, + }, + { transaction }, + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async down(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.removeColumn('player_season_stats', 'seasonId', { + transaction, + }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1756839155088.js b/backend/src/db/migrations/1756839155088.js new file mode 100644 index 0000000..c5918c7 --- /dev/null +++ b/backend/src/db/migrations/1756839155088.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( + 'player_season_stats', + 'totalpoints', + { + 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('player_season_stats', 'totalpoints', { + transaction, + }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1756839183953.js b/backend/src/db/migrations/1756839183953.js new file mode 100644 index 0000000..7f6ed92 --- /dev/null +++ b/backend/src/db/migrations/1756839183953.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( + 'player_season_stats', + 'gamesplayed', + { + 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('player_season_stats', 'gamesplayed', { + transaction, + }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1756839212457.js b/backend/src/db/migrations/1756839212457.js new file mode 100644 index 0000000..173936c --- /dev/null +++ b/backend/src/db/migrations/1756839212457.js @@ -0,0 +1,51 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.addColumn( + 'player_season_stats', + 'eightballruns', + { + 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( + 'player_season_stats', + 'eightballruns', + { transaction }, + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1756839243210.js b/backend/src/db/migrations/1756839243210.js new file mode 100644 index 0000000..f0656f9 --- /dev/null +++ b/backend/src/db/migrations/1756839243210.js @@ -0,0 +1,51 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.addColumn( + 'player_season_stats', + 'eightballbreaks', + { + 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( + 'player_season_stats', + 'eightballbreaks', + { transaction }, + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1756839276003.js b/backend/src/db/migrations/1756839276003.js new file mode 100644 index 0000000..26994ad --- /dev/null +++ b/backend/src/db/migrations/1756839276003.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( + 'player_season_stats', + 'nineballruns', + { + 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('player_season_stats', 'nineballruns', { + transaction, + }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1756839309071.js b/backend/src/db/migrations/1756839309071.js new file mode 100644 index 0000000..91d6f20 --- /dev/null +++ b/backend/src/db/migrations/1756839309071.js @@ -0,0 +1,90 @@ +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( + 'team_season_stats', + { + 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 queryInterface.addColumn( + 'team_season_stats', + 'leaguesId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'leagues', + key: 'id', + }, + }, + { transaction }, + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async down(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.removeColumn('team_season_stats', 'leaguesId', { + transaction, + }); + + await queryInterface.dropTable('team_season_stats', { transaction }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1756839330961.js b/backend/src/db/migrations/1756839330961.js new file mode 100644 index 0000000..d2dc7ca --- /dev/null +++ b/backend/src/db/migrations/1756839330961.js @@ -0,0 +1,54 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.addColumn( + 'team_season_stats', + 'teamId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'teams', + key: 'id', + }, + }, + { transaction }, + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async down(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.removeColumn('team_season_stats', 'teamId', { + transaction, + }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1756839362416.js b/backend/src/db/migrations/1756839362416.js new file mode 100644 index 0000000..3578001 --- /dev/null +++ b/backend/src/db/migrations/1756839362416.js @@ -0,0 +1,54 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.addColumn( + 'team_season_stats', + 'seasonId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'seasons', + key: 'id', + }, + }, + { transaction }, + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async down(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.removeColumn('team_season_stats', 'seasonId', { + transaction, + }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1756839362565.js b/backend/src/db/migrations/1756839362565.js new file mode 100644 index 0000000..e6bfba3 --- /dev/null +++ b/backend/src/db/migrations/1756839362565.js @@ -0,0 +1,36 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async down(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/models/games.js b/backend/src/db/models/games.js index 8a5fdab..276f980 100644 --- a/backend/src/db/models/games.js +++ b/backend/src/db/models/games.js @@ -42,6 +42,14 @@ module.exports = function (sequelize, DataTypes) { games.associate = (db) => { /// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity + db.games.hasMany(db.player_game_scores, { + as: 'player_game_scores_game', + foreignKey: { + name: 'gameId', + }, + constraints: false, + }); + //end loop db.games.belongsTo(db.leagues, { @@ -68,6 +76,14 @@ module.exports = function (sequelize, DataTypes) { constraints: false, }); + db.games.belongsTo(db.seasons, { + as: 'season', + foreignKey: { + name: 'seasonId', + }, + constraints: false, + }); + db.games.belongsTo(db.users, { as: 'createdBy', }); diff --git a/backend/src/db/models/leagues.js b/backend/src/db/models/leagues.js index c8d40bd..d24cc19 100644 --- a/backend/src/db/models/leagues.js +++ b/backend/src/db/models/leagues.js @@ -18,6 +18,10 @@ module.exports = function (sequelize, DataTypes) { type: DataTypes.TEXT, }, + handicapformula: { + type: DataTypes.TEXT, + }, + importHash: { type: DataTypes.STRING(255), allowNull: true, @@ -82,6 +86,46 @@ module.exports = function (sequelize, DataTypes) { constraints: false, }); + db.leagues.hasMany(db.seasons, { + as: 'seasons_leagues', + foreignKey: { + name: 'leaguesId', + }, + constraints: false, + }); + + db.leagues.hasMany(db.seasons, { + as: 'seasons_league', + foreignKey: { + name: 'leagueId', + }, + constraints: false, + }); + + db.leagues.hasMany(db.player_game_scores, { + as: 'player_game_scores_leagues', + foreignKey: { + name: 'leaguesId', + }, + constraints: false, + }); + + db.leagues.hasMany(db.player_season_stats, { + as: 'player_season_stats_leagues', + foreignKey: { + name: 'leaguesId', + }, + constraints: false, + }); + + db.leagues.hasMany(db.team_season_stats, { + as: 'team_season_stats_leagues', + foreignKey: { + name: 'leaguesId', + }, + constraints: false, + }); + //end loop db.leagues.belongsTo(db.users, { diff --git a/backend/src/db/models/player_game_scores.js b/backend/src/db/models/player_game_scores.js new file mode 100644 index 0000000..6bed1f3 --- /dev/null +++ b/backend/src/db/models/player_game_scores.js @@ -0,0 +1,77 @@ +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 player_game_scores = sequelize.define( + 'player_game_scores', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + player_game_scores.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.player_game_scores.belongsTo(db.leagues, { + as: 'leagues', + foreignKey: { + name: 'leaguesId', + }, + constraints: false, + }); + + db.player_game_scores.belongsTo(db.games, { + as: 'game', + foreignKey: { + name: 'gameId', + }, + constraints: false, + }); + + db.player_game_scores.belongsTo(db.players, { + as: 'player', + foreignKey: { + name: 'playerId', + }, + constraints: false, + }); + + db.player_game_scores.belongsTo(db.teams, { + as: 'team', + foreignKey: { + name: 'teamId', + }, + constraints: false, + }); + + db.player_game_scores.belongsTo(db.users, { + as: 'createdBy', + }); + + db.player_game_scores.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return player_game_scores; +}; diff --git a/backend/src/db/models/player_season_stats.js b/backend/src/db/models/player_season_stats.js new file mode 100644 index 0000000..90765b5 --- /dev/null +++ b/backend/src/db/models/player_season_stats.js @@ -0,0 +1,89 @@ +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 player_season_stats = sequelize.define( + 'player_season_stats', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + + totalpoints: { + type: DataTypes.INTEGER, + }, + + gamesplayed: { + type: DataTypes.INTEGER, + }, + + eightballruns: { + type: DataTypes.INTEGER, + }, + + eightballbreaks: { + type: DataTypes.INTEGER, + }, + + nineballruns: { + type: DataTypes.INTEGER, + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + player_season_stats.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.player_season_stats.belongsTo(db.leagues, { + as: 'leagues', + foreignKey: { + name: 'leaguesId', + }, + constraints: false, + }); + + db.player_season_stats.belongsTo(db.players, { + as: 'player', + foreignKey: { + name: 'playerId', + }, + constraints: false, + }); + + db.player_season_stats.belongsTo(db.seasons, { + as: 'season', + foreignKey: { + name: 'seasonId', + }, + constraints: false, + }); + + db.player_season_stats.belongsTo(db.users, { + as: 'createdBy', + }); + + db.player_season_stats.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return player_season_stats; +}; diff --git a/backend/src/db/models/players.js b/backend/src/db/models/players.js index b44b60f..1ec6bac 100644 --- a/backend/src/db/models/players.js +++ b/backend/src/db/models/players.js @@ -46,6 +46,22 @@ module.exports = function (sequelize, DataTypes) { players.associate = (db) => { /// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity + db.players.hasMany(db.player_game_scores, { + as: 'player_game_scores_player', + foreignKey: { + name: 'playerId', + }, + constraints: false, + }); + + db.players.hasMany(db.player_season_stats, { + as: 'player_season_stats_player', + foreignKey: { + name: 'playerId', + }, + constraints: false, + }); + //end loop db.players.belongsTo(db.teams, { diff --git a/backend/src/db/models/seasons.js b/backend/src/db/models/seasons.js new file mode 100644 index 0000000..43c36fa --- /dev/null +++ b/backend/src/db/models/seasons.js @@ -0,0 +1,109 @@ +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 seasons = sequelize.define( + 'seasons', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + + name: { + type: DataTypes.TEXT, + }, + + startdate: { + type: DataTypes.DATEONLY, + + get: function () { + return this.getDataValue('startdate') + ? moment.utc(this.getDataValue('startdate')).format('YYYY-MM-DD') + : null; + }, + }, + + enddate: { + type: DataTypes.DATEONLY, + + get: function () { + return this.getDataValue('enddate') + ? moment.utc(this.getDataValue('enddate')).format('YYYY-MM-DD') + : null; + }, + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + seasons.associate = (db) => { + /// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity + + db.seasons.hasMany(db.games, { + as: 'games_season', + foreignKey: { + name: 'seasonId', + }, + constraints: false, + }); + + db.seasons.hasMany(db.player_season_stats, { + as: 'player_season_stats_season', + foreignKey: { + name: 'seasonId', + }, + constraints: false, + }); + + db.seasons.hasMany(db.team_season_stats, { + as: 'team_season_stats_season', + foreignKey: { + name: 'seasonId', + }, + constraints: false, + }); + + //end loop + + db.seasons.belongsTo(db.leagues, { + as: 'leagues', + foreignKey: { + name: 'leaguesId', + }, + constraints: false, + }); + + db.seasons.belongsTo(db.leagues, { + as: 'league', + foreignKey: { + name: 'leagueId', + }, + constraints: false, + }); + + db.seasons.belongsTo(db.users, { + as: 'createdBy', + }); + + db.seasons.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return seasons; +}; diff --git a/backend/src/db/models/team_season_stats.js b/backend/src/db/models/team_season_stats.js new file mode 100644 index 0000000..a406c02 --- /dev/null +++ b/backend/src/db/models/team_season_stats.js @@ -0,0 +1,69 @@ +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 team_season_stats = sequelize.define( + 'team_season_stats', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + team_season_stats.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.team_season_stats.belongsTo(db.leagues, { + as: 'leagues', + foreignKey: { + name: 'leaguesId', + }, + constraints: false, + }); + + db.team_season_stats.belongsTo(db.teams, { + as: 'team', + foreignKey: { + name: 'teamId', + }, + constraints: false, + }); + + db.team_season_stats.belongsTo(db.seasons, { + as: 'season', + foreignKey: { + name: 'seasonId', + }, + constraints: false, + }); + + db.team_season_stats.belongsTo(db.users, { + as: 'createdBy', + }); + + db.team_season_stats.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return team_season_stats; +}; diff --git a/backend/src/db/models/teams.js b/backend/src/db/models/teams.js index 014f590..74d2347 100644 --- a/backend/src/db/models/teams.js +++ b/backend/src/db/models/teams.js @@ -68,6 +68,22 @@ module.exports = function (sequelize, DataTypes) { constraints: false, }); + db.teams.hasMany(db.player_game_scores, { + as: 'player_game_scores_team', + foreignKey: { + name: 'teamId', + }, + constraints: false, + }); + + db.teams.hasMany(db.team_season_stats, { + as: 'team_season_stats_team', + foreignKey: { + name: 'teamId', + }, + constraints: false, + }); + //end loop db.teams.belongsTo(db.leagues, { diff --git a/backend/src/db/seeders/20200430130760-user-roles.js b/backend/src/db/seeders/20200430130760-user-roles.js index 098f3f6..5089fff 100644 --- a/backend/src/db/seeders/20200430130760-user-roles.js +++ b/backend/src/db/seeders/20200430130760-user-roles.js @@ -104,6 +104,10 @@ module.exports = { 'roles', 'permissions', 'leagues', + 'seasons', + 'player_game_scores', + 'player_season_stats', + 'team_season_stats', , ]; await queryInterface.bulkInsert( @@ -682,6 +686,106 @@ primary key ("roles_permissionsId", "permissionId") permissionId: getId('DELETE_VENUES'), }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('CREATE_SEASONS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('READ_SEASONS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('UPDATE_SEASONS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('DELETE_SEASONS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('CREATE_PLAYER_GAME_SCORES'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('READ_PLAYER_GAME_SCORES'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('UPDATE_PLAYER_GAME_SCORES'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('DELETE_PLAYER_GAME_SCORES'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('CREATE_PLAYER_SEASON_STATS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('READ_PLAYER_SEASON_STATS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('UPDATE_PLAYER_SEASON_STATS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('DELETE_PLAYER_SEASON_STATS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('CREATE_TEAM_SEASON_STATS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('READ_TEAM_SEASON_STATS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('UPDATE_TEAM_SEASON_STATS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('DELETE_TEAM_SEASON_STATS'), + }, + { createdAt, updatedAt, @@ -882,6 +986,106 @@ primary key ("roles_permissionsId", "permissionId") permissionId: getId('DELETE_LEAGUES'), }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('CREATE_SEASONS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('READ_SEASONS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('UPDATE_SEASONS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('DELETE_SEASONS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('CREATE_PLAYER_GAME_SCORES'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('READ_PLAYER_GAME_SCORES'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('UPDATE_PLAYER_GAME_SCORES'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('DELETE_PLAYER_GAME_SCORES'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('CREATE_PLAYER_SEASON_STATS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('READ_PLAYER_SEASON_STATS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('UPDATE_PLAYER_SEASON_STATS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('DELETE_PLAYER_SEASON_STATS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('CREATE_TEAM_SEASON_STATS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('READ_TEAM_SEASON_STATS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('UPDATE_TEAM_SEASON_STATS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('DELETE_TEAM_SEASON_STATS'), + }, + { createdAt, updatedAt, diff --git a/backend/src/db/seeders/20231127130745-sample-data.js b/backend/src/db/seeders/20231127130745-sample-data.js index 1dfb94b..d6f3e96 100644 --- a/backend/src/db/seeders/20231127130745-sample-data.js +++ b/backend/src/db/seeders/20231127130745-sample-data.js @@ -11,6 +11,14 @@ const Venues = db.venues; const Leagues = db.leagues; +const Seasons = db.seasons; + +const PlayerGameScores = db.player_game_scores; + +const PlayerSeasonStats = db.player_season_stats; + +const TeamSeasonStats = db.team_season_stats; + const GamesData = [ { date: new Date('2023-10-01T18:00:00Z'), @@ -24,6 +32,8 @@ const GamesData = [ opponent_score: 3, // type code here for "relation_one" field + + // type code here for "relation_one" field }, { @@ -38,6 +48,8 @@ const GamesData = [ opponent_score: 4, // type code here for "relation_one" field + + // type code here for "relation_one" field }, { @@ -52,6 +64,40 @@ const GamesData = [ opponent_score: 2, // type code here for "relation_one" field + + // type code here for "relation_one" field + }, + + { + date: new Date('2023-10-04T21:00:00Z'), + + // type code here for "relation_one" field + + // type code here for "relation_one" field + + team_score: 3, + + opponent_score: 5, + + // type code here for "relation_one" field + + // type code here for "relation_one" field + }, + + { + date: new Date('2023-10-05T22:00:00Z'), + + // type code here for "relation_one" field + + // type code here for "relation_one" field + + team_score: 7, + + opponent_score: 1, + + // type code here for "relation_one" field + + // type code here for "relation_one" field }, ]; @@ -97,6 +143,34 @@ const PlayersData = [ // type code here for "relation_one" field }, + + { + first_name: 'Emily', + + last_name: 'Davis', + + // type code here for "relation_one" field + + total_points: 220, + + games_played: 30, + + // type code here for "relation_one" field + }, + + { + first_name: 'Chris', + + last_name: 'Brown', + + // type code here for "relation_one" field + + total_points: 170, + + games_played: 18, + + // type code here for "relation_one" field + }, ]; const TeamsData = [ @@ -135,6 +209,30 @@ const TeamsData = [ // type code here for "relation_one" field }, + + { + name: 'Bowling Brawlers', + + // type code here for "relation_one" field + + // type code here for "relation_one" field + + // type code here for "relation_many" field + + // type code here for "relation_one" field + }, + + { + name: 'Horseshoe Heroes', + + // type code here for "relation_one" field + + // type code here for "relation_one" field + + // type code here for "relation_many" field + + // type code here for "relation_one" field + }, ]; const VenuesData = [ @@ -161,19 +259,276 @@ const VenuesData = [ // type code here for "relation_many" field }, + + { + name: 'Bowling Alley', + + address: '101 Pine St, Uptown', + + // type code here for "relation_many" field + }, + + { + name: 'Horseshoe Park', + + address: '202 Maple St, Countryside', + + // type code here for "relation_many" field + }, ]; const LeaguesData = [ { name: 'Downtown Pool League', + + handicapformula: 'Stephen Hawking', }, { name: 'City Darts Championship', + + handicapformula: 'Paul Ehrlich', }, { name: 'Foosball Frenzy', + + handicapformula: 'Ludwig Boltzmann', + }, + + { + name: 'Bowling Bonanza', + + handicapformula: 'Max Delbruck', + }, + + { + name: 'Horseshoe Hoedown', + + handicapformula: 'Gustav Kirchhoff', + }, +]; + +const SeasonsData = [ + { + // type code here for "relation_one" field + + name: 'Rudolf Virchow', + + startdate: new Date(Date.now()), + + enddate: new Date(Date.now()), + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + name: 'Paul Ehrlich', + + startdate: new Date(Date.now()), + + enddate: new Date(Date.now()), + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + name: 'Pierre Simon de Laplace', + + startdate: new Date(Date.now()), + + enddate: new Date(Date.now()), + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + name: 'John von Neumann', + + startdate: new Date(Date.now()), + + enddate: new Date(Date.now()), + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + name: 'Hans Bethe', + + startdate: new Date(Date.now()), + + enddate: new Date(Date.now()), + + // type code here for "relation_one" field + }, +]; + +const PlayerGameScoresData = [ + { + // type code here for "relation_one" field + // type code here for "relation_one" field + // type code here for "relation_one" field + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + // type code here for "relation_one" field + // type code here for "relation_one" field + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + // type code here for "relation_one" field + // type code here for "relation_one" field + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + // type code here for "relation_one" field + // type code here for "relation_one" field + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + // type code here for "relation_one" field + // type code here for "relation_one" field + // type code here for "relation_one" field + }, +]; + +const PlayerSeasonStatsData = [ + { + // type code here for "relation_one" field + + // type code here for "relation_one" field + + // type code here for "relation_one" field + + totalpoints: 6, + + gamesplayed: 2, + + eightballruns: 7, + + eightballbreaks: 5, + + nineballruns: 7, + }, + + { + // type code here for "relation_one" field + + // type code here for "relation_one" field + + // type code here for "relation_one" field + + totalpoints: 6, + + gamesplayed: 8, + + eightballruns: 8, + + eightballbreaks: 8, + + nineballruns: 1, + }, + + { + // type code here for "relation_one" field + + // type code here for "relation_one" field + + // type code here for "relation_one" field + + totalpoints: 4, + + gamesplayed: 2, + + eightballruns: 9, + + eightballbreaks: 8, + + nineballruns: 2, + }, + + { + // type code here for "relation_one" field + + // type code here for "relation_one" field + + // type code here for "relation_one" field + + totalpoints: 8, + + gamesplayed: 6, + + eightballruns: 9, + + eightballbreaks: 3, + + nineballruns: 5, + }, + + { + // type code here for "relation_one" field + + // type code here for "relation_one" field + + // type code here for "relation_one" field + + totalpoints: 4, + + gamesplayed: 9, + + eightballruns: 7, + + eightballbreaks: 8, + + nineballruns: 2, + }, +]; + +const TeamSeasonStatsData = [ + { + // type code here for "relation_one" field + // type code here for "relation_one" field + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + // type code here for "relation_one" field + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + // type code here for "relation_one" field + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + // type code here for "relation_one" field + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + // type code here for "relation_one" field + // type code here for "relation_one" field }, ]; @@ -212,6 +567,28 @@ async function associateUserWithLeague() { if (User2?.setLeague) { await User2.setLeague(relatedLeague2); } + + const relatedLeague3 = await Leagues.findOne({ + offset: Math.floor(Math.random() * (await Leagues.count())), + }); + const User3 = await Users.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (User3?.setLeague) { + await User3.setLeague(relatedLeague3); + } + + const relatedLeague4 = await Leagues.findOne({ + offset: Math.floor(Math.random() * (await Leagues.count())), + }); + const User4 = await Users.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (User4?.setLeague) { + await User4.setLeague(relatedLeague4); + } } async function associateGameWithLeague() { @@ -247,6 +624,28 @@ async function associateGameWithLeague() { if (Game2?.setLeague) { await Game2.setLeague(relatedLeague2); } + + const relatedLeague3 = await Leagues.findOne({ + offset: Math.floor(Math.random() * (await Leagues.count())), + }); + const Game3 = await Games.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (Game3?.setLeague) { + await Game3.setLeague(relatedLeague3); + } + + const relatedLeague4 = await Leagues.findOne({ + offset: Math.floor(Math.random() * (await Leagues.count())), + }); + const Game4 = await Games.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (Game4?.setLeague) { + await Game4.setLeague(relatedLeague4); + } } async function associateGameWithTeam() { @@ -282,6 +681,28 @@ async function associateGameWithTeam() { if (Game2?.setTeam) { await Game2.setTeam(relatedTeam2); } + + const relatedTeam3 = await Teams.findOne({ + offset: Math.floor(Math.random() * (await Teams.count())), + }); + const Game3 = await Games.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (Game3?.setTeam) { + await Game3.setTeam(relatedTeam3); + } + + const relatedTeam4 = await Teams.findOne({ + offset: Math.floor(Math.random() * (await Teams.count())), + }); + const Game4 = await Games.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (Game4?.setTeam) { + await Game4.setTeam(relatedTeam4); + } } async function associateGameWithLeague() { @@ -317,6 +738,85 @@ async function associateGameWithLeague() { if (Game2?.setLeague) { await Game2.setLeague(relatedLeague2); } + + const relatedLeague3 = await Leagues.findOne({ + offset: Math.floor(Math.random() * (await Leagues.count())), + }); + const Game3 = await Games.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (Game3?.setLeague) { + await Game3.setLeague(relatedLeague3); + } + + const relatedLeague4 = await Leagues.findOne({ + offset: Math.floor(Math.random() * (await Leagues.count())), + }); + const Game4 = await Games.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (Game4?.setLeague) { + await Game4.setLeague(relatedLeague4); + } +} + +async function associateGameWithSeason() { + const relatedSeason0 = await Seasons.findOne({ + offset: Math.floor(Math.random() * (await Seasons.count())), + }); + const Game0 = await Games.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (Game0?.setSeason) { + await Game0.setSeason(relatedSeason0); + } + + const relatedSeason1 = await Seasons.findOne({ + offset: Math.floor(Math.random() * (await Seasons.count())), + }); + const Game1 = await Games.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (Game1?.setSeason) { + await Game1.setSeason(relatedSeason1); + } + + const relatedSeason2 = await Seasons.findOne({ + offset: Math.floor(Math.random() * (await Seasons.count())), + }); + const Game2 = await Games.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (Game2?.setSeason) { + await Game2.setSeason(relatedSeason2); + } + + const relatedSeason3 = await Seasons.findOne({ + offset: Math.floor(Math.random() * (await Seasons.count())), + }); + const Game3 = await Games.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (Game3?.setSeason) { + await Game3.setSeason(relatedSeason3); + } + + const relatedSeason4 = await Seasons.findOne({ + offset: Math.floor(Math.random() * (await Seasons.count())), + }); + const Game4 = await Games.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (Game4?.setSeason) { + await Game4.setSeason(relatedSeason4); + } } async function associatePlayerWithTeam() { @@ -352,6 +852,28 @@ async function associatePlayerWithTeam() { if (Player2?.setTeam) { await Player2.setTeam(relatedTeam2); } + + const relatedTeam3 = await Teams.findOne({ + offset: Math.floor(Math.random() * (await Teams.count())), + }); + const Player3 = await Players.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (Player3?.setTeam) { + await Player3.setTeam(relatedTeam3); + } + + const relatedTeam4 = await Teams.findOne({ + offset: Math.floor(Math.random() * (await Teams.count())), + }); + const Player4 = await Players.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (Player4?.setTeam) { + await Player4.setTeam(relatedTeam4); + } } async function associatePlayerWithLeague() { @@ -387,6 +909,28 @@ async function associatePlayerWithLeague() { if (Player2?.setLeague) { await Player2.setLeague(relatedLeague2); } + + const relatedLeague3 = await Leagues.findOne({ + offset: Math.floor(Math.random() * (await Leagues.count())), + }); + const Player3 = await Players.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (Player3?.setLeague) { + await Player3.setLeague(relatedLeague3); + } + + const relatedLeague4 = await Leagues.findOne({ + offset: Math.floor(Math.random() * (await Leagues.count())), + }); + const Player4 = await Players.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (Player4?.setLeague) { + await Player4.setLeague(relatedLeague4); + } } async function associateTeamWithLeague() { @@ -422,6 +966,28 @@ async function associateTeamWithLeague() { if (Team2?.setLeague) { await Team2.setLeague(relatedLeague2); } + + const relatedLeague3 = await Leagues.findOne({ + offset: Math.floor(Math.random() * (await Leagues.count())), + }); + const Team3 = await Teams.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (Team3?.setLeague) { + await Team3.setLeague(relatedLeague3); + } + + const relatedLeague4 = await Leagues.findOne({ + offset: Math.floor(Math.random() * (await Leagues.count())), + }); + const Team4 = await Teams.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (Team4?.setLeague) { + await Team4.setLeague(relatedLeague4); + } } async function associateTeamWithCaptain() { @@ -457,6 +1023,28 @@ async function associateTeamWithCaptain() { if (Team2?.setCaptain) { await Team2.setCaptain(relatedCaptain2); } + + const relatedCaptain3 = await Users.findOne({ + offset: Math.floor(Math.random() * (await Users.count())), + }); + const Team3 = await Teams.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (Team3?.setCaptain) { + await Team3.setCaptain(relatedCaptain3); + } + + const relatedCaptain4 = await Users.findOne({ + offset: Math.floor(Math.random() * (await Users.count())), + }); + const Team4 = await Teams.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (Team4?.setCaptain) { + await Team4.setCaptain(relatedCaptain4); + } } // Similar logic for "relation_many" @@ -494,10 +1082,716 @@ async function associateTeamWithLeague() { if (Team2?.setLeague) { await Team2.setLeague(relatedLeague2); } + + const relatedLeague3 = await Leagues.findOne({ + offset: Math.floor(Math.random() * (await Leagues.count())), + }); + const Team3 = await Teams.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (Team3?.setLeague) { + await Team3.setLeague(relatedLeague3); + } + + const relatedLeague4 = await Leagues.findOne({ + offset: Math.floor(Math.random() * (await Leagues.count())), + }); + const Team4 = await Teams.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (Team4?.setLeague) { + await Team4.setLeague(relatedLeague4); + } } // Similar logic for "relation_many" +async function associateSeasonWithLeague() { + const relatedLeague0 = await Leagues.findOne({ + offset: Math.floor(Math.random() * (await Leagues.count())), + }); + const Season0 = await Seasons.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (Season0?.setLeague) { + await Season0.setLeague(relatedLeague0); + } + + const relatedLeague1 = await Leagues.findOne({ + offset: Math.floor(Math.random() * (await Leagues.count())), + }); + const Season1 = await Seasons.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (Season1?.setLeague) { + await Season1.setLeague(relatedLeague1); + } + + const relatedLeague2 = await Leagues.findOne({ + offset: Math.floor(Math.random() * (await Leagues.count())), + }); + const Season2 = await Seasons.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (Season2?.setLeague) { + await Season2.setLeague(relatedLeague2); + } + + const relatedLeague3 = await Leagues.findOne({ + offset: Math.floor(Math.random() * (await Leagues.count())), + }); + const Season3 = await Seasons.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (Season3?.setLeague) { + await Season3.setLeague(relatedLeague3); + } + + const relatedLeague4 = await Leagues.findOne({ + offset: Math.floor(Math.random() * (await Leagues.count())), + }); + const Season4 = await Seasons.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (Season4?.setLeague) { + await Season4.setLeague(relatedLeague4); + } +} + +async function associateSeasonWithLeague() { + const relatedLeague0 = await Leagues.findOne({ + offset: Math.floor(Math.random() * (await Leagues.count())), + }); + const Season0 = await Seasons.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (Season0?.setLeague) { + await Season0.setLeague(relatedLeague0); + } + + const relatedLeague1 = await Leagues.findOne({ + offset: Math.floor(Math.random() * (await Leagues.count())), + }); + const Season1 = await Seasons.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (Season1?.setLeague) { + await Season1.setLeague(relatedLeague1); + } + + const relatedLeague2 = await Leagues.findOne({ + offset: Math.floor(Math.random() * (await Leagues.count())), + }); + const Season2 = await Seasons.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (Season2?.setLeague) { + await Season2.setLeague(relatedLeague2); + } + + const relatedLeague3 = await Leagues.findOne({ + offset: Math.floor(Math.random() * (await Leagues.count())), + }); + const Season3 = await Seasons.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (Season3?.setLeague) { + await Season3.setLeague(relatedLeague3); + } + + const relatedLeague4 = await Leagues.findOne({ + offset: Math.floor(Math.random() * (await Leagues.count())), + }); + const Season4 = await Seasons.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (Season4?.setLeague) { + await Season4.setLeague(relatedLeague4); + } +} + +async function associatePlayerGameScoreWithLeague() { + const relatedLeague0 = await Leagues.findOne({ + offset: Math.floor(Math.random() * (await Leagues.count())), + }); + const PlayerGameScore0 = await PlayerGameScores.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (PlayerGameScore0?.setLeague) { + await PlayerGameScore0.setLeague(relatedLeague0); + } + + const relatedLeague1 = await Leagues.findOne({ + offset: Math.floor(Math.random() * (await Leagues.count())), + }); + const PlayerGameScore1 = await PlayerGameScores.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (PlayerGameScore1?.setLeague) { + await PlayerGameScore1.setLeague(relatedLeague1); + } + + const relatedLeague2 = await Leagues.findOne({ + offset: Math.floor(Math.random() * (await Leagues.count())), + }); + const PlayerGameScore2 = await PlayerGameScores.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (PlayerGameScore2?.setLeague) { + await PlayerGameScore2.setLeague(relatedLeague2); + } + + const relatedLeague3 = await Leagues.findOne({ + offset: Math.floor(Math.random() * (await Leagues.count())), + }); + const PlayerGameScore3 = await PlayerGameScores.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (PlayerGameScore3?.setLeague) { + await PlayerGameScore3.setLeague(relatedLeague3); + } + + const relatedLeague4 = await Leagues.findOne({ + offset: Math.floor(Math.random() * (await Leagues.count())), + }); + const PlayerGameScore4 = await PlayerGameScores.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (PlayerGameScore4?.setLeague) { + await PlayerGameScore4.setLeague(relatedLeague4); + } +} + +async function associatePlayerGameScoreWithGame() { + const relatedGame0 = await Games.findOne({ + offset: Math.floor(Math.random() * (await Games.count())), + }); + const PlayerGameScore0 = await PlayerGameScores.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (PlayerGameScore0?.setGame) { + await PlayerGameScore0.setGame(relatedGame0); + } + + const relatedGame1 = await Games.findOne({ + offset: Math.floor(Math.random() * (await Games.count())), + }); + const PlayerGameScore1 = await PlayerGameScores.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (PlayerGameScore1?.setGame) { + await PlayerGameScore1.setGame(relatedGame1); + } + + const relatedGame2 = await Games.findOne({ + offset: Math.floor(Math.random() * (await Games.count())), + }); + const PlayerGameScore2 = await PlayerGameScores.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (PlayerGameScore2?.setGame) { + await PlayerGameScore2.setGame(relatedGame2); + } + + const relatedGame3 = await Games.findOne({ + offset: Math.floor(Math.random() * (await Games.count())), + }); + const PlayerGameScore3 = await PlayerGameScores.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (PlayerGameScore3?.setGame) { + await PlayerGameScore3.setGame(relatedGame3); + } + + const relatedGame4 = await Games.findOne({ + offset: Math.floor(Math.random() * (await Games.count())), + }); + const PlayerGameScore4 = await PlayerGameScores.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (PlayerGameScore4?.setGame) { + await PlayerGameScore4.setGame(relatedGame4); + } +} + +async function associatePlayerGameScoreWithPlayer() { + const relatedPlayer0 = await Players.findOne({ + offset: Math.floor(Math.random() * (await Players.count())), + }); + const PlayerGameScore0 = await PlayerGameScores.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (PlayerGameScore0?.setPlayer) { + await PlayerGameScore0.setPlayer(relatedPlayer0); + } + + const relatedPlayer1 = await Players.findOne({ + offset: Math.floor(Math.random() * (await Players.count())), + }); + const PlayerGameScore1 = await PlayerGameScores.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (PlayerGameScore1?.setPlayer) { + await PlayerGameScore1.setPlayer(relatedPlayer1); + } + + const relatedPlayer2 = await Players.findOne({ + offset: Math.floor(Math.random() * (await Players.count())), + }); + const PlayerGameScore2 = await PlayerGameScores.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (PlayerGameScore2?.setPlayer) { + await PlayerGameScore2.setPlayer(relatedPlayer2); + } + + const relatedPlayer3 = await Players.findOne({ + offset: Math.floor(Math.random() * (await Players.count())), + }); + const PlayerGameScore3 = await PlayerGameScores.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (PlayerGameScore3?.setPlayer) { + await PlayerGameScore3.setPlayer(relatedPlayer3); + } + + const relatedPlayer4 = await Players.findOne({ + offset: Math.floor(Math.random() * (await Players.count())), + }); + const PlayerGameScore4 = await PlayerGameScores.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (PlayerGameScore4?.setPlayer) { + await PlayerGameScore4.setPlayer(relatedPlayer4); + } +} + +async function associatePlayerGameScoreWithTeam() { + const relatedTeam0 = await Teams.findOne({ + offset: Math.floor(Math.random() * (await Teams.count())), + }); + const PlayerGameScore0 = await PlayerGameScores.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (PlayerGameScore0?.setTeam) { + await PlayerGameScore0.setTeam(relatedTeam0); + } + + const relatedTeam1 = await Teams.findOne({ + offset: Math.floor(Math.random() * (await Teams.count())), + }); + const PlayerGameScore1 = await PlayerGameScores.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (PlayerGameScore1?.setTeam) { + await PlayerGameScore1.setTeam(relatedTeam1); + } + + const relatedTeam2 = await Teams.findOne({ + offset: Math.floor(Math.random() * (await Teams.count())), + }); + const PlayerGameScore2 = await PlayerGameScores.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (PlayerGameScore2?.setTeam) { + await PlayerGameScore2.setTeam(relatedTeam2); + } + + const relatedTeam3 = await Teams.findOne({ + offset: Math.floor(Math.random() * (await Teams.count())), + }); + const PlayerGameScore3 = await PlayerGameScores.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (PlayerGameScore3?.setTeam) { + await PlayerGameScore3.setTeam(relatedTeam3); + } + + const relatedTeam4 = await Teams.findOne({ + offset: Math.floor(Math.random() * (await Teams.count())), + }); + const PlayerGameScore4 = await PlayerGameScores.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (PlayerGameScore4?.setTeam) { + await PlayerGameScore4.setTeam(relatedTeam4); + } +} + +async function associatePlayerSeasonStatWithLeague() { + const relatedLeague0 = await Leagues.findOne({ + offset: Math.floor(Math.random() * (await Leagues.count())), + }); + const PlayerSeasonStat0 = await PlayerSeasonStats.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (PlayerSeasonStat0?.setLeague) { + await PlayerSeasonStat0.setLeague(relatedLeague0); + } + + const relatedLeague1 = await Leagues.findOne({ + offset: Math.floor(Math.random() * (await Leagues.count())), + }); + const PlayerSeasonStat1 = await PlayerSeasonStats.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (PlayerSeasonStat1?.setLeague) { + await PlayerSeasonStat1.setLeague(relatedLeague1); + } + + const relatedLeague2 = await Leagues.findOne({ + offset: Math.floor(Math.random() * (await Leagues.count())), + }); + const PlayerSeasonStat2 = await PlayerSeasonStats.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (PlayerSeasonStat2?.setLeague) { + await PlayerSeasonStat2.setLeague(relatedLeague2); + } + + const relatedLeague3 = await Leagues.findOne({ + offset: Math.floor(Math.random() * (await Leagues.count())), + }); + const PlayerSeasonStat3 = await PlayerSeasonStats.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (PlayerSeasonStat3?.setLeague) { + await PlayerSeasonStat3.setLeague(relatedLeague3); + } + + const relatedLeague4 = await Leagues.findOne({ + offset: Math.floor(Math.random() * (await Leagues.count())), + }); + const PlayerSeasonStat4 = await PlayerSeasonStats.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (PlayerSeasonStat4?.setLeague) { + await PlayerSeasonStat4.setLeague(relatedLeague4); + } +} + +async function associatePlayerSeasonStatWithPlayer() { + const relatedPlayer0 = await Players.findOne({ + offset: Math.floor(Math.random() * (await Players.count())), + }); + const PlayerSeasonStat0 = await PlayerSeasonStats.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (PlayerSeasonStat0?.setPlayer) { + await PlayerSeasonStat0.setPlayer(relatedPlayer0); + } + + const relatedPlayer1 = await Players.findOne({ + offset: Math.floor(Math.random() * (await Players.count())), + }); + const PlayerSeasonStat1 = await PlayerSeasonStats.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (PlayerSeasonStat1?.setPlayer) { + await PlayerSeasonStat1.setPlayer(relatedPlayer1); + } + + const relatedPlayer2 = await Players.findOne({ + offset: Math.floor(Math.random() * (await Players.count())), + }); + const PlayerSeasonStat2 = await PlayerSeasonStats.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (PlayerSeasonStat2?.setPlayer) { + await PlayerSeasonStat2.setPlayer(relatedPlayer2); + } + + const relatedPlayer3 = await Players.findOne({ + offset: Math.floor(Math.random() * (await Players.count())), + }); + const PlayerSeasonStat3 = await PlayerSeasonStats.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (PlayerSeasonStat3?.setPlayer) { + await PlayerSeasonStat3.setPlayer(relatedPlayer3); + } + + const relatedPlayer4 = await Players.findOne({ + offset: Math.floor(Math.random() * (await Players.count())), + }); + const PlayerSeasonStat4 = await PlayerSeasonStats.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (PlayerSeasonStat4?.setPlayer) { + await PlayerSeasonStat4.setPlayer(relatedPlayer4); + } +} + +async function associatePlayerSeasonStatWithSeason() { + const relatedSeason0 = await Seasons.findOne({ + offset: Math.floor(Math.random() * (await Seasons.count())), + }); + const PlayerSeasonStat0 = await PlayerSeasonStats.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (PlayerSeasonStat0?.setSeason) { + await PlayerSeasonStat0.setSeason(relatedSeason0); + } + + const relatedSeason1 = await Seasons.findOne({ + offset: Math.floor(Math.random() * (await Seasons.count())), + }); + const PlayerSeasonStat1 = await PlayerSeasonStats.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (PlayerSeasonStat1?.setSeason) { + await PlayerSeasonStat1.setSeason(relatedSeason1); + } + + const relatedSeason2 = await Seasons.findOne({ + offset: Math.floor(Math.random() * (await Seasons.count())), + }); + const PlayerSeasonStat2 = await PlayerSeasonStats.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (PlayerSeasonStat2?.setSeason) { + await PlayerSeasonStat2.setSeason(relatedSeason2); + } + + const relatedSeason3 = await Seasons.findOne({ + offset: Math.floor(Math.random() * (await Seasons.count())), + }); + const PlayerSeasonStat3 = await PlayerSeasonStats.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (PlayerSeasonStat3?.setSeason) { + await PlayerSeasonStat3.setSeason(relatedSeason3); + } + + const relatedSeason4 = await Seasons.findOne({ + offset: Math.floor(Math.random() * (await Seasons.count())), + }); + const PlayerSeasonStat4 = await PlayerSeasonStats.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (PlayerSeasonStat4?.setSeason) { + await PlayerSeasonStat4.setSeason(relatedSeason4); + } +} + +async function associateTeamSeasonStatWithLeague() { + const relatedLeague0 = await Leagues.findOne({ + offset: Math.floor(Math.random() * (await Leagues.count())), + }); + const TeamSeasonStat0 = await TeamSeasonStats.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (TeamSeasonStat0?.setLeague) { + await TeamSeasonStat0.setLeague(relatedLeague0); + } + + const relatedLeague1 = await Leagues.findOne({ + offset: Math.floor(Math.random() * (await Leagues.count())), + }); + const TeamSeasonStat1 = await TeamSeasonStats.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (TeamSeasonStat1?.setLeague) { + await TeamSeasonStat1.setLeague(relatedLeague1); + } + + const relatedLeague2 = await Leagues.findOne({ + offset: Math.floor(Math.random() * (await Leagues.count())), + }); + const TeamSeasonStat2 = await TeamSeasonStats.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (TeamSeasonStat2?.setLeague) { + await TeamSeasonStat2.setLeague(relatedLeague2); + } + + const relatedLeague3 = await Leagues.findOne({ + offset: Math.floor(Math.random() * (await Leagues.count())), + }); + const TeamSeasonStat3 = await TeamSeasonStats.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (TeamSeasonStat3?.setLeague) { + await TeamSeasonStat3.setLeague(relatedLeague3); + } + + const relatedLeague4 = await Leagues.findOne({ + offset: Math.floor(Math.random() * (await Leagues.count())), + }); + const TeamSeasonStat4 = await TeamSeasonStats.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (TeamSeasonStat4?.setLeague) { + await TeamSeasonStat4.setLeague(relatedLeague4); + } +} + +async function associateTeamSeasonStatWithTeam() { + const relatedTeam0 = await Teams.findOne({ + offset: Math.floor(Math.random() * (await Teams.count())), + }); + const TeamSeasonStat0 = await TeamSeasonStats.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (TeamSeasonStat0?.setTeam) { + await TeamSeasonStat0.setTeam(relatedTeam0); + } + + const relatedTeam1 = await Teams.findOne({ + offset: Math.floor(Math.random() * (await Teams.count())), + }); + const TeamSeasonStat1 = await TeamSeasonStats.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (TeamSeasonStat1?.setTeam) { + await TeamSeasonStat1.setTeam(relatedTeam1); + } + + const relatedTeam2 = await Teams.findOne({ + offset: Math.floor(Math.random() * (await Teams.count())), + }); + const TeamSeasonStat2 = await TeamSeasonStats.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (TeamSeasonStat2?.setTeam) { + await TeamSeasonStat2.setTeam(relatedTeam2); + } + + const relatedTeam3 = await Teams.findOne({ + offset: Math.floor(Math.random() * (await Teams.count())), + }); + const TeamSeasonStat3 = await TeamSeasonStats.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (TeamSeasonStat3?.setTeam) { + await TeamSeasonStat3.setTeam(relatedTeam3); + } + + const relatedTeam4 = await Teams.findOne({ + offset: Math.floor(Math.random() * (await Teams.count())), + }); + const TeamSeasonStat4 = await TeamSeasonStats.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (TeamSeasonStat4?.setTeam) { + await TeamSeasonStat4.setTeam(relatedTeam4); + } +} + +async function associateTeamSeasonStatWithSeason() { + const relatedSeason0 = await Seasons.findOne({ + offset: Math.floor(Math.random() * (await Seasons.count())), + }); + const TeamSeasonStat0 = await TeamSeasonStats.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (TeamSeasonStat0?.setSeason) { + await TeamSeasonStat0.setSeason(relatedSeason0); + } + + const relatedSeason1 = await Seasons.findOne({ + offset: Math.floor(Math.random() * (await Seasons.count())), + }); + const TeamSeasonStat1 = await TeamSeasonStats.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (TeamSeasonStat1?.setSeason) { + await TeamSeasonStat1.setSeason(relatedSeason1); + } + + const relatedSeason2 = await Seasons.findOne({ + offset: Math.floor(Math.random() * (await Seasons.count())), + }); + const TeamSeasonStat2 = await TeamSeasonStats.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (TeamSeasonStat2?.setSeason) { + await TeamSeasonStat2.setSeason(relatedSeason2); + } + + const relatedSeason3 = await Seasons.findOne({ + offset: Math.floor(Math.random() * (await Seasons.count())), + }); + const TeamSeasonStat3 = await TeamSeasonStats.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (TeamSeasonStat3?.setSeason) { + await TeamSeasonStat3.setSeason(relatedSeason3); + } + + const relatedSeason4 = await Seasons.findOne({ + offset: Math.floor(Math.random() * (await Seasons.count())), + }); + const TeamSeasonStat4 = await TeamSeasonStats.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (TeamSeasonStat4?.setSeason) { + await TeamSeasonStat4.setSeason(relatedSeason4); + } +} + module.exports = { up: async (queryInterface, Sequelize) => { await Games.bulkCreate(GamesData); @@ -510,6 +1804,14 @@ module.exports = { await Leagues.bulkCreate(LeaguesData); + await Seasons.bulkCreate(SeasonsData); + + await PlayerGameScores.bulkCreate(PlayerGameScoresData); + + await PlayerSeasonStats.bulkCreate(PlayerSeasonStatsData); + + await TeamSeasonStats.bulkCreate(TeamSeasonStatsData); + await Promise.all([ // Similar logic for "relation_many" @@ -521,6 +1823,8 @@ module.exports = { await associateGameWithLeague(), + await associateGameWithSeason(), + await associatePlayerWithTeam(), await associatePlayerWithLeague(), @@ -534,6 +1838,30 @@ module.exports = { await associateTeamWithLeague(), // Similar logic for "relation_many" + + await associateSeasonWithLeague(), + + await associateSeasonWithLeague(), + + await associatePlayerGameScoreWithLeague(), + + await associatePlayerGameScoreWithGame(), + + await associatePlayerGameScoreWithPlayer(), + + await associatePlayerGameScoreWithTeam(), + + await associatePlayerSeasonStatWithLeague(), + + await associatePlayerSeasonStatWithPlayer(), + + await associatePlayerSeasonStatWithSeason(), + + await associateTeamSeasonStatWithLeague(), + + await associateTeamSeasonStatWithTeam(), + + await associateTeamSeasonStatWithSeason(), ]); }, @@ -547,5 +1875,13 @@ module.exports = { await queryInterface.bulkDelete('venues', null, {}); await queryInterface.bulkDelete('leagues', null, {}); + + await queryInterface.bulkDelete('seasons', null, {}); + + await queryInterface.bulkDelete('player_game_scores', null, {}); + + await queryInterface.bulkDelete('player_season_stats', null, {}); + + await queryInterface.bulkDelete('team_season_stats', null, {}); }, }; diff --git a/backend/src/db/seeders/20250902184514.js b/backend/src/db/seeders/20250902184514.js new file mode 100644 index 0000000..4bd5cc5 --- /dev/null +++ b/backend/src/db/seeders/20250902184514.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 = ['seasons']; + + 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.super_admin }, + }); + + if (adminRole) { + // Add permissions to admin role if it exists + await adminRole.addPermissions(permissionsIds); + } + }, + down: async (queryInterface, Sequelize) => { + await queryInterface.bulkDelete( + 'permissions', + entities.flatMap(createPermissions), + ); + }, +}; diff --git a/backend/src/db/seeders/20250902184826.js b/backend/src/db/seeders/20250902184826.js new file mode 100644 index 0000000..79afb04 --- /dev/null +++ b/backend/src/db/seeders/20250902184826.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 = ['player_game_scores']; + + 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.super_admin }, + }); + + if (adminRole) { + // Add permissions to admin role if it exists + await adminRole.addPermissions(permissionsIds); + } + }, + down: async (queryInterface, Sequelize) => { + await queryInterface.bulkDelete( + 'permissions', + entities.flatMap(createPermissions), + ); + }, +}; diff --git a/backend/src/db/seeders/20250902185104.js b/backend/src/db/seeders/20250902185104.js new file mode 100644 index 0000000..674d155 --- /dev/null +++ b/backend/src/db/seeders/20250902185104.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 = ['player_season_stats']; + + 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.super_admin }, + }); + + if (adminRole) { + // Add permissions to admin role if it exists + await adminRole.addPermissions(permissionsIds); + } + }, + down: async (queryInterface, Sequelize) => { + await queryInterface.bulkDelete( + 'permissions', + entities.flatMap(createPermissions), + ); + }, +}; diff --git a/backend/src/db/seeders/20250902185509.js b/backend/src/db/seeders/20250902185509.js new file mode 100644 index 0000000..6126eae --- /dev/null +++ b/backend/src/db/seeders/20250902185509.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 = ['team_season_stats']; + + 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.super_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 59b68b1..3cca09d 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -37,6 +37,14 @@ const permissionsRoutes = require('./routes/permissions'); const leaguesRoutes = require('./routes/leagues'); +const seasonsRoutes = require('./routes/seasons'); + +const player_game_scoresRoutes = require('./routes/player_game_scores'); + +const player_season_statsRoutes = require('./routes/player_season_stats'); + +const team_season_statsRoutes = require('./routes/team_season_stats'); + const getBaseUrl = (url) => { if (!url) return ''; return url.endsWith('/api') ? url.slice(0, -4) : url; @@ -150,6 +158,30 @@ app.use( leaguesRoutes, ); +app.use( + '/api/seasons', + passport.authenticate('jwt', { session: false }), + seasonsRoutes, +); + +app.use( + '/api/player_game_scores', + passport.authenticate('jwt', { session: false }), + player_game_scoresRoutes, +); + +app.use( + '/api/player_season_stats', + passport.authenticate('jwt', { session: false }), + player_season_statsRoutes, +); + +app.use( + '/api/team_season_stats', + passport.authenticate('jwt', { session: false }), + team_season_statsRoutes, +); + app.use( '/api/openai', passport.authenticate('jwt', { session: false }), diff --git a/backend/src/routes/leagues.js b/backend/src/routes/leagues.js index ae71f02..51b7812 100644 --- a/backend/src/routes/leagues.js +++ b/backend/src/routes/leagues.js @@ -25,6 +25,9 @@ router.use(checkCrudPermissions('leagues')); * name: * type: string * default: name + * handicapformula: + * type: string + * default: handicapformula */ @@ -310,7 +313,7 @@ router.get( currentUser, }); if (filetype && filetype === 'csv') { - const fields = ['id', 'name']; + const fields = ['id', 'name', 'handicapformula']; const opts = { fields }; try { const csv = parse(payload.rows, opts); diff --git a/backend/src/routes/player_game_scores.js b/backend/src/routes/player_game_scores.js new file mode 100644 index 0000000..c9c4d55 --- /dev/null +++ b/backend/src/routes/player_game_scores.js @@ -0,0 +1,455 @@ +const express = require('express'); + +const Player_game_scoresService = require('../services/player_game_scores'); +const Player_game_scoresDBApi = require('../db/api/player_game_scores'); +const wrapAsync = require('../helpers').wrapAsync; + +const config = require('../config'); + +const router = express.Router(); + +const { parse } = require('json2csv'); + +const { checkCrudPermissions } = require('../middlewares/check-permissions'); + +router.use(checkCrudPermissions('player_game_scores')); + +/** + * @swagger + * components: + * schemas: + * Player_game_scores: + * type: object + * properties: + + */ + +/** + * @swagger + * tags: + * name: Player_game_scores + * description: The Player_game_scores managing API + */ + +/** + * @swagger + * /api/player_game_scores: + * post: + * security: + * - bearerAuth: [] + * tags: [Player_game_scores] + * 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/Player_game_scores" + * responses: + * 200: + * description: The item was successfully added + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Player_game_scores" + * 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 Player_game_scoresService.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: [Player_game_scores] + * 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/Player_game_scores" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Player_game_scores" + * 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 Player_game_scoresService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/player_game_scores/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Player_game_scores] + * 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/Player_game_scores" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Player_game_scores" + * 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 Player_game_scoresService.update( + req.body.data, + req.body.id, + req.currentUser, + ); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/player_game_scores/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Player_game_scores] + * 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/Player_game_scores" + * 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 Player_game_scoresService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/player_game_scores/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Player_game_scores] + * 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/Player_game_scores" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post( + '/deleteByIds', + wrapAsync(async (req, res) => { + await Player_game_scoresService.deleteByIds(req.body.data, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/player_game_scores: + * get: + * security: + * - bearerAuth: [] + * tags: [Player_game_scores] + * summary: Get all player_game_scores + * description: Get all player_game_scores + * responses: + * 200: + * description: Player_game_scores list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Player_game_scores" + * 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 globalAccess = req.currentUser.app_role.globalAccess; + + const currentUser = req.currentUser; + const payload = await Player_game_scoresDBApi.findAll( + req.query, + globalAccess, + { currentUser }, + ); + if (filetype && filetype === 'csv') { + const fields = ['id']; + const opts = { fields }; + try { + const csv = parse(payload.rows, opts); + res.status(200).attachment(csv); + res.send(csv); + } catch (err) { + console.error(err); + } + } else { + res.status(200).send(payload); + } + }), +); + +/** + * @swagger + * /api/player_game_scores/count: + * get: + * security: + * - bearerAuth: [] + * tags: [Player_game_scores] + * summary: Count all player_game_scores + * description: Count all player_game_scores + * responses: + * 200: + * description: Player_game_scores count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Player_game_scores" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get( + '/count', + wrapAsync(async (req, res) => { + const globalAccess = req.currentUser.app_role.globalAccess; + + const currentUser = req.currentUser; + const payload = await Player_game_scoresDBApi.findAll( + req.query, + globalAccess, + { countOnly: true, currentUser }, + ); + + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/player_game_scores/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [Player_game_scores] + * summary: Find all player_game_scores that match search criteria + * description: Find all player_game_scores that match search criteria + * responses: + * 200: + * description: Player_game_scores list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Player_game_scores" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/autocomplete', async (req, res) => { + const globalAccess = req.currentUser.app_role.globalAccess; + + const organizationId = req.currentUser.organization?.id; + + const payload = await Player_game_scoresDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + globalAccess, + organizationId, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/player_game_scores/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Player_game_scores] + * 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/Player_game_scores" + * 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 Player_game_scoresDBApi.findBy({ id: req.params.id }); + + res.status(200).send(payload); + }), +); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/routes/player_season_stats.js b/backend/src/routes/player_season_stats.js new file mode 100644 index 0000000..3b631ab --- /dev/null +++ b/backend/src/routes/player_season_stats.js @@ -0,0 +1,483 @@ +const express = require('express'); + +const Player_season_statsService = require('../services/player_season_stats'); +const Player_season_statsDBApi = require('../db/api/player_season_stats'); +const wrapAsync = require('../helpers').wrapAsync; + +const config = require('../config'); + +const router = express.Router(); + +const { parse } = require('json2csv'); + +const { checkCrudPermissions } = require('../middlewares/check-permissions'); + +router.use(checkCrudPermissions('player_season_stats')); + +/** + * @swagger + * components: + * schemas: + * Player_season_stats: + * type: object + * properties: + + * totalpoints: + * type: integer + * format: int64 + * gamesplayed: + * type: integer + * format: int64 + * eightballruns: + * type: integer + * format: int64 + * eightballbreaks: + * type: integer + * format: int64 + * nineballruns: + * type: integer + * format: int64 + + */ + +/** + * @swagger + * tags: + * name: Player_season_stats + * description: The Player_season_stats managing API + */ + +/** + * @swagger + * /api/player_season_stats: + * post: + * security: + * - bearerAuth: [] + * tags: [Player_season_stats] + * 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/Player_season_stats" + * responses: + * 200: + * description: The item was successfully added + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Player_season_stats" + * 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 Player_season_statsService.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: [Player_season_stats] + * 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/Player_season_stats" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Player_season_stats" + * 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 Player_season_statsService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/player_season_stats/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Player_season_stats] + * 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/Player_season_stats" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Player_season_stats" + * 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 Player_season_statsService.update( + req.body.data, + req.body.id, + req.currentUser, + ); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/player_season_stats/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Player_season_stats] + * 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/Player_season_stats" + * 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 Player_season_statsService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/player_season_stats/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Player_season_stats] + * 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/Player_season_stats" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post( + '/deleteByIds', + wrapAsync(async (req, res) => { + await Player_season_statsService.deleteByIds( + req.body.data, + req.currentUser, + ); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/player_season_stats: + * get: + * security: + * - bearerAuth: [] + * tags: [Player_season_stats] + * summary: Get all player_season_stats + * description: Get all player_season_stats + * responses: + * 200: + * description: Player_season_stats list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Player_season_stats" + * 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 globalAccess = req.currentUser.app_role.globalAccess; + + const currentUser = req.currentUser; + const payload = await Player_season_statsDBApi.findAll( + req.query, + globalAccess, + { currentUser }, + ); + if (filetype && filetype === 'csv') { + const fields = [ + 'id', + 'totalpoints', + 'gamesplayed', + 'eightballruns', + 'eightballbreaks', + 'nineballruns', + ]; + 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/player_season_stats/count: + * get: + * security: + * - bearerAuth: [] + * tags: [Player_season_stats] + * summary: Count all player_season_stats + * description: Count all player_season_stats + * responses: + * 200: + * description: Player_season_stats count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Player_season_stats" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get( + '/count', + wrapAsync(async (req, res) => { + const globalAccess = req.currentUser.app_role.globalAccess; + + const currentUser = req.currentUser; + const payload = await Player_season_statsDBApi.findAll( + req.query, + globalAccess, + { countOnly: true, currentUser }, + ); + + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/player_season_stats/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [Player_season_stats] + * summary: Find all player_season_stats that match search criteria + * description: Find all player_season_stats that match search criteria + * responses: + * 200: + * description: Player_season_stats list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Player_season_stats" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/autocomplete', async (req, res) => { + const globalAccess = req.currentUser.app_role.globalAccess; + + const organizationId = req.currentUser.organization?.id; + + const payload = await Player_season_statsDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + globalAccess, + organizationId, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/player_season_stats/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Player_season_stats] + * 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/Player_season_stats" + * 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 Player_season_statsDBApi.findBy({ + id: req.params.id, + }); + + res.status(200).send(payload); + }), +); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/routes/seasons.js b/backend/src/routes/seasons.js new file mode 100644 index 0000000..801eca8 --- /dev/null +++ b/backend/src/routes/seasons.js @@ -0,0 +1,452 @@ +const express = require('express'); + +const SeasonsService = require('../services/seasons'); +const SeasonsDBApi = require('../db/api/seasons'); +const wrapAsync = require('../helpers').wrapAsync; + +const config = require('../config'); + +const router = express.Router(); + +const { parse } = require('json2csv'); + +const { checkCrudPermissions } = require('../middlewares/check-permissions'); + +router.use(checkCrudPermissions('seasons')); + +/** + * @swagger + * components: + * schemas: + * Seasons: + * type: object + * properties: + + * name: + * type: string + * default: name + + */ + +/** + * @swagger + * tags: + * name: Seasons + * description: The Seasons managing API + */ + +/** + * @swagger + * /api/seasons: + * post: + * security: + * - bearerAuth: [] + * tags: [Seasons] + * 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/Seasons" + * responses: + * 200: + * description: The item was successfully added + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Seasons" + * 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 SeasonsService.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: [Seasons] + * 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/Seasons" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Seasons" + * 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 SeasonsService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/seasons/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Seasons] + * 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/Seasons" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Seasons" + * 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 SeasonsService.update(req.body.data, req.body.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/seasons/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Seasons] + * 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/Seasons" + * 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 SeasonsService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/seasons/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Seasons] + * 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/Seasons" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post( + '/deleteByIds', + wrapAsync(async (req, res) => { + await SeasonsService.deleteByIds(req.body.data, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/seasons: + * get: + * security: + * - bearerAuth: [] + * tags: [Seasons] + * summary: Get all seasons + * description: Get all seasons + * responses: + * 200: + * description: Seasons list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Seasons" + * 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 globalAccess = req.currentUser.app_role.globalAccess; + + const currentUser = req.currentUser; + const payload = await SeasonsDBApi.findAll(req.query, globalAccess, { + currentUser, + }); + if (filetype && filetype === 'csv') { + const fields = ['id', 'name', 'startdate', 'enddate']; + 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/seasons/count: + * get: + * security: + * - bearerAuth: [] + * tags: [Seasons] + * summary: Count all seasons + * description: Count all seasons + * responses: + * 200: + * description: Seasons count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Seasons" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get( + '/count', + wrapAsync(async (req, res) => { + const globalAccess = req.currentUser.app_role.globalAccess; + + const currentUser = req.currentUser; + const payload = await SeasonsDBApi.findAll(req.query, globalAccess, { + countOnly: true, + currentUser, + }); + + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/seasons/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [Seasons] + * summary: Find all seasons that match search criteria + * description: Find all seasons that match search criteria + * responses: + * 200: + * description: Seasons list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Seasons" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/autocomplete', async (req, res) => { + const globalAccess = req.currentUser.app_role.globalAccess; + + const organizationId = req.currentUser.organization?.id; + + const payload = await SeasonsDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + globalAccess, + organizationId, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/seasons/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Seasons] + * 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/Seasons" + * 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 SeasonsDBApi.findBy({ id: req.params.id }); + + res.status(200).send(payload); + }), +); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/routes/team_season_stats.js b/backend/src/routes/team_season_stats.js new file mode 100644 index 0000000..819bf79 --- /dev/null +++ b/backend/src/routes/team_season_stats.js @@ -0,0 +1,455 @@ +const express = require('express'); + +const Team_season_statsService = require('../services/team_season_stats'); +const Team_season_statsDBApi = require('../db/api/team_season_stats'); +const wrapAsync = require('../helpers').wrapAsync; + +const config = require('../config'); + +const router = express.Router(); + +const { parse } = require('json2csv'); + +const { checkCrudPermissions } = require('../middlewares/check-permissions'); + +router.use(checkCrudPermissions('team_season_stats')); + +/** + * @swagger + * components: + * schemas: + * Team_season_stats: + * type: object + * properties: + + */ + +/** + * @swagger + * tags: + * name: Team_season_stats + * description: The Team_season_stats managing API + */ + +/** + * @swagger + * /api/team_season_stats: + * post: + * security: + * - bearerAuth: [] + * tags: [Team_season_stats] + * 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/Team_season_stats" + * responses: + * 200: + * description: The item was successfully added + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Team_season_stats" + * 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 Team_season_statsService.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: [Team_season_stats] + * 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/Team_season_stats" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Team_season_stats" + * 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 Team_season_statsService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/team_season_stats/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Team_season_stats] + * 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/Team_season_stats" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Team_season_stats" + * 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 Team_season_statsService.update( + req.body.data, + req.body.id, + req.currentUser, + ); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/team_season_stats/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Team_season_stats] + * 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/Team_season_stats" + * 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 Team_season_statsService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/team_season_stats/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Team_season_stats] + * 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/Team_season_stats" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post( + '/deleteByIds', + wrapAsync(async (req, res) => { + await Team_season_statsService.deleteByIds(req.body.data, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/team_season_stats: + * get: + * security: + * - bearerAuth: [] + * tags: [Team_season_stats] + * summary: Get all team_season_stats + * description: Get all team_season_stats + * responses: + * 200: + * description: Team_season_stats list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Team_season_stats" + * 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 globalAccess = req.currentUser.app_role.globalAccess; + + const currentUser = req.currentUser; + const payload = await Team_season_statsDBApi.findAll( + req.query, + globalAccess, + { currentUser }, + ); + if (filetype && filetype === 'csv') { + const fields = ['id']; + const opts = { fields }; + try { + const csv = parse(payload.rows, opts); + res.status(200).attachment(csv); + res.send(csv); + } catch (err) { + console.error(err); + } + } else { + res.status(200).send(payload); + } + }), +); + +/** + * @swagger + * /api/team_season_stats/count: + * get: + * security: + * - bearerAuth: [] + * tags: [Team_season_stats] + * summary: Count all team_season_stats + * description: Count all team_season_stats + * responses: + * 200: + * description: Team_season_stats count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Team_season_stats" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get( + '/count', + wrapAsync(async (req, res) => { + const globalAccess = req.currentUser.app_role.globalAccess; + + const currentUser = req.currentUser; + const payload = await Team_season_statsDBApi.findAll( + req.query, + globalAccess, + { countOnly: true, currentUser }, + ); + + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/team_season_stats/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [Team_season_stats] + * summary: Find all team_season_stats that match search criteria + * description: Find all team_season_stats that match search criteria + * responses: + * 200: + * description: Team_season_stats list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Team_season_stats" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/autocomplete', async (req, res) => { + const globalAccess = req.currentUser.app_role.globalAccess; + + const organizationId = req.currentUser.organization?.id; + + const payload = await Team_season_statsDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + globalAccess, + organizationId, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/team_season_stats/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Team_season_stats] + * 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/Team_season_stats" + * 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 Team_season_statsDBApi.findBy({ id: req.params.id }); + + res.status(200).send(payload); + }), +); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/services/player_game_scores.js b/backend/src/services/player_game_scores.js new file mode 100644 index 0000000..440916c --- /dev/null +++ b/backend/src/services/player_game_scores.js @@ -0,0 +1,121 @@ +const db = require('../db/models'); +const Player_game_scoresDBApi = require('../db/api/player_game_scores'); +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 Player_game_scoresService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await Player_game_scoresDBApi.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 Player_game_scoresDBApi.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 player_game_scores = await Player_game_scoresDBApi.findBy( + { id }, + { transaction }, + ); + + if (!player_game_scores) { + throw new ValidationError('player_game_scoresNotFound'); + } + + const updatedPlayer_game_scores = await Player_game_scoresDBApi.update( + id, + data, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + return updatedPlayer_game_scores; + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await Player_game_scoresDBApi.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 Player_game_scoresDBApi.remove(id, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; diff --git a/backend/src/services/player_season_stats.js b/backend/src/services/player_season_stats.js new file mode 100644 index 0000000..0648e67 --- /dev/null +++ b/backend/src/services/player_season_stats.js @@ -0,0 +1,121 @@ +const db = require('../db/models'); +const Player_season_statsDBApi = require('../db/api/player_season_stats'); +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 Player_season_statsService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await Player_season_statsDBApi.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 Player_season_statsDBApi.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 player_season_stats = await Player_season_statsDBApi.findBy( + { id }, + { transaction }, + ); + + if (!player_season_stats) { + throw new ValidationError('player_season_statsNotFound'); + } + + const updatedPlayer_season_stats = await Player_season_statsDBApi.update( + id, + data, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + return updatedPlayer_season_stats; + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await Player_season_statsDBApi.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 Player_season_statsDBApi.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 bc83e75..2243d5c 100644 --- a/backend/src/services/search.js +++ b/backend/src/services/search.js @@ -49,12 +49,26 @@ module.exports = class SearchService { venues: ['name', 'address'], - leagues: ['name'], + leagues: ['name', 'handicapformula'], + + seasons: ['name'], }; const columnsInt = { games: ['team_score', 'opponent_score'], players: ['total_points', 'games_played'], + + player_season_stats: [ + 'totalpoints', + + 'gamesplayed', + + 'eightballruns', + + 'eightballbreaks', + + 'nineballruns', + ], }; let allFoundRecords = []; diff --git a/backend/src/services/seasons.js b/backend/src/services/seasons.js new file mode 100644 index 0000000..788a942 --- /dev/null +++ b/backend/src/services/seasons.js @@ -0,0 +1,114 @@ +const db = require('../db/models'); +const SeasonsDBApi = require('../db/api/seasons'); +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 SeasonsService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await SeasonsDBApi.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 SeasonsDBApi.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 seasons = await SeasonsDBApi.findBy({ id }, { transaction }); + + if (!seasons) { + throw new ValidationError('seasonsNotFound'); + } + + const updatedSeasons = await SeasonsDBApi.update(id, data, { + currentUser, + transaction, + }); + + await transaction.commit(); + return updatedSeasons; + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await SeasonsDBApi.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 SeasonsDBApi.remove(id, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; diff --git a/backend/src/services/team_season_stats.js b/backend/src/services/team_season_stats.js new file mode 100644 index 0000000..2616cf8 --- /dev/null +++ b/backend/src/services/team_season_stats.js @@ -0,0 +1,121 @@ +const db = require('../db/models'); +const Team_season_statsDBApi = require('../db/api/team_season_stats'); +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 Team_season_statsService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await Team_season_statsDBApi.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 Team_season_statsDBApi.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 team_season_stats = await Team_season_statsDBApi.findBy( + { id }, + { transaction }, + ); + + if (!team_season_stats) { + throw new ValidationError('team_season_statsNotFound'); + } + + const updatedTeam_season_stats = await Team_season_statsDBApi.update( + id, + data, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + return updatedTeam_season_stats; + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await Team_season_statsDBApi.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 Team_season_statsDBApi.remove(id, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; diff --git a/frontend/src/components/Games/CardGames.tsx b/frontend/src/components/Games/CardGames.tsx index dd19002..8f6613e 100644 --- a/frontend/src/components/Games/CardGames.tsx +++ b/frontend/src/components/Games/CardGames.tsx @@ -128,6 +128,17 @@ const CardGames = ({ + +
+
+ Season +
+
+
+ {dataFormatter.seasonsOneListFormatter(item.season)} +
+
+
))} diff --git a/frontend/src/components/Games/ListGames.tsx b/frontend/src/components/Games/ListGames.tsx index 99f27a9..35f03ef 100644 --- a/frontend/src/components/Games/ListGames.tsx +++ b/frontend/src/components/Games/ListGames.tsx @@ -83,6 +83,13 @@ const ListGames = ({

{item.opponent_score}

+ +
+

Season

+

+ {dataFormatter.seasonsOneListFormatter(item.season)} +

+
value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('seasons'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + }, + { field: 'actions', type: 'actions', diff --git a/frontend/src/components/Leagues/CardLeagues.tsx b/frontend/src/components/Leagues/CardLeagues.tsx index 0726da4..1e22e06 100644 --- a/frontend/src/components/Leagues/CardLeagues.tsx +++ b/frontend/src/components/Leagues/CardLeagues.tsx @@ -82,6 +82,17 @@ const CardLeagues = ({
{item.name}
+ +
+
+ Handicapformula +
+
+
+ {item.handicapformula} +
+
+
))} diff --git a/frontend/src/components/Leagues/ListLeagues.tsx b/frontend/src/components/Leagues/ListLeagues.tsx index 944542d..a85aa75 100644 --- a/frontend/src/components/Leagues/ListLeagues.tsx +++ b/frontend/src/components/Leagues/ListLeagues.tsx @@ -55,6 +55,13 @@ const ListLeagues = ({

Name

{item.name}

+ +
+

+ Handicapformula +

+

{item.handicapformula}

+
void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const CardPlayer_game_scores = ({ + player_game_scores, + 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_PLAYER_GAME_SCORES', + ); + + return ( +
+ {loading && } +
    + {!loading && + player_game_scores.map((item, index) => ( +
  • +
    + + {item.id} + + +
    + +
    +
    +
    +
    +
    Game
    +
    +
    + {dataFormatter.gamesOneListFormatter(item.game)} +
    +
    +
    + +
    +
    + Player +
    +
    +
    + {dataFormatter.playersOneListFormatter(item.player)} +
    +
    +
    + +
    +
    Team
    +
    +
    + {dataFormatter.teamsOneListFormatter(item.team)} +
    +
    +
    +
    +
  • + ))} + {!loading && player_game_scores.length === 0 && ( +
    +

    No data to display

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

Game

+

+ {dataFormatter.gamesOneListFormatter(item.game)} +

+
+ +
+

Player

+

+ {dataFormatter.playersOneListFormatter(item.player)} +

+
+ +
+

Team

+

+ {dataFormatter.teamsOneListFormatter(item.team)} +

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

No data to display

+
+ )} +
+
+ +
+ + ); +}; + +export default ListPlayer_game_scores; diff --git a/frontend/src/components/Player_game_scores/TablePlayer_game_scores.tsx b/frontend/src/components/Player_game_scores/TablePlayer_game_scores.tsx new file mode 100644 index 0000000..81af32b --- /dev/null +++ b/frontend/src/components/Player_game_scores/TablePlayer_game_scores.tsx @@ -0,0 +1,486 @@ +import React, { useEffect, useState, useMemo } from 'react'; +import { createPortal } from 'react-dom'; +import { ToastContainer, toast } from 'react-toastify'; +import BaseButton from '../BaseButton'; +import CardBoxModal from '../CardBoxModal'; +import CardBox from '../CardBox'; +import { + fetch, + update, + deleteItem, + setRefetch, + deleteItemsByIds, +} from '../../stores/player_game_scores/player_game_scoresSlice'; +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 './configurePlayer_game_scoresCols'; +import _ from 'lodash'; +import dataFormatter from '../../helpers/dataFormatter'; +import { dataGridStyles } from '../../styles'; + +const perPage = 10; + +const TableSamplePlayer_game_scores = ({ + 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 { + player_game_scores, + loading, + count, + notify: player_game_scoresNotify, + refetch, + } = useAppSelector((state) => state.player_game_scores); + 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 (player_game_scoresNotify.showNotification) { + notify( + player_game_scoresNotify.typeNotification, + player_game_scoresNotify.textNotification, + ); + } + }, [player_game_scoresNotify.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, + `player_game_scores`, + 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={player_game_scores ?? []} + 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 TableSamplePlayer_game_scores; diff --git a/frontend/src/components/Player_game_scores/configurePlayer_game_scoresCols.tsx b/frontend/src/components/Player_game_scores/configurePlayer_game_scoresCols.tsx new file mode 100644 index 0000000..6ee0164 --- /dev/null +++ b/frontend/src/components/Player_game_scores/configurePlayer_game_scoresCols.tsx @@ -0,0 +1,122 @@ +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_PLAYER_GAME_SCORES'); + + return [ + { + field: 'game', + headerName: 'Game', + 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('games'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + }, + + { + field: 'player', + headerName: 'Player', + 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('players'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + }, + + { + field: 'team', + headerName: 'Team', + 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('teams'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + }, + + { + field: 'actions', + type: 'actions', + minWidth: 30, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + getActions: (params: GridRowParams) => { + return [ +
+ +
, + ]; + }, + }, + ]; +}; diff --git a/frontend/src/components/Player_season_stats/CardPlayer_season_stats.tsx b/frontend/src/components/Player_season_stats/CardPlayer_season_stats.tsx new file mode 100644 index 0000000..7d96977 --- /dev/null +++ b/frontend/src/components/Player_season_stats/CardPlayer_season_stats.tsx @@ -0,0 +1,178 @@ +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 = { + player_season_stats: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const CardPlayer_season_stats = ({ + player_season_stats, + 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_PLAYER_SEASON_STATS', + ); + + return ( +
+ {loading && } +
    + {!loading && + player_season_stats.map((item, index) => ( +
  • +
    + + {item.gamesplayed} + + +
    + +
    +
    +
    +
    +
    + Player +
    +
    +
    + {dataFormatter.playersOneListFormatter(item.player)} +
    +
    +
    + +
    +
    + Season +
    +
    +
    + {dataFormatter.seasonsOneListFormatter(item.season)} +
    +
    +
    + +
    +
    + Totalpoints +
    +
    +
    + {item.totalpoints} +
    +
    +
    + +
    +
    + Gamesplayed +
    +
    +
    + {item.gamesplayed} +
    +
    +
    + +
    +
    + Eightballruns +
    +
    +
    + {item.eightballruns} +
    +
    +
    + +
    +
    + Eightballbreaks +
    +
    +
    + {item.eightballbreaks} +
    +
    +
    + +
    +
    + Nineballruns +
    +
    +
    + {item.nineballruns} +
    +
    +
    +
    +
  • + ))} + {!loading && player_season_stats.length === 0 && ( +
    +

    No data to display

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

Player

+

+ {dataFormatter.playersOneListFormatter(item.player)} +

+
+ +
+

Season

+

+ {dataFormatter.seasonsOneListFormatter(item.season)} +

+
+ +
+

Totalpoints

+

{item.totalpoints}

+
+ +
+

Gamesplayed

+

{item.gamesplayed}

+
+ +
+

+ Eightballruns +

+

{item.eightballruns}

+
+ +
+

+ Eightballbreaks +

+

{item.eightballbreaks}

+
+ +
+

Nineballruns

+

{item.nineballruns}

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

No data to display

+
+ )} +
+
+ +
+ + ); +}; + +export default ListPlayer_season_stats; diff --git a/frontend/src/components/Player_season_stats/TablePlayer_season_stats.tsx b/frontend/src/components/Player_season_stats/TablePlayer_season_stats.tsx new file mode 100644 index 0000000..d3e9a32 --- /dev/null +++ b/frontend/src/components/Player_season_stats/TablePlayer_season_stats.tsx @@ -0,0 +1,486 @@ +import React, { useEffect, useState, useMemo } from 'react'; +import { createPortal } from 'react-dom'; +import { ToastContainer, toast } from 'react-toastify'; +import BaseButton from '../BaseButton'; +import CardBoxModal from '../CardBoxModal'; +import CardBox from '../CardBox'; +import { + fetch, + update, + deleteItem, + setRefetch, + deleteItemsByIds, +} from '../../stores/player_season_stats/player_season_statsSlice'; +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 './configurePlayer_season_statsCols'; +import _ from 'lodash'; +import dataFormatter from '../../helpers/dataFormatter'; +import { dataGridStyles } from '../../styles'; + +const perPage = 10; + +const TableSamplePlayer_season_stats = ({ + 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 { + player_season_stats, + loading, + count, + notify: player_season_statsNotify, + refetch, + } = useAppSelector((state) => state.player_season_stats); + 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 (player_season_statsNotify.showNotification) { + notify( + player_season_statsNotify.typeNotification, + player_season_statsNotify.textNotification, + ); + } + }, [player_season_statsNotify.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, + `player_season_stats`, + 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={player_season_stats ?? []} + 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 TableSamplePlayer_season_stats; diff --git a/frontend/src/components/Player_season_stats/configurePlayer_season_statsCols.tsx b/frontend/src/components/Player_season_stats/configurePlayer_season_statsCols.tsx new file mode 100644 index 0000000..352208a --- /dev/null +++ b/frontend/src/components/Player_season_stats/configurePlayer_season_statsCols.tsx @@ -0,0 +1,172 @@ +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_PLAYER_SEASON_STATS'); + + return [ + { + field: 'player', + headerName: 'Player', + 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('players'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + }, + + { + field: 'season', + headerName: 'Season', + 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('seasons'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + }, + + { + field: 'totalpoints', + headerName: 'Totalpoints', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + type: 'number', + }, + + { + field: 'gamesplayed', + headerName: 'Gamesplayed', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + type: 'number', + }, + + { + field: 'eightballruns', + headerName: 'Eightballruns', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + type: 'number', + }, + + { + field: 'eightballbreaks', + headerName: 'Eightballbreaks', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + type: 'number', + }, + + { + field: 'nineballruns', + headerName: 'Nineballruns', + 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/Seasons/CardSeasons.tsx b/frontend/src/components/Seasons/CardSeasons.tsx new file mode 100644 index 0000000..f573654 --- /dev/null +++ b/frontend/src/components/Seasons/CardSeasons.tsx @@ -0,0 +1,138 @@ +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 = { + seasons: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const CardSeasons = ({ + seasons, + 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_SEASONS'); + + return ( +
+ {loading && } +
    + {!loading && + seasons.map((item, index) => ( +
  • +
    + + {item.name} + + +
    + +
    +
    +
    +
    +
    Name
    +
    +
    {item.name}
    +
    +
    + +
    +
    + Startdate +
    +
    +
    + {dataFormatter.dateFormatter(item.startdate)} +
    +
    +
    + +
    +
    + Enddate +
    +
    +
    + {dataFormatter.dateFormatter(item.enddate)} +
    +
    +
    + +
    +
    + League +
    +
    +
    + {dataFormatter.leaguesOneListFormatter(item.league)} +
    +
    +
    +
    +
  • + ))} + {!loading && seasons.length === 0 && ( +
    +

    No data to display

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

Name

+

{item.name}

+
+ +
+

Startdate

+

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

+
+ +
+

Enddate

+

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

+
+ +
+

League

+

+ {dataFormatter.leaguesOneListFormatter(item.league)} +

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

No data to display

+
+ )} +
+
+ +
+ + ); +}; + +export default ListSeasons; diff --git a/frontend/src/components/Seasons/TableSeasons.tsx b/frontend/src/components/Seasons/TableSeasons.tsx new file mode 100644 index 0000000..1bff74a --- /dev/null +++ b/frontend/src/components/Seasons/TableSeasons.tsx @@ -0,0 +1,481 @@ +import React, { useEffect, useState, useMemo } from 'react'; +import { createPortal } from 'react-dom'; +import { ToastContainer, toast } from 'react-toastify'; +import BaseButton from '../BaseButton'; +import CardBoxModal from '../CardBoxModal'; +import CardBox from '../CardBox'; +import { + fetch, + update, + deleteItem, + setRefetch, + deleteItemsByIds, +} from '../../stores/seasons/seasonsSlice'; +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 './configureSeasonsCols'; +import _ from 'lodash'; +import dataFormatter from '../../helpers/dataFormatter'; +import { dataGridStyles } from '../../styles'; + +const perPage = 10; + +const TableSampleSeasons = ({ + 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 { + seasons, + loading, + count, + notify: seasonsNotify, + refetch, + } = useAppSelector((state) => state.seasons); + 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 (seasonsNotify.showNotification) { + notify(seasonsNotify.typeNotification, seasonsNotify.textNotification); + } + }, [seasonsNotify.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, `seasons`, 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={seasons ?? []} + 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 TableSampleSeasons; diff --git a/frontend/src/components/Seasons/configureSeasonsCols.tsx b/frontend/src/components/Seasons/configureSeasonsCols.tsx new file mode 100644 index 0000000..3e729f4 --- /dev/null +++ b/frontend/src/components/Seasons/configureSeasonsCols.tsx @@ -0,0 +1,126 @@ +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_SEASONS'); + + return [ + { + field: 'name', + headerName: 'Name', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + + { + field: 'startdate', + headerName: 'Startdate', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + type: 'date', + valueGetter: (params: GridValueGetterParams) => + new Date(params.row.startdate), + }, + + { + field: 'enddate', + headerName: 'Enddate', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + type: 'date', + valueGetter: (params: GridValueGetterParams) => + new Date(params.row.enddate), + }, + + { + field: 'league', + headerName: 'League', + 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('leagues'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + }, + + { + field: 'actions', + type: 'actions', + minWidth: 30, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + getActions: (params: GridRowParams) => { + return [ +
+ +
, + ]; + }, + }, + ]; +}; diff --git a/frontend/src/components/Team_season_stats/CardTeam_season_stats.tsx b/frontend/src/components/Team_season_stats/CardTeam_season_stats.tsx new file mode 100644 index 0000000..83b5811 --- /dev/null +++ b/frontend/src/components/Team_season_stats/CardTeam_season_stats.tsx @@ -0,0 +1,121 @@ +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 = { + team_season_stats: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const CardTeam_season_stats = ({ + team_season_stats, + 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_TEAM_SEASON_STATS', + ); + + return ( +
+ {loading && } +
    + {!loading && + team_season_stats.map((item, index) => ( +
  • +
    + + {item.id} + + +
    + +
    +
    +
    +
    +
    Team
    +
    +
    + {dataFormatter.teamsOneListFormatter(item.team)} +
    +
    +
    + +
    +
    + Season +
    +
    +
    + {dataFormatter.seasonsOneListFormatter(item.season)} +
    +
    +
    +
    +
  • + ))} + {!loading && team_season_stats.length === 0 && ( +
    +

    No data to display

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

Team

+

+ {dataFormatter.teamsOneListFormatter(item.team)} +

+
+ +
+

Season

+

+ {dataFormatter.seasonsOneListFormatter(item.season)} +

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

No data to display

+
+ )} +
+
+ +
+ + ); +}; + +export default ListTeam_season_stats; diff --git a/frontend/src/components/Team_season_stats/TableTeam_season_stats.tsx b/frontend/src/components/Team_season_stats/TableTeam_season_stats.tsx new file mode 100644 index 0000000..945808a --- /dev/null +++ b/frontend/src/components/Team_season_stats/TableTeam_season_stats.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/team_season_stats/team_season_statsSlice'; +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 './configureTeam_season_statsCols'; +import _ from 'lodash'; +import dataFormatter from '../../helpers/dataFormatter'; +import { dataGridStyles } from '../../styles'; + +const perPage = 10; + +const TableSampleTeam_season_stats = ({ + 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 { + team_season_stats, + loading, + count, + notify: team_season_statsNotify, + refetch, + } = useAppSelector((state) => state.team_season_stats); + 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 (team_season_statsNotify.showNotification) { + notify( + team_season_statsNotify.typeNotification, + team_season_statsNotify.textNotification, + ); + } + }, [team_season_statsNotify.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, `team_season_stats`, 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={team_season_stats ?? []} + 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 TableSampleTeam_season_stats; diff --git a/frontend/src/components/Team_season_stats/configureTeam_season_statsCols.tsx b/frontend/src/components/Team_season_stats/configureTeam_season_statsCols.tsx new file mode 100644 index 0000000..c1be68c --- /dev/null +++ b/frontend/src/components/Team_season_stats/configureTeam_season_statsCols.tsx @@ -0,0 +1,102 @@ +import React from 'react'; +import BaseIcon from '../BaseIcon'; +import { mdiEye, mdiTrashCan, mdiPencilOutline } from '@mdi/js'; +import axios from 'axios'; +import { + GridActionsCellItem, + GridRowParams, + GridValueGetterParams, +} from '@mui/x-data-grid'; +import ImageField from '../ImageField'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import DataGridMultiSelect from '../DataGridMultiSelect'; +import ListActionsPopover from '../ListActionsPopover'; + +import { hasPermission } from '../../helpers/userPermissions'; + +type Params = (id: string) => void; + +export const loadColumns = async ( + onDelete: Params, + entityName: string, + + user, +) => { + async function callOptionsApi(entityName: string) { + if (!hasPermission(user, 'READ_' + entityName.toUpperCase())) return []; + + try { + const data = await axios(`/${entityName}/autocomplete?limit=100`); + return data.data; + } catch (error) { + console.log(error); + return []; + } + } + + const hasUpdatePermission = hasPermission(user, 'UPDATE_TEAM_SEASON_STATS'); + + return [ + { + field: 'team', + headerName: 'Team', + 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('teams'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + }, + + { + field: 'season', + headerName: 'Season', + 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('seasons'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + }, + + { + field: 'actions', + type: 'actions', + minWidth: 30, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + getActions: (params: GridRowParams) => { + return [ +
+ +
, + ]; + }, + }, + ]; +}; diff --git a/frontend/src/components/WebPageComponents/Footer.tsx b/frontend/src/components/WebPageComponents/Footer.tsx index b9818da..dde7f9e 100644 --- a/frontend/src/components/WebPageComponents/Footer.tsx +++ b/frontend/src/components/WebPageComponents/Footer.tsx @@ -19,7 +19,7 @@ export default function WebSiteFooter({ projectName }: WebSiteFooterProps) { const style = FooterStyle.WITH_PROJECT_NAME; - const design = FooterDesigns.DESIGN_DIVERSITY; + const design = FooterDesigns.DEFAULT_DESIGN; return (
state.style.websiteHeder); const borders = useAppSelector((state) => state.style.borders); - const style = HeaderStyle.PAGES_RIGHT; + const style = HeaderStyle.PAGES_LEFT; - const design = HeaderDesigns.DEFAULT_DESIGN; + const design = HeaderDesigns.DESIGN_DIVERSITY; return (
item.date); + }, + gamesOneListFormatter(val) { + if (!val) return ''; + return val.date; + }, + gamesManyListFormatterEdit(val) { + if (!val || !val.length) return []; + return val.map((item) => { + return { id: item.id, label: item.date }; + }); + }, + gamesOneListFormatterEdit(val) { + if (!val) return ''; + return { label: val.date, id: val.id }; + }, + + playersManyListFormatter(val) { + if (!val || !val.length) return []; + return val.map((item) => item.first_name); + }, + playersOneListFormatter(val) { + if (!val) return ''; + return val.first_name; + }, + playersManyListFormatterEdit(val) { + if (!val || !val.length) return []; + return val.map((item) => { + return { id: item.id, label: item.first_name }; + }); + }, + playersOneListFormatterEdit(val) { + if (!val) return ''; + return { label: val.first_name, id: val.id }; + }, + teamsManyListFormatter(val) { if (!val || !val.length) return []; return val.map((item) => item.name); @@ -133,4 +171,23 @@ export default { if (!val) return ''; return { label: val.name, id: val.id }; }, + + seasonsManyListFormatter(val) { + if (!val || !val.length) return []; + return val.map((item) => item.name); + }, + seasonsOneListFormatter(val) { + if (!val) return ''; + return val.name; + }, + seasonsManyListFormatterEdit(val) { + if (!val || !val.length) return []; + return val.map((item) => { + return { id: item.id, label: item.name }; + }); + }, + seasonsOneListFormatterEdit(val) { + if (!val) return ''; + return { label: val.name, id: val.id }; + }, }; diff --git a/frontend/src/menuAside.ts b/frontend/src/menuAside.ts index 739015b..b19a025 100644 --- a/frontend/src/menuAside.ts +++ b/frontend/src/menuAside.ts @@ -84,6 +84,38 @@ const menuAside: MenuAsideItem[] = [ icon: icon.mdiTable ?? icon.mdiTable, permissions: 'READ_LEAGUES', }, + { + href: '/seasons/seasons-list', + label: 'Seasons', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: icon.mdiTable ?? icon.mdiTable, + permissions: 'READ_SEASONS', + }, + { + href: '/player_game_scores/player_game_scores-list', + label: 'Player game scores', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: icon.mdiTable ?? icon.mdiTable, + permissions: 'READ_PLAYER_GAME_SCORES', + }, + { + href: '/player_season_stats/player_season_stats-list', + label: 'Player season stats', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: icon.mdiTable ?? icon.mdiTable, + permissions: 'READ_PLAYER_SEASON_STATS', + }, + { + href: '/team_season_stats/team_season_stats-list', + label: 'Team season stats', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: icon.mdiTable ?? icon.mdiTable, + permissions: 'READ_TEAM_SEASON_STATS', + }, { href: '/profile', label: 'Profile', diff --git a/frontend/src/pages/dashboard.tsx b/frontend/src/pages/dashboard.tsx index 9f7fa3c..44cf465 100644 --- a/frontend/src/pages/dashboard.tsx +++ b/frontend/src/pages/dashboard.tsx @@ -36,6 +36,13 @@ const Dashboard = () => { const [roles, setRoles] = React.useState(loadingMessage); const [permissions, setPermissions] = React.useState(loadingMessage); const [leagues, setLeagues] = React.useState(loadingMessage); + const [seasons, setSeasons] = React.useState(loadingMessage); + const [player_game_scores, setPlayer_game_scores] = + React.useState(loadingMessage); + const [player_season_stats, setPlayer_season_stats] = + React.useState(loadingMessage); + const [team_season_stats, setTeam_season_stats] = + React.useState(loadingMessage); const [widgetsRole, setWidgetsRole] = React.useState({ role: { value: '', label: '' }, @@ -57,6 +64,10 @@ const Dashboard = () => { 'roles', 'permissions', 'leagues', + 'seasons', + 'player_game_scores', + 'player_season_stats', + 'team_season_stats', ]; const fns = [ setUsers, @@ -67,6 +78,10 @@ const Dashboard = () => { setRoles, setPermissions, setLeagues, + setSeasons, + setPlayer_game_scores, + setPlayer_season_stats, + setTeam_season_stats, ]; const requests = entities.map((entity, index) => { @@ -452,6 +467,134 @@ const Dashboard = () => {
)} + + {hasPermission(currentUser, 'READ_SEASONS') && ( + +
+
+
+
+ Seasons +
+
+ {seasons} +
+
+
+ +
+
+
+ + )} + + {hasPermission(currentUser, 'READ_PLAYER_GAME_SCORES') && ( + +
+
+
+
+ Player game scores +
+
+ {player_game_scores} +
+
+
+ +
+
+
+ + )} + + {hasPermission(currentUser, 'READ_PLAYER_SEASON_STATS') && ( + +
+
+
+
+ Player season stats +
+
+ {player_season_stats} +
+
+
+ +
+
+
+ + )} + + {hasPermission(currentUser, 'READ_TEAM_SEASON_STATS') && ( + +
+
+
+
+ Team season stats +
+
+ {team_season_stats} +
+
+
+ +
+
+
+ + )}
diff --git a/frontend/src/pages/games/[gamesId].tsx b/frontend/src/pages/games/[gamesId].tsx index 4336bbd..ef8f7a5 100644 --- a/frontend/src/pages/games/[gamesId].tsx +++ b/frontend/src/pages/games/[gamesId].tsx @@ -49,6 +49,8 @@ const EditGames = () => { opponent_score: '', leagues: null, + + season: null, }; const [initialValues, setInitialValues] = useState(initVals); @@ -169,6 +171,17 @@ const EditGames = () => { > + + + + diff --git a/frontend/src/pages/games/games-edit.tsx b/frontend/src/pages/games/games-edit.tsx index 9bcd227..115368f 100644 --- a/frontend/src/pages/games/games-edit.tsx +++ b/frontend/src/pages/games/games-edit.tsx @@ -49,6 +49,8 @@ const EditGamesPage = () => { opponent_score: '', leagues: null, + + season: null, }; const [initialValues, setInitialValues] = useState(initVals); @@ -167,6 +169,17 @@ const EditGamesPage = () => { > + + + + diff --git a/frontend/src/pages/games/games-list.tsx b/frontend/src/pages/games/games-list.tsx index abf1f06..1a65f08 100644 --- a/frontend/src/pages/games/games-list.tsx +++ b/frontend/src/pages/games/games-list.tsx @@ -35,6 +35,8 @@ const GamesTablesPage = () => { { label: 'GameDate', title: 'date', date: 'true' }, { label: 'Team', title: 'team' }, + + { label: 'Season', title: 'season' }, ]); const hasCreatePermission = diff --git a/frontend/src/pages/games/games-new.tsx b/frontend/src/pages/games/games-new.tsx index 26a1c9f..f1489ed 100644 --- a/frontend/src/pages/games/games-new.tsx +++ b/frontend/src/pages/games/games-new.tsx @@ -44,6 +44,8 @@ const initialValues = { opponent_score: '', leagues: '', + + season: '', }; const GamesNew = () => { @@ -127,6 +129,16 @@ const GamesNew = () => { > + + + + diff --git a/frontend/src/pages/games/games-table.tsx b/frontend/src/pages/games/games-table.tsx index 8b5371c..dab5d32 100644 --- a/frontend/src/pages/games/games-table.tsx +++ b/frontend/src/pages/games/games-table.tsx @@ -35,6 +35,8 @@ const GamesTablesPage = () => { { label: 'GameDate', title: 'date', date: 'true' }, { label: 'Team', title: 'team' }, + + { label: 'Season', title: 'season' }, ]); const hasCreatePermission = diff --git a/frontend/src/pages/games/games-view.tsx b/frontend/src/pages/games/games-view.tsx index 7500d31..a668da8 100644 --- a/frontend/src/pages/games/games-view.tsx +++ b/frontend/src/pages/games/games-view.tsx @@ -103,6 +103,45 @@ const GamesView = () => {

{games?.leagues?.name ?? 'No data'}

+
+

Season

+ +

{games?.season?.name ?? 'No data'}

+
+ + <> +

Player_game_scores Game

+ +
+ + + + + + {games.player_game_scores_game && + Array.isArray(games.player_game_scores_game) && + games.player_game_scores_game.map((item: any) => ( + + router.push( + `/player_game_scores/player_game_scores-view/?id=${item.id}`, + ) + } + > + ))} + +
+
+ {!games?.player_game_scores_game?.length && ( +
No data
+ )} +
+ + { const dispatch = useAppDispatch(); const initVals = { name: '', + + handicapformula: '', }; const [initialValues, setInitialValues] = useState(initVals); @@ -97,6 +99,10 @@ const EditLeagues = () => { + + + + diff --git a/frontend/src/pages/leagues/leagues-edit.tsx b/frontend/src/pages/leagues/leagues-edit.tsx index 2f570f1..8c1a38d 100644 --- a/frontend/src/pages/leagues/leagues-edit.tsx +++ b/frontend/src/pages/leagues/leagues-edit.tsx @@ -39,6 +39,8 @@ const EditLeaguesPage = () => { const dispatch = useAppDispatch(); const initVals = { name: '', + + handicapformula: '', }; const [initialValues, setInitialValues] = useState(initVals); @@ -95,6 +97,10 @@ const EditLeaguesPage = () => { + + + + diff --git a/frontend/src/pages/leagues/leagues-list.tsx b/frontend/src/pages/leagues/leagues-list.tsx index 32966b7..e455077 100644 --- a/frontend/src/pages/leagues/leagues-list.tsx +++ b/frontend/src/pages/leagues/leagues-list.tsx @@ -28,7 +28,10 @@ const LeaguesTablesPage = () => { const dispatch = useAppDispatch(); - const [filters] = useState([{ label: 'Name', title: 'name' }]); + const [filters] = useState([ + { label: 'Name', title: 'name' }, + { label: 'Handicapformula', title: 'handicapformula' }, + ]); const hasCreatePermission = currentUser && hasPermission(currentUser, 'CREATE_LEAGUES'); diff --git a/frontend/src/pages/leagues/leagues-new.tsx b/frontend/src/pages/leagues/leagues-new.tsx index 2f97b5f..c13feb9 100644 --- a/frontend/src/pages/leagues/leagues-new.tsx +++ b/frontend/src/pages/leagues/leagues-new.tsx @@ -34,6 +34,8 @@ import moment from 'moment'; const initialValues = { name: '', + + handicapformula: '', }; const LeaguesNew = () => { @@ -67,6 +69,10 @@ const LeaguesNew = () => { + + + + diff --git a/frontend/src/pages/leagues/leagues-table.tsx b/frontend/src/pages/leagues/leagues-table.tsx index c4d1734..07246fd 100644 --- a/frontend/src/pages/leagues/leagues-table.tsx +++ b/frontend/src/pages/leagues/leagues-table.tsx @@ -28,7 +28,10 @@ const LeaguesTablesPage = () => { const dispatch = useAppDispatch(); - const [filters] = useState([{ label: 'Name', title: 'name' }]); + const [filters] = useState([ + { label: 'Name', title: 'name' }, + { label: 'Handicapformula', title: 'handicapformula' }, + ]); const hasCreatePermission = currentUser && hasPermission(currentUser, 'CREATE_LEAGUES'); diff --git a/frontend/src/pages/leagues/leagues-view.tsx b/frontend/src/pages/leagues/leagues-view.tsx index 5234ca6..5c4c154 100644 --- a/frontend/src/pages/leagues/leagues-view.tsx +++ b/frontend/src/pages/leagues/leagues-view.tsx @@ -63,6 +63,11 @@ const LeaguesView = () => {

{leagues?.name}

+
+

Handicapformula

+

{leagues?.handicapformula}

+
+ <>

Users Leagues

{ + <> +

Seasons leagues

+ +
+ + + + + + + + + + + + {leagues.seasons_leagues && + Array.isArray(leagues.seasons_leagues) && + leagues.seasons_leagues.map((item: any) => ( + + router.push(`/seasons/seasons-view/?id=${item.id}`) + } + > + + + + + + + ))} + +
NameStartdateEnddate
{item.name} + {dataFormatter.dateFormatter(item.startdate)} + + {dataFormatter.dateFormatter(item.enddate)} +
+
+ {!leagues?.seasons_leagues?.length && ( +
No data
+ )} +
+ + + <> +

Seasons League

+ +
+ + + + + + + + + + + + {leagues.seasons_league && + Array.isArray(leagues.seasons_league) && + leagues.seasons_league.map((item: any) => ( + + router.push(`/seasons/seasons-view/?id=${item.id}`) + } + > + + + + + + + ))} + +
NameStartdateEnddate
{item.name} + {dataFormatter.dateFormatter(item.startdate)} + + {dataFormatter.dateFormatter(item.enddate)} +
+
+ {!leagues?.seasons_league?.length && ( +
No data
+ )} +
+ + + <> +

Player_game_scores leagues

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

+ Player_season_stats leagues +

+ +
+ + + + + + + + + + + + + + + + {leagues.player_season_stats_leagues && + Array.isArray(leagues.player_season_stats_leagues) && + leagues.player_season_stats_leagues.map((item: any) => ( + + router.push( + `/player_season_stats/player_season_stats-view/?id=${item.id}`, + ) + } + > + + + + + + + + + + + ))} + +
TotalpointsGamesplayedEightballrunsEightballbreaksNineballruns
{item.totalpoints}{item.gamesplayed} + {item.eightballruns} + + {item.eightballbreaks} + {item.nineballruns}
+
+ {!leagues?.player_season_stats_leagues?.length && ( +
No data
+ )} +
+ + + <> +

Team_season_stats leagues

+ +
+ + + + + + {leagues.team_season_stats_leagues && + Array.isArray(leagues.team_season_stats_leagues) && + leagues.team_season_stats_leagues.map((item: any) => ( + + router.push( + `/team_season_stats/team_season_stats-view/?id=${item.id}`, + ) + } + > + ))} + +
+
+ {!leagues?.team_season_stats_leagues?.length && ( +
No data
+ )} +
+ + { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + leagues: null, + + game: null, + + player: null, + + team: null, + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { player_game_scores } = useAppSelector( + (state) => state.player_game_scores, + ); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { player_game_scoresId } = router.query; + + useEffect(() => { + dispatch(fetch({ id: player_game_scoresId })); + }, [player_game_scoresId]); + + useEffect(() => { + if (typeof player_game_scores === 'object') { + setInitialValues(player_game_scores); + } + }, [player_game_scores]); + + useEffect(() => { + if (typeof player_game_scores === 'object') { + const newInitialVal = { ...initVals }; + + Object.keys(initVals).forEach( + (el) => (newInitialVal[el] = player_game_scores[el]), + ); + + setInitialValues(newInitialVal); + } + }, [player_game_scores]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: player_game_scoresId, data })); + await router.push('/player_game_scores/player_game_scores-list'); + }; + + return ( + <> + + {getPageTitle('Edit player_game_scores')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + + + + + + + + + + + + + + + + + + router.push('/player_game_scores/player_game_scores-list') + } + /> + + +
+
+
+ + ); +}; + +EditPlayer_game_scores.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditPlayer_game_scores; diff --git a/frontend/src/pages/player_game_scores/player_game_scores-edit.tsx b/frontend/src/pages/player_game_scores/player_game_scores-edit.tsx new file mode 100644 index 0000000..a801c92 --- /dev/null +++ b/frontend/src/pages/player_game_scores/player_game_scores-edit.tsx @@ -0,0 +1,181 @@ +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/player_game_scores/player_game_scoresSlice'; +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 EditPlayer_game_scoresPage = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + leagues: null, + + game: null, + + player: null, + + team: null, + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { player_game_scores } = useAppSelector( + (state) => state.player_game_scores, + ); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { id } = router.query; + + useEffect(() => { + dispatch(fetch({ id: id })); + }, [id]); + + useEffect(() => { + if (typeof player_game_scores === 'object') { + setInitialValues(player_game_scores); + } + }, [player_game_scores]); + + useEffect(() => { + if (typeof player_game_scores === 'object') { + const newInitialVal = { ...initVals }; + Object.keys(initVals).forEach( + (el) => (newInitialVal[el] = player_game_scores[el]), + ); + setInitialValues(newInitialVal); + } + }, [player_game_scores]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: id, data })); + await router.push('/player_game_scores/player_game_scores-list'); + }; + + return ( + <> + + {getPageTitle('Edit player_game_scores')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + + + + + + + + + + + + + + + + + + router.push('/player_game_scores/player_game_scores-list') + } + /> + + +
+
+
+ + ); +}; + +EditPlayer_game_scoresPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditPlayer_game_scoresPage; diff --git a/frontend/src/pages/player_game_scores/player_game_scores-list.tsx b/frontend/src/pages/player_game_scores/player_game_scores-list.tsx new file mode 100644 index 0000000..1c388db --- /dev/null +++ b/frontend/src/pages/player_game_scores/player_game_scores-list.tsx @@ -0,0 +1,173 @@ +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 TablePlayer_game_scores from '../../components/Player_game_scores/TablePlayer_game_scores'; +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/player_game_scores/player_game_scoresSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const Player_game_scoresTablesPage = () => { + 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: 'Game', title: 'game' }, + + { label: 'Player', title: 'player' }, + + { label: 'Team', title: 'team' }, + ]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_PLAYER_GAME_SCORES'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getPlayer_game_scoresCSV = async () => { + const response = await axios({ + url: '/player_game_scores?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 = 'player_game_scoresCSV.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('Player_game_scores')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
+
+
+
+ + + + +
+ + + + + ); +}; + +Player_game_scoresTablesPage.getLayout = function getLayout( + page: ReactElement, +) { + return ( + + {page} + + ); +}; + +export default Player_game_scoresTablesPage; diff --git a/frontend/src/pages/player_game_scores/player_game_scores-new.tsx b/frontend/src/pages/player_game_scores/player_game_scores-new.tsx new file mode 100644 index 0000000..9ab400b --- /dev/null +++ b/frontend/src/pages/player_game_scores/player_game_scores-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/player_game_scores/player_game_scoresSlice'; +import { useAppDispatch } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import moment from 'moment'; + +const initialValues = { + leagues: '', + + game: '', + + player: '', + + team: '', +}; + +const Player_game_scoresNew = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + + const handleSubmit = async (data) => { + await dispatch(create(data)); + await router.push('/player_game_scores/player_game_scores-list'); + }; + return ( + <> + + {getPageTitle('New Item')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + + + + + + + + + + + + + + + + + + router.push('/player_game_scores/player_game_scores-list') + } + /> + + +
+
+
+ + ); +}; + +Player_game_scoresNew.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Player_game_scoresNew; diff --git a/frontend/src/pages/player_game_scores/player_game_scores-table.tsx b/frontend/src/pages/player_game_scores/player_game_scores-table.tsx new file mode 100644 index 0000000..ff6d82a --- /dev/null +++ b/frontend/src/pages/player_game_scores/player_game_scores-table.tsx @@ -0,0 +1,172 @@ +import { mdiChartTimelineVariant } from '@mdi/js'; +import Head from 'next/head'; +import { uniqueId } from 'lodash'; +import React, { ReactElement, useState } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; +import TablePlayer_game_scores from '../../components/Player_game_scores/TablePlayer_game_scores'; +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/player_game_scores/player_game_scoresSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const Player_game_scoresTablesPage = () => { + 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: 'Game', title: 'game' }, + + { label: 'Player', title: 'player' }, + + { label: 'Team', title: 'team' }, + ]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_PLAYER_GAME_SCORES'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getPlayer_game_scoresCSV = async () => { + const response = await axios({ + url: '/player_game_scores?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 = 'player_game_scoresCSV.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('Player_game_scores')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
+
+
+
+ + + +
+ + + + + ); +}; + +Player_game_scoresTablesPage.getLayout = function getLayout( + page: ReactElement, +) { + return ( + + {page} + + ); +}; + +export default Player_game_scoresTablesPage; diff --git a/frontend/src/pages/player_game_scores/player_game_scores-view.tsx b/frontend/src/pages/player_game_scores/player_game_scores-view.tsx new file mode 100644 index 0000000..fe70bf8 --- /dev/null +++ b/frontend/src/pages/player_game_scores/player_game_scores-view.tsx @@ -0,0 +1,110 @@ +import React, { ReactElement, useEffect } from 'react'; +import Head from 'next/head'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { fetch } from '../../stores/player_game_scores/player_game_scoresSlice'; +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'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const Player_game_scoresView = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const { player_game_scores } = useAppSelector( + (state) => state.player_game_scores, + ); + + 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 player_game_scores')} + + + + + + +
+

leagues

+ +

{player_game_scores?.leagues?.name ?? 'No data'}

+
+ +
+

Game

+ +

{player_game_scores?.game?.date ?? 'No data'}

+
+ +
+

Player

+ +

{player_game_scores?.player?.first_name ?? 'No data'}

+
+ +
+

Team

+ +

{player_game_scores?.team?.name ?? 'No data'}

+
+ + + + + router.push('/player_game_scores/player_game_scores-list') + } + /> +
+
+ + ); +}; + +Player_game_scoresView.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Player_game_scoresView; diff --git a/frontend/src/pages/player_season_stats/[player_season_statsId].tsx b/frontend/src/pages/player_season_stats/[player_season_statsId].tsx new file mode 100644 index 0000000..a1ab55b --- /dev/null +++ b/frontend/src/pages/player_season_stats/[player_season_statsId].tsx @@ -0,0 +1,220 @@ +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/player_season_stats/player_season_statsSlice'; +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 EditPlayer_season_stats = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + leagues: null, + + player: null, + + season: null, + + totalpoints: '', + + gamesplayed: '', + + eightballruns: '', + + eightballbreaks: '', + + nineballruns: '', + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { player_season_stats } = useAppSelector( + (state) => state.player_season_stats, + ); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { player_season_statsId } = router.query; + + useEffect(() => { + dispatch(fetch({ id: player_season_statsId })); + }, [player_season_statsId]); + + useEffect(() => { + if (typeof player_season_stats === 'object') { + setInitialValues(player_season_stats); + } + }, [player_season_stats]); + + useEffect(() => { + if (typeof player_season_stats === 'object') { + const newInitialVal = { ...initVals }; + + Object.keys(initVals).forEach( + (el) => (newInitialVal[el] = player_season_stats[el]), + ); + + setInitialValues(newInitialVal); + } + }, [player_season_stats]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: player_season_statsId, data })); + await router.push('/player_season_stats/player_season_stats-list'); + }; + + return ( + <> + + {getPageTitle('Edit player_season_stats')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + router.push('/player_season_stats/player_season_stats-list') + } + /> + + +
+
+
+ + ); +}; + +EditPlayer_season_stats.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditPlayer_season_stats; diff --git a/frontend/src/pages/player_season_stats/player_season_stats-edit.tsx b/frontend/src/pages/player_season_stats/player_season_stats-edit.tsx new file mode 100644 index 0000000..35cfc80 --- /dev/null +++ b/frontend/src/pages/player_season_stats/player_season_stats-edit.tsx @@ -0,0 +1,218 @@ +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/player_season_stats/player_season_statsSlice'; +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 EditPlayer_season_statsPage = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + leagues: null, + + player: null, + + season: null, + + totalpoints: '', + + gamesplayed: '', + + eightballruns: '', + + eightballbreaks: '', + + nineballruns: '', + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { player_season_stats } = useAppSelector( + (state) => state.player_season_stats, + ); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { id } = router.query; + + useEffect(() => { + dispatch(fetch({ id: id })); + }, [id]); + + useEffect(() => { + if (typeof player_season_stats === 'object') { + setInitialValues(player_season_stats); + } + }, [player_season_stats]); + + useEffect(() => { + if (typeof player_season_stats === 'object') { + const newInitialVal = { ...initVals }; + Object.keys(initVals).forEach( + (el) => (newInitialVal[el] = player_season_stats[el]), + ); + setInitialValues(newInitialVal); + } + }, [player_season_stats]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: id, data })); + await router.push('/player_season_stats/player_season_stats-list'); + }; + + return ( + <> + + {getPageTitle('Edit player_season_stats')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + router.push('/player_season_stats/player_season_stats-list') + } + /> + + +
+
+
+ + ); +}; + +EditPlayer_season_statsPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditPlayer_season_statsPage; diff --git a/frontend/src/pages/player_season_stats/player_season_stats-list.tsx b/frontend/src/pages/player_season_stats/player_season_stats-list.tsx new file mode 100644 index 0000000..8405039 --- /dev/null +++ b/frontend/src/pages/player_season_stats/player_season_stats-list.tsx @@ -0,0 +1,177 @@ +import { mdiChartTimelineVariant } from '@mdi/js'; +import Head from 'next/head'; +import { uniqueId } from 'lodash'; +import React, { ReactElement, useState } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; +import TablePlayer_season_stats from '../../components/Player_season_stats/TablePlayer_season_stats'; +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/player_season_stats/player_season_statsSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const Player_season_statsTablesPage = () => { + 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: 'Totalpoints', title: 'totalpoints', number: 'true' }, + { label: 'Gamesplayed', title: 'gamesplayed', number: 'true' }, + { label: 'Eightballruns', title: 'eightballruns', number: 'true' }, + { label: 'Eightballbreaks', title: 'eightballbreaks', number: 'true' }, + { label: 'Nineballruns', title: 'nineballruns', number: 'true' }, + + { label: 'Player', title: 'player' }, + + { label: 'Season', title: 'season' }, + ]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_PLAYER_SEASON_STATS'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getPlayer_season_statsCSV = async () => { + const response = await axios({ + url: '/player_season_stats?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 = 'player_season_statsCSV.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('Player_season_stats')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
+
+
+
+ + + + +
+ + + + + ); +}; + +Player_season_statsTablesPage.getLayout = function getLayout( + page: ReactElement, +) { + return ( + + {page} + + ); +}; + +export default Player_season_statsTablesPage; diff --git a/frontend/src/pages/player_season_stats/player_season_stats-new.tsx b/frontend/src/pages/player_season_stats/player_season_stats-new.tsx new file mode 100644 index 0000000..a015a1c --- /dev/null +++ b/frontend/src/pages/player_season_stats/player_season_stats-new.tsx @@ -0,0 +1,180 @@ +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/player_season_stats/player_season_statsSlice'; +import { useAppDispatch } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import moment from 'moment'; + +const initialValues = { + leagues: '', + + player: '', + + season: '', + + totalpoints: '', + + gamesplayed: '', + + eightballruns: '', + + eightballbreaks: '', + + nineballruns: '', +}; + +const Player_season_statsNew = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + + const handleSubmit = async (data) => { + await dispatch(create(data)); + await router.push('/player_season_stats/player_season_stats-list'); + }; + return ( + <> + + {getPageTitle('New Item')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + router.push('/player_season_stats/player_season_stats-list') + } + /> + + +
+
+
+ + ); +}; + +Player_season_statsNew.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Player_season_statsNew; diff --git a/frontend/src/pages/player_season_stats/player_season_stats-table.tsx b/frontend/src/pages/player_season_stats/player_season_stats-table.tsx new file mode 100644 index 0000000..b358943 --- /dev/null +++ b/frontend/src/pages/player_season_stats/player_season_stats-table.tsx @@ -0,0 +1,176 @@ +import { mdiChartTimelineVariant } from '@mdi/js'; +import Head from 'next/head'; +import { uniqueId } from 'lodash'; +import React, { ReactElement, useState } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; +import TablePlayer_season_stats from '../../components/Player_season_stats/TablePlayer_season_stats'; +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/player_season_stats/player_season_statsSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const Player_season_statsTablesPage = () => { + 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: 'Totalpoints', title: 'totalpoints', number: 'true' }, + { label: 'Gamesplayed', title: 'gamesplayed', number: 'true' }, + { label: 'Eightballruns', title: 'eightballruns', number: 'true' }, + { label: 'Eightballbreaks', title: 'eightballbreaks', number: 'true' }, + { label: 'Nineballruns', title: 'nineballruns', number: 'true' }, + + { label: 'Player', title: 'player' }, + + { label: 'Season', title: 'season' }, + ]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_PLAYER_SEASON_STATS'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getPlayer_season_statsCSV = async () => { + const response = await axios({ + url: '/player_season_stats?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 = 'player_season_statsCSV.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('Player_season_stats')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
+
+
+
+ + + +
+ + + + + ); +}; + +Player_season_statsTablesPage.getLayout = function getLayout( + page: ReactElement, +) { + return ( + + {page} + + ); +}; + +export default Player_season_statsTablesPage; diff --git a/frontend/src/pages/player_season_stats/player_season_stats-view.tsx b/frontend/src/pages/player_season_stats/player_season_stats-view.tsx new file mode 100644 index 0000000..dc3a4ba --- /dev/null +++ b/frontend/src/pages/player_season_stats/player_season_stats-view.tsx @@ -0,0 +1,129 @@ +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/player_season_stats/player_season_statsSlice'; +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'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const Player_season_statsView = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const { player_season_stats } = useAppSelector( + (state) => state.player_season_stats, + ); + + 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 player_season_stats')} + + + + + + +
+

leagues

+ +

{player_season_stats?.leagues?.name ?? 'No data'}

+
+ +
+

Player

+ +

{player_season_stats?.player?.first_name ?? 'No data'}

+
+ +
+

Season

+ +

{player_season_stats?.season?.name ?? 'No data'}

+
+ +
+

Totalpoints

+

{player_season_stats?.totalpoints || 'No data'}

+
+ +
+

Gamesplayed

+

{player_season_stats?.gamesplayed || 'No data'}

+
+ +
+

Eightballruns

+

{player_season_stats?.eightballruns || 'No data'}

+
+ +
+

Eightballbreaks

+

{player_season_stats?.eightballbreaks || 'No data'}

+
+ +
+

Nineballruns

+

{player_season_stats?.nineballruns || 'No data'}

+
+ + + + + router.push('/player_season_stats/player_season_stats-list') + } + /> +
+
+ + ); +}; + +Player_season_statsView.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Player_season_statsView; diff --git a/frontend/src/pages/players/players-view.tsx b/frontend/src/pages/players/players-view.tsx index 1513ac3..532a1e8 100644 --- a/frontend/src/pages/players/players-view.tsx +++ b/frontend/src/pages/players/players-view.tsx @@ -90,6 +90,96 @@ const PlayersView = () => {

{players?.leagues?.name ?? 'No data'}

+ <> +

Player_game_scores Player

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

Player_season_stats Player

+ +
+ + + + + + + + + + + + + + + + {players.player_season_stats_player && + Array.isArray(players.player_season_stats_player) && + players.player_season_stats_player.map((item: any) => ( + + router.push( + `/player_season_stats/player_season_stats-view/?id=${item.id}`, + ) + } + > + + + + + + + + + + + ))} + +
TotalpointsGamesplayedEightballrunsEightballbreaksNineballruns
{item.totalpoints}{item.gamesplayed} + {item.eightballruns} + + {item.eightballbreaks} + {item.nineballruns}
+
+ {!players?.player_season_stats_player?.length && ( +
No data
+ )} +
+ + { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + leagues: null, + + name: '', + + startdate: new Date(), + + enddate: new Date(), + + league: null, + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { seasons } = useAppSelector((state) => state.seasons); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { seasonsId } = router.query; + + useEffect(() => { + dispatch(fetch({ id: seasonsId })); + }, [seasonsId]); + + useEffect(() => { + if (typeof seasons === 'object') { + setInitialValues(seasons); + } + }, [seasons]); + + useEffect(() => { + if (typeof seasons === 'object') { + const newInitialVal = { ...initVals }; + + Object.keys(initVals).forEach((el) => (newInitialVal[el] = seasons[el])); + + setInitialValues(newInitialVal); + } + }, [seasons]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: seasonsId, data })); + await router.push('/seasons/seasons-list'); + }; + + return ( + <> + + {getPageTitle('Edit seasons')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + + + + + + + setInitialValues({ ...initialValues, startdate: date }) + } + /> + + + + + setInitialValues({ ...initialValues, enddate: date }) + } + /> + + + + + + + + + + + router.push('/seasons/seasons-list')} + /> + + +
+
+
+ + ); +}; + +EditSeasons.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditSeasons; diff --git a/frontend/src/pages/seasons/seasons-edit.tsx b/frontend/src/pages/seasons/seasons-edit.tsx new file mode 100644 index 0000000..5a96265 --- /dev/null +++ b/frontend/src/pages/seasons/seasons-edit.tsx @@ -0,0 +1,192 @@ +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/seasons/seasonsSlice'; +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 EditSeasonsPage = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + leagues: null, + + name: '', + + startdate: new Date(), + + enddate: new Date(), + + league: null, + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { seasons } = useAppSelector((state) => state.seasons); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { id } = router.query; + + useEffect(() => { + dispatch(fetch({ id: id })); + }, [id]); + + useEffect(() => { + if (typeof seasons === 'object') { + setInitialValues(seasons); + } + }, [seasons]); + + useEffect(() => { + if (typeof seasons === 'object') { + const newInitialVal = { ...initVals }; + Object.keys(initVals).forEach((el) => (newInitialVal[el] = seasons[el])); + setInitialValues(newInitialVal); + } + }, [seasons]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: id, data })); + await router.push('/seasons/seasons-list'); + }; + + return ( + <> + + {getPageTitle('Edit seasons')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + + + + + + + setInitialValues({ ...initialValues, startdate: date }) + } + /> + + + + + setInitialValues({ ...initialValues, enddate: date }) + } + /> + + + + + + + + + + + router.push('/seasons/seasons-list')} + /> + + +
+
+
+ + ); +}; + +EditSeasonsPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditSeasonsPage; diff --git a/frontend/src/pages/seasons/seasons-list.tsx b/frontend/src/pages/seasons/seasons-list.tsx new file mode 100644 index 0000000..e1b1aef --- /dev/null +++ b/frontend/src/pages/seasons/seasons-list.tsx @@ -0,0 +1,162 @@ +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 TableSeasons from '../../components/Seasons/TableSeasons'; +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/seasons/seasonsSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const SeasonsTablesPage = () => { + const [filterItems, setFilterItems] = useState([]); + const [csvFile, setCsvFile] = useState(null); + const [isModalActive, setIsModalActive] = useState(false); + const [showTableView, setShowTableView] = useState(false); + + const { currentUser } = useAppSelector((state) => state.auth); + + const dispatch = useAppDispatch(); + + const [filters] = useState([{ label: 'Name', title: 'name' }]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_SEASONS'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getSeasonsCSV = async () => { + const response = await axios({ + url: '/seasons?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 = 'seasonsCSV.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('Seasons')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
+
+
+
+ + + + +
+ + + + + ); +}; + +SeasonsTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default SeasonsTablesPage; diff --git a/frontend/src/pages/seasons/seasons-new.tsx b/frontend/src/pages/seasons/seasons-new.tsx new file mode 100644 index 0000000..0d1e62a --- /dev/null +++ b/frontend/src/pages/seasons/seasons-new.tsx @@ -0,0 +1,136 @@ +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/seasons/seasonsSlice'; +import { useAppDispatch } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import moment from 'moment'; + +const initialValues = { + leagues: '', + + name: '', + + startdate: '', + dateStartdate: '', + + enddate: '', + dateEnddate: '', + + league: '', +}; + +const SeasonsNew = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + + const handleSubmit = async (data) => { + await dispatch(create(data)); + await router.push('/seasons/seasons-list'); + }; + return ( + <> + + {getPageTitle('New Item')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + + + + + + + + + + + + + + + + + + + + + router.push('/seasons/seasons-list')} + /> + + +
+
+
+ + ); +}; + +SeasonsNew.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default SeasonsNew; diff --git a/frontend/src/pages/seasons/seasons-table.tsx b/frontend/src/pages/seasons/seasons-table.tsx new file mode 100644 index 0000000..f773685 --- /dev/null +++ b/frontend/src/pages/seasons/seasons-table.tsx @@ -0,0 +1,161 @@ +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 TableSeasons from '../../components/Seasons/TableSeasons'; +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/seasons/seasonsSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const SeasonsTablesPage = () => { + const [filterItems, setFilterItems] = useState([]); + const [csvFile, setCsvFile] = useState(null); + const [isModalActive, setIsModalActive] = useState(false); + const [showTableView, setShowTableView] = useState(false); + + const { currentUser } = useAppSelector((state) => state.auth); + + const dispatch = useAppDispatch(); + + const [filters] = useState([{ label: 'Name', title: 'name' }]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_SEASONS'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getSeasonsCSV = async () => { + const response = await axios({ + url: '/seasons?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 = 'seasonsCSV.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('Seasons')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
+
+
+
+ + + +
+ + + + + ); +}; + +SeasonsTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default SeasonsTablesPage; diff --git a/frontend/src/pages/seasons/seasons-view.tsx b/frontend/src/pages/seasons/seasons-view.tsx new file mode 100644 index 0000000..66962b0 --- /dev/null +++ b/frontend/src/pages/seasons/seasons-view.tsx @@ -0,0 +1,274 @@ +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/seasons/seasonsSlice'; +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'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const SeasonsView = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const { seasons } = useAppSelector((state) => state.seasons); + + 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 seasons')} + + + + + + +
+

leagues

+ +

{seasons?.leagues?.name ?? 'No data'}

+
+ +
+

Name

+

{seasons?.name}

+
+ + + {seasons.startdate ? ( + + ) : ( +

No Startdate

+ )} +
+ + + {seasons.enddate ? ( + + ) : ( +

No Enddate

+ )} +
+ +
+

League

+ +

{seasons?.league?.name ?? 'No data'}

+
+ + <> +

Games Season

+ +
+ + + + + + + + + + + + {seasons.games_season && + Array.isArray(seasons.games_season) && + seasons.games_season.map((item: any) => ( + + router.push(`/games/games-view/?id=${item.id}`) + } + > + + + + + + + ))} + +
GameDateTeamScoreOpponentScore
+ {dataFormatter.dateTimeFormatter(item.date)} + {item.team_score} + {item.opponent_score} +
+
+ {!seasons?.games_season?.length && ( +
No data
+ )} +
+ + + <> +

Player_season_stats Season

+ +
+ + + + + + + + + + + + + + + + {seasons.player_season_stats_season && + Array.isArray(seasons.player_season_stats_season) && + seasons.player_season_stats_season.map((item: any) => ( + + router.push( + `/player_season_stats/player_season_stats-view/?id=${item.id}`, + ) + } + > + + + + + + + + + + + ))} + +
TotalpointsGamesplayedEightballrunsEightballbreaksNineballruns
{item.totalpoints}{item.gamesplayed} + {item.eightballruns} + + {item.eightballbreaks} + {item.nineballruns}
+
+ {!seasons?.player_season_stats_season?.length && ( +
No data
+ )} +
+ + + <> +

Team_season_stats Season

+ +
+ + + + + + {seasons.team_season_stats_season && + Array.isArray(seasons.team_season_stats_season) && + seasons.team_season_stats_season.map((item: any) => ( + + router.push( + `/team_season_stats/team_season_stats-view/?id=${item.id}`, + ) + } + > + ))} + +
+
+ {!seasons?.team_season_stats_season?.length && ( +
No data
+ )} +
+ + + + + router.push('/seasons/seasons-list')} + /> +
+
+ + ); +}; + +SeasonsView.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default SeasonsView; diff --git a/frontend/src/pages/team_season_stats/[team_season_statsId].tsx b/frontend/src/pages/team_season_stats/[team_season_statsId].tsx new file mode 100644 index 0000000..f119740 --- /dev/null +++ b/frontend/src/pages/team_season_stats/[team_season_statsId].tsx @@ -0,0 +1,170 @@ +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/team_season_stats/team_season_statsSlice'; +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 EditTeam_season_stats = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + leagues: null, + + team: null, + + season: null, + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { team_season_stats } = useAppSelector( + (state) => state.team_season_stats, + ); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { team_season_statsId } = router.query; + + useEffect(() => { + dispatch(fetch({ id: team_season_statsId })); + }, [team_season_statsId]); + + useEffect(() => { + if (typeof team_season_stats === 'object') { + setInitialValues(team_season_stats); + } + }, [team_season_stats]); + + useEffect(() => { + if (typeof team_season_stats === 'object') { + const newInitialVal = { ...initVals }; + + Object.keys(initVals).forEach( + (el) => (newInitialVal[el] = team_season_stats[el]), + ); + + setInitialValues(newInitialVal); + } + }, [team_season_stats]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: team_season_statsId, data })); + await router.push('/team_season_stats/team_season_stats-list'); + }; + + return ( + <> + + {getPageTitle('Edit team_season_stats')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + + + + + + + + + + + + + + router.push('/team_season_stats/team_season_stats-list') + } + /> + + +
+
+
+ + ); +}; + +EditTeam_season_stats.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditTeam_season_stats; diff --git a/frontend/src/pages/team_season_stats/team_season_stats-edit.tsx b/frontend/src/pages/team_season_stats/team_season_stats-edit.tsx new file mode 100644 index 0000000..c4441ba --- /dev/null +++ b/frontend/src/pages/team_season_stats/team_season_stats-edit.tsx @@ -0,0 +1,168 @@ +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/team_season_stats/team_season_statsSlice'; +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 EditTeam_season_statsPage = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + leagues: null, + + team: null, + + season: null, + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { team_season_stats } = useAppSelector( + (state) => state.team_season_stats, + ); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { id } = router.query; + + useEffect(() => { + dispatch(fetch({ id: id })); + }, [id]); + + useEffect(() => { + if (typeof team_season_stats === 'object') { + setInitialValues(team_season_stats); + } + }, [team_season_stats]); + + useEffect(() => { + if (typeof team_season_stats === 'object') { + const newInitialVal = { ...initVals }; + Object.keys(initVals).forEach( + (el) => (newInitialVal[el] = team_season_stats[el]), + ); + setInitialValues(newInitialVal); + } + }, [team_season_stats]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: id, data })); + await router.push('/team_season_stats/team_season_stats-list'); + }; + + return ( + <> + + {getPageTitle('Edit team_season_stats')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + + + + + + + + + + + + + + router.push('/team_season_stats/team_season_stats-list') + } + /> + + +
+
+
+ + ); +}; + +EditTeam_season_statsPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditTeam_season_statsPage; diff --git a/frontend/src/pages/team_season_stats/team_season_stats-list.tsx b/frontend/src/pages/team_season_stats/team_season_stats-list.tsx new file mode 100644 index 0000000..4d1dd96 --- /dev/null +++ b/frontend/src/pages/team_season_stats/team_season_stats-list.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 TableTeam_season_stats from '../../components/Team_season_stats/TableTeam_season_stats'; +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/team_season_stats/team_season_statsSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const Team_season_statsTablesPage = () => { + 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', title: 'team' }, + + { label: 'Season', title: 'season' }, + ]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_TEAM_SEASON_STATS'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getTeam_season_statsCSV = async () => { + const response = await axios({ + url: '/team_season_stats?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 = 'team_season_statsCSV.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('Team_season_stats')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
+
+
+
+ + + + +
+ + + + + ); +}; + +Team_season_statsTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Team_season_statsTablesPage; diff --git a/frontend/src/pages/team_season_stats/team_season_stats-new.tsx b/frontend/src/pages/team_season_stats/team_season_stats-new.tsx new file mode 100644 index 0000000..ec4bf8c --- /dev/null +++ b/frontend/src/pages/team_season_stats/team_season_stats-new.tsx @@ -0,0 +1,130 @@ +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/team_season_stats/team_season_statsSlice'; +import { useAppDispatch } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import moment from 'moment'; + +const initialValues = { + leagues: '', + + team: '', + + season: '', +}; + +const Team_season_statsNew = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + + const handleSubmit = async (data) => { + await dispatch(create(data)); + await router.push('/team_season_stats/team_season_stats-list'); + }; + return ( + <> + + {getPageTitle('New Item')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + + + + + + + + + + + + + + router.push('/team_season_stats/team_season_stats-list') + } + /> + + +
+
+
+ + ); +}; + +Team_season_statsNew.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Team_season_statsNew; diff --git a/frontend/src/pages/team_season_stats/team_season_stats-table.tsx b/frontend/src/pages/team_season_stats/team_season_stats-table.tsx new file mode 100644 index 0000000..da2e772 --- /dev/null +++ b/frontend/src/pages/team_season_stats/team_season_stats-table.tsx @@ -0,0 +1,168 @@ +import { mdiChartTimelineVariant } from '@mdi/js'; +import Head from 'next/head'; +import { uniqueId } from 'lodash'; +import React, { ReactElement, useState } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; +import TableTeam_season_stats from '../../components/Team_season_stats/TableTeam_season_stats'; +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/team_season_stats/team_season_statsSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const Team_season_statsTablesPage = () => { + 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', title: 'team' }, + + { label: 'Season', title: 'season' }, + ]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_TEAM_SEASON_STATS'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getTeam_season_statsCSV = async () => { + const response = await axios({ + url: '/team_season_stats?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 = 'team_season_statsCSV.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('Team_season_stats')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
+
+
+
+ + + +
+ + + + + ); +}; + +Team_season_statsTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Team_season_statsTablesPage; diff --git a/frontend/src/pages/team_season_stats/team_season_stats-view.tsx b/frontend/src/pages/team_season_stats/team_season_stats-view.tsx new file mode 100644 index 0000000..2628f0a --- /dev/null +++ b/frontend/src/pages/team_season_stats/team_season_stats-view.tsx @@ -0,0 +1,104 @@ +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/team_season_stats/team_season_statsSlice'; +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'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const Team_season_statsView = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const { team_season_stats } = useAppSelector( + (state) => state.team_season_stats, + ); + + 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 team_season_stats')} + + + + + + +
+

leagues

+ +

{team_season_stats?.leagues?.name ?? 'No data'}

+
+ +
+

Team

+ +

{team_season_stats?.team?.name ?? 'No data'}

+
+ +
+

Season

+ +

{team_season_stats?.season?.name ?? 'No data'}

+
+ + + + + router.push('/team_season_stats/team_season_stats-list') + } + /> +
+
+ + ); +}; + +Team_season_statsView.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Team_season_statsView; diff --git a/frontend/src/pages/teams/teams-view.tsx b/frontend/src/pages/teams/teams-view.tsx index f539d8c..da267eb 100644 --- a/frontend/src/pages/teams/teams-view.tsx +++ b/frontend/src/pages/teams/teams-view.tsx @@ -248,6 +248,72 @@ const TeamsView = () => { + <> +

Player_game_scores Team

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

Team_season_stats Team

+ +
+ + + + + + {teams.team_season_stats_team && + Array.isArray(teams.team_season_stats_team) && + teams.team_season_stats_team.map((item: any) => ( + + router.push( + `/team_season_stats/team_season_stats-view/?id=${item.id}`, + ) + } + > + ))} + +
+
+ {!teams?.team_season_stats_team?.length && ( +
No data
+ )} +
+ + { Name + + Handicapformula @@ -92,6 +94,10 @@ const VenuesView = () => { } > {item.name} + + + {item.handicapformula} + ))} diff --git a/frontend/src/stores/player_game_scores/player_game_scoresSlice.ts b/frontend/src/stores/player_game_scores/player_game_scoresSlice.ts new file mode 100644 index 0000000..359c04c --- /dev/null +++ b/frontend/src/stores/player_game_scores/player_game_scoresSlice.ts @@ -0,0 +1,250 @@ +import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit'; +import axios from 'axios'; +import { + fulfilledNotify, + rejectNotify, + resetNotify, +} from '../../helpers/notifyStateHandler'; + +interface MainState { + player_game_scores: any; + loading: boolean; + count: number; + refetch: boolean; + rolesWidgets: any[]; + notify: { + showNotification: boolean; + textNotification: string; + typeNotification: string; + }; +} + +const initialState: MainState = { + player_game_scores: [], + loading: false, + count: 0, + refetch: false, + rolesWidgets: [], + notify: { + showNotification: false, + textNotification: '', + typeNotification: 'warn', + }, +}; + +export const fetch = createAsyncThunk( + 'player_game_scores/fetch', + async (data: any) => { + const { id, query } = data; + const result = await axios.get( + `player_game_scores${query || (id ? `/${id}` : '')}`, + ); + return id + ? result.data + : { rows: result.data.rows, count: result.data.count }; + }, +); + +export const deleteItemsByIds = createAsyncThunk( + 'player_game_scores/deleteByIds', + async (data: any, { rejectWithValue }) => { + try { + await axios.post('player_game_scores/deleteByIds', { data }); + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const deleteItem = createAsyncThunk( + 'player_game_scores/deletePlayer_game_scores', + async (id: string, { rejectWithValue }) => { + try { + await axios.delete(`player_game_scores/${id}`); + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const create = createAsyncThunk( + 'player_game_scores/createPlayer_game_scores', + async (data: any, { rejectWithValue }) => { + try { + const result = await axios.post('player_game_scores', { data }); + return result.data; + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const uploadCsv = createAsyncThunk( + 'player_game_scores/uploadCsv', + async (file: File, { rejectWithValue }) => { + try { + const data = new FormData(); + data.append('file', file); + data.append('filename', file.name); + + const result = await axios.post('player_game_scores/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( + 'player_game_scores/updatePlayer_game_scores', + async (payload: any, { rejectWithValue }) => { + try { + const result = await axios.put(`player_game_scores/${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 player_game_scoresSlice = createSlice({ + name: 'player_game_scores', + 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.player_game_scores = action.payload.rows; + state.count = action.payload.count; + } else { + state.player_game_scores = 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, 'Player_game_scores 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, + `${'Player_game_scores'.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, + `${'Player_game_scores'.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, + `${'Player_game_scores'.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, 'Player_game_scores 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 } = player_game_scoresSlice.actions; + +export default player_game_scoresSlice.reducer; diff --git a/frontend/src/stores/player_season_stats/player_season_statsSlice.ts b/frontend/src/stores/player_season_stats/player_season_statsSlice.ts new file mode 100644 index 0000000..43a59c3 --- /dev/null +++ b/frontend/src/stores/player_season_stats/player_season_statsSlice.ts @@ -0,0 +1,250 @@ +import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit'; +import axios from 'axios'; +import { + fulfilledNotify, + rejectNotify, + resetNotify, +} from '../../helpers/notifyStateHandler'; + +interface MainState { + player_season_stats: any; + loading: boolean; + count: number; + refetch: boolean; + rolesWidgets: any[]; + notify: { + showNotification: boolean; + textNotification: string; + typeNotification: string; + }; +} + +const initialState: MainState = { + player_season_stats: [], + loading: false, + count: 0, + refetch: false, + rolesWidgets: [], + notify: { + showNotification: false, + textNotification: '', + typeNotification: 'warn', + }, +}; + +export const fetch = createAsyncThunk( + 'player_season_stats/fetch', + async (data: any) => { + const { id, query } = data; + const result = await axios.get( + `player_season_stats${query || (id ? `/${id}` : '')}`, + ); + return id + ? result.data + : { rows: result.data.rows, count: result.data.count }; + }, +); + +export const deleteItemsByIds = createAsyncThunk( + 'player_season_stats/deleteByIds', + async (data: any, { rejectWithValue }) => { + try { + await axios.post('player_season_stats/deleteByIds', { data }); + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const deleteItem = createAsyncThunk( + 'player_season_stats/deletePlayer_season_stats', + async (id: string, { rejectWithValue }) => { + try { + await axios.delete(`player_season_stats/${id}`); + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const create = createAsyncThunk( + 'player_season_stats/createPlayer_season_stats', + async (data: any, { rejectWithValue }) => { + try { + const result = await axios.post('player_season_stats', { data }); + return result.data; + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const uploadCsv = createAsyncThunk( + 'player_season_stats/uploadCsv', + async (file: File, { rejectWithValue }) => { + try { + const data = new FormData(); + data.append('file', file); + data.append('filename', file.name); + + const result = await axios.post('player_season_stats/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( + 'player_season_stats/updatePlayer_season_stats', + async (payload: any, { rejectWithValue }) => { + try { + const result = await axios.put(`player_season_stats/${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 player_season_statsSlice = createSlice({ + name: 'player_season_stats', + 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.player_season_stats = action.payload.rows; + state.count = action.payload.count; + } else { + state.player_season_stats = 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, 'Player_season_stats 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, + `${'Player_season_stats'.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, + `${'Player_season_stats'.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, + `${'Player_season_stats'.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, 'Player_season_stats 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 } = player_season_statsSlice.actions; + +export default player_season_statsSlice.reducer; diff --git a/frontend/src/stores/seasons/seasonsSlice.ts b/frontend/src/stores/seasons/seasonsSlice.ts new file mode 100644 index 0000000..a719de1 --- /dev/null +++ b/frontend/src/stores/seasons/seasonsSlice.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 { + seasons: any; + loading: boolean; + count: number; + refetch: boolean; + rolesWidgets: any[]; + notify: { + showNotification: boolean; + textNotification: string; + typeNotification: string; + }; +} + +const initialState: MainState = { + seasons: [], + loading: false, + count: 0, + refetch: false, + rolesWidgets: [], + notify: { + showNotification: false, + textNotification: '', + typeNotification: 'warn', + }, +}; + +export const fetch = createAsyncThunk('seasons/fetch', async (data: any) => { + const { id, query } = data; + const result = await axios.get(`seasons${query || (id ? `/${id}` : '')}`); + return id + ? result.data + : { rows: result.data.rows, count: result.data.count }; +}); + +export const deleteItemsByIds = createAsyncThunk( + 'seasons/deleteByIds', + async (data: any, { rejectWithValue }) => { + try { + await axios.post('seasons/deleteByIds', { data }); + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const deleteItem = createAsyncThunk( + 'seasons/deleteSeasons', + async (id: string, { rejectWithValue }) => { + try { + await axios.delete(`seasons/${id}`); + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const create = createAsyncThunk( + 'seasons/createSeasons', + async (data: any, { rejectWithValue }) => { + try { + const result = await axios.post('seasons', { data }); + return result.data; + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const uploadCsv = createAsyncThunk( + 'seasons/uploadCsv', + async (file: File, { rejectWithValue }) => { + try { + const data = new FormData(); + data.append('file', file); + data.append('filename', file.name); + + const result = await axios.post('seasons/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( + 'seasons/updateSeasons', + async (payload: any, { rejectWithValue }) => { + try { + const result = await axios.put(`seasons/${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 seasonsSlice = createSlice({ + name: 'seasons', + 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.seasons = action.payload.rows; + state.count = action.payload.count; + } else { + state.seasons = 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, 'Seasons 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, `${'Seasons'.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, `${'Seasons'.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, `${'Seasons'.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, 'Seasons 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 } = seasonsSlice.actions; + +export default seasonsSlice.reducer; diff --git a/frontend/src/stores/store.ts b/frontend/src/stores/store.ts index f67ef82..d2a8a9a 100644 --- a/frontend/src/stores/store.ts +++ b/frontend/src/stores/store.ts @@ -12,6 +12,10 @@ import venuesSlice from './venues/venuesSlice'; import rolesSlice from './roles/rolesSlice'; import permissionsSlice from './permissions/permissionsSlice'; import leaguesSlice from './leagues/leaguesSlice'; +import seasonsSlice from './seasons/seasonsSlice'; +import player_game_scoresSlice from './player_game_scores/player_game_scoresSlice'; +import player_season_statsSlice from './player_season_stats/player_season_statsSlice'; +import team_season_statsSlice from './team_season_stats/team_season_statsSlice'; export const store = configureStore({ reducer: { @@ -28,6 +32,10 @@ export const store = configureStore({ roles: rolesSlice, permissions: permissionsSlice, leagues: leaguesSlice, + seasons: seasonsSlice, + player_game_scores: player_game_scoresSlice, + player_season_stats: player_season_statsSlice, + team_season_stats: team_season_statsSlice, }, }); diff --git a/frontend/src/stores/team_season_stats/team_season_statsSlice.ts b/frontend/src/stores/team_season_stats/team_season_statsSlice.ts new file mode 100644 index 0000000..d1dff4e --- /dev/null +++ b/frontend/src/stores/team_season_stats/team_season_statsSlice.ts @@ -0,0 +1,250 @@ +import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit'; +import axios from 'axios'; +import { + fulfilledNotify, + rejectNotify, + resetNotify, +} from '../../helpers/notifyStateHandler'; + +interface MainState { + team_season_stats: any; + loading: boolean; + count: number; + refetch: boolean; + rolesWidgets: any[]; + notify: { + showNotification: boolean; + textNotification: string; + typeNotification: string; + }; +} + +const initialState: MainState = { + team_season_stats: [], + loading: false, + count: 0, + refetch: false, + rolesWidgets: [], + notify: { + showNotification: false, + textNotification: '', + typeNotification: 'warn', + }, +}; + +export const fetch = createAsyncThunk( + 'team_season_stats/fetch', + async (data: any) => { + const { id, query } = data; + const result = await axios.get( + `team_season_stats${query || (id ? `/${id}` : '')}`, + ); + return id + ? result.data + : { rows: result.data.rows, count: result.data.count }; + }, +); + +export const deleteItemsByIds = createAsyncThunk( + 'team_season_stats/deleteByIds', + async (data: any, { rejectWithValue }) => { + try { + await axios.post('team_season_stats/deleteByIds', { data }); + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const deleteItem = createAsyncThunk( + 'team_season_stats/deleteTeam_season_stats', + async (id: string, { rejectWithValue }) => { + try { + await axios.delete(`team_season_stats/${id}`); + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const create = createAsyncThunk( + 'team_season_stats/createTeam_season_stats', + async (data: any, { rejectWithValue }) => { + try { + const result = await axios.post('team_season_stats', { data }); + return result.data; + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const uploadCsv = createAsyncThunk( + 'team_season_stats/uploadCsv', + async (file: File, { rejectWithValue }) => { + try { + const data = new FormData(); + data.append('file', file); + data.append('filename', file.name); + + const result = await axios.post('team_season_stats/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( + 'team_season_stats/updateTeam_season_stats', + async (payload: any, { rejectWithValue }) => { + try { + const result = await axios.put(`team_season_stats/${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 team_season_statsSlice = createSlice({ + name: 'team_season_stats', + 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.team_season_stats = action.payload.rows; + state.count = action.payload.count; + } else { + state.team_season_stats = 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, 'Team_season_stats 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, + `${'Team_season_stats'.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, + `${'Team_season_stats'.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, + `${'Team_season_stats'.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, 'Team_season_stats 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 } = team_season_statsSlice.actions; + +export default team_season_statsSlice.reducer;