some cool changes!
This commit is contained in:
parent
d2941d795a
commit
99840b4b67
316
backend/src/db/api/feedback.js
Normal file
316
backend/src/db/api/feedback.js
Normal file
@ -0,0 +1,316 @@
|
||||
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 FeedbackDBApi {
|
||||
static async create(data, options) {
|
||||
const currentUser = (options && options.currentUser) || { id: null };
|
||||
const transaction = (options && options.transaction) || undefined;
|
||||
|
||||
const feedback = await db.feedback.create(
|
||||
{
|
||||
id: data.id || undefined,
|
||||
|
||||
title: data.title || null,
|
||||
description: data.description || null,
|
||||
importHash: data.importHash || null,
|
||||
createdById: currentUser.id,
|
||||
updatedById: currentUser.id,
|
||||
},
|
||||
{ transaction },
|
||||
);
|
||||
|
||||
await feedback.setOrganizations(data.organizations || null, {
|
||||
transaction,
|
||||
});
|
||||
|
||||
return feedback;
|
||||
}
|
||||
|
||||
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 feedbackData = data.map((item, index) => ({
|
||||
id: item.id || undefined,
|
||||
|
||||
title: item.title || null,
|
||||
description: item.description || null,
|
||||
importHash: item.importHash || null,
|
||||
createdById: currentUser.id,
|
||||
updatedById: currentUser.id,
|
||||
createdAt: new Date(Date.now() + index * 1000),
|
||||
}));
|
||||
|
||||
// Bulk create items
|
||||
const feedback = await db.feedback.bulkCreate(feedbackData, {
|
||||
transaction,
|
||||
});
|
||||
|
||||
// For each item created, replace relation files
|
||||
|
||||
return feedback;
|
||||
}
|
||||
|
||||
static async update(id, data, options) {
|
||||
const currentUser = (options && options.currentUser) || { id: null };
|
||||
const transaction = (options && options.transaction) || undefined;
|
||||
const globalAccess = currentUser.app_role?.globalAccess;
|
||||
|
||||
const feedback = await db.feedback.findByPk(id, {}, { transaction });
|
||||
|
||||
const updatePayload = {};
|
||||
|
||||
if (data.title !== undefined) updatePayload.title = data.title;
|
||||
|
||||
if (data.description !== undefined)
|
||||
updatePayload.description = data.description;
|
||||
|
||||
updatePayload.updatedById = currentUser.id;
|
||||
|
||||
await feedback.update(updatePayload, { transaction });
|
||||
|
||||
if (data.organizations !== undefined) {
|
||||
await feedback.setOrganizations(
|
||||
data.organizations,
|
||||
|
||||
{ transaction },
|
||||
);
|
||||
}
|
||||
|
||||
return feedback;
|
||||
}
|
||||
|
||||
static async deleteByIds(ids, options) {
|
||||
const currentUser = (options && options.currentUser) || { id: null };
|
||||
const transaction = (options && options.transaction) || undefined;
|
||||
|
||||
const feedback = await db.feedback.findAll({
|
||||
where: {
|
||||
id: {
|
||||
[Op.in]: ids,
|
||||
},
|
||||
},
|
||||
transaction,
|
||||
});
|
||||
|
||||
await db.sequelize.transaction(async (transaction) => {
|
||||
for (const record of feedback) {
|
||||
await record.update({ deletedBy: currentUser.id }, { transaction });
|
||||
}
|
||||
for (const record of feedback) {
|
||||
await record.destroy({ transaction });
|
||||
}
|
||||
});
|
||||
|
||||
return feedback;
|
||||
}
|
||||
|
||||
static async remove(id, options) {
|
||||
const currentUser = (options && options.currentUser) || { id: null };
|
||||
const transaction = (options && options.transaction) || undefined;
|
||||
|
||||
const feedback = await db.feedback.findByPk(id, options);
|
||||
|
||||
await feedback.update(
|
||||
{
|
||||
deletedBy: currentUser.id,
|
||||
},
|
||||
{
|
||||
transaction,
|
||||
},
|
||||
);
|
||||
|
||||
await feedback.destroy({
|
||||
transaction,
|
||||
});
|
||||
|
||||
return feedback;
|
||||
}
|
||||
|
||||
static async findBy(where, options) {
|
||||
const transaction = (options && options.transaction) || undefined;
|
||||
|
||||
const feedback = await db.feedback.findOne({ where }, { transaction });
|
||||
|
||||
if (!feedback) {
|
||||
return feedback;
|
||||
}
|
||||
|
||||
const output = feedback.get({ plain: true });
|
||||
|
||||
output.organizations = await feedback.getOrganizations({
|
||||
transaction,
|
||||
});
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
static async findAll(filter, globalAccess, options) {
|
||||
const limit = filter.limit || 0;
|
||||
let offset = 0;
|
||||
let where = {};
|
||||
const currentPage = +filter.page;
|
||||
|
||||
const user = (options && options.currentUser) || null;
|
||||
const userOrganizations = (user && user.organizations?.id) || null;
|
||||
|
||||
if (userOrganizations) {
|
||||
if (options?.currentUser?.organizationsId) {
|
||||
where.organizationsId = options.currentUser.organizationsId;
|
||||
}
|
||||
}
|
||||
|
||||
offset = currentPage * limit;
|
||||
|
||||
const orderBy = null;
|
||||
|
||||
const transaction = (options && options.transaction) || undefined;
|
||||
|
||||
let include = [
|
||||
{
|
||||
model: db.organizations,
|
||||
as: 'organizations',
|
||||
},
|
||||
];
|
||||
|
||||
if (filter) {
|
||||
if (filter.id) {
|
||||
where = {
|
||||
...where,
|
||||
['id']: Utils.uuid(filter.id),
|
||||
};
|
||||
}
|
||||
|
||||
if (filter.title) {
|
||||
where = {
|
||||
...where,
|
||||
[Op.and]: Utils.ilike('feedback', 'title', filter.title),
|
||||
};
|
||||
}
|
||||
|
||||
if (filter.description) {
|
||||
where = {
|
||||
...where,
|
||||
[Op.and]: Utils.ilike('feedback', 'description', filter.description),
|
||||
};
|
||||
}
|
||||
|
||||
if (filter.active !== undefined) {
|
||||
where = {
|
||||
...where,
|
||||
active: filter.active === true || filter.active === 'true',
|
||||
};
|
||||
}
|
||||
|
||||
if (filter.organizations) {
|
||||
const listItems = filter.organizations.split('|').map((item) => {
|
||||
return Utils.uuid(item);
|
||||
});
|
||||
|
||||
where = {
|
||||
...where,
|
||||
organizationsId: { [Op.or]: listItems },
|
||||
};
|
||||
}
|
||||
|
||||
if (filter.createdAtRange) {
|
||||
const [start, end] = filter.createdAtRange;
|
||||
|
||||
if (start !== undefined && start !== null && start !== '') {
|
||||
where = {
|
||||
...where,
|
||||
['createdAt']: {
|
||||
...where.createdAt,
|
||||
[Op.gte]: start,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (end !== undefined && end !== null && end !== '') {
|
||||
where = {
|
||||
...where,
|
||||
['createdAt']: {
|
||||
...where.createdAt,
|
||||
[Op.lte]: end,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (globalAccess) {
|
||||
delete where.organizationsId;
|
||||
}
|
||||
|
||||
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.feedback.findAndCountAll(queryOptions);
|
||||
|
||||
return {
|
||||
rows: options?.countOnly ? [] : rows,
|
||||
count: count,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error executing query:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
static async findAllAutocomplete(
|
||||
query,
|
||||
limit,
|
||||
offset,
|
||||
globalAccess,
|
||||
organizationId,
|
||||
) {
|
||||
let where = {};
|
||||
|
||||
if (!globalAccess && organizationId) {
|
||||
where.organizationId = organizationId;
|
||||
}
|
||||
|
||||
if (query) {
|
||||
where = {
|
||||
[Op.or]: [
|
||||
{ ['id']: Utils.uuid(query) },
|
||||
Utils.ilike('feedback', 'description', query),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
const records = await db.feedback.findAll({
|
||||
attributes: ['id', 'description'],
|
||||
where,
|
||||
limit: limit ? Number(limit) : undefined,
|
||||
offset: offset ? Number(offset) : undefined,
|
||||
orderBy: [['description', 'ASC']],
|
||||
});
|
||||
|
||||
return records.map((record) => ({
|
||||
id: record.id,
|
||||
label: record.description,
|
||||
}));
|
||||
}
|
||||
};
|
||||
@ -156,6 +156,11 @@ module.exports = class OrganizationsDBApi {
|
||||
transaction,
|
||||
});
|
||||
|
||||
output.feedback_organizations =
|
||||
await organizations.getFeedback_organizations({
|
||||
transaction,
|
||||
});
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
|
||||
90
backend/src/db/migrations/1744037125930.js
Normal file
90
backend/src/db/migrations/1744037125930.js
Normal file
@ -0,0 +1,90 @@
|
||||
module.exports = {
|
||||
/**
|
||||
* @param {QueryInterface} queryInterface
|
||||
* @param {Sequelize} Sequelize
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async up(queryInterface, Sequelize) {
|
||||
/**
|
||||
* @type {Transaction}
|
||||
*/
|
||||
const transaction = await queryInterface.sequelize.transaction();
|
||||
try {
|
||||
await queryInterface.createTable(
|
||||
'feedback',
|
||||
{
|
||||
id: {
|
||||
type: Sequelize.DataTypes.UUID,
|
||||
defaultValue: Sequelize.DataTypes.UUIDV4,
|
||||
primaryKey: true,
|
||||
},
|
||||
createdById: {
|
||||
type: Sequelize.DataTypes.UUID,
|
||||
references: {
|
||||
key: 'id',
|
||||
model: 'users',
|
||||
},
|
||||
},
|
||||
updatedById: {
|
||||
type: Sequelize.DataTypes.UUID,
|
||||
references: {
|
||||
key: 'id',
|
||||
model: 'users',
|
||||
},
|
||||
},
|
||||
createdAt: { type: Sequelize.DataTypes.DATE },
|
||||
updatedAt: { type: Sequelize.DataTypes.DATE },
|
||||
deletedAt: { type: Sequelize.DataTypes.DATE },
|
||||
importHash: {
|
||||
type: Sequelize.DataTypes.STRING(255),
|
||||
allowNull: true,
|
||||
unique: true,
|
||||
},
|
||||
},
|
||||
{ transaction },
|
||||
);
|
||||
|
||||
await queryInterface.addColumn(
|
||||
'feedback',
|
||||
'organizationsId',
|
||||
{
|
||||
type: Sequelize.DataTypes.UUID,
|
||||
|
||||
references: {
|
||||
model: 'organizations',
|
||||
key: 'id',
|
||||
},
|
||||
},
|
||||
{ transaction },
|
||||
);
|
||||
|
||||
await transaction.commit();
|
||||
} catch (err) {
|
||||
await transaction.rollback();
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @param {QueryInterface} queryInterface
|
||||
* @param {Sequelize} Sequelize
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async down(queryInterface, Sequelize) {
|
||||
/**
|
||||
* @type {Transaction}
|
||||
*/
|
||||
const transaction = await queryInterface.sequelize.transaction();
|
||||
try {
|
||||
await queryInterface.removeColumn('feedback', 'organizationsId', {
|
||||
transaction,
|
||||
});
|
||||
|
||||
await queryInterface.dropTable('feedback', { transaction });
|
||||
|
||||
await transaction.commit();
|
||||
} catch (err) {
|
||||
await transaction.rollback();
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
};
|
||||
47
backend/src/db/migrations/1744037151790.js
Normal file
47
backend/src/db/migrations/1744037151790.js
Normal file
@ -0,0 +1,47 @@
|
||||
module.exports = {
|
||||
/**
|
||||
* @param {QueryInterface} queryInterface
|
||||
* @param {Sequelize} Sequelize
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async up(queryInterface, Sequelize) {
|
||||
/**
|
||||
* @type {Transaction}
|
||||
*/
|
||||
const transaction = await queryInterface.sequelize.transaction();
|
||||
try {
|
||||
await queryInterface.addColumn(
|
||||
'feedback',
|
||||
'title',
|
||||
{
|
||||
type: Sequelize.DataTypes.TEXT,
|
||||
},
|
||||
{ transaction },
|
||||
);
|
||||
|
||||
await transaction.commit();
|
||||
} catch (err) {
|
||||
await transaction.rollback();
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @param {QueryInterface} queryInterface
|
||||
* @param {Sequelize} Sequelize
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async down(queryInterface, Sequelize) {
|
||||
/**
|
||||
* @type {Transaction}
|
||||
*/
|
||||
const transaction = await queryInterface.sequelize.transaction();
|
||||
try {
|
||||
await queryInterface.removeColumn('feedback', 'title', { transaction });
|
||||
|
||||
await transaction.commit();
|
||||
} catch (err) {
|
||||
await transaction.rollback();
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
};
|
||||
49
backend/src/db/migrations/1744037176226.js
Normal file
49
backend/src/db/migrations/1744037176226.js
Normal file
@ -0,0 +1,49 @@
|
||||
module.exports = {
|
||||
/**
|
||||
* @param {QueryInterface} queryInterface
|
||||
* @param {Sequelize} Sequelize
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async up(queryInterface, Sequelize) {
|
||||
/**
|
||||
* @type {Transaction}
|
||||
*/
|
||||
const transaction = await queryInterface.sequelize.transaction();
|
||||
try {
|
||||
await queryInterface.addColumn(
|
||||
'feedback',
|
||||
'description',
|
||||
{
|
||||
type: Sequelize.DataTypes.TEXT,
|
||||
},
|
||||
{ transaction },
|
||||
);
|
||||
|
||||
await transaction.commit();
|
||||
} catch (err) {
|
||||
await transaction.rollback();
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @param {QueryInterface} queryInterface
|
||||
* @param {Sequelize} Sequelize
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async down(queryInterface, Sequelize) {
|
||||
/**
|
||||
* @type {Transaction}
|
||||
*/
|
||||
const transaction = await queryInterface.sequelize.transaction();
|
||||
try {
|
||||
await queryInterface.removeColumn('feedback', 'description', {
|
||||
transaction,
|
||||
});
|
||||
|
||||
await transaction.commit();
|
||||
} catch (err) {
|
||||
await transaction.rollback();
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
};
|
||||
61
backend/src/db/models/feedback.js
Normal file
61
backend/src/db/models/feedback.js
Normal file
@ -0,0 +1,61 @@
|
||||
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 feedback = sequelize.define(
|
||||
'feedback',
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true,
|
||||
},
|
||||
|
||||
title: {
|
||||
type: DataTypes.TEXT,
|
||||
},
|
||||
|
||||
description: {
|
||||
type: DataTypes.TEXT,
|
||||
},
|
||||
|
||||
importHash: {
|
||||
type: DataTypes.STRING(255),
|
||||
allowNull: true,
|
||||
unique: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
paranoid: true,
|
||||
freezeTableName: true,
|
||||
},
|
||||
);
|
||||
|
||||
feedback.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.feedback.belongsTo(db.organizations, {
|
||||
as: 'organizations',
|
||||
foreignKey: {
|
||||
name: 'organizationsId',
|
||||
},
|
||||
constraints: false,
|
||||
});
|
||||
|
||||
db.feedback.belongsTo(db.users, {
|
||||
as: 'createdBy',
|
||||
});
|
||||
|
||||
db.feedback.belongsTo(db.users, {
|
||||
as: 'updatedBy',
|
||||
});
|
||||
};
|
||||
|
||||
return feedback;
|
||||
};
|
||||
@ -74,6 +74,14 @@ module.exports = function (sequelize, DataTypes) {
|
||||
constraints: false,
|
||||
});
|
||||
|
||||
db.organizations.hasMany(db.feedback, {
|
||||
as: 'feedback_organizations',
|
||||
foreignKey: {
|
||||
name: 'organizationsId',
|
||||
},
|
||||
constraints: false,
|
||||
});
|
||||
|
||||
//end loop
|
||||
|
||||
db.organizations.belongsTo(db.users, {
|
||||
|
||||
@ -84,6 +84,7 @@ module.exports = {
|
||||
'permissions',
|
||||
'organizations',
|
||||
'tags',
|
||||
'feedback',
|
||||
,
|
||||
];
|
||||
await queryInterface.bulkInsert(
|
||||
@ -249,6 +250,31 @@ primary key ("roles_permissionsId", "permissionId")
|
||||
permissionId: getId('DELETE_TAGS'),
|
||||
},
|
||||
|
||||
{
|
||||
createdAt,
|
||||
updatedAt,
|
||||
roles_permissionsId: getId('User'),
|
||||
permissionId: getId('CREATE_FEEDBACK'),
|
||||
},
|
||||
{
|
||||
createdAt,
|
||||
updatedAt,
|
||||
roles_permissionsId: getId('User'),
|
||||
permissionId: getId('READ_FEEDBACK'),
|
||||
},
|
||||
{
|
||||
createdAt,
|
||||
updatedAt,
|
||||
roles_permissionsId: getId('User'),
|
||||
permissionId: getId('UPDATE_FEEDBACK'),
|
||||
},
|
||||
{
|
||||
createdAt,
|
||||
updatedAt,
|
||||
roles_permissionsId: getId('User'),
|
||||
permissionId: getId('DELETE_FEEDBACK'),
|
||||
},
|
||||
|
||||
{
|
||||
createdAt,
|
||||
updatedAt,
|
||||
@ -381,6 +407,31 @@ primary key ("roles_permissionsId", "permissionId")
|
||||
permissionId: getId('DELETE_TAGS'),
|
||||
},
|
||||
|
||||
{
|
||||
createdAt,
|
||||
updatedAt,
|
||||
roles_permissionsId: getId('Administrator'),
|
||||
permissionId: getId('CREATE_FEEDBACK'),
|
||||
},
|
||||
{
|
||||
createdAt,
|
||||
updatedAt,
|
||||
roles_permissionsId: getId('Administrator'),
|
||||
permissionId: getId('READ_FEEDBACK'),
|
||||
},
|
||||
{
|
||||
createdAt,
|
||||
updatedAt,
|
||||
roles_permissionsId: getId('Administrator'),
|
||||
permissionId: getId('UPDATE_FEEDBACK'),
|
||||
},
|
||||
{
|
||||
createdAt,
|
||||
updatedAt,
|
||||
roles_permissionsId: getId('Administrator'),
|
||||
permissionId: getId('DELETE_FEEDBACK'),
|
||||
},
|
||||
|
||||
{
|
||||
createdAt,
|
||||
updatedAt,
|
||||
@ -581,6 +632,31 @@ primary key ("roles_permissionsId", "permissionId")
|
||||
permissionId: getId('DELETE_TAGS'),
|
||||
},
|
||||
|
||||
{
|
||||
createdAt,
|
||||
updatedAt,
|
||||
roles_permissionsId: getId('SuperAdmin'),
|
||||
permissionId: getId('CREATE_FEEDBACK'),
|
||||
},
|
||||
{
|
||||
createdAt,
|
||||
updatedAt,
|
||||
roles_permissionsId: getId('SuperAdmin'),
|
||||
permissionId: getId('READ_FEEDBACK'),
|
||||
},
|
||||
{
|
||||
createdAt,
|
||||
updatedAt,
|
||||
roles_permissionsId: getId('SuperAdmin'),
|
||||
permissionId: getId('UPDATE_FEEDBACK'),
|
||||
},
|
||||
{
|
||||
createdAt,
|
||||
updatedAt,
|
||||
roles_permissionsId: getId('SuperAdmin'),
|
||||
permissionId: getId('DELETE_FEEDBACK'),
|
||||
},
|
||||
|
||||
{
|
||||
createdAt,
|
||||
updatedAt,
|
||||
|
||||
@ -11,9 +11,11 @@ const Organizations = db.organizations;
|
||||
|
||||
const Tags = db.tags;
|
||||
|
||||
const Feedback = db.feedback;
|
||||
|
||||
const CategoriesData = [
|
||||
{
|
||||
name: 'Jonas Salk',
|
||||
name: 'William Herschel',
|
||||
|
||||
// type code here for "relation_many" field
|
||||
|
||||
@ -21,7 +23,7 @@ const CategoriesData = [
|
||||
},
|
||||
|
||||
{
|
||||
name: 'Francis Crick',
|
||||
name: 'Frederick Gowland Hopkins',
|
||||
|
||||
// type code here for "relation_many" field
|
||||
|
||||
@ -29,23 +31,7 @@ const CategoriesData = [
|
||||
},
|
||||
|
||||
{
|
||||
name: 'Pierre Simon de Laplace',
|
||||
|
||||
// type code here for "relation_many" field
|
||||
|
||||
// type code here for "relation_one" field
|
||||
},
|
||||
|
||||
{
|
||||
name: 'Andreas Vesalius',
|
||||
|
||||
// type code here for "relation_many" field
|
||||
|
||||
// type code here for "relation_one" field
|
||||
},
|
||||
|
||||
{
|
||||
name: 'Justus Liebig',
|
||||
name: 'Archimedes',
|
||||
|
||||
// type code here for "relation_many" field
|
||||
|
||||
@ -55,10 +41,10 @@ const CategoriesData = [
|
||||
|
||||
const ProjectsData = [
|
||||
{
|
||||
name: 'Archimedes',
|
||||
name: 'Werner Heisenberg',
|
||||
|
||||
description:
|
||||
'Like fire across the galaxy the Clone Wars spread. In league with the wicked Count Dooku, more and more planets slip. Against this threat, upon the Jedi Knights falls the duty to lead the newly formed army of the Republic. And as the heat of war grows, so, to, grows the prowess of one most gifted student of the Force.',
|
||||
'Size matters not. Look at me. Judge me by my size, do you? Hmm? Hmm. And well you should not. For my ally is the Force, and a powerful ally it is. Life creates it, makes it grow. Its energy surrounds us and binds us. Luminous beings are we, not this crude matter. You must feel the Force around you; here, between you, me, the tree, the rock, everywhere, yes. Even between the land and the ship.',
|
||||
|
||||
// type code here for "relation_many" field
|
||||
|
||||
@ -68,10 +54,10 @@ const ProjectsData = [
|
||||
},
|
||||
|
||||
{
|
||||
name: 'Claude Levi-Strauss',
|
||||
name: 'Karl Landsteiner',
|
||||
|
||||
description:
|
||||
'Strong is Vader. Mind what you have learned. Save you it can.',
|
||||
'Do not assume anything Obi-Wan. Clear your mind must be if you are to discover the real villains behind this plot.',
|
||||
|
||||
// type code here for "relation_many" field
|
||||
|
||||
@ -81,35 +67,9 @@ const ProjectsData = [
|
||||
},
|
||||
|
||||
{
|
||||
name: 'Francis Galton',
|
||||
name: 'James Watson',
|
||||
|
||||
description:
|
||||
'Soon will I rest, yes, forever sleep. Earned it I have. Twilight is upon me, soon night must fall.',
|
||||
|
||||
// type code here for "relation_many" field
|
||||
|
||||
// type code here for "relation_many" field
|
||||
|
||||
// type code here for "relation_one" field
|
||||
},
|
||||
|
||||
{
|
||||
name: 'Charles Sherrington',
|
||||
|
||||
description: 'To answer power with power, the Jedi way this is',
|
||||
|
||||
// type code here for "relation_many" field
|
||||
|
||||
// type code here for "relation_many" field
|
||||
|
||||
// type code here for "relation_one" field
|
||||
},
|
||||
|
||||
{
|
||||
name: 'Ludwig Boltzmann',
|
||||
|
||||
description:
|
||||
'Death is a natural part of life. Rejoice for those around you who transform into the Force. Mourn them do not. Miss them do not. Attachment leads to jealously. The shadow of greed, that is.',
|
||||
description: 'Luminous beings are we - not this crude matter.',
|
||||
|
||||
// type code here for "relation_many" field
|
||||
|
||||
@ -121,10 +81,9 @@ const ProjectsData = [
|
||||
|
||||
const TasksData = [
|
||||
{
|
||||
title: 'No one tells me shit',
|
||||
title: 'I got that scurvy',
|
||||
|
||||
description:
|
||||
'Through the Force, things you will see. Other places. The future - the past. Old friends long gone.',
|
||||
description: 'Feel the force!',
|
||||
|
||||
status: 'ToDo',
|
||||
|
||||
@ -134,9 +93,9 @@ const TasksData = [
|
||||
|
||||
// type code here for "relation_one" field
|
||||
|
||||
start_date: new Date('2024-08-22'),
|
||||
start_date: new Date('2024-12-05'),
|
||||
|
||||
end_date: new Date('2024-04-20'),
|
||||
end_date: new Date('2024-12-01'),
|
||||
|
||||
// type code here for "relation_one" field
|
||||
|
||||
@ -144,9 +103,9 @@ const TasksData = [
|
||||
},
|
||||
|
||||
{
|
||||
title: 'No one tells me shit',
|
||||
title: 'That damn gimble',
|
||||
|
||||
description: 'That is why you fail.',
|
||||
description: 'Adventure. Excitement. A Jedi craves not these things.',
|
||||
|
||||
status: 'InProgress',
|
||||
|
||||
@ -156,9 +115,9 @@ const TasksData = [
|
||||
|
||||
// type code here for "relation_one" field
|
||||
|
||||
start_date: new Date('2024-07-25'),
|
||||
start_date: new Date('2024-06-22'),
|
||||
|
||||
end_date: new Date('2025-01-16'),
|
||||
end_date: new Date('2024-05-30'),
|
||||
|
||||
// type code here for "relation_one" field
|
||||
|
||||
@ -166,12 +125,11 @@ const TasksData = [
|
||||
},
|
||||
|
||||
{
|
||||
title: 'Let me tell ya',
|
||||
title: 'That damn diabetes',
|
||||
|
||||
description:
|
||||
'Much to learn you still have my old padawan. ... This is just the beginning!',
|
||||
description: 'Ow, ow, OW! On my ear you are!',
|
||||
|
||||
status: 'ToDo',
|
||||
status: 'InProgress',
|
||||
|
||||
// type code here for "relation_one" field
|
||||
|
||||
@ -179,53 +137,9 @@ const TasksData = [
|
||||
|
||||
// type code here for "relation_one" field
|
||||
|
||||
start_date: new Date('2024-10-08'),
|
||||
start_date: new Date('2024-06-13'),
|
||||
|
||||
end_date: new Date('2024-05-10'),
|
||||
|
||||
// type code here for "relation_one" field
|
||||
|
||||
// type code here for "relation_many" field
|
||||
},
|
||||
|
||||
{
|
||||
title: "How 'bout them Cowboys",
|
||||
|
||||
description: 'Luminous beings are we - not this crude matter.',
|
||||
|
||||
status: 'ToDo',
|
||||
|
||||
// type code here for "relation_one" field
|
||||
|
||||
// type code here for "relation_one" field
|
||||
|
||||
// type code here for "relation_one" field
|
||||
|
||||
start_date: new Date('2024-12-06'),
|
||||
|
||||
end_date: new Date('2025-03-25'),
|
||||
|
||||
// type code here for "relation_one" field
|
||||
|
||||
// type code here for "relation_many" field
|
||||
},
|
||||
|
||||
{
|
||||
title: 'I want my 5$ back',
|
||||
|
||||
description: 'Not if anything to say about it I have',
|
||||
|
||||
status: 'ToDo',
|
||||
|
||||
// type code here for "relation_one" field
|
||||
|
||||
// type code here for "relation_one" field
|
||||
|
||||
// type code here for "relation_one" field
|
||||
|
||||
start_date: new Date('2024-07-05'),
|
||||
|
||||
end_date: new Date('2024-06-13'),
|
||||
end_date: new Date('2024-10-17'),
|
||||
|
||||
// type code here for "relation_one" field
|
||||
|
||||
@ -235,23 +149,15 @@ const TasksData = [
|
||||
|
||||
const OrganizationsData = [
|
||||
{
|
||||
name: 'Pierre Simon de Laplace',
|
||||
name: 'Karl Landsteiner',
|
||||
},
|
||||
|
||||
{
|
||||
name: 'Isaac Newton',
|
||||
name: 'Thomas Hunt Morgan',
|
||||
},
|
||||
|
||||
{
|
||||
name: 'Enrico Fermi',
|
||||
},
|
||||
|
||||
{
|
||||
name: 'Isaac Newton',
|
||||
},
|
||||
|
||||
{
|
||||
name: 'Ernst Haeckel',
|
||||
name: 'Thomas Hunt Morgan',
|
||||
},
|
||||
];
|
||||
|
||||
@ -259,31 +165,45 @@ const TagsData = [
|
||||
{
|
||||
// type code here for "relation_one" field
|
||||
|
||||
name: 'Enrico Fermi',
|
||||
name: 'Neils Bohr',
|
||||
},
|
||||
|
||||
{
|
||||
// type code here for "relation_one" field
|
||||
|
||||
name: 'Francis Galton',
|
||||
name: 'Emil Fischer',
|
||||
},
|
||||
|
||||
{
|
||||
// type code here for "relation_one" field
|
||||
|
||||
name: 'Anton van Leeuwenhoek',
|
||||
name: 'Jean Piaget',
|
||||
},
|
||||
];
|
||||
|
||||
const FeedbackData = [
|
||||
{
|
||||
// type code here for "relation_one" field
|
||||
|
||||
title: 'That damn diabetes',
|
||||
|
||||
description: 'Let me tell ya',
|
||||
},
|
||||
|
||||
{
|
||||
// type code here for "relation_one" field
|
||||
|
||||
name: 'John von Neumann',
|
||||
title: 'Like a red-headed stepchild',
|
||||
|
||||
description: 'Yup',
|
||||
},
|
||||
|
||||
{
|
||||
// type code here for "relation_one" field
|
||||
|
||||
name: 'Alfred Binet',
|
||||
title: 'Always the last one to the party',
|
||||
|
||||
description: 'Come on now',
|
||||
},
|
||||
];
|
||||
|
||||
@ -322,28 +242,6 @@ async function associateUserWithOrganization() {
|
||||
if (User2?.setOrganization) {
|
||||
await User2.setOrganization(relatedOrganization2);
|
||||
}
|
||||
|
||||
const relatedOrganization3 = await Organizations.findOne({
|
||||
offset: Math.floor(Math.random() * (await Organizations.count())),
|
||||
});
|
||||
const User3 = await Users.findOne({
|
||||
order: [['id', 'ASC']],
|
||||
offset: 3,
|
||||
});
|
||||
if (User3?.setOrganization) {
|
||||
await User3.setOrganization(relatedOrganization3);
|
||||
}
|
||||
|
||||
const relatedOrganization4 = await Organizations.findOne({
|
||||
offset: Math.floor(Math.random() * (await Organizations.count())),
|
||||
});
|
||||
const User4 = await Users.findOne({
|
||||
order: [['id', 'ASC']],
|
||||
offset: 4,
|
||||
});
|
||||
if (User4?.setOrganization) {
|
||||
await User4.setOrganization(relatedOrganization4);
|
||||
}
|
||||
}
|
||||
|
||||
// Similar logic for "relation_many"
|
||||
@ -381,28 +279,6 @@ async function associateCategoryWithOrganization() {
|
||||
if (Category2?.setOrganization) {
|
||||
await Category2.setOrganization(relatedOrganization2);
|
||||
}
|
||||
|
||||
const relatedOrganization3 = await Organizations.findOne({
|
||||
offset: Math.floor(Math.random() * (await Organizations.count())),
|
||||
});
|
||||
const Category3 = await Categories.findOne({
|
||||
order: [['id', 'ASC']],
|
||||
offset: 3,
|
||||
});
|
||||
if (Category3?.setOrganization) {
|
||||
await Category3.setOrganization(relatedOrganization3);
|
||||
}
|
||||
|
||||
const relatedOrganization4 = await Organizations.findOne({
|
||||
offset: Math.floor(Math.random() * (await Organizations.count())),
|
||||
});
|
||||
const Category4 = await Categories.findOne({
|
||||
order: [['id', 'ASC']],
|
||||
offset: 4,
|
||||
});
|
||||
if (Category4?.setOrganization) {
|
||||
await Category4.setOrganization(relatedOrganization4);
|
||||
}
|
||||
}
|
||||
|
||||
// Similar logic for "relation_many"
|
||||
@ -442,28 +318,6 @@ async function associateProjectWithOrganization() {
|
||||
if (Project2?.setOrganization) {
|
||||
await Project2.setOrganization(relatedOrganization2);
|
||||
}
|
||||
|
||||
const relatedOrganization3 = await Organizations.findOne({
|
||||
offset: Math.floor(Math.random() * (await Organizations.count())),
|
||||
});
|
||||
const Project3 = await Projects.findOne({
|
||||
order: [['id', 'ASC']],
|
||||
offset: 3,
|
||||
});
|
||||
if (Project3?.setOrganization) {
|
||||
await Project3.setOrganization(relatedOrganization3);
|
||||
}
|
||||
|
||||
const relatedOrganization4 = await Organizations.findOne({
|
||||
offset: Math.floor(Math.random() * (await Organizations.count())),
|
||||
});
|
||||
const Project4 = await Projects.findOne({
|
||||
order: [['id', 'ASC']],
|
||||
offset: 4,
|
||||
});
|
||||
if (Project4?.setOrganization) {
|
||||
await Project4.setOrganization(relatedOrganization4);
|
||||
}
|
||||
}
|
||||
|
||||
async function associateTaskWithProject() {
|
||||
@ -499,28 +353,6 @@ async function associateTaskWithProject() {
|
||||
if (Task2?.setProject) {
|
||||
await Task2.setProject(relatedProject2);
|
||||
}
|
||||
|
||||
const relatedProject3 = await Projects.findOne({
|
||||
offset: Math.floor(Math.random() * (await Projects.count())),
|
||||
});
|
||||
const Task3 = await Tasks.findOne({
|
||||
order: [['id', 'ASC']],
|
||||
offset: 3,
|
||||
});
|
||||
if (Task3?.setProject) {
|
||||
await Task3.setProject(relatedProject3);
|
||||
}
|
||||
|
||||
const relatedProject4 = await Projects.findOne({
|
||||
offset: Math.floor(Math.random() * (await Projects.count())),
|
||||
});
|
||||
const Task4 = await Tasks.findOne({
|
||||
order: [['id', 'ASC']],
|
||||
offset: 4,
|
||||
});
|
||||
if (Task4?.setProject) {
|
||||
await Task4.setProject(relatedProject4);
|
||||
}
|
||||
}
|
||||
|
||||
async function associateTaskWithAssigned_to() {
|
||||
@ -556,28 +388,6 @@ async function associateTaskWithAssigned_to() {
|
||||
if (Task2?.setAssigned_to) {
|
||||
await Task2.setAssigned_to(relatedAssigned_to2);
|
||||
}
|
||||
|
||||
const relatedAssigned_to3 = await Users.findOne({
|
||||
offset: Math.floor(Math.random() * (await Users.count())),
|
||||
});
|
||||
const Task3 = await Tasks.findOne({
|
||||
order: [['id', 'ASC']],
|
||||
offset: 3,
|
||||
});
|
||||
if (Task3?.setAssigned_to) {
|
||||
await Task3.setAssigned_to(relatedAssigned_to3);
|
||||
}
|
||||
|
||||
const relatedAssigned_to4 = await Users.findOne({
|
||||
offset: Math.floor(Math.random() * (await Users.count())),
|
||||
});
|
||||
const Task4 = await Tasks.findOne({
|
||||
order: [['id', 'ASC']],
|
||||
offset: 4,
|
||||
});
|
||||
if (Task4?.setAssigned_to) {
|
||||
await Task4.setAssigned_to(relatedAssigned_to4);
|
||||
}
|
||||
}
|
||||
|
||||
async function associateTaskWithCategory() {
|
||||
@ -613,28 +423,6 @@ async function associateTaskWithCategory() {
|
||||
if (Task2?.setCategory) {
|
||||
await Task2.setCategory(relatedCategory2);
|
||||
}
|
||||
|
||||
const relatedCategory3 = await Categories.findOne({
|
||||
offset: Math.floor(Math.random() * (await Categories.count())),
|
||||
});
|
||||
const Task3 = await Tasks.findOne({
|
||||
order: [['id', 'ASC']],
|
||||
offset: 3,
|
||||
});
|
||||
if (Task3?.setCategory) {
|
||||
await Task3.setCategory(relatedCategory3);
|
||||
}
|
||||
|
||||
const relatedCategory4 = await Categories.findOne({
|
||||
offset: Math.floor(Math.random() * (await Categories.count())),
|
||||
});
|
||||
const Task4 = await Tasks.findOne({
|
||||
order: [['id', 'ASC']],
|
||||
offset: 4,
|
||||
});
|
||||
if (Task4?.setCategory) {
|
||||
await Task4.setCategory(relatedCategory4);
|
||||
}
|
||||
}
|
||||
|
||||
async function associateTaskWithOrganization() {
|
||||
@ -670,28 +458,6 @@ async function associateTaskWithOrganization() {
|
||||
if (Task2?.setOrganization) {
|
||||
await Task2.setOrganization(relatedOrganization2);
|
||||
}
|
||||
|
||||
const relatedOrganization3 = await Organizations.findOne({
|
||||
offset: Math.floor(Math.random() * (await Organizations.count())),
|
||||
});
|
||||
const Task3 = await Tasks.findOne({
|
||||
order: [['id', 'ASC']],
|
||||
offset: 3,
|
||||
});
|
||||
if (Task3?.setOrganization) {
|
||||
await Task3.setOrganization(relatedOrganization3);
|
||||
}
|
||||
|
||||
const relatedOrganization4 = await Organizations.findOne({
|
||||
offset: Math.floor(Math.random() * (await Organizations.count())),
|
||||
});
|
||||
const Task4 = await Tasks.findOne({
|
||||
order: [['id', 'ASC']],
|
||||
offset: 4,
|
||||
});
|
||||
if (Task4?.setOrganization) {
|
||||
await Task4.setOrganization(relatedOrganization4);
|
||||
}
|
||||
}
|
||||
|
||||
// Similar logic for "relation_many"
|
||||
@ -729,27 +495,40 @@ async function associateTagWithOrganization() {
|
||||
if (Tag2?.setOrganization) {
|
||||
await Tag2.setOrganization(relatedOrganization2);
|
||||
}
|
||||
}
|
||||
|
||||
const relatedOrganization3 = await Organizations.findOne({
|
||||
async function associateFeedbackWithOrganization() {
|
||||
const relatedOrganization0 = await Organizations.findOne({
|
||||
offset: Math.floor(Math.random() * (await Organizations.count())),
|
||||
});
|
||||
const Tag3 = await Tags.findOne({
|
||||
const Feedback0 = await Feedback.findOne({
|
||||
order: [['id', 'ASC']],
|
||||
offset: 3,
|
||||
offset: 0,
|
||||
});
|
||||
if (Tag3?.setOrganization) {
|
||||
await Tag3.setOrganization(relatedOrganization3);
|
||||
if (Feedback0?.setOrganization) {
|
||||
await Feedback0.setOrganization(relatedOrganization0);
|
||||
}
|
||||
|
||||
const relatedOrganization4 = await Organizations.findOne({
|
||||
const relatedOrganization1 = await Organizations.findOne({
|
||||
offset: Math.floor(Math.random() * (await Organizations.count())),
|
||||
});
|
||||
const Tag4 = await Tags.findOne({
|
||||
const Feedback1 = await Feedback.findOne({
|
||||
order: [['id', 'ASC']],
|
||||
offset: 4,
|
||||
offset: 1,
|
||||
});
|
||||
if (Tag4?.setOrganization) {
|
||||
await Tag4.setOrganization(relatedOrganization4);
|
||||
if (Feedback1?.setOrganization) {
|
||||
await Feedback1.setOrganization(relatedOrganization1);
|
||||
}
|
||||
|
||||
const relatedOrganization2 = await Organizations.findOne({
|
||||
offset: Math.floor(Math.random() * (await Organizations.count())),
|
||||
});
|
||||
const Feedback2 = await Feedback.findOne({
|
||||
order: [['id', 'ASC']],
|
||||
offset: 2,
|
||||
});
|
||||
if (Feedback2?.setOrganization) {
|
||||
await Feedback2.setOrganization(relatedOrganization2);
|
||||
}
|
||||
}
|
||||
|
||||
@ -765,6 +544,8 @@ module.exports = {
|
||||
|
||||
await Tags.bulkCreate(TagsData);
|
||||
|
||||
await Feedback.bulkCreate(FeedbackData);
|
||||
|
||||
await Promise.all([
|
||||
// Similar logic for "relation_many"
|
||||
|
||||
@ -791,6 +572,8 @@ module.exports = {
|
||||
// Similar logic for "relation_many"
|
||||
|
||||
await associateTagWithOrganization(),
|
||||
|
||||
await associateFeedbackWithOrganization(),
|
||||
]);
|
||||
},
|
||||
|
||||
@ -804,5 +587,7 @@ module.exports = {
|
||||
await queryInterface.bulkDelete('organizations', null, {});
|
||||
|
||||
await queryInterface.bulkDelete('tags', null, {});
|
||||
|
||||
await queryInterface.bulkDelete('feedback', null, {});
|
||||
},
|
||||
};
|
||||
|
||||
87
backend/src/db/seeders/20250407144525.js
Normal file
87
backend/src/db/seeders/20250407144525.js
Normal file
@ -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<void>}
|
||||
*/
|
||||
async up(queryInterface) {
|
||||
const createdAt = new Date();
|
||||
const updatedAt = new Date();
|
||||
|
||||
/** @type {Map<string, string>} */
|
||||
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 = ['feedback'];
|
||||
|
||||
const createdPermissions = entities.flatMap(createPermissions);
|
||||
|
||||
// Add permissions to database
|
||||
await queryInterface.bulkInsert('permissions', createdPermissions);
|
||||
// Get permissions ids
|
||||
const permissionsIds = createdPermissions.map((p) => p.id);
|
||||
// Get admin role
|
||||
const adminRole = await db.roles.findOne({
|
||||
where: { name: config.roles.super_admin },
|
||||
});
|
||||
|
||||
if (adminRole) {
|
||||
// Add permissions to admin role if it exists
|
||||
await adminRole.addPermissions(permissionsIds);
|
||||
}
|
||||
},
|
||||
down: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.bulkDelete(
|
||||
'permissions',
|
||||
entities.flatMap(createPermissions),
|
||||
);
|
||||
},
|
||||
};
|
||||
@ -35,6 +35,8 @@ const organizationsRoutes = require('./routes/organizations');
|
||||
|
||||
const tagsRoutes = require('./routes/tags');
|
||||
|
||||
const feedbackRoutes = require('./routes/feedback');
|
||||
|
||||
const getBaseUrl = (url) => {
|
||||
if (!url) return '';
|
||||
return url.endsWith('/api') ? url.slice(0, -4) : url;
|
||||
@ -148,6 +150,12 @@ app.use(
|
||||
tagsRoutes,
|
||||
);
|
||||
|
||||
app.use(
|
||||
'/api/feedback',
|
||||
passport.authenticate('jwt', { session: false }),
|
||||
feedbackRoutes,
|
||||
);
|
||||
|
||||
app.use(
|
||||
'/api/openai',
|
||||
passport.authenticate('jwt', { session: false }),
|
||||
|
||||
455
backend/src/routes/feedback.js
Normal file
455
backend/src/routes/feedback.js
Normal file
@ -0,0 +1,455 @@
|
||||
const express = require('express');
|
||||
|
||||
const FeedbackService = require('../services/feedback');
|
||||
const FeedbackDBApi = require('../db/api/feedback');
|
||||
const wrapAsync = require('../helpers').wrapAsync;
|
||||
|
||||
const config = require('../config');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
const { parse } = require('json2csv');
|
||||
|
||||
const { checkCrudPermissions } = require('../middlewares/check-permissions');
|
||||
|
||||
router.use(checkCrudPermissions('feedback'));
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* components:
|
||||
* schemas:
|
||||
* Feedback:
|
||||
* type: object
|
||||
* properties:
|
||||
|
||||
* title:
|
||||
* type: string
|
||||
* default: title
|
||||
* description:
|
||||
* type: string
|
||||
* default: description
|
||||
|
||||
*/
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* tags:
|
||||
* name: Feedback
|
||||
* description: The Feedback managing API
|
||||
*/
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/feedback:
|
||||
* post:
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* tags: [Feedback]
|
||||
* 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/Feedback"
|
||||
* responses:
|
||||
* 200:
|
||||
* description: The item was successfully added
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: "#/components/schemas/Feedback"
|
||||
* 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 FeedbackService.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: [Feedback]
|
||||
* 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/Feedback"
|
||||
* responses:
|
||||
* 200:
|
||||
* description: The items were successfully imported
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: "#/components/schemas/Feedback"
|
||||
* 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 FeedbackService.bulkImport(req, res, true, link.host);
|
||||
const payload = true;
|
||||
res.status(200).send(payload);
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/feedback/{id}:
|
||||
* put:
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* tags: [Feedback]
|
||||
* 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/Feedback"
|
||||
* required:
|
||||
* - id
|
||||
* responses:
|
||||
* 200:
|
||||
* description: The item data was successfully updated
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: "#/components/schemas/Feedback"
|
||||
* 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 FeedbackService.update(req.body.data, req.body.id, req.currentUser);
|
||||
const payload = true;
|
||||
res.status(200).send(payload);
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/feedback/{id}:
|
||||
* delete:
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* tags: [Feedback]
|
||||
* 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/Feedback"
|
||||
* 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 FeedbackService.remove(req.params.id, req.currentUser);
|
||||
const payload = true;
|
||||
res.status(200).send(payload);
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/feedback/deleteByIds:
|
||||
* post:
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* tags: [Feedback]
|
||||
* 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/Feedback"
|
||||
* 401:
|
||||
* $ref: "#/components/responses/UnauthorizedError"
|
||||
* 404:
|
||||
* description: Items not found
|
||||
* 500:
|
||||
* description: Some server error
|
||||
*/
|
||||
router.post(
|
||||
'/deleteByIds',
|
||||
wrapAsync(async (req, res) => {
|
||||
await FeedbackService.deleteByIds(req.body.data, req.currentUser);
|
||||
const payload = true;
|
||||
res.status(200).send(payload);
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/feedback:
|
||||
* get:
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* tags: [Feedback]
|
||||
* summary: Get all feedback
|
||||
* description: Get all feedback
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Feedback list successfully received
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: "#/components/schemas/Feedback"
|
||||
* 401:
|
||||
* $ref: "#/components/responses/UnauthorizedError"
|
||||
* 404:
|
||||
* description: Data not found
|
||||
* 500:
|
||||
* description: Some server error
|
||||
*/
|
||||
router.get(
|
||||
'/',
|
||||
wrapAsync(async (req, res) => {
|
||||
const filetype = req.query.filetype;
|
||||
|
||||
const globalAccess = req.currentUser.app_role.globalAccess;
|
||||
|
||||
const currentUser = req.currentUser;
|
||||
const payload = await FeedbackDBApi.findAll(req.query, globalAccess, {
|
||||
currentUser,
|
||||
});
|
||||
if (filetype && filetype === 'csv') {
|
||||
const fields = ['id', 'title', 'description'];
|
||||
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/feedback/count:
|
||||
* get:
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* tags: [Feedback]
|
||||
* summary: Count all feedback
|
||||
* description: Count all feedback
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Feedback count successfully received
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: "#/components/schemas/Feedback"
|
||||
* 401:
|
||||
* $ref: "#/components/responses/UnauthorizedError"
|
||||
* 404:
|
||||
* description: Data not found
|
||||
* 500:
|
||||
* description: Some server error
|
||||
*/
|
||||
router.get(
|
||||
'/count',
|
||||
wrapAsync(async (req, res) => {
|
||||
const globalAccess = req.currentUser.app_role.globalAccess;
|
||||
|
||||
const currentUser = req.currentUser;
|
||||
const payload = await FeedbackDBApi.findAll(req.query, globalAccess, {
|
||||
countOnly: true,
|
||||
currentUser,
|
||||
});
|
||||
|
||||
res.status(200).send(payload);
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/feedback/autocomplete:
|
||||
* get:
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* tags: [Feedback]
|
||||
* summary: Find all feedback that match search criteria
|
||||
* description: Find all feedback that match search criteria
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Feedback list successfully received
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: "#/components/schemas/Feedback"
|
||||
* 401:
|
||||
* $ref: "#/components/responses/UnauthorizedError"
|
||||
* 404:
|
||||
* description: Data not found
|
||||
* 500:
|
||||
* description: Some server error
|
||||
*/
|
||||
router.get('/autocomplete', async (req, res) => {
|
||||
const globalAccess = req.currentUser.app_role.globalAccess;
|
||||
|
||||
const organizationId = req.currentUser.organization?.id;
|
||||
|
||||
const payload = await FeedbackDBApi.findAllAutocomplete(
|
||||
req.query.query,
|
||||
req.query.limit,
|
||||
req.query.offset,
|
||||
globalAccess,
|
||||
organizationId,
|
||||
);
|
||||
|
||||
res.status(200).send(payload);
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/feedback/{id}:
|
||||
* get:
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* tags: [Feedback]
|
||||
* 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/Feedback"
|
||||
* 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 FeedbackDBApi.findBy({ id: req.params.id });
|
||||
|
||||
res.status(200).send(payload);
|
||||
}),
|
||||
);
|
||||
|
||||
router.use('/', require('../helpers').commonErrorHandler);
|
||||
|
||||
module.exports = router;
|
||||
114
backend/src/services/feedback.js
Normal file
114
backend/src/services/feedback.js
Normal file
@ -0,0 +1,114 @@
|
||||
const db = require('../db/models');
|
||||
const FeedbackDBApi = require('../db/api/feedback');
|
||||
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 FeedbackService {
|
||||
static async create(data, currentUser) {
|
||||
const transaction = await db.sequelize.transaction();
|
||||
try {
|
||||
await FeedbackDBApi.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 FeedbackDBApi.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 feedback = await FeedbackDBApi.findBy({ id }, { transaction });
|
||||
|
||||
if (!feedback) {
|
||||
throw new ValidationError('feedbackNotFound');
|
||||
}
|
||||
|
||||
const updatedFeedback = await FeedbackDBApi.update(id, data, {
|
||||
currentUser,
|
||||
transaction,
|
||||
});
|
||||
|
||||
await transaction.commit();
|
||||
return updatedFeedback;
|
||||
} catch (error) {
|
||||
await transaction.rollback();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
static async deleteByIds(ids, currentUser) {
|
||||
const transaction = await db.sequelize.transaction();
|
||||
|
||||
try {
|
||||
await FeedbackDBApi.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 FeedbackDBApi.remove(id, {
|
||||
currentUser,
|
||||
transaction,
|
||||
});
|
||||
|
||||
await transaction.commit();
|
||||
} catch (error) {
|
||||
await transaction.rollback();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -52,6 +52,8 @@ module.exports = class SearchService {
|
||||
organizations: ['name'],
|
||||
|
||||
tags: ['name'],
|
||||
|
||||
feedback: ['title', 'description'],
|
||||
};
|
||||
const columnsInt = {};
|
||||
|
||||
|
||||
@ -42,6 +42,8 @@ export default function BaseButton({
|
||||
roundedFull = false,
|
||||
onClick,
|
||||
}: Props) {
|
||||
const buttonLabels = ["new item", "filter", "download csv", "upload csv"];
|
||||
const finalColor = (label && buttonLabels.includes(label.trim().toLowerCase())) ? "danger" : color;
|
||||
const corners = useAppSelector((state) => state.style.corners);
|
||||
const componentClass = [
|
||||
'inline-flex',
|
||||
@ -54,9 +56,9 @@ export default function BaseButton({
|
||||
'duration-150',
|
||||
'border',
|
||||
disabled ? 'cursor-not-allowed' : 'cursor-pointer',
|
||||
roundedFull ? 'rounded-full' : `${corners}`,
|
||||
getButtonColor(color, outline, !disabled, active),
|
||||
className,
|
||||
getButtonColor(finalColor, outline, !disabled, active),
|
||||
getButtonColor(finalColor, outline, !disabled, active),
|
||||
getButtonColor(finalColor, outline, !disabled, active),
|
||||
];
|
||||
|
||||
if (!label && icon) {
|
||||
|
||||
116
frontend/src/components/Feedback/CardFeedback.tsx
Normal file
116
frontend/src/components/Feedback/CardFeedback.tsx
Normal file
@ -0,0 +1,116 @@
|
||||
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 = {
|
||||
feedback: any[];
|
||||
loading: boolean;
|
||||
onDelete: (id: string) => void;
|
||||
currentPage: number;
|
||||
numPages: number;
|
||||
onPageChange: (page: number) => void;
|
||||
};
|
||||
|
||||
const CardFeedback = ({
|
||||
feedback,
|
||||
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_FEEDBACK');
|
||||
|
||||
return (
|
||||
<div className={'p-4'}>
|
||||
{loading && <LoadingSpinner />}
|
||||
<ul
|
||||
role='list'
|
||||
className='grid grid-cols-1 gap-x-6 gap-y-8 lg:grid-cols-3 2xl:grid-cols-4 xl:gap-x-8'
|
||||
>
|
||||
{!loading &&
|
||||
feedback.map((item, index) => (
|
||||
<li
|
||||
key={item.id}
|
||||
className={`overflow-hidden ${
|
||||
corners !== 'rounded-full' ? corners : 'rounded-3xl'
|
||||
} border ${focusRing} border-gray-200 dark:border-dark-700 ${
|
||||
darkMode ? 'aside-scrollbars-[slate]' : asideScrollbarsStyle
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`flex items-center ${bgColor} p-6 gap-x-4 border-b border-gray-900/5 bg-gray-50 dark:bg-dark-800 relative`}
|
||||
>
|
||||
<Link
|
||||
href={`/feedback/feedback-view/?id=${item.id}`}
|
||||
className='text-lg font-bold leading-6 line-clamp-1'
|
||||
>
|
||||
{item.description}
|
||||
</Link>
|
||||
|
||||
<div className='ml-auto '>
|
||||
<ListActionsPopover
|
||||
onDelete={onDelete}
|
||||
itemId={item.id}
|
||||
pathEdit={`/feedback/feedback-edit/?id=${item.id}`}
|
||||
pathView={`/feedback/feedback-view/?id=${item.id}`}
|
||||
hasUpdatePermission={hasUpdatePermission}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<dl className='divide-y divide-stone-300 dark:divide-dark-700 px-6 py-4 text-sm leading-6 h-64 overflow-y-auto'>
|
||||
<div className='flex justify-between gap-x-4 py-3'>
|
||||
<dt className=' text-gray-500 dark:text-dark-600'>Title</dt>
|
||||
<dd className='flex items-start gap-x-2'>
|
||||
<div className='font-medium line-clamp-4'>{item.title}</div>
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
<div className='flex justify-between gap-x-4 py-3'>
|
||||
<dt className=' text-gray-500 dark:text-dark-600'>
|
||||
Description
|
||||
</dt>
|
||||
<dd className='flex items-start gap-x-2'>
|
||||
<div className='font-medium line-clamp-4'>
|
||||
{item.description}
|
||||
</div>
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</li>
|
||||
))}
|
||||
{!loading && feedback.length === 0 && (
|
||||
<div className='col-span-full flex items-center justify-center h-40'>
|
||||
<p className=''>No data to display</p>
|
||||
</div>
|
||||
)}
|
||||
</ul>
|
||||
<div className={'flex items-center justify-center my-6'}>
|
||||
<Pagination
|
||||
currentPage={currentPage}
|
||||
numPages={numPages}
|
||||
setCurrentPage={onPageChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CardFeedback;
|
||||
95
frontend/src/components/Feedback/ListFeedback.tsx
Normal file
95
frontend/src/components/Feedback/ListFeedback.tsx
Normal file
@ -0,0 +1,95 @@
|
||||
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 = {
|
||||
feedback: any[];
|
||||
loading: boolean;
|
||||
onDelete: (id: string) => void;
|
||||
currentPage: number;
|
||||
numPages: number;
|
||||
onPageChange: (page: number) => void;
|
||||
};
|
||||
|
||||
const ListFeedback = ({
|
||||
feedback,
|
||||
loading,
|
||||
onDelete,
|
||||
currentPage,
|
||||
numPages,
|
||||
onPageChange,
|
||||
}: Props) => {
|
||||
const currentUser = useAppSelector((state) => state.auth.currentUser);
|
||||
const hasUpdatePermission = hasPermission(currentUser, 'UPDATE_FEEDBACK');
|
||||
|
||||
const corners = useAppSelector((state) => state.style.corners);
|
||||
const bgColor = useAppSelector((state) => state.style.cardsColor);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='relative overflow-x-auto p-4 space-y-4'>
|
||||
{loading && <LoadingSpinner />}
|
||||
{!loading &&
|
||||
feedback.map((item) => (
|
||||
<CardBox
|
||||
hasTable
|
||||
isList
|
||||
key={item.id}
|
||||
className={'rounded shadow-none'}
|
||||
>
|
||||
<div
|
||||
className={`flex rounded dark:bg-dark-900 border border-stone-300 items-center overflow-hidden`}
|
||||
>
|
||||
<Link
|
||||
href={`/feedback/feedback-view/?id=${item.id}`}
|
||||
className={
|
||||
'flex-1 px-4 py-6 h-24 flex divide-x-2 divide-stone-300 items-center overflow-hidden`}> dark:divide-dark-700 overflow-x-auto'
|
||||
}
|
||||
>
|
||||
<div className={'flex-1 px-3'}>
|
||||
<p className={'text-xs text-gray-500 '}>Title</p>
|
||||
<p className={'line-clamp-2'}>{item.title}</p>
|
||||
</div>
|
||||
|
||||
<div className={'flex-1 px-3'}>
|
||||
<p className={'text-xs text-gray-500 '}>Description</p>
|
||||
<p className={'line-clamp-2'}>{item.description}</p>
|
||||
</div>
|
||||
</Link>
|
||||
<ListActionsPopover
|
||||
onDelete={onDelete}
|
||||
itemId={item.id}
|
||||
pathEdit={`/feedback/feedback-edit/?id=${item.id}`}
|
||||
pathView={`/feedback/feedback-view/?id=${item.id}`}
|
||||
hasUpdatePermission={hasUpdatePermission}
|
||||
/>
|
||||
</div>
|
||||
</CardBox>
|
||||
))}
|
||||
{!loading && feedback.length === 0 && (
|
||||
<div className='col-span-full flex items-center justify-center h-40'>
|
||||
<p className=''>No data to display</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={'flex items-center justify-center my-6'}>
|
||||
<Pagination
|
||||
currentPage={currentPage}
|
||||
numPages={numPages}
|
||||
setCurrentPage={onPageChange}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ListFeedback;
|
||||
481
frontend/src/components/Feedback/TableFeedback.tsx
Normal file
481
frontend/src/components/Feedback/TableFeedback.tsx
Normal file
@ -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/feedback/feedbackSlice';
|
||||
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 './configureFeedbackCols';
|
||||
import _ from 'lodash';
|
||||
import dataFormatter from '../../helpers/dataFormatter';
|
||||
import { dataGridStyles } from '../../styles';
|
||||
|
||||
const perPage = 10;
|
||||
|
||||
const TableSampleFeedback = ({
|
||||
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<GridColDef[]>([]);
|
||||
const [selectedRows, setSelectedRows] = useState([]);
|
||||
const [sortModel, setSortModel] = useState([
|
||||
{
|
||||
field: '',
|
||||
sort: 'desc',
|
||||
},
|
||||
]);
|
||||
|
||||
const {
|
||||
feedback,
|
||||
loading,
|
||||
count,
|
||||
notify: feedbackNotify,
|
||||
refetch,
|
||||
} = useAppSelector((state) => state.feedback);
|
||||
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 (feedbackNotify.showNotification) {
|
||||
notify(feedbackNotify.typeNotification, feedbackNotify.textNotification);
|
||||
}
|
||||
}, [feedbackNotify.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, `feedback`, 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 = (
|
||||
<div className='relative overflow-x-auto'>
|
||||
<DataGrid
|
||||
autoHeight
|
||||
rowHeight={64}
|
||||
sx={dataGridStyles}
|
||||
className={'datagrid--table'}
|
||||
getRowClassName={() => `datagrid--row`}
|
||||
rows={feedback ?? []}
|
||||
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);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{filterItems && Array.isArray(filterItems) && filterItems.length ? (
|
||||
<CardBox>
|
||||
<Formik
|
||||
initialValues={{
|
||||
checkboxes: ['lorem'],
|
||||
switches: ['lorem'],
|
||||
radio: 'lorem',
|
||||
}}
|
||||
onSubmit={() => null}
|
||||
>
|
||||
<Form>
|
||||
<>
|
||||
{filterItems &&
|
||||
filterItems.map((filterItem) => {
|
||||
return (
|
||||
<div key={filterItem.id} className='flex mb-4'>
|
||||
<div className='flex flex-col w-full mr-3'>
|
||||
<div className=' text-gray-500 font-bold'>
|
||||
Filter
|
||||
</div>
|
||||
<Field
|
||||
className={controlClasses}
|
||||
name='selectedField'
|
||||
id='selectedField'
|
||||
component='select'
|
||||
value={filterItem?.fields?.selectedField || ''}
|
||||
onChange={handleChange(filterItem.id)}
|
||||
>
|
||||
{filters.map((selectOption) => (
|
||||
<option
|
||||
key={selectOption.title}
|
||||
value={`${selectOption.title}`}
|
||||
>
|
||||
{selectOption.label}
|
||||
</option>
|
||||
))}
|
||||
</Field>
|
||||
</div>
|
||||
{filters.find(
|
||||
(filter) =>
|
||||
filter.title === filterItem?.fields?.selectedField,
|
||||
)?.type === 'enum' ? (
|
||||
<div className='flex flex-col w-full mr-3'>
|
||||
<div className='text-gray-500 font-bold'>Value</div>
|
||||
<Field
|
||||
className={controlClasses}
|
||||
name='filterValue'
|
||||
id='filterValue'
|
||||
component='select'
|
||||
value={filterItem?.fields?.filterValue || ''}
|
||||
onChange={handleChange(filterItem.id)}
|
||||
>
|
||||
<option value=''>Select Value</option>
|
||||
{filters
|
||||
.find(
|
||||
(filter) =>
|
||||
filter.title ===
|
||||
filterItem?.fields?.selectedField,
|
||||
)
|
||||
?.options?.map((option) => (
|
||||
<option key={option} value={option}>
|
||||
{option}
|
||||
</option>
|
||||
))}
|
||||
</Field>
|
||||
</div>
|
||||
) : filters.find(
|
||||
(filter) =>
|
||||
filter.title ===
|
||||
filterItem?.fields?.selectedField,
|
||||
)?.number ? (
|
||||
<div className='flex flex-row w-full mr-3'>
|
||||
<div className='flex flex-col w-full mr-3'>
|
||||
<div className=' text-gray-500 font-bold'>
|
||||
From
|
||||
</div>
|
||||
<Field
|
||||
className={controlClasses}
|
||||
name='filterValueFrom'
|
||||
placeholder='From'
|
||||
id='filterValueFrom'
|
||||
value={
|
||||
filterItem?.fields?.filterValueFrom || ''
|
||||
}
|
||||
onChange={handleChange(filterItem.id)}
|
||||
/>
|
||||
</div>
|
||||
<div className='flex flex-col w-full'>
|
||||
<div className=' text-gray-500 font-bold'>
|
||||
To
|
||||
</div>
|
||||
<Field
|
||||
className={controlClasses}
|
||||
name='filterValueTo'
|
||||
placeholder='to'
|
||||
id='filterValueTo'
|
||||
value={filterItem?.fields?.filterValueTo || ''}
|
||||
onChange={handleChange(filterItem.id)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : filters.find(
|
||||
(filter) =>
|
||||
filter.title ===
|
||||
filterItem?.fields?.selectedField,
|
||||
)?.date ? (
|
||||
<div className='flex flex-row w-full mr-3'>
|
||||
<div className='flex flex-col w-full mr-3'>
|
||||
<div className=' text-gray-500 font-bold'>
|
||||
From
|
||||
</div>
|
||||
<Field
|
||||
className={controlClasses}
|
||||
name='filterValueFrom'
|
||||
placeholder='From'
|
||||
id='filterValueFrom'
|
||||
type='datetime-local'
|
||||
value={
|
||||
filterItem?.fields?.filterValueFrom || ''
|
||||
}
|
||||
onChange={handleChange(filterItem.id)}
|
||||
/>
|
||||
</div>
|
||||
<div className='flex flex-col w-full'>
|
||||
<div className=' text-gray-500 font-bold'>
|
||||
To
|
||||
</div>
|
||||
<Field
|
||||
className={controlClasses}
|
||||
name='filterValueTo'
|
||||
placeholder='to'
|
||||
id='filterValueTo'
|
||||
type='datetime-local'
|
||||
value={filterItem?.fields?.filterValueTo || ''}
|
||||
onChange={handleChange(filterItem.id)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className='flex flex-col w-full mr-3'>
|
||||
<div className=' text-gray-500 font-bold'>
|
||||
Contains
|
||||
</div>
|
||||
<Field
|
||||
className={controlClasses}
|
||||
name='filterValue'
|
||||
placeholder='Contained'
|
||||
id='filterValue'
|
||||
value={filterItem?.fields?.filterValue || ''}
|
||||
onChange={handleChange(filterItem.id)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className='flex flex-col'>
|
||||
<div className=' text-gray-500 font-bold'>
|
||||
Action
|
||||
</div>
|
||||
<BaseButton
|
||||
className='my-2'
|
||||
type='reset'
|
||||
color='danger'
|
||||
label='Delete'
|
||||
onClick={() => {
|
||||
deleteFilter(filterItem.id);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div className='flex'>
|
||||
<BaseButton
|
||||
className='my-2 mr-3'
|
||||
color='success'
|
||||
label='Apply'
|
||||
onClick={handleSubmit}
|
||||
/>
|
||||
<BaseButton
|
||||
className='my-2'
|
||||
color='info'
|
||||
label='Cancel'
|
||||
onClick={handleReset}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
</Form>
|
||||
</Formik>
|
||||
</CardBox>
|
||||
) : null}
|
||||
<CardBoxModal
|
||||
title='Please confirm'
|
||||
buttonColor='info'
|
||||
buttonLabel={loading ? 'Deleting...' : 'Confirm'}
|
||||
isActive={isModalTrashActive}
|
||||
onConfirm={handleDeleteAction}
|
||||
onCancel={handleModalAction}
|
||||
>
|
||||
<p>Are you sure you want to delete this item?</p>
|
||||
</CardBoxModal>
|
||||
|
||||
{dataGrid}
|
||||
|
||||
{selectedRows.length > 0 &&
|
||||
createPortal(
|
||||
<BaseButton
|
||||
className='me-4'
|
||||
color='danger'
|
||||
label={`Delete ${selectedRows.length === 1 ? 'Row' : 'Rows'}`}
|
||||
onClick={() => onDeleteRows(selectedRows)}
|
||||
/>,
|
||||
document.getElementById('delete-rows-button'),
|
||||
)}
|
||||
<ToastContainer />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default TableSampleFeedback;
|
||||
85
frontend/src/components/Feedback/configureFeedbackCols.tsx
Normal file
85
frontend/src/components/Feedback/configureFeedbackCols.tsx
Normal file
@ -0,0 +1,85 @@
|
||||
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_FEEDBACK');
|
||||
|
||||
return [
|
||||
{
|
||||
field: 'title',
|
||||
headerName: 'Title',
|
||||
flex: 1,
|
||||
minWidth: 120,
|
||||
filterable: false,
|
||||
headerClassName: 'datagrid--header',
|
||||
cellClassName: 'datagrid--cell',
|
||||
|
||||
editable: hasUpdatePermission,
|
||||
},
|
||||
|
||||
{
|
||||
field: 'description',
|
||||
headerName: 'Description',
|
||||
flex: 1,
|
||||
minWidth: 120,
|
||||
filterable: false,
|
||||
headerClassName: 'datagrid--header',
|
||||
cellClassName: 'datagrid--cell',
|
||||
|
||||
editable: hasUpdatePermission,
|
||||
},
|
||||
|
||||
{
|
||||
field: 'actions',
|
||||
type: 'actions',
|
||||
minWidth: 30,
|
||||
headerClassName: 'datagrid--header',
|
||||
cellClassName: 'datagrid--cell',
|
||||
getActions: (params: GridRowParams) => {
|
||||
return [
|
||||
<ListActionsPopover
|
||||
onDelete={onDelete}
|
||||
itemId={params?.row?.id}
|
||||
pathEdit={`/feedback/feedback-edit/?id=${params?.row?.id}`}
|
||||
pathView={`/feedback/feedback-view/?id=${params?.row?.id}`}
|
||||
key={1}
|
||||
hasUpdatePermission={hasUpdatePermission}
|
||||
/>,
|
||||
];
|
||||
},
|
||||
},
|
||||
];
|
||||
};
|
||||
@ -76,6 +76,14 @@ const menuAside: MenuAsideItem[] = [
|
||||
icon: icon.mdiTable ? icon.mdiTable : icon.mdiTable,
|
||||
permissions: 'READ_TAGS',
|
||||
},
|
||||
{
|
||||
href: '/feedback/feedback-list',
|
||||
label: 'Feedback',
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
icon: icon.mdiTable ? icon.mdiTable : icon.mdiTable,
|
||||
permissions: 'READ_FEEDBACK',
|
||||
},
|
||||
{
|
||||
href: '/profile',
|
||||
label: 'Profile',
|
||||
|
||||
@ -30,6 +30,7 @@ const Dashboard = () => {
|
||||
const [permissions, setPermissions] = React.useState('Loading...');
|
||||
const [organizations, setOrganizations] = React.useState('Loading...');
|
||||
const [tags, setTags] = React.useState('Loading...');
|
||||
const [feedback, setFeedback] = React.useState('Loading...');
|
||||
|
||||
const [widgetsRole, setWidgetsRole] = React.useState({
|
||||
role: { value: '', label: '' },
|
||||
@ -51,6 +52,7 @@ const Dashboard = () => {
|
||||
'permissions',
|
||||
'organizations',
|
||||
'tags',
|
||||
'feedback',
|
||||
];
|
||||
const fns = [
|
||||
setUsers,
|
||||
@ -61,6 +63,7 @@ const Dashboard = () => {
|
||||
setPermissions,
|
||||
setOrganizations,
|
||||
setTags,
|
||||
setFeedback,
|
||||
];
|
||||
|
||||
const requests = entities.map((entity, index) => {
|
||||
@ -424,6 +427,38 @@ const Dashboard = () => {
|
||||
</div>
|
||||
</Link>
|
||||
)}
|
||||
|
||||
{hasPermission(currentUser, 'READ_FEEDBACK') && (
|
||||
<Link href={'/feedback/feedback-list'}>
|
||||
<div
|
||||
className={`${
|
||||
corners !== 'rounded-full' ? corners : 'rounded-3xl'
|
||||
} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
||||
>
|
||||
<div className='flex justify-between align-center'>
|
||||
<div>
|
||||
<div className='text-lg leading-tight text-gray-500 dark:text-gray-400'>
|
||||
Feedback
|
||||
</div>
|
||||
<div className='text-3xl leading-tight font-semibold'>
|
||||
{feedback}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<BaseIcon
|
||||
className={`${iconsColor}`}
|
||||
w='w-16'
|
||||
h='h-16'
|
||||
size={48}
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
path={icon.mdiTable || icon.mdiTable}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</SectionMain>
|
||||
</>
|
||||
|
||||
147
frontend/src/pages/feedback/[feedbackId].tsx
Normal file
147
frontend/src/pages/feedback/[feedbackId].tsx
Normal file
@ -0,0 +1,147 @@
|
||||
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/feedback/feedbackSlice';
|
||||
import { useAppDispatch, useAppSelector } from '../../stores/hooks';
|
||||
import { useRouter } from 'next/router';
|
||||
import { saveFile } from '../../helpers/fileSaver';
|
||||
import dataFormatter from '../../helpers/dataFormatter';
|
||||
import ImageField from '../../components/ImageField';
|
||||
|
||||
import { hasPermission } from '../../helpers/userPermissions';
|
||||
|
||||
const EditFeedback = () => {
|
||||
const router = useRouter();
|
||||
const dispatch = useAppDispatch();
|
||||
const initVals = {
|
||||
organizations: null,
|
||||
|
||||
title: '',
|
||||
|
||||
description: '',
|
||||
};
|
||||
const [initialValues, setInitialValues] = useState(initVals);
|
||||
|
||||
const { feedback } = useAppSelector((state) => state.feedback);
|
||||
|
||||
const { currentUser } = useAppSelector((state) => state.auth);
|
||||
|
||||
const { feedbackId } = router.query;
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetch({ id: feedbackId }));
|
||||
}, [feedbackId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof feedback === 'object') {
|
||||
setInitialValues(feedback);
|
||||
}
|
||||
}, [feedback]);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof feedback === 'object') {
|
||||
const newInitialVal = { ...initVals };
|
||||
|
||||
Object.keys(initVals).forEach((el) => (newInitialVal[el] = feedback[el]));
|
||||
|
||||
setInitialValues(newInitialVal);
|
||||
}
|
||||
}, [feedback]);
|
||||
|
||||
const handleSubmit = async (data) => {
|
||||
await dispatch(update({ id: feedbackId, data }));
|
||||
await router.push('/feedback/feedback-list');
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{getPageTitle('Edit feedback')}</title>
|
||||
</Head>
|
||||
<SectionMain>
|
||||
<SectionTitleLineWithButton
|
||||
icon={mdiChartTimelineVariant}
|
||||
title={'Edit feedback'}
|
||||
main
|
||||
>
|
||||
{''}
|
||||
</SectionTitleLineWithButton>
|
||||
<CardBox>
|
||||
<Formik
|
||||
enableReinitialize
|
||||
initialValues={initialValues}
|
||||
onSubmit={(values) => handleSubmit(values)}
|
||||
>
|
||||
<Form>
|
||||
<FormField label='organizations' labelFor='organizations'>
|
||||
<Field
|
||||
name='organizations'
|
||||
id='organizations'
|
||||
component={SelectField}
|
||||
options={initialValues.organizations}
|
||||
itemRef={'organizations'}
|
||||
showField={'name'}
|
||||
></Field>
|
||||
</FormField>
|
||||
|
||||
<FormField label='Title'>
|
||||
<Field name='title' placeholder='Title' />
|
||||
</FormField>
|
||||
|
||||
<FormField label='Description'>
|
||||
<Field name='description' placeholder='Description' />
|
||||
</FormField>
|
||||
|
||||
<BaseDivider />
|
||||
<BaseButtons>
|
||||
<BaseButton type='submit' color='info' label='Submit' />
|
||||
<BaseButton type='reset' color='info' outline label='Reset' />
|
||||
<BaseButton
|
||||
type='reset'
|
||||
color='danger'
|
||||
outline
|
||||
label='Cancel'
|
||||
onClick={() => router.push('/feedback/feedback-list')}
|
||||
/>
|
||||
</BaseButtons>
|
||||
</Form>
|
||||
</Formik>
|
||||
</CardBox>
|
||||
</SectionMain>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
EditFeedback.getLayout = function getLayout(page: ReactElement) {
|
||||
return (
|
||||
<LayoutAuthenticated permission={'UPDATE_FEEDBACK'}>
|
||||
{page}
|
||||
</LayoutAuthenticated>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditFeedback;
|
||||
145
frontend/src/pages/feedback/feedback-edit.tsx
Normal file
145
frontend/src/pages/feedback/feedback-edit.tsx
Normal file
@ -0,0 +1,145 @@
|
||||
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/feedback/feedbackSlice';
|
||||
import { useAppDispatch, useAppSelector } from '../../stores/hooks';
|
||||
import { useRouter } from 'next/router';
|
||||
import { saveFile } from '../../helpers/fileSaver';
|
||||
import dataFormatter from '../../helpers/dataFormatter';
|
||||
import ImageField from '../../components/ImageField';
|
||||
|
||||
import { hasPermission } from '../../helpers/userPermissions';
|
||||
|
||||
const EditFeedbackPage = () => {
|
||||
const router = useRouter();
|
||||
const dispatch = useAppDispatch();
|
||||
const initVals = {
|
||||
organizations: null,
|
||||
|
||||
title: '',
|
||||
|
||||
description: '',
|
||||
};
|
||||
const [initialValues, setInitialValues] = useState(initVals);
|
||||
|
||||
const { feedback } = useAppSelector((state) => state.feedback);
|
||||
|
||||
const { currentUser } = useAppSelector((state) => state.auth);
|
||||
|
||||
const { id } = router.query;
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetch({ id: id }));
|
||||
}, [id]);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof feedback === 'object') {
|
||||
setInitialValues(feedback);
|
||||
}
|
||||
}, [feedback]);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof feedback === 'object') {
|
||||
const newInitialVal = { ...initVals };
|
||||
Object.keys(initVals).forEach((el) => (newInitialVal[el] = feedback[el]));
|
||||
setInitialValues(newInitialVal);
|
||||
}
|
||||
}, [feedback]);
|
||||
|
||||
const handleSubmit = async (data) => {
|
||||
await dispatch(update({ id: id, data }));
|
||||
await router.push('/feedback/feedback-list');
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{getPageTitle('Edit feedback')}</title>
|
||||
</Head>
|
||||
<SectionMain>
|
||||
<SectionTitleLineWithButton
|
||||
icon={mdiChartTimelineVariant}
|
||||
title={'Edit feedback'}
|
||||
main
|
||||
>
|
||||
{''}
|
||||
</SectionTitleLineWithButton>
|
||||
<CardBox>
|
||||
<Formik
|
||||
enableReinitialize
|
||||
initialValues={initialValues}
|
||||
onSubmit={(values) => handleSubmit(values)}
|
||||
>
|
||||
<Form>
|
||||
<FormField label='organizations' labelFor='organizations'>
|
||||
<Field
|
||||
name='organizations'
|
||||
id='organizations'
|
||||
component={SelectField}
|
||||
options={initialValues.organizations}
|
||||
itemRef={'organizations'}
|
||||
showField={'name'}
|
||||
></Field>
|
||||
</FormField>
|
||||
|
||||
<FormField label='Title'>
|
||||
<Field name='title' placeholder='Title' />
|
||||
</FormField>
|
||||
|
||||
<FormField label='Description'>
|
||||
<Field name='description' placeholder='Description' />
|
||||
</FormField>
|
||||
|
||||
<BaseDivider />
|
||||
<BaseButtons>
|
||||
<BaseButton type='submit' color='info' label='Submit' />
|
||||
<BaseButton type='reset' color='info' outline label='Reset' />
|
||||
<BaseButton
|
||||
type='reset'
|
||||
color='danger'
|
||||
outline
|
||||
label='Cancel'
|
||||
onClick={() => router.push('/feedback/feedback-list')}
|
||||
/>
|
||||
</BaseButtons>
|
||||
</Form>
|
||||
</Formik>
|
||||
</CardBox>
|
||||
</SectionMain>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
EditFeedbackPage.getLayout = function getLayout(page: ReactElement) {
|
||||
return (
|
||||
<LayoutAuthenticated permission={'UPDATE_FEEDBACK'}>
|
||||
{page}
|
||||
</LayoutAuthenticated>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditFeedbackPage;
|
||||
165
frontend/src/pages/feedback/feedback-list.tsx
Normal file
165
frontend/src/pages/feedback/feedback-list.tsx
Normal file
@ -0,0 +1,165 @@
|
||||
import { mdiChartTimelineVariant } from '@mdi/js';
|
||||
import Head from 'next/head';
|
||||
import { uniqueId } from 'lodash';
|
||||
import React, { ReactElement, useState } from 'react';
|
||||
import CardBox from '../../components/CardBox';
|
||||
import LayoutAuthenticated from '../../layouts/Authenticated';
|
||||
import SectionMain from '../../components/SectionMain';
|
||||
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton';
|
||||
import { getPageTitle } from '../../config';
|
||||
import TableFeedback from '../../components/Feedback/TableFeedback';
|
||||
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/feedback/feedbackSlice';
|
||||
|
||||
import { hasPermission } from '../../helpers/userPermissions';
|
||||
|
||||
const FeedbackTablesPage = () => {
|
||||
const [filterItems, setFilterItems] = useState([]);
|
||||
const [csvFile, setCsvFile] = useState<File | null>(null);
|
||||
const [isModalActive, setIsModalActive] = useState(false);
|
||||
const [showTableView, setShowTableView] = useState(false);
|
||||
|
||||
const { currentUser } = useAppSelector((state) => state.auth);
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const [filters] = useState([
|
||||
{ label: 'Title', title: 'title' },
|
||||
{ label: 'Description', title: 'description' },
|
||||
]);
|
||||
|
||||
const hasCreatePermission =
|
||||
currentUser && hasPermission(currentUser, 'CREATE_FEEDBACK');
|
||||
|
||||
const addFilter = () => {
|
||||
const newItem = {
|
||||
id: uniqueId(),
|
||||
fields: {
|
||||
filterValue: '',
|
||||
filterValueFrom: '',
|
||||
filterValueTo: '',
|
||||
selectedField: '',
|
||||
},
|
||||
};
|
||||
newItem.fields.selectedField = filters[0].title;
|
||||
setFilterItems([...filterItems, newItem]);
|
||||
};
|
||||
|
||||
const getFeedbackCSV = async () => {
|
||||
const response = await axios({
|
||||
url: '/feedback?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 = 'feedbackCSV.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 (
|
||||
<>
|
||||
<Head>
|
||||
<title>{getPageTitle('Feedback')}</title>
|
||||
</Head>
|
||||
<SectionMain>
|
||||
<SectionTitleLineWithButton
|
||||
icon={mdiChartTimelineVariant}
|
||||
title='Feedback'
|
||||
main
|
||||
>
|
||||
{''}
|
||||
</SectionTitleLineWithButton>
|
||||
<CardBox className='mb-6' cardBoxClassName='flex flex-wrap'>
|
||||
{hasCreatePermission && (
|
||||
<BaseButton
|
||||
className={'mr-3'}
|
||||
href={'/feedback/feedback-new'}
|
||||
color='info'
|
||||
label='New Item'
|
||||
/>
|
||||
)}
|
||||
|
||||
<BaseButton
|
||||
className={'mr-3'}
|
||||
color='info'
|
||||
label='Filter'
|
||||
onClick={addFilter}
|
||||
/>
|
||||
<BaseButton
|
||||
className={'mr-3'}
|
||||
color='info'
|
||||
label='Download CSV'
|
||||
onClick={getFeedbackCSV}
|
||||
/>
|
||||
|
||||
{hasCreatePermission && (
|
||||
<BaseButton
|
||||
color='info'
|
||||
label='Upload CSV'
|
||||
onClick={() => setIsModalActive(true)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className='md:inline-flex items-center ms-auto'>
|
||||
<div id='delete-rows-button'></div>
|
||||
</div>
|
||||
</CardBox>
|
||||
|
||||
<CardBox className='mb-6' hasTable>
|
||||
<TableFeedback
|
||||
filterItems={filterItems}
|
||||
setFilterItems={setFilterItems}
|
||||
filters={filters}
|
||||
showGrid={false}
|
||||
/>
|
||||
</CardBox>
|
||||
</SectionMain>
|
||||
<CardBoxModal
|
||||
title='Upload CSV'
|
||||
buttonColor='info'
|
||||
buttonLabel={'Confirm'}
|
||||
// buttonLabel={false ? 'Deleting...' : 'Confirm'}
|
||||
isActive={isModalActive}
|
||||
onConfirm={onModalConfirm}
|
||||
onCancel={onModalCancel}
|
||||
>
|
||||
<DragDropFilePicker
|
||||
file={csvFile}
|
||||
setFile={setCsvFile}
|
||||
formats={'.csv'}
|
||||
/>
|
||||
</CardBoxModal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
FeedbackTablesPage.getLayout = function getLayout(page: ReactElement) {
|
||||
return (
|
||||
<LayoutAuthenticated permission={'READ_FEEDBACK'}>
|
||||
{page}
|
||||
</LayoutAuthenticated>
|
||||
);
|
||||
};
|
||||
|
||||
export default FeedbackTablesPage;
|
||||
116
frontend/src/pages/feedback/feedback-new.tsx
Normal file
116
frontend/src/pages/feedback/feedback-new.tsx
Normal file
@ -0,0 +1,116 @@
|
||||
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/feedback/feedbackSlice';
|
||||
import { useAppDispatch } from '../../stores/hooks';
|
||||
import { useRouter } from 'next/router';
|
||||
import moment from 'moment';
|
||||
|
||||
const initialValues = {
|
||||
organizations: '',
|
||||
|
||||
title: '',
|
||||
|
||||
description: '',
|
||||
};
|
||||
|
||||
const FeedbackNew = () => {
|
||||
const router = useRouter();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const handleSubmit = async (data) => {
|
||||
await dispatch(create(data));
|
||||
await router.push('/feedback/feedback-list');
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{getPageTitle('New Item')}</title>
|
||||
</Head>
|
||||
<SectionMain>
|
||||
<SectionTitleLineWithButton
|
||||
icon={mdiChartTimelineVariant}
|
||||
title='New Item'
|
||||
main
|
||||
>
|
||||
{''}
|
||||
</SectionTitleLineWithButton>
|
||||
<CardBox>
|
||||
<Formik
|
||||
initialValues={initialValues}
|
||||
onSubmit={(values) => handleSubmit(values)}
|
||||
>
|
||||
<Form>
|
||||
<FormField label='organizations' labelFor='organizations'>
|
||||
<Field
|
||||
name='organizations'
|
||||
id='organizations'
|
||||
component={SelectField}
|
||||
options={[]}
|
||||
itemRef={'organizations'}
|
||||
></Field>
|
||||
</FormField>
|
||||
|
||||
<FormField label='Title'>
|
||||
<Field name='title' placeholder='Title' />
|
||||
</FormField>
|
||||
|
||||
<FormField label='Description'>
|
||||
<Field name='description' placeholder='Description' />
|
||||
</FormField>
|
||||
|
||||
<BaseDivider />
|
||||
<BaseButtons>
|
||||
<BaseButton type='submit' color='info' label='Submit' />
|
||||
<BaseButton type='reset' color='info' outline label='Reset' />
|
||||
<BaseButton
|
||||
type='reset'
|
||||
color='danger'
|
||||
outline
|
||||
label='Cancel'
|
||||
onClick={() => router.push('/feedback/feedback-list')}
|
||||
/>
|
||||
</BaseButtons>
|
||||
</Form>
|
||||
</Formik>
|
||||
</CardBox>
|
||||
</SectionMain>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
FeedbackNew.getLayout = function getLayout(page: ReactElement) {
|
||||
return (
|
||||
<LayoutAuthenticated permission={'CREATE_FEEDBACK'}>
|
||||
{page}
|
||||
</LayoutAuthenticated>
|
||||
);
|
||||
};
|
||||
|
||||
export default FeedbackNew;
|
||||
164
frontend/src/pages/feedback/feedback-table.tsx
Normal file
164
frontend/src/pages/feedback/feedback-table.tsx
Normal file
@ -0,0 +1,164 @@
|
||||
import { mdiChartTimelineVariant } from '@mdi/js';
|
||||
import Head from 'next/head';
|
||||
import { uniqueId } from 'lodash';
|
||||
import React, { ReactElement, useState } from 'react';
|
||||
import CardBox from '../../components/CardBox';
|
||||
import LayoutAuthenticated from '../../layouts/Authenticated';
|
||||
import SectionMain from '../../components/SectionMain';
|
||||
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton';
|
||||
import { getPageTitle } from '../../config';
|
||||
import TableFeedback from '../../components/Feedback/TableFeedback';
|
||||
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/feedback/feedbackSlice';
|
||||
|
||||
import { hasPermission } from '../../helpers/userPermissions';
|
||||
|
||||
const FeedbackTablesPage = () => {
|
||||
const [filterItems, setFilterItems] = useState([]);
|
||||
const [csvFile, setCsvFile] = useState<File | null>(null);
|
||||
const [isModalActive, setIsModalActive] = useState(false);
|
||||
const [showTableView, setShowTableView] = useState(false);
|
||||
|
||||
const { currentUser } = useAppSelector((state) => state.auth);
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const [filters] = useState([
|
||||
{ label: 'Title', title: 'title' },
|
||||
{ label: 'Description', title: 'description' },
|
||||
]);
|
||||
|
||||
const hasCreatePermission =
|
||||
currentUser && hasPermission(currentUser, 'CREATE_FEEDBACK');
|
||||
|
||||
const addFilter = () => {
|
||||
const newItem = {
|
||||
id: uniqueId(),
|
||||
fields: {
|
||||
filterValue: '',
|
||||
filterValueFrom: '',
|
||||
filterValueTo: '',
|
||||
selectedField: '',
|
||||
},
|
||||
};
|
||||
newItem.fields.selectedField = filters[0].title;
|
||||
setFilterItems([...filterItems, newItem]);
|
||||
};
|
||||
|
||||
const getFeedbackCSV = async () => {
|
||||
const response = await axios({
|
||||
url: '/feedback?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 = 'feedbackCSV.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 (
|
||||
<>
|
||||
<Head>
|
||||
<title>{getPageTitle('Feedback')}</title>
|
||||
</Head>
|
||||
<SectionMain>
|
||||
<SectionTitleLineWithButton
|
||||
icon={mdiChartTimelineVariant}
|
||||
title='Feedback'
|
||||
main
|
||||
>
|
||||
{''}
|
||||
</SectionTitleLineWithButton>
|
||||
<CardBox className='mb-6' cardBoxClassName='flex flex-wrap'>
|
||||
{hasCreatePermission && (
|
||||
<BaseButton
|
||||
className={'mr-3'}
|
||||
href={'/feedback/feedback-new'}
|
||||
color='info'
|
||||
label='New Item'
|
||||
/>
|
||||
)}
|
||||
|
||||
<BaseButton
|
||||
className={'mr-3'}
|
||||
color='info'
|
||||
label='Filter'
|
||||
onClick={addFilter}
|
||||
/>
|
||||
<BaseButton
|
||||
className={'mr-3'}
|
||||
color='info'
|
||||
label='Download CSV'
|
||||
onClick={getFeedbackCSV}
|
||||
/>
|
||||
|
||||
{hasCreatePermission && (
|
||||
<BaseButton
|
||||
color='info'
|
||||
label='Upload CSV'
|
||||
onClick={() => setIsModalActive(true)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className='md:inline-flex items-center ms-auto'>
|
||||
<div id='delete-rows-button'></div>
|
||||
</div>
|
||||
</CardBox>
|
||||
<CardBox className='mb-6' hasTable>
|
||||
<TableFeedback
|
||||
filterItems={filterItems}
|
||||
setFilterItems={setFilterItems}
|
||||
filters={filters}
|
||||
showGrid={true}
|
||||
/>
|
||||
</CardBox>
|
||||
</SectionMain>
|
||||
<CardBoxModal
|
||||
title='Upload CSV'
|
||||
buttonColor='info'
|
||||
buttonLabel={'Confirm'}
|
||||
// buttonLabel={false ? 'Deleting...' : 'Confirm'}
|
||||
isActive={isModalActive}
|
||||
onConfirm={onModalConfirm}
|
||||
onCancel={onModalCancel}
|
||||
>
|
||||
<DragDropFilePicker
|
||||
file={csvFile}
|
||||
setFile={setCsvFile}
|
||||
formats={'.csv'}
|
||||
/>
|
||||
</CardBoxModal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
FeedbackTablesPage.getLayout = function getLayout(page: ReactElement) {
|
||||
return (
|
||||
<LayoutAuthenticated permission={'READ_FEEDBACK'}>
|
||||
{page}
|
||||
</LayoutAuthenticated>
|
||||
);
|
||||
};
|
||||
|
||||
export default FeedbackTablesPage;
|
||||
98
frontend/src/pages/feedback/feedback-view.tsx
Normal file
98
frontend/src/pages/feedback/feedback-view.tsx
Normal file
@ -0,0 +1,98 @@
|
||||
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/feedback/feedbackSlice';
|
||||
import { saveFile } from '../../helpers/fileSaver';
|
||||
import dataFormatter from '../../helpers/dataFormatter';
|
||||
import ImageField from '../../components/ImageField';
|
||||
import LayoutAuthenticated from '../../layouts/Authenticated';
|
||||
import { getPageTitle } from '../../config';
|
||||
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton';
|
||||
import SectionMain from '../../components/SectionMain';
|
||||
import CardBox from '../../components/CardBox';
|
||||
import BaseButton from '../../components/BaseButton';
|
||||
import BaseDivider from '../../components/BaseDivider';
|
||||
import { mdiChartTimelineVariant } from '@mdi/js';
|
||||
import { SwitchField } from '../../components/SwitchField';
|
||||
import FormField from '../../components/FormField';
|
||||
|
||||
import { hasPermission } from '../../helpers/userPermissions';
|
||||
|
||||
const FeedbackView = () => {
|
||||
const router = useRouter();
|
||||
const dispatch = useAppDispatch();
|
||||
const { feedback } = useAppSelector((state) => state.feedback);
|
||||
|
||||
const { currentUser } = useAppSelector((state) => state.auth);
|
||||
|
||||
const { id } = router.query;
|
||||
|
||||
function removeLastCharacter(str) {
|
||||
console.log(str, `str`);
|
||||
return str.slice(0, -1);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetch({ id }));
|
||||
}, [dispatch, id]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{getPageTitle('View feedback')}</title>
|
||||
</Head>
|
||||
<SectionMain>
|
||||
<SectionTitleLineWithButton
|
||||
icon={mdiChartTimelineVariant}
|
||||
title={removeLastCharacter('View feedback')}
|
||||
main
|
||||
>
|
||||
<BaseButton
|
||||
color='info'
|
||||
label='Edit'
|
||||
href={`/feedback/feedback-edit/?id=${id}`}
|
||||
/>
|
||||
</SectionTitleLineWithButton>
|
||||
<CardBox>
|
||||
<div className={'mb-4'}>
|
||||
<p className={'block font-bold mb-2'}>organizations</p>
|
||||
|
||||
<p>{feedback?.organizations?.name ?? 'No data'}</p>
|
||||
</div>
|
||||
|
||||
<div className={'mb-4'}>
|
||||
<p className={'block font-bold mb-2'}>Title</p>
|
||||
<p>{feedback?.title}</p>
|
||||
</div>
|
||||
|
||||
<div className={'mb-4'}>
|
||||
<p className={'block font-bold mb-2'}>Description</p>
|
||||
<p>{feedback?.description}</p>
|
||||
</div>
|
||||
|
||||
<BaseDivider />
|
||||
|
||||
<BaseButton
|
||||
color='info'
|
||||
label='Back'
|
||||
onClick={() => router.push('/feedback/feedback-list')}
|
||||
/>
|
||||
</CardBox>
|
||||
</SectionMain>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
FeedbackView.getLayout = function getLayout(page: ReactElement) {
|
||||
return (
|
||||
<LayoutAuthenticated permission={'READ_FEEDBACK'}>
|
||||
{page}
|
||||
</LayoutAuthenticated>
|
||||
);
|
||||
};
|
||||
|
||||
export default FeedbackView;
|
||||
@ -282,6 +282,47 @@ const OrganizationsView = () => {
|
||||
</CardBox>
|
||||
</>
|
||||
|
||||
<>
|
||||
<p className={'block font-bold mb-2'}>Feedback organizations</p>
|
||||
<CardBox
|
||||
className='mb-6 border border-gray-300 rounded overflow-hidden'
|
||||
hasTable
|
||||
>
|
||||
<div className='overflow-x-auto'>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Title</th>
|
||||
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{organizations.feedback_organizations &&
|
||||
Array.isArray(organizations.feedback_organizations) &&
|
||||
organizations.feedback_organizations.map((item: any) => (
|
||||
<tr
|
||||
key={item.id}
|
||||
onClick={() =>
|
||||
router.push(
|
||||
`/feedback/feedback-view/?id=${item.id}`,
|
||||
)
|
||||
}
|
||||
>
|
||||
<td data-label='title'>{item.title}</td>
|
||||
|
||||
<td data-label='description'>{item.description}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{!organizations?.feedback_organizations?.length && (
|
||||
<div className={'text-center py-4'}>No data</div>
|
||||
)}
|
||||
</CardBox>
|
||||
</>
|
||||
|
||||
<BaseDivider />
|
||||
|
||||
<BaseButton
|
||||
|
||||
236
frontend/src/stores/feedback/feedbackSlice.ts
Normal file
236
frontend/src/stores/feedback/feedbackSlice.ts
Normal file
@ -0,0 +1,236 @@
|
||||
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
|
||||
import axios from 'axios';
|
||||
import {
|
||||
fulfilledNotify,
|
||||
rejectNotify,
|
||||
resetNotify,
|
||||
} from '../../helpers/notifyStateHandler';
|
||||
|
||||
interface MainState {
|
||||
feedback: any;
|
||||
loading: boolean;
|
||||
count: number;
|
||||
refetch: boolean;
|
||||
rolesWidgets: any[];
|
||||
notify: {
|
||||
showNotification: boolean;
|
||||
textNotification: string;
|
||||
typeNotification: string;
|
||||
};
|
||||
}
|
||||
|
||||
const initialState: MainState = {
|
||||
feedback: [],
|
||||
loading: false,
|
||||
count: 0,
|
||||
refetch: false,
|
||||
rolesWidgets: [],
|
||||
notify: {
|
||||
showNotification: false,
|
||||
textNotification: '',
|
||||
typeNotification: 'warn',
|
||||
},
|
||||
};
|
||||
|
||||
export const fetch = createAsyncThunk('feedback/fetch', async (data: any) => {
|
||||
const { id, query } = data;
|
||||
const result = await axios.get(`feedback${query || (id ? `/${id}` : '')}`);
|
||||
return id
|
||||
? result.data
|
||||
: { rows: result.data.rows, count: result.data.count };
|
||||
});
|
||||
|
||||
export const deleteItemsByIds = createAsyncThunk(
|
||||
'feedback/deleteByIds',
|
||||
async (data: any, { rejectWithValue }) => {
|
||||
try {
|
||||
await axios.post('feedback/deleteByIds', { data });
|
||||
} catch (error) {
|
||||
if (!error.response) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return rejectWithValue(error.response.data);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
export const deleteItem = createAsyncThunk(
|
||||
'feedback/deleteFeedback',
|
||||
async (id: string, { rejectWithValue }) => {
|
||||
try {
|
||||
await axios.delete(`feedback/${id}`);
|
||||
} catch (error) {
|
||||
if (!error.response) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return rejectWithValue(error.response.data);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
export const create = createAsyncThunk(
|
||||
'feedback/createFeedback',
|
||||
async (data: any, { rejectWithValue }) => {
|
||||
try {
|
||||
const result = await axios.post('feedback', { data });
|
||||
return result.data;
|
||||
} catch (error) {
|
||||
if (!error.response) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return rejectWithValue(error.response.data);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
export const uploadCsv = createAsyncThunk(
|
||||
'feedback/uploadCsv',
|
||||
async (file: File, { rejectWithValue }) => {
|
||||
try {
|
||||
const data = new FormData();
|
||||
data.append('file', file);
|
||||
data.append('filename', file.name);
|
||||
|
||||
const result = await axios.post('feedback/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(
|
||||
'feedback/updateFeedback',
|
||||
async (payload: any, { rejectWithValue }) => {
|
||||
try {
|
||||
const result = await axios.put(`feedback/${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 feedbackSlice = createSlice({
|
||||
name: 'feedback',
|
||||
initialState,
|
||||
reducers: {
|
||||
setRefetch: (state, action: PayloadAction<boolean>) => {
|
||||
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.feedback = action.payload.rows;
|
||||
state.count = action.payload.count;
|
||||
} else {
|
||||
state.feedback = 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, 'Feedback 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, `${'Feedback'.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, `${'Feedback'.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, `${'Feedback'.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, 'Feedback 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 } = feedbackSlice.actions;
|
||||
|
||||
export default feedbackSlice.reducer;
|
||||
@ -12,6 +12,7 @@ import rolesSlice from './roles/rolesSlice';
|
||||
import permissionsSlice from './permissions/permissionsSlice';
|
||||
import organizationsSlice from './organizations/organizationsSlice';
|
||||
import tagsSlice from './tags/tagsSlice';
|
||||
import feedbackSlice from './feedback/feedbackSlice';
|
||||
|
||||
export const store = configureStore({
|
||||
reducer: {
|
||||
@ -28,6 +29,7 @@ export const store = configureStore({
|
||||
permissions: permissionsSlice,
|
||||
organizations: organizationsSlice,
|
||||
tags: tagsSlice,
|
||||
feedback: feedbackSlice,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user