some cool changes!

This commit is contained in:
Flatlogic Bot 2025-04-07 14:55:33 +00:00
parent d2941d795a
commit 99840b4b67
30 changed files with 3332 additions and 293 deletions

View 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,
}));
}
};

View File

@ -156,6 +156,11 @@ module.exports = class OrganizationsDBApi {
transaction, transaction,
}); });
output.feedback_organizations =
await organizations.getFeedback_organizations({
transaction,
});
return output; return output;
} }

View 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;
}
},
};

View 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;
}
},
};

View 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;
}
},
};

View 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;
};

View File

@ -74,6 +74,14 @@ module.exports = function (sequelize, DataTypes) {
constraints: false, constraints: false,
}); });
db.organizations.hasMany(db.feedback, {
as: 'feedback_organizations',
foreignKey: {
name: 'organizationsId',
},
constraints: false,
});
//end loop //end loop
db.organizations.belongsTo(db.users, { db.organizations.belongsTo(db.users, {

View File

@ -84,6 +84,7 @@ module.exports = {
'permissions', 'permissions',
'organizations', 'organizations',
'tags', 'tags',
'feedback',
, ,
]; ];
await queryInterface.bulkInsert( await queryInterface.bulkInsert(
@ -249,6 +250,31 @@ primary key ("roles_permissionsId", "permissionId")
permissionId: getId('DELETE_TAGS'), 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, createdAt,
updatedAt, updatedAt,
@ -381,6 +407,31 @@ primary key ("roles_permissionsId", "permissionId")
permissionId: getId('DELETE_TAGS'), 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, createdAt,
updatedAt, updatedAt,
@ -581,6 +632,31 @@ primary key ("roles_permissionsId", "permissionId")
permissionId: getId('DELETE_TAGS'), 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, createdAt,
updatedAt, updatedAt,

View File

@ -11,9 +11,11 @@ const Organizations = db.organizations;
const Tags = db.tags; const Tags = db.tags;
const Feedback = db.feedback;
const CategoriesData = [ const CategoriesData = [
{ {
name: 'Jonas Salk', name: 'William Herschel',
// type code here for "relation_many" field // 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 // type code here for "relation_many" field
@ -29,23 +31,7 @@ const CategoriesData = [
}, },
{ {
name: 'Pierre Simon de Laplace', name: 'Archimedes',
// 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',
// type code here for "relation_many" field // type code here for "relation_many" field
@ -55,10 +41,10 @@ const CategoriesData = [
const ProjectsData = [ const ProjectsData = [
{ {
name: 'Archimedes', name: 'Werner Heisenberg',
description: 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 // type code here for "relation_many" field
@ -68,10 +54,10 @@ const ProjectsData = [
}, },
{ {
name: 'Claude Levi-Strauss', name: 'Karl Landsteiner',
description: 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 // type code here for "relation_many" field
@ -81,35 +67,9 @@ const ProjectsData = [
}, },
{ {
name: 'Francis Galton', name: 'James Watson',
description: description: 'Luminous beings are we - not this crude matter.',
'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.',
// type code here for "relation_many" field // type code here for "relation_many" field
@ -121,10 +81,9 @@ const ProjectsData = [
const TasksData = [ const TasksData = [
{ {
title: 'No one tells me shit', title: 'I got that scurvy',
description: description: 'Feel the force!',
'Through the Force, things you will see. Other places. The future - the past. Old friends long gone.',
status: 'ToDo', status: 'ToDo',
@ -134,9 +93,9 @@ const TasksData = [
// type code here for "relation_one" field // 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 // 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', status: 'InProgress',
@ -156,9 +115,9 @@ const TasksData = [
// type code here for "relation_one" field // 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 // type code here for "relation_one" field
@ -166,12 +125,11 @@ const TasksData = [
}, },
{ {
title: 'Let me tell ya', title: 'That damn diabetes',
description: description: 'Ow, ow, OW! On my ear you are!',
'Much to learn you still have my old padawan. ... This is just the beginning!',
status: 'ToDo', status: 'InProgress',
// type code here for "relation_one" field // type code here for "relation_one" field
@ -179,53 +137,9 @@ const TasksData = [
// type code here for "relation_one" field // 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'), end_date: new Date('2024-10-17'),
// 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'),
// type code here for "relation_one" field // type code here for "relation_one" field
@ -235,23 +149,15 @@ const TasksData = [
const OrganizationsData = [ const OrganizationsData = [
{ {
name: 'Pierre Simon de Laplace', name: 'Karl Landsteiner',
}, },
{ {
name: 'Isaac Newton', name: 'Thomas Hunt Morgan',
}, },
{ {
name: 'Enrico Fermi', name: 'Thomas Hunt Morgan',
},
{
name: 'Isaac Newton',
},
{
name: 'Ernst Haeckel',
}, },
]; ];
@ -259,31 +165,45 @@ const TagsData = [
{ {
// type code here for "relation_one" field // type code here for "relation_one" field
name: 'Enrico Fermi', name: 'Neils Bohr',
}, },
{ {
// type code here for "relation_one" field // type code here for "relation_one" field
name: 'Francis Galton', name: 'Emil Fischer',
}, },
{ {
// type code here for "relation_one" field // 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 // 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 // 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) { if (User2?.setOrganization) {
await User2.setOrganization(relatedOrganization2); 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" // Similar logic for "relation_many"
@ -381,28 +279,6 @@ async function associateCategoryWithOrganization() {
if (Category2?.setOrganization) { if (Category2?.setOrganization) {
await Category2.setOrganization(relatedOrganization2); 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" // Similar logic for "relation_many"
@ -442,28 +318,6 @@ async function associateProjectWithOrganization() {
if (Project2?.setOrganization) { if (Project2?.setOrganization) {
await Project2.setOrganization(relatedOrganization2); 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() { async function associateTaskWithProject() {
@ -499,28 +353,6 @@ async function associateTaskWithProject() {
if (Task2?.setProject) { if (Task2?.setProject) {
await Task2.setProject(relatedProject2); 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() { async function associateTaskWithAssigned_to() {
@ -556,28 +388,6 @@ async function associateTaskWithAssigned_to() {
if (Task2?.setAssigned_to) { if (Task2?.setAssigned_to) {
await Task2.setAssigned_to(relatedAssigned_to2); 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() { async function associateTaskWithCategory() {
@ -613,28 +423,6 @@ async function associateTaskWithCategory() {
if (Task2?.setCategory) { if (Task2?.setCategory) {
await Task2.setCategory(relatedCategory2); 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() { async function associateTaskWithOrganization() {
@ -670,28 +458,6 @@ async function associateTaskWithOrganization() {
if (Task2?.setOrganization) { if (Task2?.setOrganization) {
await Task2.setOrganization(relatedOrganization2); 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" // Similar logic for "relation_many"
@ -729,27 +495,40 @@ async function associateTagWithOrganization() {
if (Tag2?.setOrganization) { if (Tag2?.setOrganization) {
await Tag2.setOrganization(relatedOrganization2); 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())), offset: Math.floor(Math.random() * (await Organizations.count())),
}); });
const Tag3 = await Tags.findOne({ const Feedback0 = await Feedback.findOne({
order: [['id', 'ASC']], order: [['id', 'ASC']],
offset: 3, offset: 0,
}); });
if (Tag3?.setOrganization) { if (Feedback0?.setOrganization) {
await Tag3.setOrganization(relatedOrganization3); await Feedback0.setOrganization(relatedOrganization0);
} }
const relatedOrganization4 = await Organizations.findOne({ const relatedOrganization1 = await Organizations.findOne({
offset: Math.floor(Math.random() * (await Organizations.count())), offset: Math.floor(Math.random() * (await Organizations.count())),
}); });
const Tag4 = await Tags.findOne({ const Feedback1 = await Feedback.findOne({
order: [['id', 'ASC']], order: [['id', 'ASC']],
offset: 4, offset: 1,
}); });
if (Tag4?.setOrganization) { if (Feedback1?.setOrganization) {
await Tag4.setOrganization(relatedOrganization4); 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 Tags.bulkCreate(TagsData);
await Feedback.bulkCreate(FeedbackData);
await Promise.all([ await Promise.all([
// Similar logic for "relation_many" // Similar logic for "relation_many"
@ -791,6 +572,8 @@ module.exports = {
// Similar logic for "relation_many" // Similar logic for "relation_many"
await associateTagWithOrganization(), await associateTagWithOrganization(),
await associateFeedbackWithOrganization(),
]); ]);
}, },
@ -804,5 +587,7 @@ module.exports = {
await queryInterface.bulkDelete('organizations', null, {}); await queryInterface.bulkDelete('organizations', null, {});
await queryInterface.bulkDelete('tags', null, {}); await queryInterface.bulkDelete('tags', null, {});
await queryInterface.bulkDelete('feedback', null, {});
}, },
}; };

View 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),
);
},
};

View File

@ -35,6 +35,8 @@ const organizationsRoutes = require('./routes/organizations');
const tagsRoutes = require('./routes/tags'); const tagsRoutes = require('./routes/tags');
const feedbackRoutes = require('./routes/feedback');
const getBaseUrl = (url) => { const getBaseUrl = (url) => {
if (!url) return ''; if (!url) return '';
return url.endsWith('/api') ? url.slice(0, -4) : url; return url.endsWith('/api') ? url.slice(0, -4) : url;
@ -148,6 +150,12 @@ app.use(
tagsRoutes, tagsRoutes,
); );
app.use(
'/api/feedback',
passport.authenticate('jwt', { session: false }),
feedbackRoutes,
);
app.use( app.use(
'/api/openai', '/api/openai',
passport.authenticate('jwt', { session: false }), passport.authenticate('jwt', { session: false }),

View 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;

View 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;
}
}
};

View File

@ -52,6 +52,8 @@ module.exports = class SearchService {
organizations: ['name'], organizations: ['name'],
tags: ['name'], tags: ['name'],
feedback: ['title', 'description'],
}; };
const columnsInt = {}; const columnsInt = {};

View File

@ -42,6 +42,8 @@ export default function BaseButton({
roundedFull = false, roundedFull = false,
onClick, onClick,
}: Props) { }: 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 corners = useAppSelector((state) => state.style.corners);
const componentClass = [ const componentClass = [
'inline-flex', 'inline-flex',
@ -54,9 +56,9 @@ export default function BaseButton({
'duration-150', 'duration-150',
'border', 'border',
disabled ? 'cursor-not-allowed' : 'cursor-pointer', disabled ? 'cursor-not-allowed' : 'cursor-pointer',
roundedFull ? 'rounded-full' : `${corners}`, getButtonColor(finalColor, outline, !disabled, active),
getButtonColor(color, outline, !disabled, active), getButtonColor(finalColor, outline, !disabled, active),
className, getButtonColor(finalColor, outline, !disabled, active),
]; ];
if (!label && icon) { if (!label && icon) {

View 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;

View 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;

View 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;

View 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}
/>,
];
},
},
];
};

View File

@ -76,6 +76,14 @@ const menuAside: MenuAsideItem[] = [
icon: icon.mdiTable ? icon.mdiTable : icon.mdiTable, icon: icon.mdiTable ? icon.mdiTable : icon.mdiTable,
permissions: 'READ_TAGS', 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', href: '/profile',
label: 'Profile', label: 'Profile',

View File

@ -30,6 +30,7 @@ const Dashboard = () => {
const [permissions, setPermissions] = React.useState('Loading...'); const [permissions, setPermissions] = React.useState('Loading...');
const [organizations, setOrganizations] = React.useState('Loading...'); const [organizations, setOrganizations] = React.useState('Loading...');
const [tags, setTags] = React.useState('Loading...'); const [tags, setTags] = React.useState('Loading...');
const [feedback, setFeedback] = React.useState('Loading...');
const [widgetsRole, setWidgetsRole] = React.useState({ const [widgetsRole, setWidgetsRole] = React.useState({
role: { value: '', label: '' }, role: { value: '', label: '' },
@ -51,6 +52,7 @@ const Dashboard = () => {
'permissions', 'permissions',
'organizations', 'organizations',
'tags', 'tags',
'feedback',
]; ];
const fns = [ const fns = [
setUsers, setUsers,
@ -61,6 +63,7 @@ const Dashboard = () => {
setPermissions, setPermissions,
setOrganizations, setOrganizations,
setTags, setTags,
setFeedback,
]; ];
const requests = entities.map((entity, index) => { const requests = entities.map((entity, index) => {
@ -424,6 +427,38 @@ const Dashboard = () => {
</div> </div>
</Link> </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> </div>
</SectionMain> </SectionMain>
</> </>

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View File

@ -282,6 +282,47 @@ const OrganizationsView = () => {
</CardBox> </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 /> <BaseDivider />
<BaseButton <BaseButton

View 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;

View File

@ -12,6 +12,7 @@ import rolesSlice from './roles/rolesSlice';
import permissionsSlice from './permissions/permissionsSlice'; import permissionsSlice from './permissions/permissionsSlice';
import organizationsSlice from './organizations/organizationsSlice'; import organizationsSlice from './organizations/organizationsSlice';
import tagsSlice from './tags/tagsSlice'; import tagsSlice from './tags/tagsSlice';
import feedbackSlice from './feedback/feedbackSlice';
export const store = configureStore({ export const store = configureStore({
reducer: { reducer: {
@ -28,6 +29,7 @@ export const store = configureStore({
permissions: permissionsSlice, permissions: permissionsSlice,
organizations: organizationsSlice, organizations: organizationsSlice,
tags: tagsSlice, tags: tagsSlice,
feedback: feedbackSlice,
}, },
}); });