From 1a61549626db522f6e53f745283b7979bef5f299 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Mon, 11 Aug 2025 05:59:47 +0000 Subject: [PATCH] v2 --- app-shell/src/_schema.json | 3 +- backend/src/db/api/game_answers.js | 302 ++++++++ backend/src/db/api/game_questions.js | 303 ++++++++ backend/src/db/api/game_sessions.js | 302 ++++++++ backend/src/db/api/schools.js | 12 + backend/src/db/migrations/1754891761570.js | 90 +++ backend/src/db/migrations/1754891814723.js | 90 +++ backend/src/db/migrations/1754891845702.js | 90 +++ backend/src/db/models/game_answers.js | 53 ++ backend/src/db/models/game_questions.js | 53 ++ backend/src/db/models/game_sessions.js | 53 ++ backend/src/db/models/schools.js | 24 + .../db/seeders/20200430130760-user-roles.js | 153 ++++ .../db/seeders/20231127130745-sample-data.js | 685 +++++++++++++++++- backend/src/db/seeders/20250811055601.js | 87 +++ backend/src/db/seeders/20250811055654.js | 87 +++ backend/src/db/seeders/20250811055725.js | 87 +++ backend/src/index.js | 24 + backend/src/routes/game_answers.js | 452 ++++++++++++ backend/src/routes/game_questions.js | 452 ++++++++++++ backend/src/routes/game_sessions.js | 452 ++++++++++++ backend/src/services/game_answers.js | 117 +++ backend/src/services/game_questions.js | 117 +++ backend/src/services/game_sessions.js | 117 +++ .../Game_answers/CardGame_answers.tsx | 98 +++ .../Game_answers/ListGame_answers.tsx | 84 +++ .../Game_answers/TableGame_answers.tsx | 487 +++++++++++++ .../configureGame_answersCols.tsx | 62 ++ .../Game_questions/CardGame_questions.tsx | 101 +++ .../Game_questions/ListGame_questions.tsx | 87 +++ .../Game_questions/TableGame_questions.tsx | 487 +++++++++++++ .../configureGame_questionsCols.tsx | 62 ++ .../Game_sessions/CardGame_sessions.tsx | 101 +++ .../Game_sessions/ListGame_sessions.tsx | 87 +++ .../Game_sessions/TableGame_sessions.tsx | 487 +++++++++++++ .../configureGame_sessionsCols.tsx | 62 ++ .../components/WebPageComponents/Footer.tsx | 2 +- .../components/WebPageComponents/Header.tsx | 4 +- frontend/src/menuAside.ts | 24 + frontend/src/pages/dashboard.tsx | 105 +++ .../pages/game_answers/[game_answersId].tsx | 137 ++++ .../pages/game_answers/game_answers-edit.tsx | 135 ++++ .../pages/game_answers/game_answers-list.tsx | 165 +++++ .../pages/game_answers/game_answers-new.tsx | 104 +++ .../pages/game_answers/game_answers-table.tsx | 164 +++++ .../pages/game_answers/game_answers-view.tsx | 88 +++ .../game_questions/[game_questionsId].tsx | 139 ++++ .../game_questions/game_questions-edit.tsx | 137 ++++ .../game_questions/game_questions-list.tsx | 165 +++++ .../game_questions/game_questions-new.tsx | 106 +++ .../game_questions/game_questions-table.tsx | 164 +++++ .../game_questions/game_questions-view.tsx | 88 +++ .../pages/game_sessions/[game_sessionsId].tsx | 139 ++++ .../game_sessions/game_sessions-edit.tsx | 137 ++++ .../game_sessions/game_sessions-list.tsx | 165 +++++ .../pages/game_sessions/game_sessions-new.tsx | 106 +++ .../game_sessions/game_sessions-table.tsx | 164 +++++ .../game_sessions/game_sessions-view.tsx | 88 +++ frontend/src/pages/schools/schools-view.tsx | 99 +++ frontend/src/pages/web_pages/home.tsx | 2 +- frontend/src/pages/web_pages/products.tsx | 4 +- .../stores/game_answers/game_answersSlice.ts | 241 ++++++ .../game_questions/game_questionsSlice.ts | 250 +++++++ .../game_sessions/game_sessionsSlice.ts | 250 +++++++ frontend/src/stores/store.ts | 6 + 65 files changed, 9779 insertions(+), 9 deletions(-) create mode 100644 backend/src/db/api/game_answers.js create mode 100644 backend/src/db/api/game_questions.js create mode 100644 backend/src/db/api/game_sessions.js create mode 100644 backend/src/db/migrations/1754891761570.js create mode 100644 backend/src/db/migrations/1754891814723.js create mode 100644 backend/src/db/migrations/1754891845702.js create mode 100644 backend/src/db/models/game_answers.js create mode 100644 backend/src/db/models/game_questions.js create mode 100644 backend/src/db/models/game_sessions.js create mode 100644 backend/src/db/seeders/20250811055601.js create mode 100644 backend/src/db/seeders/20250811055654.js create mode 100644 backend/src/db/seeders/20250811055725.js create mode 100644 backend/src/routes/game_answers.js create mode 100644 backend/src/routes/game_questions.js create mode 100644 backend/src/routes/game_sessions.js create mode 100644 backend/src/services/game_answers.js create mode 100644 backend/src/services/game_questions.js create mode 100644 backend/src/services/game_sessions.js create mode 100644 frontend/src/components/Game_answers/CardGame_answers.tsx create mode 100644 frontend/src/components/Game_answers/ListGame_answers.tsx create mode 100644 frontend/src/components/Game_answers/TableGame_answers.tsx create mode 100644 frontend/src/components/Game_answers/configureGame_answersCols.tsx create mode 100644 frontend/src/components/Game_questions/CardGame_questions.tsx create mode 100644 frontend/src/components/Game_questions/ListGame_questions.tsx create mode 100644 frontend/src/components/Game_questions/TableGame_questions.tsx create mode 100644 frontend/src/components/Game_questions/configureGame_questionsCols.tsx create mode 100644 frontend/src/components/Game_sessions/CardGame_sessions.tsx create mode 100644 frontend/src/components/Game_sessions/ListGame_sessions.tsx create mode 100644 frontend/src/components/Game_sessions/TableGame_sessions.tsx create mode 100644 frontend/src/components/Game_sessions/configureGame_sessionsCols.tsx create mode 100644 frontend/src/pages/game_answers/[game_answersId].tsx create mode 100644 frontend/src/pages/game_answers/game_answers-edit.tsx create mode 100644 frontend/src/pages/game_answers/game_answers-list.tsx create mode 100644 frontend/src/pages/game_answers/game_answers-new.tsx create mode 100644 frontend/src/pages/game_answers/game_answers-table.tsx create mode 100644 frontend/src/pages/game_answers/game_answers-view.tsx create mode 100644 frontend/src/pages/game_questions/[game_questionsId].tsx create mode 100644 frontend/src/pages/game_questions/game_questions-edit.tsx create mode 100644 frontend/src/pages/game_questions/game_questions-list.tsx create mode 100644 frontend/src/pages/game_questions/game_questions-new.tsx create mode 100644 frontend/src/pages/game_questions/game_questions-table.tsx create mode 100644 frontend/src/pages/game_questions/game_questions-view.tsx create mode 100644 frontend/src/pages/game_sessions/[game_sessionsId].tsx create mode 100644 frontend/src/pages/game_sessions/game_sessions-edit.tsx create mode 100644 frontend/src/pages/game_sessions/game_sessions-list.tsx create mode 100644 frontend/src/pages/game_sessions/game_sessions-new.tsx create mode 100644 frontend/src/pages/game_sessions/game_sessions-table.tsx create mode 100644 frontend/src/pages/game_sessions/game_sessions-view.tsx create mode 100644 frontend/src/stores/game_answers/game_answersSlice.ts create mode 100644 frontend/src/stores/game_questions/game_questionsSlice.ts create mode 100644 frontend/src/stores/game_sessions/game_sessionsSlice.ts diff --git a/app-shell/src/_schema.json b/app-shell/src/_schema.json index d577efa..d9af839 100644 --- a/app-shell/src/_schema.json +++ b/app-shell/src/_schema.json @@ -1,4 +1,5 @@ { "Initial version": "{\"iv\":\"VS6DEtweZCzyWuCs\",\"encryptedData\":\"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\"}", - "v1": "{\"iv\":\"q1IVrVHAElQv4q0q\",\"encryptedData\":\"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\"}" + "v1": "{\"iv\":\"q1IVrVHAElQv4q0q\",\"encryptedData\":\"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\"}", + "v2": "{\"iv\":\"w5z77m4Sr4T2nnPd\",\"encryptedData\":\"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\"}" } \ No newline at end of file diff --git a/backend/src/db/api/game_answers.js b/backend/src/db/api/game_answers.js new file mode 100644 index 0000000..690949e --- /dev/null +++ b/backend/src/db/api/game_answers.js @@ -0,0 +1,302 @@ +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 Game_answersDBApi { + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const game_answers = await db.game_answers.create( + { + id: data.id || undefined, + + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + await game_answers.setSchools(data.schools || null, { + transaction, + }); + + return game_answers; + } + + 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 game_answersData = 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 game_answers = await db.game_answers.bulkCreate(game_answersData, { + transaction, + }); + + // For each item created, replace relation files + + return game_answers; + } + + 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 game_answers = await db.game_answers.findByPk( + id, + {}, + { transaction }, + ); + + const updatePayload = {}; + + updatePayload.updatedById = currentUser.id; + + await game_answers.update(updatePayload, { transaction }); + + if (data.schools !== undefined) { + await game_answers.setSchools( + data.schools, + + { transaction }, + ); + } + + return game_answers; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const game_answers = await db.game_answers.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of game_answers) { + await record.update({ deletedBy: currentUser.id }, { transaction }); + } + for (const record of game_answers) { + await record.destroy({ transaction }); + } + }); + + return game_answers; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const game_answers = await db.game_answers.findByPk(id, options); + + await game_answers.update( + { + deletedBy: currentUser.id, + }, + { + transaction, + }, + ); + + await game_answers.destroy({ + transaction, + }); + + return game_answers; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const game_answers = await db.game_answers.findOne( + { where }, + { transaction }, + ); + + if (!game_answers) { + return game_answers; + } + + const output = game_answers.get({ plain: true }); + + output.schools = await game_answers.getSchools({ + 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 userSchools = (user && user.schools?.id) || null; + + if (userSchools) { + if (options?.currentUser?.schoolsId) { + where.schoolsId = options.currentUser.schoolsId; + } + } + + offset = currentPage * limit; + + const orderBy = null; + + const transaction = (options && options.transaction) || undefined; + + let include = [ + { + model: db.schools, + as: 'schools', + }, + ]; + + 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.schools) { + const listItems = filter.schools.split('|').map((item) => { + return Utils.uuid(item); + }); + + where = { + ...where, + schoolsId: { [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.schoolsId; + } + + 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.game_answers.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('game_answers', 'id', query), + ], + }; + } + + const records = await db.game_answers.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/game_questions.js b/backend/src/db/api/game_questions.js new file mode 100644 index 0000000..90bb9dd --- /dev/null +++ b/backend/src/db/api/game_questions.js @@ -0,0 +1,303 @@ +const db = require('../models'); +const FileDBApi = require('./file'); +const crypto = require('crypto'); +const Utils = require('../utils'); + +const Sequelize = db.Sequelize; +const Op = Sequelize.Op; + +module.exports = class Game_questionsDBApi { + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const game_questions = await db.game_questions.create( + { + id: data.id || undefined, + + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + await game_questions.setSchools(data.schools || null, { + transaction, + }); + + return game_questions; + } + + 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 game_questionsData = 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 game_questions = await db.game_questions.bulkCreate( + game_questionsData, + { transaction }, + ); + + // For each item created, replace relation files + + return game_questions; + } + + 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 game_questions = await db.game_questions.findByPk( + id, + {}, + { transaction }, + ); + + const updatePayload = {}; + + updatePayload.updatedById = currentUser.id; + + await game_questions.update(updatePayload, { transaction }); + + if (data.schools !== undefined) { + await game_questions.setSchools( + data.schools, + + { transaction }, + ); + } + + return game_questions; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const game_questions = await db.game_questions.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of game_questions) { + await record.update({ deletedBy: currentUser.id }, { transaction }); + } + for (const record of game_questions) { + await record.destroy({ transaction }); + } + }); + + return game_questions; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const game_questions = await db.game_questions.findByPk(id, options); + + await game_questions.update( + { + deletedBy: currentUser.id, + }, + { + transaction, + }, + ); + + await game_questions.destroy({ + transaction, + }); + + return game_questions; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const game_questions = await db.game_questions.findOne( + { where }, + { transaction }, + ); + + if (!game_questions) { + return game_questions; + } + + const output = game_questions.get({ plain: true }); + + output.schools = await game_questions.getSchools({ + 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 userSchools = (user && user.schools?.id) || null; + + if (userSchools) { + if (options?.currentUser?.schoolsId) { + where.schoolsId = options.currentUser.schoolsId; + } + } + + offset = currentPage * limit; + + const orderBy = null; + + const transaction = (options && options.transaction) || undefined; + + let include = [ + { + model: db.schools, + as: 'schools', + }, + ]; + + 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.schools) { + const listItems = filter.schools.split('|').map((item) => { + return Utils.uuid(item); + }); + + where = { + ...where, + schoolsId: { [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.schoolsId; + } + + 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.game_questions.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('game_questions', 'id', query), + ], + }; + } + + const records = await db.game_questions.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/game_sessions.js b/backend/src/db/api/game_sessions.js new file mode 100644 index 0000000..b9d222c --- /dev/null +++ b/backend/src/db/api/game_sessions.js @@ -0,0 +1,302 @@ +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 Game_sessionsDBApi { + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const game_sessions = await db.game_sessions.create( + { + id: data.id || undefined, + + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + await game_sessions.setSchools(data.schools || null, { + transaction, + }); + + return game_sessions; + } + + 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 game_sessionsData = 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 game_sessions = await db.game_sessions.bulkCreate(game_sessionsData, { + transaction, + }); + + // For each item created, replace relation files + + return game_sessions; + } + + 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 game_sessions = await db.game_sessions.findByPk( + id, + {}, + { transaction }, + ); + + const updatePayload = {}; + + updatePayload.updatedById = currentUser.id; + + await game_sessions.update(updatePayload, { transaction }); + + if (data.schools !== undefined) { + await game_sessions.setSchools( + data.schools, + + { transaction }, + ); + } + + return game_sessions; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const game_sessions = await db.game_sessions.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of game_sessions) { + await record.update({ deletedBy: currentUser.id }, { transaction }); + } + for (const record of game_sessions) { + await record.destroy({ transaction }); + } + }); + + return game_sessions; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const game_sessions = await db.game_sessions.findByPk(id, options); + + await game_sessions.update( + { + deletedBy: currentUser.id, + }, + { + transaction, + }, + ); + + await game_sessions.destroy({ + transaction, + }); + + return game_sessions; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const game_sessions = await db.game_sessions.findOne( + { where }, + { transaction }, + ); + + if (!game_sessions) { + return game_sessions; + } + + const output = game_sessions.get({ plain: true }); + + output.schools = await game_sessions.getSchools({ + 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 userSchools = (user && user.schools?.id) || null; + + if (userSchools) { + if (options?.currentUser?.schoolsId) { + where.schoolsId = options.currentUser.schoolsId; + } + } + + offset = currentPage * limit; + + const orderBy = null; + + const transaction = (options && options.transaction) || undefined; + + let include = [ + { + model: db.schools, + as: 'schools', + }, + ]; + + 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.schools) { + const listItems = filter.schools.split('|').map((item) => { + return Utils.uuid(item); + }); + + where = { + ...where, + schoolsId: { [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.schoolsId; + } + + 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.game_sessions.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('game_sessions', 'id', query), + ], + }; + } + + const records = await db.game_sessions.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/schools.js b/backend/src/db/api/schools.js index 1df7210..c9096ff 100644 --- a/backend/src/db/api/schools.js +++ b/backend/src/db/api/schools.js @@ -154,6 +154,18 @@ module.exports = class SchoolsDBApi { transaction, }); + output.game_sessions_schools = await schools.getGame_sessions_schools({ + transaction, + }); + + output.game_questions_schools = await schools.getGame_questions_schools({ + transaction, + }); + + output.game_answers_schools = await schools.getGame_answers_schools({ + transaction, + }); + return output; } diff --git a/backend/src/db/migrations/1754891761570.js b/backend/src/db/migrations/1754891761570.js new file mode 100644 index 0000000..b6ed2c6 --- /dev/null +++ b/backend/src/db/migrations/1754891761570.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( + 'game_sessions', + { + 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( + 'game_sessions', + 'schoolsId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'schools', + 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('game_sessions', 'schoolsId', { + transaction, + }); + + await queryInterface.dropTable('game_sessions', { transaction }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1754891814723.js b/backend/src/db/migrations/1754891814723.js new file mode 100644 index 0000000..39736d6 --- /dev/null +++ b/backend/src/db/migrations/1754891814723.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( + 'game_questions', + { + 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( + 'game_questions', + 'schoolsId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'schools', + 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('game_questions', 'schoolsId', { + transaction, + }); + + await queryInterface.dropTable('game_questions', { transaction }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1754891845702.js b/backend/src/db/migrations/1754891845702.js new file mode 100644 index 0000000..1e3ffa7 --- /dev/null +++ b/backend/src/db/migrations/1754891845702.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( + 'game_answers', + { + 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( + 'game_answers', + 'schoolsId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'schools', + 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('game_answers', 'schoolsId', { + transaction, + }); + + await queryInterface.dropTable('game_answers', { transaction }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/models/game_answers.js b/backend/src/db/models/game_answers.js new file mode 100644 index 0000000..ddff402 --- /dev/null +++ b/backend/src/db/models/game_answers.js @@ -0,0 +1,53 @@ +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 game_answers = sequelize.define( + 'game_answers', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + game_answers.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.game_answers.belongsTo(db.schools, { + as: 'schools', + foreignKey: { + name: 'schoolsId', + }, + constraints: false, + }); + + db.game_answers.belongsTo(db.users, { + as: 'createdBy', + }); + + db.game_answers.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return game_answers; +}; diff --git a/backend/src/db/models/game_questions.js b/backend/src/db/models/game_questions.js new file mode 100644 index 0000000..c44258a --- /dev/null +++ b/backend/src/db/models/game_questions.js @@ -0,0 +1,53 @@ +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 game_questions = sequelize.define( + 'game_questions', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + game_questions.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.game_questions.belongsTo(db.schools, { + as: 'schools', + foreignKey: { + name: 'schoolsId', + }, + constraints: false, + }); + + db.game_questions.belongsTo(db.users, { + as: 'createdBy', + }); + + db.game_questions.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return game_questions; +}; diff --git a/backend/src/db/models/game_sessions.js b/backend/src/db/models/game_sessions.js new file mode 100644 index 0000000..79784d3 --- /dev/null +++ b/backend/src/db/models/game_sessions.js @@ -0,0 +1,53 @@ +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 game_sessions = sequelize.define( + 'game_sessions', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + game_sessions.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.game_sessions.belongsTo(db.schools, { + as: 'schools', + foreignKey: { + name: 'schoolsId', + }, + constraints: false, + }); + + db.game_sessions.belongsTo(db.users, { + as: 'createdBy', + }); + + db.game_sessions.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return game_sessions; +}; diff --git a/backend/src/db/models/schools.js b/backend/src/db/models/schools.js index 5cd7ef9..e76f379 100644 --- a/backend/src/db/models/schools.js +++ b/backend/src/db/models/schools.js @@ -90,6 +90,30 @@ module.exports = function (sequelize, DataTypes) { constraints: false, }); + db.schools.hasMany(db.game_sessions, { + as: 'game_sessions_schools', + foreignKey: { + name: 'schoolsId', + }, + constraints: false, + }); + + db.schools.hasMany(db.game_questions, { + as: 'game_questions_schools', + foreignKey: { + name: 'schoolsId', + }, + constraints: false, + }); + + db.schools.hasMany(db.game_answers, { + as: 'game_answers_schools', + foreignKey: { + name: 'schoolsId', + }, + constraints: false, + }); + //end loop db.schools.belongsTo(db.users, { diff --git a/backend/src/db/seeders/20200430130760-user-roles.js b/backend/src/db/seeders/20200430130760-user-roles.js index f58f415..2aeb03c 100644 --- a/backend/src/db/seeders/20200430130760-user-roles.js +++ b/backend/src/db/seeders/20200430130760-user-roles.js @@ -111,6 +111,9 @@ module.exports = { 'roles', 'permissions', 'schools', + 'game_sessions', + 'game_questions', + 'game_answers', , ]; await queryInterface.bulkInsert( @@ -1180,6 +1183,81 @@ primary key ("roles_permissionsId", "permissionId") permissionId: getId('DELETE_SUBMISSIONS'), }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('CREATE_GAME_SESSIONS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('READ_GAME_SESSIONS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('UPDATE_GAME_SESSIONS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('DELETE_GAME_SESSIONS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('CREATE_GAME_QUESTIONS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('READ_GAME_QUESTIONS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('UPDATE_GAME_QUESTIONS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('DELETE_GAME_QUESTIONS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('CREATE_GAME_ANSWERS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('READ_GAME_ANSWERS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('UPDATE_GAME_ANSWERS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('DELETE_GAME_ANSWERS'), + }, + { createdAt, updatedAt, @@ -1430,6 +1508,81 @@ primary key ("roles_permissionsId", "permissionId") permissionId: getId('DELETE_SCHOOLS'), }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('CREATE_GAME_SESSIONS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('READ_GAME_SESSIONS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('UPDATE_GAME_SESSIONS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('DELETE_GAME_SESSIONS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('CREATE_GAME_QUESTIONS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('READ_GAME_QUESTIONS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('UPDATE_GAME_QUESTIONS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('DELETE_GAME_QUESTIONS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('CREATE_GAME_ANSWERS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('READ_GAME_ANSWERS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('UPDATE_GAME_ANSWERS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('DELETE_GAME_ANSWERS'), + }, + { createdAt, updatedAt, diff --git a/backend/src/db/seeders/20231127130745-sample-data.js b/backend/src/db/seeders/20231127130745-sample-data.js index df74cd0..824ca4a 100644 --- a/backend/src/db/seeders/20231127130745-sample-data.js +++ b/backend/src/db/seeders/20231127130745-sample-data.js @@ -15,6 +15,12 @@ const Submissions = db.submissions; const Schools = db.schools; +const GameSessions = db.game_sessions; + +const GameQuestions = db.game_questions; + +const GameAnswers = db.game_answers; + const AssignmentsData = [ { title: 'Algebra Quiz', @@ -51,6 +57,30 @@ const AssignmentsData = [ // type code here for "relation_one" field }, + + { + title: 'Cell Structure Exam', + + due_date: new Date('2023-11-30T16:00:00Z'), + + // type code here for "relation_one" field + + // type code here for "relation_many" field + + // type code here for "relation_one" field + }, + + { + title: 'Trigonometry Challenge', + + due_date: new Date('2023-12-05T18:00:00Z'), + + // type code here for "relation_one" field + + // type code here for "relation_many" field + + // type code here for "relation_one" field + }, ]; const LeaderboardEntriesData = [ @@ -89,6 +119,30 @@ const LeaderboardEntriesData = [ // type code here for "relation_one" field }, + + { + // type code here for "relation_one" field + + // type code here for "relation_one" field + + rank: 4, + + score: 80, + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + // type code here for "relation_one" field + + rank: 5, + + score: 75, + + // type code here for "relation_one" field + }, ]; const LeaderboardsData = [ @@ -106,6 +160,16 @@ const LeaderboardsData = [ // type code here for "relation_many" field // type code here for "relation_one" field }, + + { + // type code here for "relation_many" field + // type code here for "relation_one" field + }, + + { + // type code here for "relation_many" field + // type code here for "relation_one" field + }, ]; const ModulesData = [ @@ -144,13 +208,37 @@ const ModulesData = [ // type code here for "relation_one" field }, + + { + title: 'Cell Structure', + + description: 'Detailed study of cell structure', + + // type code here for "relation_one" field + + // type code here for "relation_many" field + + // type code here for "relation_one" field + }, + + { + title: 'Trigonometry', + + description: 'Basics of trigonometry', + + // type code here for "relation_one" field + + // type code here for "relation_many" field + + // type code here for "relation_one" field + }, ]; const SubjectsData = [ { name: 'Mathematics', - category: 'maths', + category: 'chemistry', // type code here for "relation_many" field @@ -160,7 +248,7 @@ const SubjectsData = [ { name: 'Chemistry', - category: 'physics', + category: 'maths', // type code here for "relation_many" field @@ -176,6 +264,26 @@ const SubjectsData = [ // type code here for "relation_one" field }, + + { + name: 'Biology', + + category: 'chemistry', + + // type code here for "relation_many" field + + // type code here for "relation_one" field + }, + + { + name: 'Mathematics', + + category: 'biology', + + // type code here for "relation_many" field + + // type code here for "relation_one" field + }, ]; const SubmissionsData = [ @@ -214,6 +322,30 @@ const SubmissionsData = [ // type code here for "relation_one" field }, + + { + // type code here for "relation_one" field + + // type code here for "relation_one" field + + score: 88, + + submitted_at: new Date('2023-11-29T15:00:00Z'), + + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + + // type code here for "relation_one" field + + score: 80, + + submitted_at: new Date('2023-12-04T17:00:00Z'), + + // type code here for "relation_one" field + }, ]; const SchoolsData = [ @@ -228,6 +360,80 @@ const SchoolsData = [ { name: 'Riverdale School', }, + + { + name: 'Hilltop Institute', + }, + + { + name: 'Lakeside School', + }, +]; + +const GameSessionsData = [ + { + // 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 GameQuestionsData = [ + { + // 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 GameAnswersData = [ + { + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + }, + + { + // type code here for "relation_one" field + }, ]; // Similar logic for "relation_many" @@ -265,6 +471,28 @@ async function associateUserWithSchool() { if (User2?.setSchool) { await User2.setSchool(relatedSchool2); } + + const relatedSchool3 = await Schools.findOne({ + offset: Math.floor(Math.random() * (await Schools.count())), + }); + const User3 = await Users.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (User3?.setSchool) { + await User3.setSchool(relatedSchool3); + } + + const relatedSchool4 = await Schools.findOne({ + offset: Math.floor(Math.random() * (await Schools.count())), + }); + const User4 = await Users.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (User4?.setSchool) { + await User4.setSchool(relatedSchool4); + } } async function associateAssignmentWithModule() { @@ -300,6 +528,28 @@ async function associateAssignmentWithModule() { if (Assignment2?.setModule) { await Assignment2.setModule(relatedModule2); } + + const relatedModule3 = await Modules.findOne({ + offset: Math.floor(Math.random() * (await Modules.count())), + }); + const Assignment3 = await Assignments.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (Assignment3?.setModule) { + await Assignment3.setModule(relatedModule3); + } + + const relatedModule4 = await Modules.findOne({ + offset: Math.floor(Math.random() * (await Modules.count())), + }); + const Assignment4 = await Assignments.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (Assignment4?.setModule) { + await Assignment4.setModule(relatedModule4); + } } // Similar logic for "relation_many" @@ -337,6 +587,28 @@ async function associateAssignmentWithSchool() { if (Assignment2?.setSchool) { await Assignment2.setSchool(relatedSchool2); } + + const relatedSchool3 = await Schools.findOne({ + offset: Math.floor(Math.random() * (await Schools.count())), + }); + const Assignment3 = await Assignments.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (Assignment3?.setSchool) { + await Assignment3.setSchool(relatedSchool3); + } + + const relatedSchool4 = await Schools.findOne({ + offset: Math.floor(Math.random() * (await Schools.count())), + }); + const Assignment4 = await Assignments.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (Assignment4?.setSchool) { + await Assignment4.setSchool(relatedSchool4); + } } async function associateLeaderboardEntryWithLeaderboard() { @@ -372,6 +644,28 @@ async function associateLeaderboardEntryWithLeaderboard() { if (LeaderboardEntry2?.setLeaderboard) { await LeaderboardEntry2.setLeaderboard(relatedLeaderboard2); } + + const relatedLeaderboard3 = await Leaderboards.findOne({ + offset: Math.floor(Math.random() * (await Leaderboards.count())), + }); + const LeaderboardEntry3 = await LeaderboardEntries.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (LeaderboardEntry3?.setLeaderboard) { + await LeaderboardEntry3.setLeaderboard(relatedLeaderboard3); + } + + const relatedLeaderboard4 = await Leaderboards.findOne({ + offset: Math.floor(Math.random() * (await Leaderboards.count())), + }); + const LeaderboardEntry4 = await LeaderboardEntries.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (LeaderboardEntry4?.setLeaderboard) { + await LeaderboardEntry4.setLeaderboard(relatedLeaderboard4); + } } async function associateLeaderboardEntryWithStudent() { @@ -407,6 +701,28 @@ async function associateLeaderboardEntryWithStudent() { if (LeaderboardEntry2?.setStudent) { await LeaderboardEntry2.setStudent(relatedStudent2); } + + const relatedStudent3 = await Users.findOne({ + offset: Math.floor(Math.random() * (await Users.count())), + }); + const LeaderboardEntry3 = await LeaderboardEntries.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (LeaderboardEntry3?.setStudent) { + await LeaderboardEntry3.setStudent(relatedStudent3); + } + + const relatedStudent4 = await Users.findOne({ + offset: Math.floor(Math.random() * (await Users.count())), + }); + const LeaderboardEntry4 = await LeaderboardEntries.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (LeaderboardEntry4?.setStudent) { + await LeaderboardEntry4.setStudent(relatedStudent4); + } } async function associateLeaderboardEntryWithSchool() { @@ -442,6 +758,28 @@ async function associateLeaderboardEntryWithSchool() { if (LeaderboardEntry2?.setSchool) { await LeaderboardEntry2.setSchool(relatedSchool2); } + + const relatedSchool3 = await Schools.findOne({ + offset: Math.floor(Math.random() * (await Schools.count())), + }); + const LeaderboardEntry3 = await LeaderboardEntries.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (LeaderboardEntry3?.setSchool) { + await LeaderboardEntry3.setSchool(relatedSchool3); + } + + const relatedSchool4 = await Schools.findOne({ + offset: Math.floor(Math.random() * (await Schools.count())), + }); + const LeaderboardEntry4 = await LeaderboardEntries.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (LeaderboardEntry4?.setSchool) { + await LeaderboardEntry4.setSchool(relatedSchool4); + } } // Similar logic for "relation_many" @@ -479,6 +817,28 @@ async function associateLeaderboardWithSchool() { if (Leaderboard2?.setSchool) { await Leaderboard2.setSchool(relatedSchool2); } + + const relatedSchool3 = await Schools.findOne({ + offset: Math.floor(Math.random() * (await Schools.count())), + }); + const Leaderboard3 = await Leaderboards.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (Leaderboard3?.setSchool) { + await Leaderboard3.setSchool(relatedSchool3); + } + + const relatedSchool4 = await Schools.findOne({ + offset: Math.floor(Math.random() * (await Schools.count())), + }); + const Leaderboard4 = await Leaderboards.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (Leaderboard4?.setSchool) { + await Leaderboard4.setSchool(relatedSchool4); + } } async function associateModuleWithSubject() { @@ -514,6 +874,28 @@ async function associateModuleWithSubject() { if (Module2?.setSubject) { await Module2.setSubject(relatedSubject2); } + + const relatedSubject3 = await Subjects.findOne({ + offset: Math.floor(Math.random() * (await Subjects.count())), + }); + const Module3 = await Modules.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (Module3?.setSubject) { + await Module3.setSubject(relatedSubject3); + } + + const relatedSubject4 = await Subjects.findOne({ + offset: Math.floor(Math.random() * (await Subjects.count())), + }); + const Module4 = await Modules.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (Module4?.setSubject) { + await Module4.setSubject(relatedSubject4); + } } // Similar logic for "relation_many" @@ -551,6 +933,28 @@ async function associateModuleWithSchool() { if (Module2?.setSchool) { await Module2.setSchool(relatedSchool2); } + + const relatedSchool3 = await Schools.findOne({ + offset: Math.floor(Math.random() * (await Schools.count())), + }); + const Module3 = await Modules.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (Module3?.setSchool) { + await Module3.setSchool(relatedSchool3); + } + + const relatedSchool4 = await Schools.findOne({ + offset: Math.floor(Math.random() * (await Schools.count())), + }); + const Module4 = await Modules.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (Module4?.setSchool) { + await Module4.setSchool(relatedSchool4); + } } // Similar logic for "relation_many" @@ -588,6 +992,28 @@ async function associateSubjectWithSchool() { if (Subject2?.setSchool) { await Subject2.setSchool(relatedSchool2); } + + const relatedSchool3 = await Schools.findOne({ + offset: Math.floor(Math.random() * (await Schools.count())), + }); + const Subject3 = await Subjects.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (Subject3?.setSchool) { + await Subject3.setSchool(relatedSchool3); + } + + const relatedSchool4 = await Schools.findOne({ + offset: Math.floor(Math.random() * (await Schools.count())), + }); + const Subject4 = await Subjects.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (Subject4?.setSchool) { + await Subject4.setSchool(relatedSchool4); + } } async function associateSubmissionWithAssignment() { @@ -623,6 +1049,28 @@ async function associateSubmissionWithAssignment() { if (Submission2?.setAssignment) { await Submission2.setAssignment(relatedAssignment2); } + + const relatedAssignment3 = await Assignments.findOne({ + offset: Math.floor(Math.random() * (await Assignments.count())), + }); + const Submission3 = await Submissions.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (Submission3?.setAssignment) { + await Submission3.setAssignment(relatedAssignment3); + } + + const relatedAssignment4 = await Assignments.findOne({ + offset: Math.floor(Math.random() * (await Assignments.count())), + }); + const Submission4 = await Submissions.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (Submission4?.setAssignment) { + await Submission4.setAssignment(relatedAssignment4); + } } async function associateSubmissionWithStudent() { @@ -658,6 +1106,28 @@ async function associateSubmissionWithStudent() { if (Submission2?.setStudent) { await Submission2.setStudent(relatedStudent2); } + + const relatedStudent3 = await Users.findOne({ + offset: Math.floor(Math.random() * (await Users.count())), + }); + const Submission3 = await Submissions.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (Submission3?.setStudent) { + await Submission3.setStudent(relatedStudent3); + } + + const relatedStudent4 = await Users.findOne({ + offset: Math.floor(Math.random() * (await Users.count())), + }); + const Submission4 = await Submissions.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (Submission4?.setStudent) { + await Submission4.setStudent(relatedStudent4); + } } async function associateSubmissionWithSchool() { @@ -693,6 +1163,199 @@ async function associateSubmissionWithSchool() { if (Submission2?.setSchool) { await Submission2.setSchool(relatedSchool2); } + + const relatedSchool3 = await Schools.findOne({ + offset: Math.floor(Math.random() * (await Schools.count())), + }); + const Submission3 = await Submissions.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (Submission3?.setSchool) { + await Submission3.setSchool(relatedSchool3); + } + + const relatedSchool4 = await Schools.findOne({ + offset: Math.floor(Math.random() * (await Schools.count())), + }); + const Submission4 = await Submissions.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (Submission4?.setSchool) { + await Submission4.setSchool(relatedSchool4); + } +} + +async function associateGameSessionWithSchool() { + const relatedSchool0 = await Schools.findOne({ + offset: Math.floor(Math.random() * (await Schools.count())), + }); + const GameSession0 = await GameSessions.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (GameSession0?.setSchool) { + await GameSession0.setSchool(relatedSchool0); + } + + const relatedSchool1 = await Schools.findOne({ + offset: Math.floor(Math.random() * (await Schools.count())), + }); + const GameSession1 = await GameSessions.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (GameSession1?.setSchool) { + await GameSession1.setSchool(relatedSchool1); + } + + const relatedSchool2 = await Schools.findOne({ + offset: Math.floor(Math.random() * (await Schools.count())), + }); + const GameSession2 = await GameSessions.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (GameSession2?.setSchool) { + await GameSession2.setSchool(relatedSchool2); + } + + const relatedSchool3 = await Schools.findOne({ + offset: Math.floor(Math.random() * (await Schools.count())), + }); + const GameSession3 = await GameSessions.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (GameSession3?.setSchool) { + await GameSession3.setSchool(relatedSchool3); + } + + const relatedSchool4 = await Schools.findOne({ + offset: Math.floor(Math.random() * (await Schools.count())), + }); + const GameSession4 = await GameSessions.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (GameSession4?.setSchool) { + await GameSession4.setSchool(relatedSchool4); + } +} + +async function associateGameQuestionWithSchool() { + const relatedSchool0 = await Schools.findOne({ + offset: Math.floor(Math.random() * (await Schools.count())), + }); + const GameQuestion0 = await GameQuestions.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (GameQuestion0?.setSchool) { + await GameQuestion0.setSchool(relatedSchool0); + } + + const relatedSchool1 = await Schools.findOne({ + offset: Math.floor(Math.random() * (await Schools.count())), + }); + const GameQuestion1 = await GameQuestions.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (GameQuestion1?.setSchool) { + await GameQuestion1.setSchool(relatedSchool1); + } + + const relatedSchool2 = await Schools.findOne({ + offset: Math.floor(Math.random() * (await Schools.count())), + }); + const GameQuestion2 = await GameQuestions.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (GameQuestion2?.setSchool) { + await GameQuestion2.setSchool(relatedSchool2); + } + + const relatedSchool3 = await Schools.findOne({ + offset: Math.floor(Math.random() * (await Schools.count())), + }); + const GameQuestion3 = await GameQuestions.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (GameQuestion3?.setSchool) { + await GameQuestion3.setSchool(relatedSchool3); + } + + const relatedSchool4 = await Schools.findOne({ + offset: Math.floor(Math.random() * (await Schools.count())), + }); + const GameQuestion4 = await GameQuestions.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (GameQuestion4?.setSchool) { + await GameQuestion4.setSchool(relatedSchool4); + } +} + +async function associateGameAnswerWithSchool() { + const relatedSchool0 = await Schools.findOne({ + offset: Math.floor(Math.random() * (await Schools.count())), + }); + const GameAnswer0 = await GameAnswers.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (GameAnswer0?.setSchool) { + await GameAnswer0.setSchool(relatedSchool0); + } + + const relatedSchool1 = await Schools.findOne({ + offset: Math.floor(Math.random() * (await Schools.count())), + }); + const GameAnswer1 = await GameAnswers.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (GameAnswer1?.setSchool) { + await GameAnswer1.setSchool(relatedSchool1); + } + + const relatedSchool2 = await Schools.findOne({ + offset: Math.floor(Math.random() * (await Schools.count())), + }); + const GameAnswer2 = await GameAnswers.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (GameAnswer2?.setSchool) { + await GameAnswer2.setSchool(relatedSchool2); + } + + const relatedSchool3 = await Schools.findOne({ + offset: Math.floor(Math.random() * (await Schools.count())), + }); + const GameAnswer3 = await GameAnswers.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (GameAnswer3?.setSchool) { + await GameAnswer3.setSchool(relatedSchool3); + } + + const relatedSchool4 = await Schools.findOne({ + offset: Math.floor(Math.random() * (await Schools.count())), + }); + const GameAnswer4 = await GameAnswers.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (GameAnswer4?.setSchool) { + await GameAnswer4.setSchool(relatedSchool4); + } } module.exports = { @@ -711,6 +1374,12 @@ module.exports = { await Schools.bulkCreate(SchoolsData); + await GameSessions.bulkCreate(GameSessionsData); + + await GameQuestions.bulkCreate(GameQuestionsData); + + await GameAnswers.bulkCreate(GameAnswersData); + await Promise.all([ // Similar logic for "relation_many" @@ -747,6 +1416,12 @@ module.exports = { await associateSubmissionWithStudent(), await associateSubmissionWithSchool(), + + await associateGameSessionWithSchool(), + + await associateGameQuestionWithSchool(), + + await associateGameAnswerWithSchool(), ]); }, @@ -764,5 +1439,11 @@ module.exports = { await queryInterface.bulkDelete('submissions', null, {}); await queryInterface.bulkDelete('schools', null, {}); + + await queryInterface.bulkDelete('game_sessions', null, {}); + + await queryInterface.bulkDelete('game_questions', null, {}); + + await queryInterface.bulkDelete('game_answers', null, {}); }, }; diff --git a/backend/src/db/seeders/20250811055601.js b/backend/src/db/seeders/20250811055601.js new file mode 100644 index 0000000..6d4e73f --- /dev/null +++ b/backend/src/db/seeders/20250811055601.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 = ['game_sessions']; + + 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/20250811055654.js b/backend/src/db/seeders/20250811055654.js new file mode 100644 index 0000000..3e125f2 --- /dev/null +++ b/backend/src/db/seeders/20250811055654.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 = ['game_questions']; + + 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/20250811055725.js b/backend/src/db/seeders/20250811055725.js new file mode 100644 index 0000000..3cf3d26 --- /dev/null +++ b/backend/src/db/seeders/20250811055725.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 = ['game_answers']; + + 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 bf64d92..f661c89 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -41,6 +41,12 @@ const permissionsRoutes = require('./routes/permissions'); const schoolsRoutes = require('./routes/schools'); +const game_sessionsRoutes = require('./routes/game_sessions'); + +const game_questionsRoutes = require('./routes/game_questions'); + +const game_answersRoutes = require('./routes/game_answers'); + const getBaseUrl = (url) => { if (!url) return ''; return url.endsWith('/api') ? url.slice(0, -4) : url; @@ -166,6 +172,24 @@ app.use( schoolsRoutes, ); +app.use( + '/api/game_sessions', + passport.authenticate('jwt', { session: false }), + game_sessionsRoutes, +); + +app.use( + '/api/game_questions', + passport.authenticate('jwt', { session: false }), + game_questionsRoutes, +); + +app.use( + '/api/game_answers', + passport.authenticate('jwt', { session: false }), + game_answersRoutes, +); + app.use( '/api/openai', passport.authenticate('jwt', { session: false }), diff --git a/backend/src/routes/game_answers.js b/backend/src/routes/game_answers.js new file mode 100644 index 0000000..4e292d9 --- /dev/null +++ b/backend/src/routes/game_answers.js @@ -0,0 +1,452 @@ +const express = require('express'); + +const Game_answersService = require('../services/game_answers'); +const Game_answersDBApi = require('../db/api/game_answers'); +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('game_answers')); + +/** + * @swagger + * components: + * schemas: + * Game_answers: + * type: object + * properties: + + */ + +/** + * @swagger + * tags: + * name: Game_answers + * description: The Game_answers managing API + */ + +/** + * @swagger + * /api/game_answers: + * post: + * security: + * - bearerAuth: [] + * tags: [Game_answers] + * 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/Game_answers" + * responses: + * 200: + * description: The item was successfully added + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Game_answers" + * 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 Game_answersService.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: [Game_answers] + * 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/Game_answers" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Game_answers" + * 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 Game_answersService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/game_answers/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Game_answers] + * 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/Game_answers" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Game_answers" + * 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 Game_answersService.update( + req.body.data, + req.body.id, + req.currentUser, + ); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/game_answers/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Game_answers] + * 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/Game_answers" + * 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 Game_answersService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/game_answers/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Game_answers] + * 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/Game_answers" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post( + '/deleteByIds', + wrapAsync(async (req, res) => { + await Game_answersService.deleteByIds(req.body.data, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/game_answers: + * get: + * security: + * - bearerAuth: [] + * tags: [Game_answers] + * summary: Get all game_answers + * description: Get all game_answers + * responses: + * 200: + * description: Game_answers list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Game_answers" + * 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 Game_answersDBApi.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/game_answers/count: + * get: + * security: + * - bearerAuth: [] + * tags: [Game_answers] + * summary: Count all game_answers + * description: Count all game_answers + * responses: + * 200: + * description: Game_answers count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Game_answers" + * 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 Game_answersDBApi.findAll(req.query, globalAccess, { + countOnly: true, + currentUser, + }); + + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/game_answers/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [Game_answers] + * summary: Find all game_answers that match search criteria + * description: Find all game_answers that match search criteria + * responses: + * 200: + * description: Game_answers list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Game_answers" + * 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 Game_answersDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + globalAccess, + organizationId, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/game_answers/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Game_answers] + * 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/Game_answers" + * 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 Game_answersDBApi.findBy({ id: req.params.id }); + + res.status(200).send(payload); + }), +); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/routes/game_questions.js b/backend/src/routes/game_questions.js new file mode 100644 index 0000000..fe377bf --- /dev/null +++ b/backend/src/routes/game_questions.js @@ -0,0 +1,452 @@ +const express = require('express'); + +const Game_questionsService = require('../services/game_questions'); +const Game_questionsDBApi = require('../db/api/game_questions'); +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('game_questions')); + +/** + * @swagger + * components: + * schemas: + * Game_questions: + * type: object + * properties: + + */ + +/** + * @swagger + * tags: + * name: Game_questions + * description: The Game_questions managing API + */ + +/** + * @swagger + * /api/game_questions: + * post: + * security: + * - bearerAuth: [] + * tags: [Game_questions] + * 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/Game_questions" + * responses: + * 200: + * description: The item was successfully added + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Game_questions" + * 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 Game_questionsService.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: [Game_questions] + * 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/Game_questions" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Game_questions" + * 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 Game_questionsService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/game_questions/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Game_questions] + * 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/Game_questions" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Game_questions" + * 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 Game_questionsService.update( + req.body.data, + req.body.id, + req.currentUser, + ); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/game_questions/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Game_questions] + * 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/Game_questions" + * 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 Game_questionsService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/game_questions/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Game_questions] + * 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/Game_questions" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post( + '/deleteByIds', + wrapAsync(async (req, res) => { + await Game_questionsService.deleteByIds(req.body.data, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/game_questions: + * get: + * security: + * - bearerAuth: [] + * tags: [Game_questions] + * summary: Get all game_questions + * description: Get all game_questions + * responses: + * 200: + * description: Game_questions list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Game_questions" + * 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 Game_questionsDBApi.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/game_questions/count: + * get: + * security: + * - bearerAuth: [] + * tags: [Game_questions] + * summary: Count all game_questions + * description: Count all game_questions + * responses: + * 200: + * description: Game_questions count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Game_questions" + * 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 Game_questionsDBApi.findAll(req.query, globalAccess, { + countOnly: true, + currentUser, + }); + + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/game_questions/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [Game_questions] + * summary: Find all game_questions that match search criteria + * description: Find all game_questions that match search criteria + * responses: + * 200: + * description: Game_questions list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Game_questions" + * 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 Game_questionsDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + globalAccess, + organizationId, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/game_questions/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Game_questions] + * 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/Game_questions" + * 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 Game_questionsDBApi.findBy({ id: req.params.id }); + + res.status(200).send(payload); + }), +); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/routes/game_sessions.js b/backend/src/routes/game_sessions.js new file mode 100644 index 0000000..305d606 --- /dev/null +++ b/backend/src/routes/game_sessions.js @@ -0,0 +1,452 @@ +const express = require('express'); + +const Game_sessionsService = require('../services/game_sessions'); +const Game_sessionsDBApi = require('../db/api/game_sessions'); +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('game_sessions')); + +/** + * @swagger + * components: + * schemas: + * Game_sessions: + * type: object + * properties: + + */ + +/** + * @swagger + * tags: + * name: Game_sessions + * description: The Game_sessions managing API + */ + +/** + * @swagger + * /api/game_sessions: + * post: + * security: + * - bearerAuth: [] + * tags: [Game_sessions] + * 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/Game_sessions" + * responses: + * 200: + * description: The item was successfully added + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Game_sessions" + * 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 Game_sessionsService.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: [Game_sessions] + * 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/Game_sessions" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Game_sessions" + * 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 Game_sessionsService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/game_sessions/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Game_sessions] + * 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/Game_sessions" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Game_sessions" + * 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 Game_sessionsService.update( + req.body.data, + req.body.id, + req.currentUser, + ); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/game_sessions/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Game_sessions] + * 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/Game_sessions" + * 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 Game_sessionsService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/game_sessions/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Game_sessions] + * 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/Game_sessions" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post( + '/deleteByIds', + wrapAsync(async (req, res) => { + await Game_sessionsService.deleteByIds(req.body.data, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/game_sessions: + * get: + * security: + * - bearerAuth: [] + * tags: [Game_sessions] + * summary: Get all game_sessions + * description: Get all game_sessions + * responses: + * 200: + * description: Game_sessions list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Game_sessions" + * 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 Game_sessionsDBApi.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/game_sessions/count: + * get: + * security: + * - bearerAuth: [] + * tags: [Game_sessions] + * summary: Count all game_sessions + * description: Count all game_sessions + * responses: + * 200: + * description: Game_sessions count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Game_sessions" + * 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 Game_sessionsDBApi.findAll(req.query, globalAccess, { + countOnly: true, + currentUser, + }); + + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/game_sessions/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [Game_sessions] + * summary: Find all game_sessions that match search criteria + * description: Find all game_sessions that match search criteria + * responses: + * 200: + * description: Game_sessions list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Game_sessions" + * 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 Game_sessionsDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + globalAccess, + organizationId, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/game_sessions/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Game_sessions] + * 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/Game_sessions" + * 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 Game_sessionsDBApi.findBy({ id: req.params.id }); + + res.status(200).send(payload); + }), +); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/services/game_answers.js b/backend/src/services/game_answers.js new file mode 100644 index 0000000..c536cdd --- /dev/null +++ b/backend/src/services/game_answers.js @@ -0,0 +1,117 @@ +const db = require('../db/models'); +const Game_answersDBApi = require('../db/api/game_answers'); +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 Game_answersService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await Game_answersDBApi.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 Game_answersDBApi.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 game_answers = await Game_answersDBApi.findBy( + { id }, + { transaction }, + ); + + if (!game_answers) { + throw new ValidationError('game_answersNotFound'); + } + + const updatedGame_answers = await Game_answersDBApi.update(id, data, { + currentUser, + transaction, + }); + + await transaction.commit(); + return updatedGame_answers; + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await Game_answersDBApi.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 Game_answersDBApi.remove(id, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; diff --git a/backend/src/services/game_questions.js b/backend/src/services/game_questions.js new file mode 100644 index 0000000..c9f8f5d --- /dev/null +++ b/backend/src/services/game_questions.js @@ -0,0 +1,117 @@ +const db = require('../db/models'); +const Game_questionsDBApi = require('../db/api/game_questions'); +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 Game_questionsService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await Game_questionsDBApi.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 Game_questionsDBApi.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 game_questions = await Game_questionsDBApi.findBy( + { id }, + { transaction }, + ); + + if (!game_questions) { + throw new ValidationError('game_questionsNotFound'); + } + + const updatedGame_questions = await Game_questionsDBApi.update(id, data, { + currentUser, + transaction, + }); + + await transaction.commit(); + return updatedGame_questions; + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await Game_questionsDBApi.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 Game_questionsDBApi.remove(id, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; diff --git a/backend/src/services/game_sessions.js b/backend/src/services/game_sessions.js new file mode 100644 index 0000000..6ccdb62 --- /dev/null +++ b/backend/src/services/game_sessions.js @@ -0,0 +1,117 @@ +const db = require('../db/models'); +const Game_sessionsDBApi = require('../db/api/game_sessions'); +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 Game_sessionsService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await Game_sessionsDBApi.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 Game_sessionsDBApi.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 game_sessions = await Game_sessionsDBApi.findBy( + { id }, + { transaction }, + ); + + if (!game_sessions) { + throw new ValidationError('game_sessionsNotFound'); + } + + const updatedGame_sessions = await Game_sessionsDBApi.update(id, data, { + currentUser, + transaction, + }); + + await transaction.commit(); + return updatedGame_sessions; + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await Game_sessionsDBApi.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 Game_sessionsDBApi.remove(id, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; diff --git a/frontend/src/components/Game_answers/CardGame_answers.tsx b/frontend/src/components/Game_answers/CardGame_answers.tsx new file mode 100644 index 0000000..9effac1 --- /dev/null +++ b/frontend/src/components/Game_answers/CardGame_answers.tsx @@ -0,0 +1,98 @@ +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 = { + game_answers: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const CardGame_answers = ({ + game_answers, + 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_GAME_ANSWERS'); + + return ( +
+ {loading && } +
    + {!loading && + game_answers.map((item, index) => ( +
  • +
    + + {item.id} + + +
    + +
    +
    +
    +
  • + ))} + {!loading && game_answers.length === 0 && ( +
    +

    No data to display

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

No data to display

+
+ )} +
+
+ +
+ + ); +}; + +export default ListGame_answers; diff --git a/frontend/src/components/Game_answers/TableGame_answers.tsx b/frontend/src/components/Game_answers/TableGame_answers.tsx new file mode 100644 index 0000000..d25f74d --- /dev/null +++ b/frontend/src/components/Game_answers/TableGame_answers.tsx @@ -0,0 +1,487 @@ +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/game_answers/game_answersSlice'; +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 './configureGame_answersCols'; +import _ from 'lodash'; +import dataFormatter from '../../helpers/dataFormatter'; +import { dataGridStyles } from '../../styles'; + +const perPage = 10; + +const TableSampleGame_answers = ({ + 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 { + game_answers, + loading, + count, + notify: game_answersNotify, + refetch, + } = useAppSelector((state) => state.game_answers); + 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 (game_answersNotify.showNotification) { + notify( + game_answersNotify.typeNotification, + game_answersNotify.textNotification, + ); + } + }, [game_answersNotify.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, `game_answers`, 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={game_answers ?? []} + columns={columns} + initialState={{ + pagination: { + paginationModel: { + pageSize: 10, + }, + }, + }} + disableRowSelectionOnClick + onProcessRowUpdateError={(params) => { + console.log('Error', params); + }} + processRowUpdate={async (newRow, oldRow) => { + const data = dataFormatter.dataGridEditFormatter(newRow); + + try { + await handleTableSubmit(newRow.id, data); + return newRow; + } catch { + return oldRow; + } + }} + sortingMode={'server'} + checkboxSelection + onRowSelectionModelChange={(ids) => { + setSelectedRows(ids); + }} + onSortModelChange={(params) => { + params.length + ? setSortModel(params) + : setSortModel([{ field: '', sort: 'desc' }]); + }} + rowCount={count} + pageSizeOptions={[10]} + paginationMode={'server'} + loading={loading} + onPaginationModelChange={(params) => { + onPageChange(params.page); + }} + /> +
+ ); + + return ( + <> + {filterItems && Array.isArray(filterItems) && filterItems.length ? ( + + null} + > +
+ <> + {filterItems && + filterItems.map((filterItem) => { + return ( +
+
+
+ Filter +
+ + {filters.map((selectOption) => ( + + ))} + +
+ {filters.find( + (filter) => + filter.title === filterItem?.fields?.selectedField, + )?.type === 'enum' ? ( +
+
Value
+ + + {filters + .find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + ) + ?.options?.map((option) => ( + + ))} + +
+ ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + )?.number ? ( +
+
+
+ From +
+ +
+
+
+ To +
+ +
+
+ ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + )?.date ? ( +
+
+
+ From +
+ +
+
+
+ To +
+ +
+
+ ) : ( +
+
+ Contains +
+ +
+ )} +
+
+ Action +
+ { + deleteFilter(filterItem.id); + }} + /> +
+
+ ); + })} +
+ + +
+ +
+
+
+ ) : null} + +

Are you sure you want to delete this item?

+
+ + {dataGrid} + + {selectedRows.length > 0 && + createPortal( + onDeleteRows(selectedRows)} + />, + document.getElementById('delete-rows-button'), + )} + + + ); +}; + +export default TableSampleGame_answers; diff --git a/frontend/src/components/Game_answers/configureGame_answersCols.tsx b/frontend/src/components/Game_answers/configureGame_answersCols.tsx new file mode 100644 index 0000000..1454c2c --- /dev/null +++ b/frontend/src/components/Game_answers/configureGame_answersCols.tsx @@ -0,0 +1,62 @@ +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_GAME_ANSWERS'); + + return [ + { + field: 'actions', + type: 'actions', + minWidth: 30, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + getActions: (params: GridRowParams) => { + return [ +
+ +
, + ]; + }, + }, + ]; +}; diff --git a/frontend/src/components/Game_questions/CardGame_questions.tsx b/frontend/src/components/Game_questions/CardGame_questions.tsx new file mode 100644 index 0000000..8a65c42 --- /dev/null +++ b/frontend/src/components/Game_questions/CardGame_questions.tsx @@ -0,0 +1,101 @@ +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 = { + game_questions: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const CardGame_questions = ({ + game_questions, + 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_GAME_QUESTIONS', + ); + + return ( +
+ {loading && } +
    + {!loading && + game_questions.map((item, index) => ( +
  • +
    + + {item.id} + + +
    + +
    +
    +
    +
  • + ))} + {!loading && game_questions.length === 0 && ( +
    +

    No data to display

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

No data to display

+
+ )} +
+
+ +
+ + ); +}; + +export default ListGame_questions; diff --git a/frontend/src/components/Game_questions/TableGame_questions.tsx b/frontend/src/components/Game_questions/TableGame_questions.tsx new file mode 100644 index 0000000..1cd0882 --- /dev/null +++ b/frontend/src/components/Game_questions/TableGame_questions.tsx @@ -0,0 +1,487 @@ +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/game_questions/game_questionsSlice'; +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 './configureGame_questionsCols'; +import _ from 'lodash'; +import dataFormatter from '../../helpers/dataFormatter'; +import { dataGridStyles } from '../../styles'; + +const perPage = 10; + +const TableSampleGame_questions = ({ + 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 { + game_questions, + loading, + count, + notify: game_questionsNotify, + refetch, + } = useAppSelector((state) => state.game_questions); + 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 (game_questionsNotify.showNotification) { + notify( + game_questionsNotify.typeNotification, + game_questionsNotify.textNotification, + ); + } + }, [game_questionsNotify.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, `game_questions`, 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={game_questions ?? []} + columns={columns} + initialState={{ + pagination: { + paginationModel: { + pageSize: 10, + }, + }, + }} + disableRowSelectionOnClick + onProcessRowUpdateError={(params) => { + console.log('Error', params); + }} + processRowUpdate={async (newRow, oldRow) => { + const data = dataFormatter.dataGridEditFormatter(newRow); + + try { + await handleTableSubmit(newRow.id, data); + return newRow; + } catch { + return oldRow; + } + }} + sortingMode={'server'} + checkboxSelection + onRowSelectionModelChange={(ids) => { + setSelectedRows(ids); + }} + onSortModelChange={(params) => { + params.length + ? setSortModel(params) + : setSortModel([{ field: '', sort: 'desc' }]); + }} + rowCount={count} + pageSizeOptions={[10]} + paginationMode={'server'} + loading={loading} + onPaginationModelChange={(params) => { + onPageChange(params.page); + }} + /> +
+ ); + + return ( + <> + {filterItems && Array.isArray(filterItems) && filterItems.length ? ( + + null} + > +
+ <> + {filterItems && + filterItems.map((filterItem) => { + return ( +
+
+
+ Filter +
+ + {filters.map((selectOption) => ( + + ))} + +
+ {filters.find( + (filter) => + filter.title === filterItem?.fields?.selectedField, + )?.type === 'enum' ? ( +
+
Value
+ + + {filters + .find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + ) + ?.options?.map((option) => ( + + ))} + +
+ ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + )?.number ? ( +
+
+
+ From +
+ +
+
+
+ To +
+ +
+
+ ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + )?.date ? ( +
+
+
+ From +
+ +
+
+
+ To +
+ +
+
+ ) : ( +
+
+ Contains +
+ +
+ )} +
+
+ Action +
+ { + deleteFilter(filterItem.id); + }} + /> +
+
+ ); + })} +
+ + +
+ +
+
+
+ ) : null} + +

Are you sure you want to delete this item?

+
+ + {dataGrid} + + {selectedRows.length > 0 && + createPortal( + onDeleteRows(selectedRows)} + />, + document.getElementById('delete-rows-button'), + )} + + + ); +}; + +export default TableSampleGame_questions; diff --git a/frontend/src/components/Game_questions/configureGame_questionsCols.tsx b/frontend/src/components/Game_questions/configureGame_questionsCols.tsx new file mode 100644 index 0000000..6a9a7f1 --- /dev/null +++ b/frontend/src/components/Game_questions/configureGame_questionsCols.tsx @@ -0,0 +1,62 @@ +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_GAME_QUESTIONS'); + + return [ + { + field: 'actions', + type: 'actions', + minWidth: 30, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + getActions: (params: GridRowParams) => { + return [ +
+ +
, + ]; + }, + }, + ]; +}; diff --git a/frontend/src/components/Game_sessions/CardGame_sessions.tsx b/frontend/src/components/Game_sessions/CardGame_sessions.tsx new file mode 100644 index 0000000..6b96480 --- /dev/null +++ b/frontend/src/components/Game_sessions/CardGame_sessions.tsx @@ -0,0 +1,101 @@ +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 = { + game_sessions: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const CardGame_sessions = ({ + game_sessions, + 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_GAME_SESSIONS', + ); + + return ( +
+ {loading && } +
    + {!loading && + game_sessions.map((item, index) => ( +
  • +
    + + {item.id} + + +
    + +
    +
    +
    +
  • + ))} + {!loading && game_sessions.length === 0 && ( +
    +

    No data to display

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

No data to display

+
+ )} +
+
+ +
+ + ); +}; + +export default ListGame_sessions; diff --git a/frontend/src/components/Game_sessions/TableGame_sessions.tsx b/frontend/src/components/Game_sessions/TableGame_sessions.tsx new file mode 100644 index 0000000..fd55d32 --- /dev/null +++ b/frontend/src/components/Game_sessions/TableGame_sessions.tsx @@ -0,0 +1,487 @@ +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/game_sessions/game_sessionsSlice'; +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 './configureGame_sessionsCols'; +import _ from 'lodash'; +import dataFormatter from '../../helpers/dataFormatter'; +import { dataGridStyles } from '../../styles'; + +const perPage = 10; + +const TableSampleGame_sessions = ({ + 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 { + game_sessions, + loading, + count, + notify: game_sessionsNotify, + refetch, + } = useAppSelector((state) => state.game_sessions); + 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 (game_sessionsNotify.showNotification) { + notify( + game_sessionsNotify.typeNotification, + game_sessionsNotify.textNotification, + ); + } + }, [game_sessionsNotify.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, `game_sessions`, 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={game_sessions ?? []} + columns={columns} + initialState={{ + pagination: { + paginationModel: { + pageSize: 10, + }, + }, + }} + disableRowSelectionOnClick + onProcessRowUpdateError={(params) => { + console.log('Error', params); + }} + processRowUpdate={async (newRow, oldRow) => { + const data = dataFormatter.dataGridEditFormatter(newRow); + + try { + await handleTableSubmit(newRow.id, data); + return newRow; + } catch { + return oldRow; + } + }} + sortingMode={'server'} + checkboxSelection + onRowSelectionModelChange={(ids) => { + setSelectedRows(ids); + }} + onSortModelChange={(params) => { + params.length + ? setSortModel(params) + : setSortModel([{ field: '', sort: 'desc' }]); + }} + rowCount={count} + pageSizeOptions={[10]} + paginationMode={'server'} + loading={loading} + onPaginationModelChange={(params) => { + onPageChange(params.page); + }} + /> +
+ ); + + return ( + <> + {filterItems && Array.isArray(filterItems) && filterItems.length ? ( + + null} + > +
+ <> + {filterItems && + filterItems.map((filterItem) => { + return ( +
+
+
+ Filter +
+ + {filters.map((selectOption) => ( + + ))} + +
+ {filters.find( + (filter) => + filter.title === filterItem?.fields?.selectedField, + )?.type === 'enum' ? ( +
+
Value
+ + + {filters + .find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + ) + ?.options?.map((option) => ( + + ))} + +
+ ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + )?.number ? ( +
+
+
+ From +
+ +
+
+
+ To +
+ +
+
+ ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + )?.date ? ( +
+
+
+ From +
+ +
+
+
+ To +
+ +
+
+ ) : ( +
+
+ Contains +
+ +
+ )} +
+
+ Action +
+ { + deleteFilter(filterItem.id); + }} + /> +
+
+ ); + })} +
+ + +
+ +
+
+
+ ) : null} + +

Are you sure you want to delete this item?

+
+ + {dataGrid} + + {selectedRows.length > 0 && + createPortal( + onDeleteRows(selectedRows)} + />, + document.getElementById('delete-rows-button'), + )} + + + ); +}; + +export default TableSampleGame_sessions; diff --git a/frontend/src/components/Game_sessions/configureGame_sessionsCols.tsx b/frontend/src/components/Game_sessions/configureGame_sessionsCols.tsx new file mode 100644 index 0000000..ad0e539 --- /dev/null +++ b/frontend/src/components/Game_sessions/configureGame_sessionsCols.tsx @@ -0,0 +1,62 @@ +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_GAME_SESSIONS'); + + return [ + { + 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 6210a74..e963070 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_PAGES; - 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 (
{ const [roles, setRoles] = React.useState(loadingMessage); const [permissions, setPermissions] = React.useState(loadingMessage); const [schools, setSchools] = React.useState(loadingMessage); + const [game_sessions, setGame_sessions] = React.useState(loadingMessage); + const [game_questions, setGame_questions] = React.useState(loadingMessage); + const [game_answers, setGame_answers] = React.useState(loadingMessage); const [widgetsRole, setWidgetsRole] = React.useState({ role: { value: '', label: '' }, @@ -62,6 +65,9 @@ const Dashboard = () => { 'roles', 'permissions', 'schools', + 'game_sessions', + 'game_questions', + 'game_answers', ]; const fns = [ setUsers, @@ -74,6 +80,9 @@ const Dashboard = () => { setRoles, setPermissions, setSchools, + setGame_sessions, + setGame_questions, + setGame_answers, ]; const requests = entities.map((entity, index) => { @@ -533,6 +542,102 @@ const Dashboard = () => {
)} + + {hasPermission(currentUser, 'READ_GAME_SESSIONS') && ( + +
+
+
+
+ Game sessions +
+
+ {game_sessions} +
+
+
+ +
+
+
+ + )} + + {hasPermission(currentUser, 'READ_GAME_QUESTIONS') && ( + +
+
+
+
+ Game questions +
+
+ {game_questions} +
+
+
+ +
+
+
+ + )} + + {hasPermission(currentUser, 'READ_GAME_ANSWERS') && ( + +
+
+
+
+ Game answers +
+
+ {game_answers} +
+
+
+ +
+
+
+ + )}
diff --git a/frontend/src/pages/game_answers/[game_answersId].tsx b/frontend/src/pages/game_answers/[game_answersId].tsx new file mode 100644 index 0000000..62c9c68 --- /dev/null +++ b/frontend/src/pages/game_answers/[game_answersId].tsx @@ -0,0 +1,137 @@ +import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement, useEffect, useState } from 'react'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; + +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { SwitchField } from '../../components/SwitchField'; +import { RichTextField } from '../../components/RichTextField'; + +import { update, fetch } from '../../stores/game_answers/game_answersSlice'; +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 EditGame_answers = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + schools: null, + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { game_answers } = useAppSelector((state) => state.game_answers); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { game_answersId } = router.query; + + useEffect(() => { + dispatch(fetch({ id: game_answersId })); + }, [game_answersId]); + + useEffect(() => { + if (typeof game_answers === 'object') { + setInitialValues(game_answers); + } + }, [game_answers]); + + useEffect(() => { + if (typeof game_answers === 'object') { + const newInitialVal = { ...initVals }; + + Object.keys(initVals).forEach( + (el) => (newInitialVal[el] = game_answers[el]), + ); + + setInitialValues(newInitialVal); + } + }, [game_answers]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: game_answersId, data })); + await router.push('/game_answers/game_answers-list'); + }; + + return ( + <> + + {getPageTitle('Edit game_answers')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + + + + + router.push('/game_answers/game_answers-list')} + /> + + +
+
+
+ + ); +}; + +EditGame_answers.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditGame_answers; diff --git a/frontend/src/pages/game_answers/game_answers-edit.tsx b/frontend/src/pages/game_answers/game_answers-edit.tsx new file mode 100644 index 0000000..25dc7ba --- /dev/null +++ b/frontend/src/pages/game_answers/game_answers-edit.tsx @@ -0,0 +1,135 @@ +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/game_answers/game_answersSlice'; +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 EditGame_answersPage = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + schools: null, + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { game_answers } = useAppSelector((state) => state.game_answers); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { id } = router.query; + + useEffect(() => { + dispatch(fetch({ id: id })); + }, [id]); + + useEffect(() => { + if (typeof game_answers === 'object') { + setInitialValues(game_answers); + } + }, [game_answers]); + + useEffect(() => { + if (typeof game_answers === 'object') { + const newInitialVal = { ...initVals }; + Object.keys(initVals).forEach( + (el) => (newInitialVal[el] = game_answers[el]), + ); + setInitialValues(newInitialVal); + } + }, [game_answers]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: id, data })); + await router.push('/game_answers/game_answers-list'); + }; + + return ( + <> + + {getPageTitle('Edit game_answers')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + + + + + router.push('/game_answers/game_answers-list')} + /> + + +
+
+
+ + ); +}; + +EditGame_answersPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditGame_answersPage; diff --git a/frontend/src/pages/game_answers/game_answers-list.tsx b/frontend/src/pages/game_answers/game_answers-list.tsx new file mode 100644 index 0000000..b5a9bad --- /dev/null +++ b/frontend/src/pages/game_answers/game_answers-list.tsx @@ -0,0 +1,165 @@ +import { mdiChartTimelineVariant } from '@mdi/js'; +import Head from 'next/head'; +import { uniqueId } from 'lodash'; +import React, { ReactElement, useState } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; +import TableGame_answers from '../../components/Game_answers/TableGame_answers'; +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/game_answers/game_answersSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const Game_answersTablesPage = () => { + 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([]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_GAME_ANSWERS'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getGame_answersCSV = async () => { + const response = await axios({ + url: '/game_answers?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 = 'game_answersCSV.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('Game_answers')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
+
+
+
+ + + + +
+ + + + + ); +}; + +Game_answersTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Game_answersTablesPage; diff --git a/frontend/src/pages/game_answers/game_answers-new.tsx b/frontend/src/pages/game_answers/game_answers-new.tsx new file mode 100644 index 0000000..1d473ce --- /dev/null +++ b/frontend/src/pages/game_answers/game_answers-new.tsx @@ -0,0 +1,104 @@ +import { + mdiAccount, + mdiChartTimelineVariant, + mdiMail, + mdiUpload, +} from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SwitchField } from '../../components/SwitchField'; + +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { RichTextField } from '../../components/RichTextField'; + +import { create } from '../../stores/game_answers/game_answersSlice'; +import { useAppDispatch } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import moment from 'moment'; + +const initialValues = { + schools: '', +}; + +const Game_answersNew = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + + const handleSubmit = async (data) => { + await dispatch(create(data)); + await router.push('/game_answers/game_answers-list'); + }; + return ( + <> + + {getPageTitle('New Item')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + + + + + router.push('/game_answers/game_answers-list')} + /> + + +
+
+
+ + ); +}; + +Game_answersNew.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Game_answersNew; diff --git a/frontend/src/pages/game_answers/game_answers-table.tsx b/frontend/src/pages/game_answers/game_answers-table.tsx new file mode 100644 index 0000000..444be6c --- /dev/null +++ b/frontend/src/pages/game_answers/game_answers-table.tsx @@ -0,0 +1,164 @@ +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 TableGame_answers from '../../components/Game_answers/TableGame_answers'; +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/game_answers/game_answersSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const Game_answersTablesPage = () => { + 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([]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_GAME_ANSWERS'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getGame_answersCSV = async () => { + const response = await axios({ + url: '/game_answers?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 = 'game_answersCSV.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('Game_answers')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
+
+
+
+ + + +
+ + + + + ); +}; + +Game_answersTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Game_answersTablesPage; diff --git a/frontend/src/pages/game_answers/game_answers-view.tsx b/frontend/src/pages/game_answers/game_answers-view.tsx new file mode 100644 index 0000000..bd60559 --- /dev/null +++ b/frontend/src/pages/game_answers/game_answers-view.tsx @@ -0,0 +1,88 @@ +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/game_answers/game_answersSlice'; +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 Game_answersView = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const { game_answers } = useAppSelector((state) => state.game_answers); + + 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 game_answers')} + + + + + + +
+

schools

+ +

{game_answers?.schools?.name ?? 'No data'}

+
+ + + + router.push('/game_answers/game_answers-list')} + /> +
+
+ + ); +}; + +Game_answersView.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Game_answersView; diff --git a/frontend/src/pages/game_questions/[game_questionsId].tsx b/frontend/src/pages/game_questions/[game_questionsId].tsx new file mode 100644 index 0000000..008b820 --- /dev/null +++ b/frontend/src/pages/game_questions/[game_questionsId].tsx @@ -0,0 +1,139 @@ +import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement, useEffect, useState } from 'react'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; + +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { SwitchField } from '../../components/SwitchField'; +import { RichTextField } from '../../components/RichTextField'; + +import { update, fetch } from '../../stores/game_questions/game_questionsSlice'; +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 EditGame_questions = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + schools: null, + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { game_questions } = useAppSelector((state) => state.game_questions); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { game_questionsId } = router.query; + + useEffect(() => { + dispatch(fetch({ id: game_questionsId })); + }, [game_questionsId]); + + useEffect(() => { + if (typeof game_questions === 'object') { + setInitialValues(game_questions); + } + }, [game_questions]); + + useEffect(() => { + if (typeof game_questions === 'object') { + const newInitialVal = { ...initVals }; + + Object.keys(initVals).forEach( + (el) => (newInitialVal[el] = game_questions[el]), + ); + + setInitialValues(newInitialVal); + } + }, [game_questions]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: game_questionsId, data })); + await router.push('/game_questions/game_questions-list'); + }; + + return ( + <> + + {getPageTitle('Edit game_questions')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + + + + + + router.push('/game_questions/game_questions-list') + } + /> + + +
+
+
+ + ); +}; + +EditGame_questions.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditGame_questions; diff --git a/frontend/src/pages/game_questions/game_questions-edit.tsx b/frontend/src/pages/game_questions/game_questions-edit.tsx new file mode 100644 index 0000000..230f210 --- /dev/null +++ b/frontend/src/pages/game_questions/game_questions-edit.tsx @@ -0,0 +1,137 @@ +import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement, useEffect, useState } from 'react'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; + +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { SwitchField } from '../../components/SwitchField'; +import { RichTextField } from '../../components/RichTextField'; + +import { update, fetch } from '../../stores/game_questions/game_questionsSlice'; +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 EditGame_questionsPage = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + schools: null, + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { game_questions } = useAppSelector((state) => state.game_questions); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { id } = router.query; + + useEffect(() => { + dispatch(fetch({ id: id })); + }, [id]); + + useEffect(() => { + if (typeof game_questions === 'object') { + setInitialValues(game_questions); + } + }, [game_questions]); + + useEffect(() => { + if (typeof game_questions === 'object') { + const newInitialVal = { ...initVals }; + Object.keys(initVals).forEach( + (el) => (newInitialVal[el] = game_questions[el]), + ); + setInitialValues(newInitialVal); + } + }, [game_questions]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: id, data })); + await router.push('/game_questions/game_questions-list'); + }; + + return ( + <> + + {getPageTitle('Edit game_questions')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + + + + + + router.push('/game_questions/game_questions-list') + } + /> + + +
+
+
+ + ); +}; + +EditGame_questionsPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditGame_questionsPage; diff --git a/frontend/src/pages/game_questions/game_questions-list.tsx b/frontend/src/pages/game_questions/game_questions-list.tsx new file mode 100644 index 0000000..81ce7e4 --- /dev/null +++ b/frontend/src/pages/game_questions/game_questions-list.tsx @@ -0,0 +1,165 @@ +import { mdiChartTimelineVariant } from '@mdi/js'; +import Head from 'next/head'; +import { uniqueId } from 'lodash'; +import React, { ReactElement, useState } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; +import TableGame_questions from '../../components/Game_questions/TableGame_questions'; +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/game_questions/game_questionsSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const Game_questionsTablesPage = () => { + 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([]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_GAME_QUESTIONS'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getGame_questionsCSV = async () => { + const response = await axios({ + url: '/game_questions?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 = 'game_questionsCSV.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('Game_questions')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
+
+
+
+ + + + +
+ + + + + ); +}; + +Game_questionsTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Game_questionsTablesPage; diff --git a/frontend/src/pages/game_questions/game_questions-new.tsx b/frontend/src/pages/game_questions/game_questions-new.tsx new file mode 100644 index 0000000..34e8211 --- /dev/null +++ b/frontend/src/pages/game_questions/game_questions-new.tsx @@ -0,0 +1,106 @@ +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/game_questions/game_questionsSlice'; +import { useAppDispatch } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import moment from 'moment'; + +const initialValues = { + schools: '', +}; + +const Game_questionsNew = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + + const handleSubmit = async (data) => { + await dispatch(create(data)); + await router.push('/game_questions/game_questions-list'); + }; + return ( + <> + + {getPageTitle('New Item')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + + + + + + router.push('/game_questions/game_questions-list') + } + /> + + +
+
+
+ + ); +}; + +Game_questionsNew.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Game_questionsNew; diff --git a/frontend/src/pages/game_questions/game_questions-table.tsx b/frontend/src/pages/game_questions/game_questions-table.tsx new file mode 100644 index 0000000..2a6fa91 --- /dev/null +++ b/frontend/src/pages/game_questions/game_questions-table.tsx @@ -0,0 +1,164 @@ +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 TableGame_questions from '../../components/Game_questions/TableGame_questions'; +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/game_questions/game_questionsSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const Game_questionsTablesPage = () => { + 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([]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_GAME_QUESTIONS'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getGame_questionsCSV = async () => { + const response = await axios({ + url: '/game_questions?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 = 'game_questionsCSV.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('Game_questions')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
+
+
+
+ + + +
+ + + + + ); +}; + +Game_questionsTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Game_questionsTablesPage; diff --git a/frontend/src/pages/game_questions/game_questions-view.tsx b/frontend/src/pages/game_questions/game_questions-view.tsx new file mode 100644 index 0000000..64e7594 --- /dev/null +++ b/frontend/src/pages/game_questions/game_questions-view.tsx @@ -0,0 +1,88 @@ +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/game_questions/game_questionsSlice'; +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 Game_questionsView = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const { game_questions } = useAppSelector((state) => state.game_questions); + + 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 game_questions')} + + + + + + +
+

schools

+ +

{game_questions?.schools?.name ?? 'No data'}

+
+ + + + router.push('/game_questions/game_questions-list')} + /> +
+
+ + ); +}; + +Game_questionsView.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Game_questionsView; diff --git a/frontend/src/pages/game_sessions/[game_sessionsId].tsx b/frontend/src/pages/game_sessions/[game_sessionsId].tsx new file mode 100644 index 0000000..bd7686c --- /dev/null +++ b/frontend/src/pages/game_sessions/[game_sessionsId].tsx @@ -0,0 +1,139 @@ +import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement, useEffect, useState } from 'react'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; + +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { SwitchField } from '../../components/SwitchField'; +import { RichTextField } from '../../components/RichTextField'; + +import { update, fetch } from '../../stores/game_sessions/game_sessionsSlice'; +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 EditGame_sessions = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + schools: null, + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { game_sessions } = useAppSelector((state) => state.game_sessions); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { game_sessionsId } = router.query; + + useEffect(() => { + dispatch(fetch({ id: game_sessionsId })); + }, [game_sessionsId]); + + useEffect(() => { + if (typeof game_sessions === 'object') { + setInitialValues(game_sessions); + } + }, [game_sessions]); + + useEffect(() => { + if (typeof game_sessions === 'object') { + const newInitialVal = { ...initVals }; + + Object.keys(initVals).forEach( + (el) => (newInitialVal[el] = game_sessions[el]), + ); + + setInitialValues(newInitialVal); + } + }, [game_sessions]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: game_sessionsId, data })); + await router.push('/game_sessions/game_sessions-list'); + }; + + return ( + <> + + {getPageTitle('Edit game_sessions')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + + + + + + router.push('/game_sessions/game_sessions-list') + } + /> + + +
+
+
+ + ); +}; + +EditGame_sessions.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditGame_sessions; diff --git a/frontend/src/pages/game_sessions/game_sessions-edit.tsx b/frontend/src/pages/game_sessions/game_sessions-edit.tsx new file mode 100644 index 0000000..ace9a30 --- /dev/null +++ b/frontend/src/pages/game_sessions/game_sessions-edit.tsx @@ -0,0 +1,137 @@ +import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement, useEffect, useState } from 'react'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; + +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { SwitchField } from '../../components/SwitchField'; +import { RichTextField } from '../../components/RichTextField'; + +import { update, fetch } from '../../stores/game_sessions/game_sessionsSlice'; +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 EditGame_sessionsPage = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + schools: null, + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { game_sessions } = useAppSelector((state) => state.game_sessions); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { id } = router.query; + + useEffect(() => { + dispatch(fetch({ id: id })); + }, [id]); + + useEffect(() => { + if (typeof game_sessions === 'object') { + setInitialValues(game_sessions); + } + }, [game_sessions]); + + useEffect(() => { + if (typeof game_sessions === 'object') { + const newInitialVal = { ...initVals }; + Object.keys(initVals).forEach( + (el) => (newInitialVal[el] = game_sessions[el]), + ); + setInitialValues(newInitialVal); + } + }, [game_sessions]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: id, data })); + await router.push('/game_sessions/game_sessions-list'); + }; + + return ( + <> + + {getPageTitle('Edit game_sessions')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + + + + + + router.push('/game_sessions/game_sessions-list') + } + /> + + +
+
+
+ + ); +}; + +EditGame_sessionsPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditGame_sessionsPage; diff --git a/frontend/src/pages/game_sessions/game_sessions-list.tsx b/frontend/src/pages/game_sessions/game_sessions-list.tsx new file mode 100644 index 0000000..48c59be --- /dev/null +++ b/frontend/src/pages/game_sessions/game_sessions-list.tsx @@ -0,0 +1,165 @@ +import { mdiChartTimelineVariant } from '@mdi/js'; +import Head from 'next/head'; +import { uniqueId } from 'lodash'; +import React, { ReactElement, useState } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; +import TableGame_sessions from '../../components/Game_sessions/TableGame_sessions'; +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/game_sessions/game_sessionsSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const Game_sessionsTablesPage = () => { + 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([]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_GAME_SESSIONS'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getGame_sessionsCSV = async () => { + const response = await axios({ + url: '/game_sessions?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 = 'game_sessionsCSV.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('Game_sessions')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
+
+
+
+ + + + +
+ + + + + ); +}; + +Game_sessionsTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Game_sessionsTablesPage; diff --git a/frontend/src/pages/game_sessions/game_sessions-new.tsx b/frontend/src/pages/game_sessions/game_sessions-new.tsx new file mode 100644 index 0000000..5cd5e31 --- /dev/null +++ b/frontend/src/pages/game_sessions/game_sessions-new.tsx @@ -0,0 +1,106 @@ +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/game_sessions/game_sessionsSlice'; +import { useAppDispatch } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import moment from 'moment'; + +const initialValues = { + schools: '', +}; + +const Game_sessionsNew = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + + const handleSubmit = async (data) => { + await dispatch(create(data)); + await router.push('/game_sessions/game_sessions-list'); + }; + return ( + <> + + {getPageTitle('New Item')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + + + + + + router.push('/game_sessions/game_sessions-list') + } + /> + + +
+
+
+ + ); +}; + +Game_sessionsNew.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Game_sessionsNew; diff --git a/frontend/src/pages/game_sessions/game_sessions-table.tsx b/frontend/src/pages/game_sessions/game_sessions-table.tsx new file mode 100644 index 0000000..328a11e --- /dev/null +++ b/frontend/src/pages/game_sessions/game_sessions-table.tsx @@ -0,0 +1,164 @@ +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 TableGame_sessions from '../../components/Game_sessions/TableGame_sessions'; +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/game_sessions/game_sessionsSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const Game_sessionsTablesPage = () => { + 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([]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_GAME_SESSIONS'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getGame_sessionsCSV = async () => { + const response = await axios({ + url: '/game_sessions?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 = 'game_sessionsCSV.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('Game_sessions')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
+
+
+
+ + + +
+ + + + + ); +}; + +Game_sessionsTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Game_sessionsTablesPage; diff --git a/frontend/src/pages/game_sessions/game_sessions-view.tsx b/frontend/src/pages/game_sessions/game_sessions-view.tsx new file mode 100644 index 0000000..c7d1dd1 --- /dev/null +++ b/frontend/src/pages/game_sessions/game_sessions-view.tsx @@ -0,0 +1,88 @@ +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/game_sessions/game_sessionsSlice'; +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 Game_sessionsView = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const { game_sessions } = useAppSelector((state) => state.game_sessions); + + 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 game_sessions')} + + + + + + +
+

schools

+ +

{game_sessions?.schools?.name ?? 'No data'}

+
+ + + + router.push('/game_sessions/game_sessions-list')} + /> +
+
+ + ); +}; + +Game_sessionsView.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default Game_sessionsView; diff --git a/frontend/src/pages/schools/schools-view.tsx b/frontend/src/pages/schools/schools-view.tsx index a08dd07..6222b70 100644 --- a/frontend/src/pages/schools/schools-view.tsx +++ b/frontend/src/pages/schools/schools-view.tsx @@ -354,6 +354,105 @@ const SchoolsView = () => { + <> +

Game_sessions schools

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

Game_questions schools

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

Game_answers schools

+ +
+ + + + + + {schools.game_answers_schools && + Array.isArray(schools.game_answers_schools) && + schools.game_answers_schools.map((item: any) => ( + + router.push( + `/game_answers/game_answers-view/?id=${item.id}`, + ) + } + > + ))} + +
+
+ {!schools?.game_answers_schools?.length && ( +
No data
+ )} +
+ + diff --git a/frontend/src/stores/game_answers/game_answersSlice.ts b/frontend/src/stores/game_answers/game_answersSlice.ts new file mode 100644 index 0000000..b0fde43 --- /dev/null +++ b/frontend/src/stores/game_answers/game_answersSlice.ts @@ -0,0 +1,241 @@ +import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit'; +import axios from 'axios'; +import { + fulfilledNotify, + rejectNotify, + resetNotify, +} from '../../helpers/notifyStateHandler'; + +interface MainState { + game_answers: any; + loading: boolean; + count: number; + refetch: boolean; + rolesWidgets: any[]; + notify: { + showNotification: boolean; + textNotification: string; + typeNotification: string; + }; +} + +const initialState: MainState = { + game_answers: [], + loading: false, + count: 0, + refetch: false, + rolesWidgets: [], + notify: { + showNotification: false, + textNotification: '', + typeNotification: 'warn', + }, +}; + +export const fetch = createAsyncThunk( + 'game_answers/fetch', + async (data: any) => { + const { id, query } = data; + const result = await axios.get( + `game_answers${query || (id ? `/${id}` : '')}`, + ); + return id + ? result.data + : { rows: result.data.rows, count: result.data.count }; + }, +); + +export const deleteItemsByIds = createAsyncThunk( + 'game_answers/deleteByIds', + async (data: any, { rejectWithValue }) => { + try { + await axios.post('game_answers/deleteByIds', { data }); + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const deleteItem = createAsyncThunk( + 'game_answers/deleteGame_answers', + async (id: string, { rejectWithValue }) => { + try { + await axios.delete(`game_answers/${id}`); + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const create = createAsyncThunk( + 'game_answers/createGame_answers', + async (data: any, { rejectWithValue }) => { + try { + const result = await axios.post('game_answers', { data }); + return result.data; + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const uploadCsv = createAsyncThunk( + 'game_answers/uploadCsv', + async (file: File, { rejectWithValue }) => { + try { + const data = new FormData(); + data.append('file', file); + data.append('filename', file.name); + + const result = await axios.post('game_answers/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( + 'game_answers/updateGame_answers', + async (payload: any, { rejectWithValue }) => { + try { + const result = await axios.put(`game_answers/${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 game_answersSlice = createSlice({ + name: 'game_answers', + 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.game_answers = action.payload.rows; + state.count = action.payload.count; + } else { + state.game_answers = 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, 'Game_answers 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, `${'Game_answers'.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, `${'Game_answers'.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, `${'Game_answers'.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, 'Game_answers 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 } = game_answersSlice.actions; + +export default game_answersSlice.reducer; diff --git a/frontend/src/stores/game_questions/game_questionsSlice.ts b/frontend/src/stores/game_questions/game_questionsSlice.ts new file mode 100644 index 0000000..d9da595 --- /dev/null +++ b/frontend/src/stores/game_questions/game_questionsSlice.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 { + game_questions: any; + loading: boolean; + count: number; + refetch: boolean; + rolesWidgets: any[]; + notify: { + showNotification: boolean; + textNotification: string; + typeNotification: string; + }; +} + +const initialState: MainState = { + game_questions: [], + loading: false, + count: 0, + refetch: false, + rolesWidgets: [], + notify: { + showNotification: false, + textNotification: '', + typeNotification: 'warn', + }, +}; + +export const fetch = createAsyncThunk( + 'game_questions/fetch', + async (data: any) => { + const { id, query } = data; + const result = await axios.get( + `game_questions${query || (id ? `/${id}` : '')}`, + ); + return id + ? result.data + : { rows: result.data.rows, count: result.data.count }; + }, +); + +export const deleteItemsByIds = createAsyncThunk( + 'game_questions/deleteByIds', + async (data: any, { rejectWithValue }) => { + try { + await axios.post('game_questions/deleteByIds', { data }); + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const deleteItem = createAsyncThunk( + 'game_questions/deleteGame_questions', + async (id: string, { rejectWithValue }) => { + try { + await axios.delete(`game_questions/${id}`); + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const create = createAsyncThunk( + 'game_questions/createGame_questions', + async (data: any, { rejectWithValue }) => { + try { + const result = await axios.post('game_questions', { data }); + return result.data; + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const uploadCsv = createAsyncThunk( + 'game_questions/uploadCsv', + async (file: File, { rejectWithValue }) => { + try { + const data = new FormData(); + data.append('file', file); + data.append('filename', file.name); + + const result = await axios.post('game_questions/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( + 'game_questions/updateGame_questions', + async (payload: any, { rejectWithValue }) => { + try { + const result = await axios.put(`game_questions/${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 game_questionsSlice = createSlice({ + name: 'game_questions', + 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.game_questions = action.payload.rows; + state.count = action.payload.count; + } else { + state.game_questions = 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, 'Game_questions 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, + `${'Game_questions'.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, + `${'Game_questions'.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, + `${'Game_questions'.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, 'Game_questions 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 } = game_questionsSlice.actions; + +export default game_questionsSlice.reducer; diff --git a/frontend/src/stores/game_sessions/game_sessionsSlice.ts b/frontend/src/stores/game_sessions/game_sessionsSlice.ts new file mode 100644 index 0000000..8d0c22f --- /dev/null +++ b/frontend/src/stores/game_sessions/game_sessionsSlice.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 { + game_sessions: any; + loading: boolean; + count: number; + refetch: boolean; + rolesWidgets: any[]; + notify: { + showNotification: boolean; + textNotification: string; + typeNotification: string; + }; +} + +const initialState: MainState = { + game_sessions: [], + loading: false, + count: 0, + refetch: false, + rolesWidgets: [], + notify: { + showNotification: false, + textNotification: '', + typeNotification: 'warn', + }, +}; + +export const fetch = createAsyncThunk( + 'game_sessions/fetch', + async (data: any) => { + const { id, query } = data; + const result = await axios.get( + `game_sessions${query || (id ? `/${id}` : '')}`, + ); + return id + ? result.data + : { rows: result.data.rows, count: result.data.count }; + }, +); + +export const deleteItemsByIds = createAsyncThunk( + 'game_sessions/deleteByIds', + async (data: any, { rejectWithValue }) => { + try { + await axios.post('game_sessions/deleteByIds', { data }); + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const deleteItem = createAsyncThunk( + 'game_sessions/deleteGame_sessions', + async (id: string, { rejectWithValue }) => { + try { + await axios.delete(`game_sessions/${id}`); + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const create = createAsyncThunk( + 'game_sessions/createGame_sessions', + async (data: any, { rejectWithValue }) => { + try { + const result = await axios.post('game_sessions', { data }); + return result.data; + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const uploadCsv = createAsyncThunk( + 'game_sessions/uploadCsv', + async (file: File, { rejectWithValue }) => { + try { + const data = new FormData(); + data.append('file', file); + data.append('filename', file.name); + + const result = await axios.post('game_sessions/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( + 'game_sessions/updateGame_sessions', + async (payload: any, { rejectWithValue }) => { + try { + const result = await axios.put(`game_sessions/${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 game_sessionsSlice = createSlice({ + name: 'game_sessions', + 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.game_sessions = action.payload.rows; + state.count = action.payload.count; + } else { + state.game_sessions = 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, 'Game_sessions 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, + `${'Game_sessions'.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, + `${'Game_sessions'.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, + `${'Game_sessions'.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, 'Game_sessions 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 } = game_sessionsSlice.actions; + +export default game_sessionsSlice.reducer; diff --git a/frontend/src/stores/store.ts b/frontend/src/stores/store.ts index ac015b5..69a5aec 100644 --- a/frontend/src/stores/store.ts +++ b/frontend/src/stores/store.ts @@ -14,6 +14,9 @@ import submissionsSlice from './submissions/submissionsSlice'; import rolesSlice from './roles/rolesSlice'; import permissionsSlice from './permissions/permissionsSlice'; import schoolsSlice from './schools/schoolsSlice'; +import game_sessionsSlice from './game_sessions/game_sessionsSlice'; +import game_questionsSlice from './game_questions/game_questionsSlice'; +import game_answersSlice from './game_answers/game_answersSlice'; export const store = configureStore({ reducer: { @@ -32,6 +35,9 @@ export const store = configureStore({ roles: rolesSlice, permissions: permissionsSlice, schools: schoolsSlice, + game_sessions: game_sessionsSlice, + game_questions: game_questionsSlice, + game_answers: game_answersSlice, }, });