diff --git a/backend/src/db/api/courses.js b/backend/src/db/api/courses.js index 5bbbe53..a3833dd 100644 --- a/backend/src/db/api/courses.js +++ b/backend/src/db/api/courses.js @@ -159,6 +159,10 @@ module.exports = class CoursesDBApi { transaction, }); + output.likes_course = await courses.getLikes_course({ + transaction, + }); + output.instructors = await courses.getInstructors({ transaction, }); diff --git a/backend/src/db/api/likes.js b/backend/src/db/api/likes.js new file mode 100644 index 0000000..ade155b --- /dev/null +++ b/backend/src/db/api/likes.js @@ -0,0 +1,354 @@ + +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, + + createdat: data.createdat + || + null + , + + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + await likes.setCourse( data.course || null, { + transaction, + }); + + await likes.setUser( data.user || null, { + 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, + + createdat: item.createdat + || + null + , + + 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 = {}; + + if (data.createdat !== undefined) updatePayload.createdat = data.createdat; + + updatePayload.updatedById = currentUser.id; + + await likes.update(updatePayload, {transaction}); + + if (data.course !== undefined) { + await likes.setCourse( + + data.course, + + { transaction } + ); + } + + if (data.user !== undefined) { + await likes.setUser( + + data.user, + + { 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}); + + output.course = await likes.getCourse({ + transaction + }); + + output.user = await likes.getUser({ + transaction + }); + + 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 = [ + + { + model: db.courses, + as: 'course', + + where: filter.course ? { + [Op.or]: [ + { id: { [Op.in]: filter.course.split('|').map(term => Utils.uuid(term)) } }, + { + title: { + [Op.or]: filter.course.split('|').map(term => ({ [Op.iLike]: `%${term}%` })) + } + }, + ] + } : {}, + + }, + + { + model: db.users, + as: 'user', + + where: filter.user ? { + [Op.or]: [ + { id: { [Op.in]: filter.user.split('|').map(term => Utils.uuid(term)) } }, + { + firstName: { + [Op.or]: filter.user.split('|').map(term => ({ [Op.iLike]: `%${term}%` })) + } + }, + ] + } : {}, + + }, + + ]; + + if (filter) { + if (filter.id) { + where = { + ...where, + ['id']: Utils.uuid(filter.id), + }; + } + + 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 (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', + '', + query, + ), + ], + }; + } + + const records = await db.likes.findAll({ + attributes: [ 'id', '' ], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + orderBy: [['', 'ASC']], + }); + + return records.map((record) => ({ + id: record.id, + label: record., + })); + } + +}; + diff --git a/backend/src/db/api/users.js b/backend/src/db/api/users.js index 0d049ac..b3b20b2 100644 --- a/backend/src/db/api/users.js +++ b/backend/src/db/api/users.js @@ -267,6 +267,10 @@ module.exports = class UsersDBApi { const output = users.get({ plain: true }); + output.likes_user = await users.getLikes_user({ + transaction, + }); + output.avatar = await users.getAvatar({ transaction, }); @@ -275,12 +279,6 @@ module.exports = class UsersDBApi { transaction, }); - if (output.app_role) { - output.app_role_permissions = await output.app_role.getPermissions({ - transaction, - }); - } - output.custom_permissions = await users.getCustom_permissions({ transaction, }); diff --git a/backend/src/db/migrations/1741787181409.js b/backend/src/db/migrations/1741787181409.js new file mode 100644 index 0000000..3ab60dc --- /dev/null +++ b/backend/src/db/migrations/1741787181409.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/migrations/1741787223993.js b/backend/src/db/migrations/1741787223993.js new file mode 100644 index 0000000..eb98338 --- /dev/null +++ b/backend/src/db/migrations/1741787223993.js @@ -0,0 +1,52 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.addColumn( + 'likes', + 'courseId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'courses', + 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('likes', 'courseId', { transaction }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1741787245547.js b/backend/src/db/migrations/1741787245547.js new file mode 100644 index 0000000..1741f3b --- /dev/null +++ b/backend/src/db/migrations/1741787245547.js @@ -0,0 +1,52 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.addColumn( + 'likes', + 'userId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'users', + 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('likes', 'userId', { transaction }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1741787264888.js b/backend/src/db/migrations/1741787264888.js new file mode 100644 index 0000000..b4b0089 --- /dev/null +++ b/backend/src/db/migrations/1741787264888.js @@ -0,0 +1,47 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.addColumn( + 'likes', + 'createdat', + { + type: Sequelize.DataTypes.DATE, + }, + { 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('likes', 'createdat', { transaction }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/models/courses.js b/backend/src/db/models/courses.js index 84f8df3..f8ae9a3 100644 --- a/backend/src/db/models/courses.js +++ b/backend/src/db/models/courses.js @@ -98,6 +98,14 @@ module.exports = function (sequelize, DataTypes) { constraints: false, }); + db.courses.hasMany(db.likes, { + as: 'likes_course', + foreignKey: { + name: 'courseId', + }, + constraints: false, + }); + //end loop db.courses.belongsTo(db.users, { diff --git a/backend/src/db/models/likes.js b/backend/src/db/models/likes.js new file mode 100644 index 0000000..60e5946 --- /dev/null +++ b/backend/src/db/models/likes.js @@ -0,0 +1,65 @@ +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, + }, + + createdat: { + type: DataTypes.DATE, + }, + + 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.courses, { + as: 'course', + foreignKey: { + name: 'courseId', + }, + constraints: false, + }); + + db.likes.belongsTo(db.users, { + as: 'user', + foreignKey: { + name: 'userId', + }, + constraints: false, + }); + + db.likes.belongsTo(db.users, { + as: 'createdBy', + }); + + db.likes.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return likes; +}; diff --git a/backend/src/db/models/users.js b/backend/src/db/models/users.js index 74e549e..fd8e1e4 100644 --- a/backend/src/db/models/users.js +++ b/backend/src/db/models/users.js @@ -102,6 +102,14 @@ module.exports = function (sequelize, DataTypes) { /// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity + db.users.hasMany(db.likes, { + as: 'likes_user', + foreignKey: { + name: 'userId', + }, + constraints: false, + }); + //end loop db.users.belongsTo(db.roles, { diff --git a/backend/src/db/seeders/20231127130745-sample-data.js b/backend/src/db/seeders/20231127130745-sample-data.js index 0a409dc..a36c618 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_one" field @@ -43,6 +45,26 @@ const AnalyticsData = [ instructor_performance: 90, }, + + { + // type code here for "relation_one" field + + student_engagement: 70, + + completion_rate: 75, + + instructor_performance: 78, + }, + + { + // type code here for "relation_one" field + + student_engagement: 88, + + completion_rate: 85, + + instructor_performance: 87, + }, ]; const CoursesData = [ @@ -76,6 +98,27 @@ const CoursesData = [ // type code here for "relation_many" field }, + + { + title: 'Business Management', + + description: + 'Understand the fundamentals of managing a business effectively.', + + // type code here for "relation_many" field + + // type code here for "relation_many" field + }, + + { + title: 'Environmental Science', + + description: 'Study the impact of human activities on the environment.', + + // type code here for "relation_many" field + + // type code here for "relation_many" field + }, ]; const DiscussionBoardsData = [ @@ -102,6 +145,22 @@ const DiscussionBoardsData = [ // type code here for "relation_many" field }, + + { + // type code here for "relation_one" field + + topic: 'Business Strategies', + + // type code here for "relation_many" field + }, + + { + // type code here for "relation_one" field + + topic: 'Environmental Impact', + + // type code here for "relation_many" field + }, ]; const EnrollmentsData = [ @@ -118,7 +177,7 @@ const EnrollmentsData = [ // type code here for "relation_one" field - payment_status: 'Pending', + payment_status: 'Failed', }, { @@ -126,7 +185,23 @@ const EnrollmentsData = [ // type code here for "relation_one" field - payment_status: 'Pending', + payment_status: 'Failed', + }, + + { + // type code here for "relation_one" field + + // type code here for "relation_one" field + + payment_status: 'Failed', + }, + + { + // type code here for "relation_one" field + + // type code here for "relation_one" field + + payment_status: 'Failed', }, ]; @@ -154,6 +229,22 @@ const InstructorsData = [ // type code here for "relation_many" field }, + + { + first_name: 'Michael', + + last_name: 'Johnson', + + // type code here for "relation_many" field + }, + + { + first_name: 'Sarah', + + last_name: 'Lee', + + // type code here for "relation_many" field + }, ]; const StudentsData = [ @@ -180,6 +271,64 @@ const StudentsData = [ // type code here for "relation_many" field }, + + { + first_name: 'David', + + last_name: 'Williams', + + // type code here for "relation_many" field + }, + + { + first_name: 'Eve', + + last_name: 'Davis', + + // type code here for "relation_many" field + }, +]; + +const LikesData = [ + { + // type code here for "relation_one" field + + // type code here for "relation_one" field + + createdat: new Date(Date.now()), + }, + + { + // type code here for "relation_one" field + + // type code here for "relation_one" field + + createdat: new Date(Date.now()), + }, + + { + // type code here for "relation_one" field + + // type code here for "relation_one" field + + createdat: new Date(Date.now()), + }, + + { + // type code here for "relation_one" field + + // type code here for "relation_one" field + + createdat: new Date(Date.now()), + }, + + { + // type code here for "relation_one" field + + // type code here for "relation_one" field + + createdat: new Date(Date.now()), + }, ]; // Similar logic for "relation_many" @@ -217,6 +366,28 @@ async function associateAnalyticWithCourse() { if (Analytic2?.setCourse) { await Analytic2.setCourse(relatedCourse2); } + + const relatedCourse3 = await Courses.findOne({ + offset: Math.floor(Math.random() * (await Courses.count())), + }); + const Analytic3 = await Analytics.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (Analytic3?.setCourse) { + await Analytic3.setCourse(relatedCourse3); + } + + const relatedCourse4 = await Courses.findOne({ + offset: Math.floor(Math.random() * (await Courses.count())), + }); + const Analytic4 = await Analytics.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (Analytic4?.setCourse) { + await Analytic4.setCourse(relatedCourse4); + } } // Similar logic for "relation_many" @@ -256,6 +427,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); + } } // Similar logic for "relation_many" @@ -293,6 +486,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() { @@ -328,12 +543,148 @@ 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" // Similar logic for "relation_many" +async function associateLikeWithCourse() { + const relatedCourse0 = await Courses.findOne({ + offset: Math.floor(Math.random() * (await Courses.count())), + }); + const Like0 = await Likes.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (Like0?.setCourse) { + await Like0.setCourse(relatedCourse0); + } + + const relatedCourse1 = await Courses.findOne({ + offset: Math.floor(Math.random() * (await Courses.count())), + }); + const Like1 = await Likes.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (Like1?.setCourse) { + await Like1.setCourse(relatedCourse1); + } + + const relatedCourse2 = await Courses.findOne({ + offset: Math.floor(Math.random() * (await Courses.count())), + }); + const Like2 = await Likes.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (Like2?.setCourse) { + await Like2.setCourse(relatedCourse2); + } + + const relatedCourse3 = await Courses.findOne({ + offset: Math.floor(Math.random() * (await Courses.count())), + }); + const Like3 = await Likes.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (Like3?.setCourse) { + await Like3.setCourse(relatedCourse3); + } + + const relatedCourse4 = await Courses.findOne({ + offset: Math.floor(Math.random() * (await Courses.count())), + }); + const Like4 = await Likes.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (Like4?.setCourse) { + await Like4.setCourse(relatedCourse4); + } +} + +async function associateLikeWithUser() { + const relatedUser0 = await Users.findOne({ + offset: Math.floor(Math.random() * (await Users.count())), + }); + const Like0 = await Likes.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (Like0?.setUser) { + await Like0.setUser(relatedUser0); + } + + const relatedUser1 = await Users.findOne({ + offset: Math.floor(Math.random() * (await Users.count())), + }); + const Like1 = await Likes.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (Like1?.setUser) { + await Like1.setUser(relatedUser1); + } + + const relatedUser2 = await Users.findOne({ + offset: Math.floor(Math.random() * (await Users.count())), + }); + const Like2 = await Likes.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (Like2?.setUser) { + await Like2.setUser(relatedUser2); + } + + const relatedUser3 = await Users.findOne({ + offset: Math.floor(Math.random() * (await Users.count())), + }); + const Like3 = await Likes.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (Like3?.setUser) { + await Like3.setUser(relatedUser3); + } + + const relatedUser4 = await Users.findOne({ + offset: Math.floor(Math.random() * (await Users.count())), + }); + const Like4 = await Likes.findOne({ + order: [['id', 'ASC']], + offset: 4, + }); + if (Like4?.setUser) { + await Like4.setUser(relatedUser4); + } +} + module.exports = { up: async (queryInterface, Sequelize) => { await Analytics.bulkCreate(AnalyticsData); @@ -348,6 +699,8 @@ module.exports = { await Students.bulkCreate(StudentsData); + await Likes.bulkCreate(LikesData); + await Promise.all([ // Similar logic for "relation_many" @@ -368,6 +721,10 @@ module.exports = { // Similar logic for "relation_many" // Similar logic for "relation_many" + + await associateLikeWithCourse(), + + await associateLikeWithUser(), ]); }, @@ -383,5 +740,7 @@ module.exports = { await queryInterface.bulkDelete('instructors', null, {}); await queryInterface.bulkDelete('students', null, {}); + + await queryInterface.bulkDelete('likes', null, {}); }, }; diff --git a/backend/src/index.js b/backend/src/index.js index 9a01516..4bd307f 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -15,8 +15,6 @@ const fileRoutes = require('./routes/file'); const searchRoutes = require('./routes/search'); const pexelsRoutes = require('./routes/pexels'); -const openaiRoutes = require('./routes/openai'); - const contactFormRoutes = require('./routes/contactForm'); const usersRoutes = require('./routes/users'); @@ -37,14 +35,16 @@ const rolesRoutes = require('./routes/roles'); const permissionsRoutes = require('./routes/permissions'); +const likesRoutes = require('./routes/likes'); + const options = { definition: { openapi: '3.0.0', info: { version: '1.0.0', - title: 'test4', + title: 'test3', description: - 'test4 Online REST API for Testing and Prototyping application. You can perform all major operations with your entities - create, delete and etc.', + 'test3 Online REST API for Testing and Prototyping application. You can perform all major operations with your entities - create, delete and etc.', }, servers: [ { @@ -151,9 +151,9 @@ app.use( ); app.use( - '/api/openai', + '/api/likes', passport.authenticate('jwt', { session: false }), - openaiRoutes, + likesRoutes, ); app.use('/api/contact-form', contactFormRoutes); diff --git a/backend/src/routes/likes.js b/backend/src/routes/likes.js new file mode 100644 index 0000000..87292f5 --- /dev/null +++ b/backend/src/routes/likes.js @@ -0,0 +1,425 @@ +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'); + +/** + * @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', 'createdat']; + 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..3b1e0e6 --- /dev/null +++ b/frontend/src/components/Likes/CardLikes.tsx @@ -0,0 +1,118 @@ +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'; + +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); + + return ( +
+ {loading && } +
    + {!loading && likes.map((item, index) => ( +
  • + +
    + + + {item.} + + +
    + +
    +
    +
    + +
    +
    Course
    +
    +
    + { dataFormatter.coursesOneListFormatter(item.course) } +
    +
    +
    + +
    +
    User
    +
    +
    + { dataFormatter.usersOneListFormatter(item.user) } +
    +
    +
    + +
    +
    Createdat
    +
    +
    + { dataFormatter.dateTimeFormatter(item.createdat) } +
    +
    +
    + +
    +
  • + ))} + {!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..ab3ef18 --- /dev/null +++ b/frontend/src/components/Likes/ListLikes.tsx @@ -0,0 +1,101 @@ +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'; + +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 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' + } + > +
+

Course

+

+ {dataFormatter.coursesOneListFormatter(item.course)} +

+
+ +
+

User

+

+ {dataFormatter.usersOneListFormatter(item.user)} +

+
+ +
+

Createdat

+

+ {dataFormatter.dateTimeFormatter(item.createdat)} +

+
+ + +
+
+ ))} + {!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..c7899b8 --- /dev/null +++ b/frontend/src/components/Likes/TableLikes.tsx @@ -0,0 +1,479 @@ +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(() => { + loadColumns(handleDeleteModalAction, `likes`).then((newCols) => + setColumns(newCols), + ); + }, []); + + 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..924bbbf --- /dev/null +++ b/frontend/src/components/Likes/configureLikesCols.tsx @@ -0,0 +1,106 @@ +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'; + +type Params = (id: string) => void; + +export const loadColumns = async (onDelete: Params, entityName: string) => { + async function callOptionsApi(entityName: string) { + try { + const data = await axios(`/${entityName}/autocomplete?limit=100`); + return data.data; + } catch (error) { + console.log(error); + return []; + } + } + + return [ + { + field: 'course', + headerName: 'Course', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + + sortable: false, + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('courses'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + }, + + { + field: 'user', + headerName: 'User', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + + sortable: false, + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('users'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + }, + + { + field: 'createdat', + headerName: 'Createdat', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + + type: 'dateTime', + valueGetter: (params: GridValueGetterParams) => + new Date(params.row.createdat), + }, + + { + 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 ab92e86..78d5af7 100644 --- a/frontend/src/components/WebPageComponents/Footer.tsx +++ b/frontend/src/components/WebPageComponents/Footer.tsx @@ -20,7 +20,7 @@ export default function WebSiteFooter({ const style = FooterStyle.WITH_PAGES; - const design = FooterDesigns.DEFAULT_DESIGN; + const design = FooterDesigns.DESIGN_DIVERSITY; return (
state.style.websiteHeder); const borders = useAppSelector((state) => state.style.borders); - const style = HeaderStyle.PAGES_RIGHT; + const style = HeaderStyle.PAGES_LEFT; const design = HeaderDesigns.DESIGN_DIVERSITY; return ( diff --git a/frontend/src/menuAside.ts b/frontend/src/menuAside.ts index 91ea3a5..8e0f186 100644 --- a/frontend/src/menuAside.ts +++ b/frontend/src/menuAside.ts @@ -88,6 +88,14 @@ 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', diff --git a/frontend/src/pages/courses/courses-view.tsx b/frontend/src/pages/courses/courses-view.tsx index 82072b2..e2394b3 100644 --- a/frontend/src/pages/courses/courses-view.tsx +++ b/frontend/src/pages/courses/courses-view.tsx @@ -317,6 +317,43 @@ const CoursesView = () => { + <> +

Likes Course

+ +
+ + + + + + + + {courses.likes_course && + Array.isArray(courses.likes_course) && + courses.likes_course.map((item: any) => ( + + router.push(`/likes/likes-view/?id=${item.id}`) + } + > + + + ))} + +
Createdat
+ {dataFormatter.dateTimeFormatter(item.createdat)} +
+
+ {!courses?.likes_course?.length && ( +
No data
+ )} +
+ + { }; CoursesView.getLayout = function getLayout(page: ReactElement) { - return ( - - {page} - - ); + return {page}; }; export default CoursesView; diff --git a/frontend/src/pages/dashboard.tsx b/frontend/src/pages/dashboard.tsx index edc96cc..369f04a 100644 --- a/frontend/src/pages/dashboard.tsx +++ b/frontend/src/pages/dashboard.tsx @@ -10,11 +10,6 @@ import BaseIcon from '../components/BaseIcon'; import { getPageTitle } from '../config'; import Link from 'next/link'; -import { hasPermission } from '../helpers/userPermissions'; -import { fetchWidgets } from '../stores/roles/rolesSlice'; -import { WidgetCreator } from '../components/WidgetCreator/WidgetCreator'; -import { SmartWidget } from '../components/SmartWidget/SmartWidget'; - import { useAppDispatch, useAppSelector } from '../stores/hooks'; const Dashboard = () => { const dispatch = useAppDispatch(); @@ -32,15 +27,10 @@ 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: '' }, - }); - const { currentUser } = useAppSelector((state) => state.auth); const { isFetchingQuery } = useAppSelector((state) => state.openAi); - const { rolesWidgets, loading } = useAppSelector((state) => state.roles); - async function loadData() { const entities = [ 'users', @@ -52,6 +42,7 @@ const Dashboard = () => { 'students', 'roles', 'permissions', + 'likes', ]; const fns = [ setUsers, @@ -63,15 +54,11 @@ const Dashboard = () => { setStudents, setRoles, setPermissions, + setLikes, ]; const requests = entities.map((entity, index) => { - if (hasPermission(currentUser, `READ_${entity.toUpperCase()}`)) { - return axios.get(`/${entity.toLowerCase()}/count`); - } else { - fns[index](null); - return Promise.resolve({ data: { count: null } }); - } + return axios.get(`/${entity.toLowerCase()}/count`); }); Promise.allSettled(requests).then((results) => { @@ -85,24 +72,9 @@ const Dashboard = () => { }); } - async function getWidgets(roleId) { - await dispatch(fetchWidgets(roleId)); - } React.useEffect(() => { - if (!currentUser) return; loadData().then(); - setWidgetsRole({ - role: { - value: currentUser?.app_role?.id, - label: currentUser?.app_role?.name, - }, - }); - }, [currentUser]); - - React.useEffect(() => { - if (!currentUser || !widgetsRole?.role?.value) return; - getWidgets(widgetsRole?.role?.value || '').then(); - }, [widgetsRole?.role?.value]); + }, []); return ( <> @@ -118,346 +90,309 @@ const Dashboard = () => { {''} - {hasPermission(currentUser, 'CREATE_ROLES') && ( - - )} - {!!rolesWidgets.length && - hasPermission(currentUser, 'CREATE_ROLES') && ( -

- {`${widgetsRole?.role?.label || 'Users'}'s widgets`} -

- )} - -
- {(isFetchingQuery || loading) && ( -
- {' '} - Loading widgets... -
- )} - - {rolesWidgets && - rolesWidgets.map((widget) => ( - - ))} -
- - {!!rolesWidgets.length &&
} -
- {hasPermission(currentUser, 'READ_USERS') && ( - -
-
-
-
- Users -
-
- {users} -
+ +
+
+
+
+ Users
-
- +
+ {users}
+
+ +
- - )} +
+ - {hasPermission(currentUser, 'READ_ANALYTICS') && ( - -
-
-
-
- Analytics -
-
- {analytics} -
+ +
+
+
+
+ Analytics
-
- +
+ {analytics}
+
+ +
- - )} +
+ - {hasPermission(currentUser, 'READ_COURSES') && ( - -
-
-
-
- Courses -
-
- {courses} -
+ +
+
+
+
+ Courses
-
- +
+ {courses}
+
+ +
- - )} +
+ - {hasPermission(currentUser, 'READ_DISCUSSION_BOARDS') && ( - -
-
-
-
- Discussion boards -
-
- {discussion_boards} -
+ +
+
+
+
+ Discussion boards
-
- +
+ {discussion_boards}
+
+ +
- - )} +
+ - {hasPermission(currentUser, 'READ_ENROLLMENTS') && ( - -
-
-
-
- Enrollments -
-
- {enrollments} -
+ +
+
+
+
+ Enrollments
-
- +
+ {enrollments}
+
+ +
- - )} +
+ - {hasPermission(currentUser, 'READ_INSTRUCTORS') && ( - -
-
-
-
- Instructors -
-
- {instructors} -
+ +
+
+
+
+ Instructors
-
- +
+ {instructors}
+
+ +
- - )} +
+ - {hasPermission(currentUser, 'READ_STUDENTS') && ( - -
-
-
-
- Students -
-
- {students} -
+ +
+
+
+
+ Students
-
- +
+ {students}
+
+ +
- - )} +
+ - {hasPermission(currentUser, 'READ_ROLES') && ( - -
-
-
-
- Roles -
-
- {roles} -
+ +
+
+
+
+ Roles
-
- +
+ {roles}
+
+ +
- - )} +
+ - {hasPermission(currentUser, 'READ_PERMISSIONS') && ( - -
-
-
-
- Permissions -
-
- {permissions} -
+ +
+
+
+
+ Permissions
-
- +
+ {permissions}
+
+ +
- - )} +
+ + + +
+
+
+
+ Likes +
+
+ {likes} +
+
+
+ +
+
+
+
diff --git a/frontend/src/pages/index.tsx b/frontend/src/pages/index.tsx index 6ccb223..dd7584e 100644 --- a/frontend/src/pages/index.tsx +++ b/frontend/src/pages/index.tsx @@ -21,7 +21,7 @@ import TestimonialsSection from '../components/WebPageComponents/TestimonialsCom export default function WebSite() { const cardsStyle = useAppSelector((state) => state.style.cardsStyle); const bgColor = useAppSelector((state) => state.style.bgLayoutColor); - const projectName = 'test4'; + const projectName = 'test3'; useEffect(() => { const darkElement = document.querySelector('body .dark'); @@ -137,10 +137,10 @@ export default function WebSite() { content={`Discover the range of services offered by ${projectName} to enhance your online education experience. From course management to analytics, we provide comprehensive solutions.`} /> - +
- +
); } diff --git a/frontend/src/pages/likes/[likesId].tsx b/frontend/src/pages/likes/[likesId].tsx new file mode 100644 index 0000000..7ade844 --- /dev/null +++ b/frontend/src/pages/likes/[likesId].tsx @@ -0,0 +1,161 @@ +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 = { + course: null, + + user: null, + + createdat: new Date(), + }; + 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)} + > +
+ + + + + + + + + + + setInitialValues({ ...initialValues, createdat: date }) + } + /> + + + + + + + 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..68ba426 --- /dev/null +++ b/frontend/src/pages/likes/likes-edit.tsx @@ -0,0 +1,159 @@ +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 = { + course: null, + + user: null, + + createdat: new Date(), + }; + 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)} + > +
+ + + + + + + + + + + setInitialValues({ ...initialValues, createdat: date }) + } + /> + + + + + + + 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..2483f25 --- /dev/null +++ b/frontend/src/pages/likes/likes-list.tsx @@ -0,0 +1,153 @@ +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'; + +const LikesTablesPage = () => { + const [filterItems, setFilterItems] = useState([]); + const [csvFile, setCsvFile] = useState(null); + const [isModalActive, setIsModalActive] = useState(false); + const [showTableView, setShowTableView] = useState(false); + + const dispatch = useAppDispatch(); + + const [filters] = useState([ + { label: 'Createdat', title: 'createdat', date: 'true' }, + + { label: 'Course', title: 'course' }, + + { label: 'User', title: 'user' }, + ]); + + 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')} + + + + {''} + + + + + + + + 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..4db3e26 --- /dev/null +++ b/frontend/src/pages/likes/likes-new.tsx @@ -0,0 +1,122 @@ +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 = { + course: '', + + user: '', + + createdat: '', +}; + +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..6b8b526 --- /dev/null +++ b/frontend/src/pages/likes/likes-table.tsx @@ -0,0 +1,152 @@ +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'; + +const LikesTablesPage = () => { + const [filterItems, setFilterItems] = useState([]); + const [csvFile, setCsvFile] = useState(null); + const [isModalActive, setIsModalActive] = useState(false); + const [showTableView, setShowTableView] = useState(false); + + const dispatch = useAppDispatch(); + + const [filters] = useState([ + { label: 'Createdat', title: 'createdat', date: 'true' }, + + { label: 'Course', title: 'course' }, + + { label: 'User', title: 'user' }, + ]); + + 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')} + + + + {''} + + + + + + + + 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..804d72e --- /dev/null +++ b/frontend/src/pages/likes/likes-view.tsx @@ -0,0 +1,105 @@ +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')} + + + + + + +
+

Course

+ +

{likes?.course?.title ?? 'No data'}

+
+ +
+

User

+ +

{likes?.user?.firstName ?? 'No data'}

+
+ + + {likes.createdat ? ( + + ) : ( +

No Createdat

+ )} +
+ + + + router.push('/likes/likes-list')} + /> +
+
+ + ); +}; + +LikesView.getLayout = function getLayout(page: ReactElement) { + return {page}; +}; + +export default LikesView; diff --git a/frontend/src/pages/users/users-view.tsx b/frontend/src/pages/users/users-view.tsx index ce0851d..2b542eb 100644 --- a/frontend/src/pages/users/users-view.tsx +++ b/frontend/src/pages/users/users-view.tsx @@ -138,6 +138,43 @@ const UsersView = () => { + <> +

Likes User

+ +
+ + + + + + + + {users.likes_user && + Array.isArray(users.likes_user) && + users.likes_user.map((item: any) => ( + + router.push(`/likes/likes-view/?id=${item.id}`) + } + > + + + ))} + +
Createdat
+ {dataFormatter.dateTimeFormatter(item.createdat)} +
+
+ {!users?.likes_user?.length && ( +
No data
+ )} +
+ + { }; UsersView.getLayout = function getLayout(page: ReactElement) { - return ( - {page} - ); + return {page}; }; export default UsersView; diff --git a/frontend/src/pages/web_pages/about.tsx b/frontend/src/pages/web_pages/about.tsx index 13282dd..38d7d65 100644 --- a/frontend/src/pages/web_pages/about.tsx +++ b/frontend/src/pages/web_pages/about.tsx @@ -24,7 +24,7 @@ import TestimonialsSection from '../../components/WebPageComponents/Testimonials export default function WebSite() { const cardsStyle = useAppSelector((state) => state.style.cardsStyle); const bgColor = useAppSelector((state) => state.style.bgLayoutColor); - const projectName = 'test4'; + const projectName = 'test3'; useEffect(() => { const darkElement = document.querySelector('body .dark'); @@ -122,10 +122,10 @@ export default function WebSite() { content={`Learn more about ${projectName}, our mission, values, and the innovative features that make us a leader in online education.`} /> - +
- +
); } diff --git a/frontend/src/pages/web_pages/home.tsx b/frontend/src/pages/web_pages/home.tsx index e5458e1..f24c0a1 100644 --- a/frontend/src/pages/web_pages/home.tsx +++ b/frontend/src/pages/web_pages/home.tsx @@ -27,7 +27,7 @@ import TestimonialsSection from '../../components/WebPageComponents/Testimonials export default function WebSite() { const cardsStyle = useAppSelector((state) => state.style.cardsStyle); const bgColor = useAppSelector((state) => state.style.bgLayoutColor); - const projectName = 'test4'; + const projectName = 'test3'; useEffect(() => { const darkElement = document.querySelector('body .dark'); @@ -125,10 +125,10 @@ export default function WebSite() { content={`Explore our all-in-one platform for managing courses, students, and instructors. Enhance your online education experience with our robust tools and features.`} /> - +
- +
); } diff --git a/frontend/src/pages/web_pages/services.tsx b/frontend/src/pages/web_pages/services.tsx index 50b8422..5f1a74c 100644 --- a/frontend/src/pages/web_pages/services.tsx +++ b/frontend/src/pages/web_pages/services.tsx @@ -21,7 +21,7 @@ import TestimonialsSection from '../../components/WebPageComponents/Testimonials export default function WebSite() { const cardsStyle = useAppSelector((state) => state.style.cardsStyle); const bgColor = useAppSelector((state) => state.style.bgLayoutColor); - const projectName = 'test4'; + const projectName = 'test3'; useEffect(() => { const darkElement = document.querySelector('body .dark'); @@ -137,10 +137,10 @@ export default function WebSite() { content={`Discover the range of services offered by ${projectName} to enhance your online education experience. From course management to analytics, we provide comprehensive solutions.`} /> - +
- +
); } 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, }, });