diff --git a/.gitignore b/.gitignore index e427ff3..d0eb167 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,8 @@ node_modules/ */node_modules/ */build/ + +**/node_modules/ +**/build/ +.DS_Store +.env \ No newline at end of file diff --git a/app-shell/src/_schema.json b/app-shell/src/_schema.json index 1ca9b51..ef4183f 100644 --- a/app-shell/src/_schema.json +++ b/app-shell/src/_schema.json @@ -1,5 +1,4 @@ - - { - "Initial version": "{\"iv\":\"supFIdux6gxPDrZp\",\"encryptedData\":\"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\"}" -} + "Initial version": "{\"iv\":\"supFIdux6gxPDrZp\",\"encryptedData\":\"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\"}", + "likes": "{\"iv\":\"o8Y4QM9Tx00ZeTJO\",\"encryptedData\":\"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\"}" +} \ No newline at end of file diff --git a/backend/src/db/api/likes.js b/backend/src/db/api/likes.js new file mode 100644 index 0000000..6705688 --- /dev/null +++ b/backend/src/db/api/likes.js @@ -0,0 +1,235 @@ +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 LikesDBApi { + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const likes = await db.likes.create( + { + id: data.id || undefined, + + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + return likes; + } + + 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 likesData = 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 likes = await db.likes.bulkCreate(likesData, { transaction }); + + // For each item created, replace relation files + + return likes; + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const likes = await db.likes.findByPk(id, {}, { transaction }); + + const updatePayload = {}; + + updatePayload.updatedById = currentUser.id; + + await likes.update(updatePayload, { transaction }); + + return likes; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const likes = await db.likes.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of likes) { + await record.update({ deletedBy: currentUser.id }, { transaction }); + } + for (const record of likes) { + await record.destroy({ transaction }); + } + }); + + return likes; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const likes = await db.likes.findByPk(id, options); + + await likes.update( + { + deletedBy: currentUser.id, + }, + { + transaction, + }, + ); + + await likes.destroy({ + transaction, + }); + + return likes; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const likes = await db.likes.findOne({ where }, { transaction }); + + if (!likes) { + return likes; + } + + const output = likes.get({ plain: true }); + + return output; + } + + static async findAll(filter, options) { + const limit = filter.limit || 0; + let offset = 0; + let where = {}; + const currentPage = +filter.page; + + offset = currentPage * limit; + + const orderBy = null; + + const transaction = (options && options.transaction) || undefined; + + let include = []; + + if (filter) { + if (filter.id) { + where = { + ...where, + ['id']: Utils.uuid(filter.id), + }; + } + + if (filter.active !== undefined) { + where = { + ...where, + active: filter.active === true || filter.active === 'true', + }; + } + + if (filter.createdAtRange) { + const [start, end] = filter.createdAtRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.lte]: end, + }, + }; + } + } + } + + const queryOptions = { + where, + include, + distinct: true, + order: + filter.field && filter.sort + ? [[filter.field, filter.sort]] + : [['createdAt', 'desc']], + transaction: options?.transaction, + logging: console.log, + }; + + if (!options?.countOnly) { + queryOptions.limit = limit ? Number(limit) : undefined; + queryOptions.offset = offset ? Number(offset) : undefined; + } + + try { + const { rows, count } = await db.likes.findAndCountAll(queryOptions); + + return { + rows: options?.countOnly ? [] : rows, + count: count, + }; + } catch (error) { + console.error('Error executing query:', error); + throw error; + } + } + + static async findAllAutocomplete(query, limit, offset) { + let where = {}; + + if (query) { + where = { + [Op.or]: [ + { ['id']: Utils.uuid(query) }, + Utils.ilike('likes', 'id', query), + ], + }; + } + + const records = await db.likes.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/migrations/1744588788792.js b/backend/src/db/migrations/1744588788792.js new file mode 100644 index 0000000..3ab60dc --- /dev/null +++ b/backend/src/db/migrations/1744588788792.js @@ -0,0 +1,72 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.createTable( + 'likes', + { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.DataTypes.UUIDV4, + primaryKey: true, + }, + createdById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + updatedById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + createdAt: { type: Sequelize.DataTypes.DATE }, + updatedAt: { type: Sequelize.DataTypes.DATE }, + deletedAt: { type: Sequelize.DataTypes.DATE }, + importHash: { + type: Sequelize.DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { transaction }, + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async down(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.dropTable('likes', { transaction }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/models/likes.js b/backend/src/db/models/likes.js new file mode 100644 index 0000000..884d1ba --- /dev/null +++ b/backend/src/db/models/likes.js @@ -0,0 +1,45 @@ +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 likes = sequelize.define( + 'likes', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + likes.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.likes.belongsTo(db.users, { + as: 'createdBy', + }); + + db.likes.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return likes; +}; diff --git a/backend/src/db/seeders/20200430130760-user-roles.js b/backend/src/db/seeders/20200430130760-user-roles.js index 54b0cb5..96f97b6 100644 --- a/backend/src/db/seeders/20200430130760-user-roles.js +++ b/backend/src/db/seeders/20200430130760-user-roles.js @@ -101,6 +101,7 @@ module.exports = { 'students', 'roles', 'permissions', + 'likes', , ]; await queryInterface.bulkInsert( @@ -948,6 +949,31 @@ primary key ("roles_permissionsId", "permissionId") permissionId: getId('DELETE_PERMISSIONS'), }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('CREATE_LIKES'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('READ_LIKES'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('UPDATE_LIKES'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('DELETE_LIKES'), + }, + { createdAt, updatedAt, diff --git a/backend/src/db/seeders/20231127130745-sample-data.js b/backend/src/db/seeders/20231127130745-sample-data.js index 9193cdd..0769ac6 100644 --- a/backend/src/db/seeders/20231127130745-sample-data.js +++ b/backend/src/db/seeders/20231127130745-sample-data.js @@ -13,6 +13,8 @@ const Instructors = db.instructors; const Students = db.students; +const Likes = db.likes; + const AnalyticsData = [ { // type code here for "relation_many" field @@ -31,6 +33,18 @@ const AnalyticsData = [ // type code here for "relation_many" field // type code here for "relation_many" field }, + + { + // type code here for "relation_many" field + // type code here for "relation_many" field + // type code here for "relation_many" field + }, + + { + // type code here for "relation_many" field + // type code here for "relation_many" field + // type code here for "relation_many" field + }, ]; const CoursesData = [ @@ -69,6 +83,30 @@ const CoursesData = [ // type code here for "relation_many" field }, + + { + title: 'Graphic Design', + + description: 'Develop skills in design software and visual communication.', + + // type code here for "relation_many" field + + // type code here for "relation_many" field + + // type code here for "relation_many" field + }, + + { + title: 'Business Management', + + description: 'Understand the fundamentals of managing a business.', + + // type code here for "relation_many" field + + // type code here for "relation_many" field + + // type code here for "relation_many" field + }, ]; const DiscussionBoardsData = [ @@ -89,25 +127,21 @@ const DiscussionBoardsData = [ // type code here for "relation_one" field }, + + { + topic: 'Design Trends', + + // type code here for "relation_one" field + }, + + { + topic: 'Leadership Skills', + + // type code here for "relation_one" field + }, ]; const EnrollmentsData = [ - { - // type code here for "relation_one" field - - // type code here for "relation_one" field - - payment_status: 'paid', - }, - - { - // type code here for "relation_one" field - - // type code here for "relation_one" field - - payment_status: 'paid', - }, - { // type code here for "relation_one" field @@ -115,6 +149,38 @@ const EnrollmentsData = [ payment_status: 'pending', }, + + { + // type code here for "relation_one" field + + // type code here for "relation_one" field + + payment_status: 'overdue', + }, + + { + // type code here for "relation_one" field + + // type code here for "relation_one" field + + payment_status: 'overdue', + }, + + { + // type code here for "relation_one" field + + // type code here for "relation_one" field + + payment_status: 'paid', + }, + + { + // type code here for "relation_one" field + + // type code here for "relation_one" field + + payment_status: 'paid', + }, ]; const InstructorsData = [ @@ -147,6 +213,26 @@ const InstructorsData = [ // type code here for "relation_many" field }, + + { + name: 'Mr. Michael Green', + + email: 'michael.green@example.com', + + qualifications: 'MSc in Mathematics', + + // type code here for "relation_many" field + }, + + { + name: 'Dr. Olivia White', + + email: 'olivia.white@example.com', + + qualifications: 'PhD in Marketing', + + // type code here for "relation_many" field + }, ]; const StudentsData = [ @@ -173,8 +259,26 @@ const StudentsData = [ // type code here for "relation_many" field }, + + { + name: 'Diana Prince', + + email: 'diana.prince@example.com', + + // type code here for "relation_many" field + }, + + { + name: 'Ethan Hunt', + + email: 'ethan.hunt@example.com', + + // type code here for "relation_many" field + }, ]; +const LikesData = [{}, {}, {}, {}, {}]; + // Similar logic for "relation_many" // Similar logic for "relation_many" @@ -222,6 +326,28 @@ async function associateDiscussionBoardWithCourse() { if (DiscussionBoard2?.setCourse) { await DiscussionBoard2.setCourse(relatedCourse2); } + + const relatedCourse3 = await Courses.findOne({ + offset: Math.floor(Math.random() * (await Courses.count())), + }); + const DiscussionBoard3 = await DiscussionBoards.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (DiscussionBoard3?.setCourse) { + await DiscussionBoard3.setCourse(relatedCourse3); + } + + const relatedCourse4 = await Courses.findOne({ + offset: Math.floor(Math.random() * (await Courses.count())), + }); + const DiscussionBoard4 = await DiscussionBoards.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (DiscussionBoard4?.setCourse) { + await DiscussionBoard4.setCourse(relatedCourse4); + } } async function associateEnrollmentWithStudent() { @@ -257,6 +383,28 @@ async function associateEnrollmentWithStudent() { if (Enrollment2?.setStudent) { await Enrollment2.setStudent(relatedStudent2); } + + const relatedStudent3 = await Students.findOne({ + offset: Math.floor(Math.random() * (await Students.count())), + }); + const Enrollment3 = await Enrollments.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (Enrollment3?.setStudent) { + await Enrollment3.setStudent(relatedStudent3); + } + + const relatedStudent4 = await Students.findOne({ + offset: Math.floor(Math.random() * (await Students.count())), + }); + const Enrollment4 = await Enrollments.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (Enrollment4?.setStudent) { + await Enrollment4.setStudent(relatedStudent4); + } } async function associateEnrollmentWithCourse() { @@ -292,6 +440,28 @@ async function associateEnrollmentWithCourse() { if (Enrollment2?.setCourse) { await Enrollment2.setCourse(relatedCourse2); } + + const relatedCourse3 = await Courses.findOne({ + offset: Math.floor(Math.random() * (await Courses.count())), + }); + const Enrollment3 = await Enrollments.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (Enrollment3?.setCourse) { + await Enrollment3.setCourse(relatedCourse3); + } + + const relatedCourse4 = await Courses.findOne({ + offset: Math.floor(Math.random() * (await Courses.count())), + }); + const Enrollment4 = await Enrollments.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (Enrollment4?.setCourse) { + await Enrollment4.setCourse(relatedCourse4); + } } // Similar logic for "relation_many" @@ -312,6 +482,8 @@ module.exports = { await Students.bulkCreate(StudentsData); + await Likes.bulkCreate(LikesData); + await Promise.all([ // Similar logic for "relation_many" @@ -351,5 +523,7 @@ module.exports = { await queryInterface.bulkDelete('instructors', null, {}); await queryInterface.bulkDelete('students', null, {}); + + await queryInterface.bulkDelete('likes', null, {}); }, }; diff --git a/backend/src/db/seeders/20250413235948.js b/backend/src/db/seeders/20250413235948.js new file mode 100644 index 0000000..ddd1a6e --- /dev/null +++ b/backend/src/db/seeders/20250413235948.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 = ['likes']; + + const createdPermissions = entities.flatMap(createPermissions); + + // Add permissions to database + await queryInterface.bulkInsert('permissions', createdPermissions); + // Get permissions ids + const permissionsIds = createdPermissions.map((p) => p.id); + // Get admin role + const adminRole = await db.roles.findOne({ + where: { name: config.roles.admin }, + }); + + if (adminRole) { + // Add permissions to admin role if it exists + await adminRole.addPermissions(permissionsIds); + } + }, + down: async (queryInterface, Sequelize) => { + await queryInterface.bulkDelete( + 'permissions', + entities.flatMap(createPermissions), + ); + }, +}; diff --git a/backend/src/index.js b/backend/src/index.js index b0518da..2a90fd2 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -37,6 +37,8 @@ const rolesRoutes = require('./routes/roles'); const permissionsRoutes = require('./routes/permissions'); +const likesRoutes = require('./routes/likes'); + const getBaseUrl = (url) => { if (!url) return ''; return url.endsWith('/api') ? url.slice(0, -4) : url; @@ -47,9 +49,9 @@ const options = { openapi: '3.0.0', info: { version: '1.0.0', - title: 'saveschematest', + title: 'save_schema_test', description: - 'saveschematest Online REST API for Testing and Prototyping application. You can perform all major operations with your entities - create, delete and etc.', + 'save_schema_test Online REST API for Testing and Prototyping application. You can perform all major operations with your entities - create, delete and etc.', }, servers: [ { @@ -156,6 +158,12 @@ app.use( permissionsRoutes, ); +app.use( + '/api/likes', + passport.authenticate('jwt', { session: false }), + likesRoutes, +); + app.use( '/api/openai', passport.authenticate('jwt', { session: false }), diff --git a/backend/src/routes/likes.js b/backend/src/routes/likes.js new file mode 100644 index 0000000..ed6c9b6 --- /dev/null +++ b/backend/src/routes/likes.js @@ -0,0 +1,429 @@ +const express = require('express'); + +const LikesService = require('../services/likes'); +const LikesDBApi = require('../db/api/likes'); +const wrapAsync = require('../helpers').wrapAsync; + +const router = express.Router(); + +const { parse } = require('json2csv'); + +const { checkCrudPermissions } = require('../middlewares/check-permissions'); + +router.use(checkCrudPermissions('likes')); + +/** + * @swagger + * components: + * schemas: + * Likes: + * type: object + * properties: + + */ + +/** + * @swagger + * tags: + * name: Likes + * description: The Likes managing API + */ + +/** + * @swagger + * /api/likes: + * post: + * security: + * - bearerAuth: [] + * tags: [Likes] + * 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/Likes" + * responses: + * 200: + * description: The item was successfully added + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Likes" + * 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 LikesService.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: [Likes] + * 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/Likes" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Likes" + * 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 LikesService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/likes/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Likes] + * 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/Likes" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Likes" + * 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 LikesService.update(req.body.data, req.body.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/likes/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Likes] + * 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/Likes" + * 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 LikesService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/likes/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Likes] + * 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/Likes" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post( + '/deleteByIds', + wrapAsync(async (req, res) => { + await LikesService.deleteByIds(req.body.data, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/likes: + * get: + * security: + * - bearerAuth: [] + * tags: [Likes] + * summary: Get all likes + * description: Get all likes + * responses: + * 200: + * description: Likes list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Likes" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get( + '/', + wrapAsync(async (req, res) => { + const filetype = req.query.filetype; + + const currentUser = req.currentUser; + const payload = await LikesDBApi.findAll(req.query, { 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/likes/count: + * get: + * security: + * - bearerAuth: [] + * tags: [Likes] + * summary: Count all likes + * description: Count all likes + * responses: + * 200: + * description: Likes count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Likes" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get( + '/count', + wrapAsync(async (req, res) => { + const currentUser = req.currentUser; + const payload = await LikesDBApi.findAll(req.query, null, { + countOnly: true, + currentUser, + }); + + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/likes/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [Likes] + * summary: Find all likes that match search criteria + * description: Find all likes that match search criteria + * responses: + * 200: + * description: Likes list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Likes" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/autocomplete', async (req, res) => { + const payload = await LikesDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/likes/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Likes] + * 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/Likes" + * 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 LikesDBApi.findBy({ id: req.params.id }); + + res.status(200).send(payload); + }), +); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/services/likes.js b/backend/src/services/likes.js new file mode 100644 index 0000000..d8f6f3e --- /dev/null +++ b/backend/src/services/likes.js @@ -0,0 +1,114 @@ +const db = require('../db/models'); +const LikesDBApi = require('../db/api/likes'); +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 LikesService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await LikesDBApi.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 LikesDBApi.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 likes = await LikesDBApi.findBy({ id }, { transaction }); + + if (!likes) { + throw new ValidationError('likesNotFound'); + } + + const updatedLikes = await LikesDBApi.update(id, data, { + currentUser, + transaction, + }); + + await transaction.commit(); + return updatedLikes; + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await LikesDBApi.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 LikesDBApi.remove(id, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; diff --git a/frontend/json/runtimeError.json b/frontend/json/runtimeError.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/frontend/json/runtimeError.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/frontend/src/components/Likes/CardLikes.tsx b/frontend/src/components/Likes/CardLikes.tsx new file mode 100644 index 0000000..92fbddc --- /dev/null +++ b/frontend/src/components/Likes/CardLikes.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 = { + likes: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const CardLikes = ({ + likes, + 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_LIKES'); + + return ( +
+ {loading && } +
    + {!loading && + likes.map((item, index) => ( +
  • +
    + + {item.id} + + +
    + +
    +
    +
    +
  • + ))} + {!loading && likes.length === 0 && ( +
    +

    No data to display

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

No data to display

+
+ )} +
+
+ +
+ + ); +}; + +export default ListLikes; diff --git a/frontend/src/components/Likes/TableLikes.tsx b/frontend/src/components/Likes/TableLikes.tsx new file mode 100644 index 0000000..e544926 --- /dev/null +++ b/frontend/src/components/Likes/TableLikes.tsx @@ -0,0 +1,481 @@ +import React, { useEffect, useState, useMemo } from 'react'; +import { createPortal } from 'react-dom'; +import { ToastContainer, toast } from 'react-toastify'; +import BaseButton from '../BaseButton'; +import CardBoxModal from '../CardBoxModal'; +import CardBox from '../CardBox'; +import { + fetch, + update, + deleteItem, + setRefetch, + deleteItemsByIds, +} from '../../stores/likes/likesSlice'; +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 './configureLikesCols'; +import _ from 'lodash'; +import dataFormatter from '../../helpers/dataFormatter'; +import { dataGridStyles } from '../../styles'; + +const perPage = 10; + +const TableSampleLikes = ({ + 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 { + likes, + loading, + count, + notify: likesNotify, + refetch, + } = useAppSelector((state) => state.likes); + 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 (likesNotify.showNotification) { + notify(likesNotify.typeNotification, likesNotify.textNotification); + } + }, [likesNotify.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, `likes`, 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={likes ?? []} + 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 TableSampleLikes; diff --git a/frontend/src/components/Likes/configureLikesCols.tsx b/frontend/src/components/Likes/configureLikesCols.tsx new file mode 100644 index 0000000..1e16b07 --- /dev/null +++ b/frontend/src/components/Likes/configureLikesCols.tsx @@ -0,0 +1,61 @@ +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_LIKES'); + + return [ + { + field: 'actions', + type: 'actions', + minWidth: 30, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + getActions: (params: GridRowParams) => { + return [ + , + ]; + }, + }, + ]; +}; diff --git a/frontend/src/components/WebPageComponents/Header.tsx b/frontend/src/components/WebPageComponents/Header.tsx index 1c882e7..14ab067 100644 --- a/frontend/src/components/WebPageComponents/Header.tsx +++ b/frontend/src/components/WebPageComponents/Header.tsx @@ -19,7 +19,7 @@ export default function WebSiteHeader({ const websiteHeder = useAppSelector((state) => 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; return ( diff --git a/frontend/src/menuAside.ts b/frontend/src/menuAside.ts index 3a41f9c..2fcaa36 100644 --- a/frontend/src/menuAside.ts +++ b/frontend/src/menuAside.ts @@ -88,18 +88,20 @@ const menuAside: MenuAsideItem[] = [ : icon.mdiTable, permissions: 'READ_PERMISSIONS', }, + { + href: '/likes/likes-list', + label: 'Likes', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: icon.mdiTable ? icon.mdiTable : icon.mdiTable, + permissions: 'READ_LIKES', + }, { href: '/profile', label: 'Profile', icon: icon.mdiAccountCircle, }, - { - href: '/home', - label: 'Home page', - icon: icon.mdiHome, - withDevider: true, - }, { href: '/api-docs', target: '_blank', diff --git a/frontend/src/pages/dashboard.tsx b/frontend/src/pages/dashboard.tsx index 25be6e2..965aaca 100644 --- a/frontend/src/pages/dashboard.tsx +++ b/frontend/src/pages/dashboard.tsx @@ -32,6 +32,7 @@ const Dashboard = () => { const [students, setStudents] = React.useState('Loading...'); const [roles, setRoles] = React.useState('Loading...'); const [permissions, setPermissions] = React.useState('Loading...'); + const [likes, setLikes] = React.useState('Loading...'); const [widgetsRole, setWidgetsRole] = React.useState({ role: { value: '', label: '' }, @@ -52,6 +53,7 @@ const Dashboard = () => { 'students', 'roles', 'permissions', + 'likes', ]; const fns = [ setUsers, @@ -63,6 +65,7 @@ const Dashboard = () => { setStudents, setRoles, setPermissions, + setLikes, ]; const requests = entities.map((entity, index) => { @@ -458,6 +461,38 @@ const Dashboard = () => { )} + + {hasPermission(currentUser, 'READ_LIKES') && ( + +
+
+
+
+ Likes +
+
+ {likes} +
+
+
+ +
+
+
+ + )} diff --git a/frontend/src/pages/likes/[likesId].tsx b/frontend/src/pages/likes/[likesId].tsx new file mode 100644 index 0000000..acc8b15 --- /dev/null +++ b/frontend/src/pages/likes/[likesId].tsx @@ -0,0 +1,118 @@ +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/likes/likesSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; + +const EditLikes = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = {}; + const [initialValues, setInitialValues] = useState(initVals); + + const { likes } = useAppSelector((state) => state.likes); + + const { likesId } = router.query; + + useEffect(() => { + dispatch(fetch({ id: likesId })); + }, [likesId]); + + useEffect(() => { + if (typeof likes === 'object') { + setInitialValues(likes); + } + }, [likes]); + + useEffect(() => { + if (typeof likes === 'object') { + const newInitialVal = { ...initVals }; + + Object.keys(initVals).forEach((el) => (newInitialVal[el] = likes[el])); + + setInitialValues(newInitialVal); + } + }, [likes]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: likesId, data })); + await router.push('/likes/likes-list'); + }; + + return ( + <> + + {getPageTitle('Edit likes')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + router.push('/likes/likes-list')} + /> + + +
+
+
+ + ); +}; + +EditLikes.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditLikes; diff --git a/frontend/src/pages/likes/likes-edit.tsx b/frontend/src/pages/likes/likes-edit.tsx new file mode 100644 index 0000000..c92db37 --- /dev/null +++ b/frontend/src/pages/likes/likes-edit.tsx @@ -0,0 +1,116 @@ +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/likes/likesSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; + +const EditLikesPage = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = {}; + const [initialValues, setInitialValues] = useState(initVals); + + const { likes } = useAppSelector((state) => state.likes); + + const { id } = router.query; + + useEffect(() => { + dispatch(fetch({ id: id })); + }, [id]); + + useEffect(() => { + if (typeof likes === 'object') { + setInitialValues(likes); + } + }, [likes]); + + useEffect(() => { + if (typeof likes === 'object') { + const newInitialVal = { ...initVals }; + Object.keys(initVals).forEach((el) => (newInitialVal[el] = likes[el])); + setInitialValues(newInitialVal); + } + }, [likes]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: id, data })); + await router.push('/likes/likes-list'); + }; + + return ( + <> + + {getPageTitle('Edit likes')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + router.push('/likes/likes-list')} + /> + + +
+
+
+ + ); +}; + +EditLikesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditLikesPage; diff --git a/frontend/src/pages/likes/likes-list.tsx b/frontend/src/pages/likes/likes-list.tsx new file mode 100644 index 0000000..841dfa7 --- /dev/null +++ b/frontend/src/pages/likes/likes-list.tsx @@ -0,0 +1,160 @@ +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 TableLikes from '../../components/Likes/TableLikes'; +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/likes/likesSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const LikesTablesPage = () => { + 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_LIKES'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getLikesCSV = async () => { + const response = await axios({ + url: '/likes?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 = 'likesCSV.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('Likes')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
+
+
+
+ + + + +
+ + + + + ); +}; + +LikesTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + {page} + ); +}; + +export default LikesTablesPage; diff --git a/frontend/src/pages/likes/likes-new.tsx b/frontend/src/pages/likes/likes-new.tsx new file mode 100644 index 0000000..ac0fdf4 --- /dev/null +++ b/frontend/src/pages/likes/likes-new.tsx @@ -0,0 +1,92 @@ +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/likes/likesSlice'; +import { useAppDispatch } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import moment from 'moment'; + +const initialValues = {}; + +const LikesNew = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + + const handleSubmit = async (data) => { + await dispatch(create(data)); + await router.push('/likes/likes-list'); + }; + return ( + <> + + {getPageTitle('New Item')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + router.push('/likes/likes-list')} + /> + + +
+
+
+ + ); +}; + +LikesNew.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default LikesNew; diff --git a/frontend/src/pages/likes/likes-table.tsx b/frontend/src/pages/likes/likes-table.tsx new file mode 100644 index 0000000..9a15606 --- /dev/null +++ b/frontend/src/pages/likes/likes-table.tsx @@ -0,0 +1,159 @@ +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 TableLikes from '../../components/Likes/TableLikes'; +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/likes/likesSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const LikesTablesPage = () => { + 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_LIKES'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getLikesCSV = async () => { + const response = await axios({ + url: '/likes?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 = 'likesCSV.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('Likes')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
+
+
+
+ + + +
+ + + + + ); +}; + +LikesTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + {page} + ); +}; + +export default LikesTablesPage; diff --git a/frontend/src/pages/likes/likes-view.tsx b/frontend/src/pages/likes/likes-view.tsx new file mode 100644 index 0000000..cdfa75b --- /dev/null +++ b/frontend/src/pages/likes/likes-view.tsx @@ -0,0 +1,76 @@ +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/likes/likesSlice'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import { getPageTitle } from '../../config'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import SectionMain from '../../components/SectionMain'; +import CardBox from '../../components/CardBox'; +import BaseButton from '../../components/BaseButton'; +import BaseDivider from '../../components/BaseDivider'; +import { mdiChartTimelineVariant } from '@mdi/js'; +import { SwitchField } from '../../components/SwitchField'; +import FormField from '../../components/FormField'; + +const LikesView = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const { likes } = useAppSelector((state) => state.likes); + + 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 likes')} + + + + + + + + + router.push('/likes/likes-list')} + /> + + + + ); +}; + +LikesView.getLayout = function getLayout(page: ReactElement) { + return ( + {page} + ); +}; + +export default LikesView; diff --git a/frontend/src/stores/likes/likesSlice.ts b/frontend/src/stores/likes/likesSlice.ts new file mode 100644 index 0000000..aadbd82 --- /dev/null +++ b/frontend/src/stores/likes/likesSlice.ts @@ -0,0 +1,236 @@ +import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit'; +import axios from 'axios'; +import { + fulfilledNotify, + rejectNotify, + resetNotify, +} from '../../helpers/notifyStateHandler'; + +interface MainState { + likes: any; + loading: boolean; + count: number; + refetch: boolean; + rolesWidgets: any[]; + notify: { + showNotification: boolean; + textNotification: string; + typeNotification: string; + }; +} + +const initialState: MainState = { + likes: [], + loading: false, + count: 0, + refetch: false, + rolesWidgets: [], + notify: { + showNotification: false, + textNotification: '', + typeNotification: 'warn', + }, +}; + +export const fetch = createAsyncThunk('likes/fetch', async (data: any) => { + const { id, query } = data; + const result = await axios.get(`likes${query || (id ? `/${id}` : '')}`); + return id + ? result.data + : { rows: result.data.rows, count: result.data.count }; +}); + +export const deleteItemsByIds = createAsyncThunk( + 'likes/deleteByIds', + async (data: any, { rejectWithValue }) => { + try { + await axios.post('likes/deleteByIds', { data }); + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const deleteItem = createAsyncThunk( + 'likes/deleteLikes', + async (id: string, { rejectWithValue }) => { + try { + await axios.delete(`likes/${id}`); + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const create = createAsyncThunk( + 'likes/createLikes', + async (data: any, { rejectWithValue }) => { + try { + const result = await axios.post('likes', { data }); + return result.data; + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const uploadCsv = createAsyncThunk( + 'likes/uploadCsv', + async (file: File, { rejectWithValue }) => { + try { + const data = new FormData(); + data.append('file', file); + data.append('filename', file.name); + + const result = await axios.post('likes/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( + 'likes/updateLikes', + async (payload: any, { rejectWithValue }) => { + try { + const result = await axios.put(`likes/${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 likesSlice = createSlice({ + name: 'likes', + 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.likes = action.payload.rows; + state.count = action.payload.count; + } else { + state.likes = 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, 'Likes 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, `${'Likes'.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, `${'Likes'.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, `${'Likes'.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, 'Likes 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 } = likesSlice.actions; + +export default likesSlice.reducer; diff --git a/frontend/src/stores/store.ts b/frontend/src/stores/store.ts index aaf78cc..3a71946 100644 --- a/frontend/src/stores/store.ts +++ b/frontend/src/stores/store.ts @@ -13,6 +13,7 @@ import instructorsSlice from './instructors/instructorsSlice'; import studentsSlice from './students/studentsSlice'; import rolesSlice from './roles/rolesSlice'; import permissionsSlice from './permissions/permissionsSlice'; +import likesSlice from './likes/likesSlice'; export const store = configureStore({ reducer: { @@ -30,6 +31,7 @@ export const store = configureStore({ students: studentsSlice, roles: rolesSlice, permissions: permissionsSlice, + likes: likesSlice, }, });