From 28025099a611a3b62aafe20121a888c1fd7e9c19 Mon Sep 17 00:00:00 2001
From: Flatlogic Bot
Date: Tue, 2 Sep 2025 19:35:22 +0000
Subject: [PATCH] Updated via schema editor on 2025-09-02 19:34
---
app-shell/src/_schema.json | 3 +-
backend/src/db/api/games.js | 46 +
backend/src/db/api/leagues.js | 39 +
backend/src/db/api/player_game_scores.js | 432 ++++++
backend/src/db/api/player_season_stats.js | 535 +++++++
backend/src/db/api/players.js | 10 +
backend/src/db/api/seasons.js | 405 +++++
backend/src/db/api/team_season_stats.js | 387 +++++
backend/src/db/api/teams.js | 8 +
backend/src/db/migrations/1756838714724.js | 90 ++
backend/src/db/migrations/1756838754108.js | 47 +
backend/src/db/migrations/1756838786759.js | 49 +
backend/src/db/migrations/1756838811807.js | 47 +
backend/src/db/migrations/1756838843347.js | 52 +
backend/src/db/migrations/1756838870227.js | 52 +
backend/src/db/migrations/1756838906955.js | 90 ++
backend/src/db/migrations/1756838945828.js | 54 +
backend/src/db/migrations/1756838975793.js | 54 +
backend/src/db/migrations/1756839010904.js | 54 +
backend/src/db/migrations/1756839033615.js | 49 +
backend/src/db/migrations/1756839064708.js | 90 ++
backend/src/db/migrations/1756839091072.js | 54 +
backend/src/db/migrations/1756839123590.js | 54 +
backend/src/db/migrations/1756839155088.js | 49 +
backend/src/db/migrations/1756839183953.js | 49 +
backend/src/db/migrations/1756839212457.js | 51 +
backend/src/db/migrations/1756839243210.js | 51 +
backend/src/db/migrations/1756839276003.js | 49 +
backend/src/db/migrations/1756839309071.js | 90 ++
backend/src/db/migrations/1756839330961.js | 54 +
backend/src/db/migrations/1756839362416.js | 54 +
backend/src/db/migrations/1756839362565.js | 36 +
backend/src/db/models/games.js | 16 +
backend/src/db/models/leagues.js | 44 +
backend/src/db/models/player_game_scores.js | 77 +
backend/src/db/models/player_season_stats.js | 89 ++
backend/src/db/models/players.js | 16 +
backend/src/db/models/seasons.js | 109 ++
backend/src/db/models/team_season_stats.js | 69 +
backend/src/db/models/teams.js | 16 +
.../db/seeders/20200430130760-user-roles.js | 204 +++
.../db/seeders/20231127130745-sample-data.js | 1336 +++++++++++++++++
backend/src/db/seeders/20250902184514.js | 87 ++
backend/src/db/seeders/20250902184826.js | 87 ++
backend/src/db/seeders/20250902185104.js | 87 ++
backend/src/db/seeders/20250902185509.js | 87 ++
backend/src/index.js | 32 +
backend/src/routes/leagues.js | 5 +-
backend/src/routes/player_game_scores.js | 455 ++++++
backend/src/routes/player_season_stats.js | 483 ++++++
backend/src/routes/seasons.js | 452 ++++++
backend/src/routes/team_season_stats.js | 455 ++++++
backend/src/services/player_game_scores.js | 121 ++
backend/src/services/player_season_stats.js | 121 ++
backend/src/services/search.js | 16 +-
backend/src/services/seasons.js | 114 ++
backend/src/services/team_season_stats.js | 121 ++
frontend/src/components/Games/CardGames.tsx | 11 +
frontend/src/components/Games/ListGames.tsx | 7 +
.../components/Games/configureGamesCols.tsx | 20 +
.../src/components/Leagues/CardLeagues.tsx | 11 +
.../src/components/Leagues/ListLeagues.tsx | 7 +
.../Leagues/configureLeaguesCols.tsx | 12 +
.../CardPlayer_game_scores.tsx | 130 ++
.../ListPlayer_game_scores.tsx | 106 ++
.../TablePlayer_game_scores.tsx | 486 ++++++
.../configurePlayer_game_scoresCols.tsx | 122 ++
.../CardPlayer_season_stats.tsx | 178 +++
.../ListPlayer_season_stats.tsx | 128 ++
.../TablePlayer_season_stats.tsx | 486 ++++++
.../configurePlayer_season_statsCols.tsx | 172 +++
.../src/components/Seasons/CardSeasons.tsx | 138 ++
.../src/components/Seasons/ListSeasons.tsx | 108 ++
.../src/components/Seasons/TableSeasons.tsx | 481 ++++++
.../Seasons/configureSeasonsCols.tsx | 126 ++
.../CardTeam_season_stats.tsx | 121 ++
.../ListTeam_season_stats.tsx | 99 ++
.../TableTeam_season_stats.tsx | 484 ++++++
.../configureTeam_season_statsCols.tsx | 102 ++
.../components/WebPageComponents/Footer.tsx | 2 +-
.../components/WebPageComponents/Header.tsx | 4 +-
frontend/src/helpers/dataFormatter.js | 57 +
frontend/src/menuAside.ts | 32 +
frontend/src/pages/dashboard.tsx | 143 ++
frontend/src/pages/games/[gamesId].tsx | 13 +
frontend/src/pages/games/games-edit.tsx | 13 +
frontend/src/pages/games/games-list.tsx | 2 +
frontend/src/pages/games/games-new.tsx | 12 +
frontend/src/pages/games/games-table.tsx | 2 +
frontend/src/pages/games/games-view.tsx | 39 +
frontend/src/pages/index.tsx | 2 +-
frontend/src/pages/leagues/[leaguesId].tsx | 6 +
frontend/src/pages/leagues/leagues-edit.tsx | 6 +
frontend/src/pages/leagues/leagues-list.tsx | 5 +-
frontend/src/pages/leagues/leagues-new.tsx | 6 +
frontend/src/pages/leagues/leagues-table.tsx | 5 +-
frontend/src/pages/leagues/leagues-view.tsx | 224 +++
.../[player_game_scoresId].tsx | 183 +++
.../player_game_scores-edit.tsx | 181 +++
.../player_game_scores-list.tsx | 173 +++
.../player_game_scores-new.tsx | 142 ++
.../player_game_scores-table.tsx | 172 +++
.../player_game_scores-view.tsx | 110 ++
.../[player_season_statsId].tsx | 220 +++
.../player_season_stats-edit.tsx | 218 +++
.../player_season_stats-list.tsx | 177 +++
.../player_season_stats-new.tsx | 180 +++
.../player_season_stats-table.tsx | 176 +++
.../player_season_stats-view.tsx | 129 ++
frontend/src/pages/players/players-view.tsx | 90 ++
frontend/src/pages/seasons/[seasonsId].tsx | 194 +++
frontend/src/pages/seasons/seasons-edit.tsx | 192 +++
frontend/src/pages/seasons/seasons-list.tsx | 162 ++
frontend/src/pages/seasons/seasons-new.tsx | 136 ++
frontend/src/pages/seasons/seasons-table.tsx | 161 ++
frontend/src/pages/seasons/seasons-view.tsx | 274 ++++
.../[team_season_statsId].tsx | 170 +++
.../team_season_stats-edit.tsx | 168 +++
.../team_season_stats-list.tsx | 169 +++
.../team_season_stats-new.tsx | 130 ++
.../team_season_stats-table.tsx | 168 +++
.../team_season_stats-view.tsx | 104 ++
frontend/src/pages/teams/teams-view.tsx | 66 +
frontend/src/pages/venues/venues-view.tsx | 6 +
.../player_game_scoresSlice.ts | 250 +++
.../player_season_statsSlice.ts | 250 +++
frontend/src/stores/seasons/seasonsSlice.ts | 236 +++
frontend/src/stores/store.ts | 8 +
.../team_season_statsSlice.ts | 250 +++
129 files changed, 17227 insertions(+), 9 deletions(-)
create mode 100644 backend/src/db/api/player_game_scores.js
create mode 100644 backend/src/db/api/player_season_stats.js
create mode 100644 backend/src/db/api/seasons.js
create mode 100644 backend/src/db/api/team_season_stats.js
create mode 100644 backend/src/db/migrations/1756838714724.js
create mode 100644 backend/src/db/migrations/1756838754108.js
create mode 100644 backend/src/db/migrations/1756838786759.js
create mode 100644 backend/src/db/migrations/1756838811807.js
create mode 100644 backend/src/db/migrations/1756838843347.js
create mode 100644 backend/src/db/migrations/1756838870227.js
create mode 100644 backend/src/db/migrations/1756838906955.js
create mode 100644 backend/src/db/migrations/1756838945828.js
create mode 100644 backend/src/db/migrations/1756838975793.js
create mode 100644 backend/src/db/migrations/1756839010904.js
create mode 100644 backend/src/db/migrations/1756839033615.js
create mode 100644 backend/src/db/migrations/1756839064708.js
create mode 100644 backend/src/db/migrations/1756839091072.js
create mode 100644 backend/src/db/migrations/1756839123590.js
create mode 100644 backend/src/db/migrations/1756839155088.js
create mode 100644 backend/src/db/migrations/1756839183953.js
create mode 100644 backend/src/db/migrations/1756839212457.js
create mode 100644 backend/src/db/migrations/1756839243210.js
create mode 100644 backend/src/db/migrations/1756839276003.js
create mode 100644 backend/src/db/migrations/1756839309071.js
create mode 100644 backend/src/db/migrations/1756839330961.js
create mode 100644 backend/src/db/migrations/1756839362416.js
create mode 100644 backend/src/db/migrations/1756839362565.js
create mode 100644 backend/src/db/models/player_game_scores.js
create mode 100644 backend/src/db/models/player_season_stats.js
create mode 100644 backend/src/db/models/seasons.js
create mode 100644 backend/src/db/models/team_season_stats.js
create mode 100644 backend/src/db/seeders/20250902184514.js
create mode 100644 backend/src/db/seeders/20250902184826.js
create mode 100644 backend/src/db/seeders/20250902185104.js
create mode 100644 backend/src/db/seeders/20250902185509.js
create mode 100644 backend/src/routes/player_game_scores.js
create mode 100644 backend/src/routes/player_season_stats.js
create mode 100644 backend/src/routes/seasons.js
create mode 100644 backend/src/routes/team_season_stats.js
create mode 100644 backend/src/services/player_game_scores.js
create mode 100644 backend/src/services/player_season_stats.js
create mode 100644 backend/src/services/seasons.js
create mode 100644 backend/src/services/team_season_stats.js
create mode 100644 frontend/src/components/Player_game_scores/CardPlayer_game_scores.tsx
create mode 100644 frontend/src/components/Player_game_scores/ListPlayer_game_scores.tsx
create mode 100644 frontend/src/components/Player_game_scores/TablePlayer_game_scores.tsx
create mode 100644 frontend/src/components/Player_game_scores/configurePlayer_game_scoresCols.tsx
create mode 100644 frontend/src/components/Player_season_stats/CardPlayer_season_stats.tsx
create mode 100644 frontend/src/components/Player_season_stats/ListPlayer_season_stats.tsx
create mode 100644 frontend/src/components/Player_season_stats/TablePlayer_season_stats.tsx
create mode 100644 frontend/src/components/Player_season_stats/configurePlayer_season_statsCols.tsx
create mode 100644 frontend/src/components/Seasons/CardSeasons.tsx
create mode 100644 frontend/src/components/Seasons/ListSeasons.tsx
create mode 100644 frontend/src/components/Seasons/TableSeasons.tsx
create mode 100644 frontend/src/components/Seasons/configureSeasonsCols.tsx
create mode 100644 frontend/src/components/Team_season_stats/CardTeam_season_stats.tsx
create mode 100644 frontend/src/components/Team_season_stats/ListTeam_season_stats.tsx
create mode 100644 frontend/src/components/Team_season_stats/TableTeam_season_stats.tsx
create mode 100644 frontend/src/components/Team_season_stats/configureTeam_season_statsCols.tsx
create mode 100644 frontend/src/pages/player_game_scores/[player_game_scoresId].tsx
create mode 100644 frontend/src/pages/player_game_scores/player_game_scores-edit.tsx
create mode 100644 frontend/src/pages/player_game_scores/player_game_scores-list.tsx
create mode 100644 frontend/src/pages/player_game_scores/player_game_scores-new.tsx
create mode 100644 frontend/src/pages/player_game_scores/player_game_scores-table.tsx
create mode 100644 frontend/src/pages/player_game_scores/player_game_scores-view.tsx
create mode 100644 frontend/src/pages/player_season_stats/[player_season_statsId].tsx
create mode 100644 frontend/src/pages/player_season_stats/player_season_stats-edit.tsx
create mode 100644 frontend/src/pages/player_season_stats/player_season_stats-list.tsx
create mode 100644 frontend/src/pages/player_season_stats/player_season_stats-new.tsx
create mode 100644 frontend/src/pages/player_season_stats/player_season_stats-table.tsx
create mode 100644 frontend/src/pages/player_season_stats/player_season_stats-view.tsx
create mode 100644 frontend/src/pages/seasons/[seasonsId].tsx
create mode 100644 frontend/src/pages/seasons/seasons-edit.tsx
create mode 100644 frontend/src/pages/seasons/seasons-list.tsx
create mode 100644 frontend/src/pages/seasons/seasons-new.tsx
create mode 100644 frontend/src/pages/seasons/seasons-table.tsx
create mode 100644 frontend/src/pages/seasons/seasons-view.tsx
create mode 100644 frontend/src/pages/team_season_stats/[team_season_statsId].tsx
create mode 100644 frontend/src/pages/team_season_stats/team_season_stats-edit.tsx
create mode 100644 frontend/src/pages/team_season_stats/team_season_stats-list.tsx
create mode 100644 frontend/src/pages/team_season_stats/team_season_stats-new.tsx
create mode 100644 frontend/src/pages/team_season_stats/team_season_stats-table.tsx
create mode 100644 frontend/src/pages/team_season_stats/team_season_stats-view.tsx
create mode 100644 frontend/src/stores/player_game_scores/player_game_scoresSlice.ts
create mode 100644 frontend/src/stores/player_season_stats/player_season_statsSlice.ts
create mode 100644 frontend/src/stores/seasons/seasonsSlice.ts
create mode 100644 frontend/src/stores/team_season_stats/team_season_statsSlice.ts
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 && (
+
+ )}
+
+
+
+ );
+};
+
+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 && (
+
+ )}
+
+
+ >
+ );
+};
+
+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}
+ >
+
+
+
+ ) : 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 && (
+
+ )}
+
+
+
+ );
+};
+
+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 && (
+
+ )}
+
+
+ >
+ );
+};
+
+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}
+ >
+
+
+
+ ) : 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 && (
+
+ )}
+
+
+
+ );
+};
+
+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'
+ }
+ >
+
+
+
+
Startdate
+
+ {dataFormatter.dateFormatter(item.startdate)}
+
+
+
+
+
Enddate
+
+ {dataFormatter.dateFormatter(item.enddate)}
+
+
+
+
+
League
+
+ {dataFormatter.leaguesOneListFormatter(item.league)}
+
+
+
+
+
+
+
+ ))}
+ {!loading && seasons.length === 0 && (
+
+ )}
+
+
+ >
+ );
+};
+
+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}
+ >
+
+
+
+ ) : 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 && (
+
+ )}
+
+
+
+ );
+};
+
+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 && (
+
+ )}
+
+
+ >
+ );
+};
+
+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}
+ >
+
+
+
+ ) : 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 (
>
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
+
+
+
+
+
+ | Name |
+
+ Startdate |
+
+ Enddate |
+
+
+
+ {leagues.seasons_leagues &&
+ Array.isArray(leagues.seasons_leagues) &&
+ leagues.seasons_leagues.map((item: any) => (
+
+ router.push(`/seasons/seasons-view/?id=${item.id}`)
+ }
+ >
+ | {item.name} |
+
+
+ {dataFormatter.dateFormatter(item.startdate)}
+ |
+
+
+ {dataFormatter.dateFormatter(item.enddate)}
+ |
+
+ ))}
+
+
+
+ {!leagues?.seasons_leagues?.length && (
+ No data
+ )}
+
+ >
+
+ <>
+ Seasons League
+
+
+
+
+
+ | Name |
+
+ Startdate |
+
+ Enddate |
+
+
+
+ {leagues.seasons_league &&
+ Array.isArray(leagues.seasons_league) &&
+ leagues.seasons_league.map((item: any) => (
+
+ router.push(`/seasons/seasons-view/?id=${item.id}`)
+ }
+ >
+ | {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
+
+
+
+
+
+
+ | Totalpoints |
+
+ Gamesplayed |
+
+ Eightballruns |
+
+ Eightballbreaks |
+
+ Nineballruns |
+
+
+
+ {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}`,
+ )
+ }
+ >
+ | {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)}
+ >
+
+
+
+
+ >
+ );
+};
+
+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)}
+ >
+
+
+
+
+ >
+ );
+};
+
+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)}
+ >
+
+
+
+
+ >
+ );
+};
+
+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)}
+ >
+
+
+
+
+ >
+ );
+};
+
+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)}
+ >
+
+
+
+
+ >
+ );
+};
+
+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)}
+ >
+
+
+
+
+ >
+ );
+};
+
+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
+
+
+
+
+
+ | Totalpoints |
+
+ Gamesplayed |
+
+ Eightballruns |
+
+ Eightballbreaks |
+
+ Nineballruns |
+
+
+
+ {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}`,
+ )
+ }
+ >
+ | {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)}
+ >
+
+
+
+
+ >
+ );
+};
+
+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)}
+ >
+
+
+
+
+ >
+ );
+};
+
+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)}
+ >
+
+
+
+
+ >
+ );
+};
+
+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
+
+
+
+
+
+ | GameDate |
+
+ TeamScore |
+
+ OpponentScore |
+
+
+
+ {seasons.games_season &&
+ Array.isArray(seasons.games_season) &&
+ seasons.games_season.map((item: any) => (
+
+ router.push(`/games/games-view/?id=${item.id}`)
+ }
+ >
+ |
+ {dataFormatter.dateTimeFormatter(item.date)}
+ |
+
+ {item.team_score} |
+
+
+ {item.opponent_score}
+ |
+
+ ))}
+
+
+
+ {!seasons?.games_season?.length && (
+ No data
+ )}
+
+ >
+
+ <>
+ Player_season_stats Season
+
+
+
+
+
+ | Totalpoints |
+
+ Gamesplayed |
+
+ Eightballruns |
+
+ Eightballbreaks |
+
+ Nineballruns |
+
+
+
+ {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}`,
+ )
+ }
+ >
+ | {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)}
+ >
+
+
+
+
+ >
+ );
+};
+
+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)}
+ >
+
+
+
+
+ >
+ );
+};
+
+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)}
+ >
+
+
+
+
+ >
+ );
+};
+
+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;