Compare commits

...

7 Commits

Author SHA1 Message Date
Flatlogic Bot
99840b4b67 some cool changes! 2025-04-07 14:55:33 +00:00
Flatlogic Bot
d2941d795a fix button, fix alert 2025-04-05 12:52:10 +00:00
Flatlogic Bot
70edd998ed add tags display 2025-04-05 11:46:04 +00:00
Flatlogic Bot
26c535c419 adding tags entity 2025-04-05 11:34:42 +00:00
Flatlogic Bot
b473ee6362 sidebar darker 2025-04-05 11:29:43 +00:00
Flatlogic Bot
5db47fd8f9 ihiu 2025-04-04 20:28:14 +00:00
Flatlogic Bot
2fbb03957e add alert 2025-04-04 20:22:07 +00:00
71 changed files with 6795 additions and 253 deletions

5
.gitignore vendored
View File

@ -1,3 +1,8 @@
node_modules/
*/node_modules/
*/build/
**/node_modules/
**/build/
.DS_Store
.env

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

@ -152,6 +152,15 @@ module.exports = class OrganizationsDBApi {
transaction,
});
output.tags_organizations = await organizations.getTags_organizations({
transaction,
});
output.feedback_organizations =
await organizations.getFeedback_organizations({
transaction,
});
return output;
}

302
backend/src/db/api/tags.js Normal file
View File

@ -0,0 +1,302 @@
const db = require('../models');
const FileDBApi = require('./file');
const crypto = require('crypto');
const Utils = require('../utils');
const Sequelize = db.Sequelize;
const Op = Sequelize.Op;
module.exports = class TagsDBApi {
static async create(data, options) {
const currentUser = (options && options.currentUser) || { id: null };
const transaction = (options && options.transaction) || undefined;
const tags = await db.tags.create(
{
id: data.id || undefined,
name: data.name || null,
importHash: data.importHash || null,
createdById: currentUser.id,
updatedById: currentUser.id,
},
{ transaction },
);
await tags.setOrganizations(data.organizations || null, {
transaction,
});
return tags;
}
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 tagsData = data.map((item, index) => ({
id: item.id || undefined,
name: item.name || null,
importHash: item.importHash || null,
createdById: currentUser.id,
updatedById: currentUser.id,
createdAt: new Date(Date.now() + index * 1000),
}));
// Bulk create items
const tags = await db.tags.bulkCreate(tagsData, { transaction });
// For each item created, replace relation files
return tags;
}
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 tags = await db.tags.findByPk(id, {}, { transaction });
const updatePayload = {};
if (data.name !== undefined) updatePayload.name = data.name;
updatePayload.updatedById = currentUser.id;
await tags.update(updatePayload, { transaction });
if (data.organizations !== undefined) {
await tags.setOrganizations(
data.organizations,
{ transaction },
);
}
return tags;
}
static async deleteByIds(ids, options) {
const currentUser = (options && options.currentUser) || { id: null };
const transaction = (options && options.transaction) || undefined;
const tags = await db.tags.findAll({
where: {
id: {
[Op.in]: ids,
},
},
transaction,
});
await db.sequelize.transaction(async (transaction) => {
for (const record of tags) {
await record.update({ deletedBy: currentUser.id }, { transaction });
}
for (const record of tags) {
await record.destroy({ transaction });
}
});
return tags;
}
static async remove(id, options) {
const currentUser = (options && options.currentUser) || { id: null };
const transaction = (options && options.transaction) || undefined;
const tags = await db.tags.findByPk(id, options);
await tags.update(
{
deletedBy: currentUser.id,
},
{
transaction,
},
);
await tags.destroy({
transaction,
});
return tags;
}
static async findBy(where, options) {
const transaction = (options && options.transaction) || undefined;
const tags = await db.tags.findOne({ where }, { transaction });
if (!tags) {
return tags;
}
const output = tags.get({ plain: true });
output.organizations = await tags.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.name) {
where = {
...where,
[Op.and]: Utils.ilike('tags', 'name', filter.name),
};
}
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.tags.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('tags', 'name', query),
],
};
}
const records = await db.tags.findAll({
attributes: ['id', 'name'],
where,
limit: limit ? Number(limit) : undefined,
offset: offset ? Number(offset) : undefined,
orderBy: [['name', 'ASC']],
});
return records.map((record) => ({
id: record.id,
label: record.name,
}));
}
};

View File

@ -43,6 +43,10 @@ module.exports = class TasksDBApi {
transaction,
});
await tasks.setTags(data.tags || [], {
transaction,
});
return tasks;
}
@ -130,6 +134,10 @@ module.exports = class TasksDBApi {
);
}
if (data.tags !== undefined) {
await tasks.setTags(data.tags, { transaction });
}
return tasks;
}
@ -207,6 +215,10 @@ module.exports = class TasksDBApi {
transaction,
});
output.tags = await tasks.getTags({
transaction,
});
return output;
}
@ -314,6 +326,12 @@ module.exports = class TasksDBApi {
model: db.organizations,
as: 'organizations',
},
{
model: db.tags,
as: 'tags',
required: false,
},
];
if (filter) {
@ -411,6 +429,38 @@ module.exports = class TasksDBApi {
};
}
if (filter.tags) {
const searchTerms = filter.tags.split('|');
include = [
{
model: db.tags,
as: 'tags_filter',
required: searchTerms.length > 0,
where:
searchTerms.length > 0
? {
[Op.or]: [
{
id: {
[Op.in]: searchTerms.map((term) => Utils.uuid(term)),
},
},
{
name: {
[Op.or]: searchTerms.map((term) => ({
[Op.iLike]: `%${term}%`,
})),
},
},
],
}
: undefined,
},
...include,
];
}
if (filter.createdAtRange) {
const [start, end] = filter.createdAtRange;

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(
'tags',
{
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(
'tags',
'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('tags', 'organizationsId', {
transaction,
});
await queryInterface.dropTable('tags', { 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(
'tags',
'name',
{
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('tags', 'name', { transaction });
await transaction.commit();
} catch (err) {
await transaction.rollback();
throw err;
}
},
};

View File

@ -0,0 +1,36 @@
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 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 transaction.commit();
} catch (err) {
await transaction.rollback();
throw err;
}
},
};

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

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

View File

@ -0,0 +1,57 @@
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 tags = sequelize.define(
'tags',
{
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true,
},
name: {
type: DataTypes.TEXT,
},
importHash: {
type: DataTypes.STRING(255),
allowNull: true,
unique: true,
},
},
{
timestamps: true,
paranoid: true,
freezeTableName: true,
},
);
tags.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.tags.belongsTo(db.organizations, {
as: 'organizations',
foreignKey: {
name: 'organizationsId',
},
constraints: false,
});
db.tags.belongsTo(db.users, {
as: 'createdBy',
});
db.tags.belongsTo(db.users, {
as: 'updatedBy',
});
};
return tags;
};

View File

@ -50,6 +50,24 @@ module.exports = function (sequelize, DataTypes) {
);
tasks.associate = (db) => {
db.tasks.belongsToMany(db.tags, {
as: 'tags',
foreignKey: {
name: 'tasks_tagsId',
},
constraints: false,
through: 'tasksTagsTags',
});
db.tasks.belongsToMany(db.tags, {
as: 'tags_filter',
foreignKey: {
name: 'tasks_tagsId',
},
constraints: false,
through: 'tasksTagsTags',
});
/// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity
//end loop

View File

@ -83,6 +83,8 @@ module.exports = {
'roles',
'permissions',
'organizations',
'tags',
'feedback',
,
];
await queryInterface.bulkInsert(
@ -223,6 +225,56 @@ primary key ("roles_permissionsId", "permissionId")
permissionId: getId('DELETE_TASKS'),
},
{
createdAt,
updatedAt,
roles_permissionsId: getId('User'),
permissionId: getId('CREATE_TAGS'),
},
{
createdAt,
updatedAt,
roles_permissionsId: getId('User'),
permissionId: getId('READ_TAGS'),
},
{
createdAt,
updatedAt,
roles_permissionsId: getId('User'),
permissionId: getId('UPDATE_TAGS'),
},
{
createdAt,
updatedAt,
roles_permissionsId: getId('User'),
permissionId: getId('DELETE_TAGS'),
},
{
createdAt,
updatedAt,
roles_permissionsId: getId('User'),
permissionId: getId('CREATE_FEEDBACK'),
},
{
createdAt,
updatedAt,
roles_permissionsId: getId('User'),
permissionId: getId('READ_FEEDBACK'),
},
{
createdAt,
updatedAt,
roles_permissionsId: getId('User'),
permissionId: getId('UPDATE_FEEDBACK'),
},
{
createdAt,
updatedAt,
roles_permissionsId: getId('User'),
permissionId: getId('DELETE_FEEDBACK'),
},
{
createdAt,
updatedAt,
@ -330,6 +382,56 @@ primary key ("roles_permissionsId", "permissionId")
permissionId: getId('DELETE_TASKS'),
},
{
createdAt,
updatedAt,
roles_permissionsId: getId('Administrator'),
permissionId: getId('CREATE_TAGS'),
},
{
createdAt,
updatedAt,
roles_permissionsId: getId('Administrator'),
permissionId: getId('READ_TAGS'),
},
{
createdAt,
updatedAt,
roles_permissionsId: getId('Administrator'),
permissionId: getId('UPDATE_TAGS'),
},
{
createdAt,
updatedAt,
roles_permissionsId: getId('Administrator'),
permissionId: getId('DELETE_TAGS'),
},
{
createdAt,
updatedAt,
roles_permissionsId: getId('Administrator'),
permissionId: getId('CREATE_FEEDBACK'),
},
{
createdAt,
updatedAt,
roles_permissionsId: getId('Administrator'),
permissionId: getId('READ_FEEDBACK'),
},
{
createdAt,
updatedAt,
roles_permissionsId: getId('Administrator'),
permissionId: getId('UPDATE_FEEDBACK'),
},
{
createdAt,
updatedAt,
roles_permissionsId: getId('Administrator'),
permissionId: getId('DELETE_FEEDBACK'),
},
{
createdAt,
updatedAt,
@ -505,6 +607,56 @@ primary key ("roles_permissionsId", "permissionId")
permissionId: getId('DELETE_ORGANIZATIONS'),
},
{
createdAt,
updatedAt,
roles_permissionsId: getId('SuperAdmin'),
permissionId: getId('CREATE_TAGS'),
},
{
createdAt,
updatedAt,
roles_permissionsId: getId('SuperAdmin'),
permissionId: getId('READ_TAGS'),
},
{
createdAt,
updatedAt,
roles_permissionsId: getId('SuperAdmin'),
permissionId: getId('UPDATE_TAGS'),
},
{
createdAt,
updatedAt,
roles_permissionsId: getId('SuperAdmin'),
permissionId: getId('DELETE_TAGS'),
},
{
createdAt,
updatedAt,
roles_permissionsId: getId('SuperAdmin'),
permissionId: getId('CREATE_FEEDBACK'),
},
{
createdAt,
updatedAt,
roles_permissionsId: getId('SuperAdmin'),
permissionId: getId('READ_FEEDBACK'),
},
{
createdAt,
updatedAt,
roles_permissionsId: getId('SuperAdmin'),
permissionId: getId('UPDATE_FEEDBACK'),
},
{
createdAt,
updatedAt,
roles_permissionsId: getId('SuperAdmin'),
permissionId: getId('DELETE_FEEDBACK'),
},
{
createdAt,
updatedAt,

View File

@ -9,9 +9,13 @@ const Tasks = db.tasks;
const Organizations = db.organizations;
const Tags = db.tags;
const Feedback = db.feedback;
const CategoriesData = [
{
name: 'Justus Liebig',
name: 'William Herschel',
// type code here for "relation_many" field
@ -19,7 +23,7 @@ const CategoriesData = [
},
{
name: 'John Bardeen',
name: 'Frederick Gowland Hopkins',
// type code here for "relation_many" field
@ -27,23 +31,7 @@ const CategoriesData = [
},
{
name: 'Jean Piaget',
// type code here for "relation_many" field
// type code here for "relation_one" field
},
{
name: 'Sheldon Glashow',
// type code here for "relation_many" field
// type code here for "relation_one" field
},
{
name: 'Linus Pauling',
name: 'Archimedes',
// type code here for "relation_many" field
@ -53,10 +41,10 @@ const CategoriesData = [
const ProjectsData = [
{
name: 'Claude Levi-Strauss',
name: 'Werner Heisenberg',
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.',
'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
@ -66,9 +54,10 @@ const ProjectsData = [
},
{
name: 'Max von Laue',
name: 'Karl Landsteiner',
description: 'You will find only what you bring in.',
description:
'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
@ -78,33 +67,9 @@ const ProjectsData = [
},
{
name: 'Gertrude Belle Elion',
name: 'James Watson',
description: 'Already know you that which you need.',
// type code here for "relation_many" field
// type code here for "relation_many" field
// type code here for "relation_one" field
},
{
name: 'Emil Kraepelin',
description: 'Good relations with the Wookiees, I have.',
// type code here for "relation_many" field
// type code here for "relation_many" field
// type code here for "relation_one" field
},
{
name: 'Ernst Haeckel',
description: 'Truly wonderful, the mind of a child is.',
description: 'Luminous beings are we - not this crude matter.',
// type code here for "relation_many" field
@ -116,29 +81,9 @@ const ProjectsData = [
const TasksData = [
{
title: 'I want my damn cart back',
title: 'I got that scurvy',
description: 'At an end your rule is, and not short enough it was!',
status: 'InProgress',
// 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-05-30'),
end_date: new Date('2024-12-14'),
// type code here for "relation_one" field
},
{
title: 'Yup',
description: 'Younglings, younglings gather round.',
description: 'Feel the force!',
status: 'ToDo',
@ -148,18 +93,19 @@ const TasksData = [
// type code here for "relation_one" field
start_date: new Date('2024-09-14'),
start_date: new Date('2024-12-05'),
end_date: new Date('2024-11-06'),
end_date: new Date('2024-12-01'),
// type code here for "relation_one" field
// type code here for "relation_many" field
},
{
title: 'Turd gone wrong',
title: 'That damn gimble',
description:
'Much to learn you still have my old padawan. ... This is just the beginning!',
description: 'Adventure. Excitement. A Jedi craves not these things.',
status: 'InProgress',
@ -169,15 +115,17 @@ const TasksData = [
// type code here for "relation_one" field
start_date: new Date('2024-11-17'),
start_date: new Date('2024-06-22'),
end_date: new Date('2024-06-18'),
end_date: new Date('2024-05-30'),
// type code here for "relation_one" field
// type code here for "relation_many" field
},
{
title: 'I want my damn cart back',
title: 'That damn diabetes',
description: 'Ow, ow, OW! On my ear you are!',
@ -189,53 +137,73 @@ const TasksData = [
// type code here for "relation_one" field
start_date: new Date('2025-02-28'),
start_date: new Date('2024-06-13'),
end_date: new Date('2024-04-05'),
// type code here for "relation_one" field
},
{
title: 'My buddy Harlen',
description: 'Around the survivors a perimeter create.',
status: 'ToDo',
end_date: new Date('2024-10-17'),
// 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-10-19'),
end_date: new Date('2024-06-01'),
// type code here for "relation_one" field
// type code here for "relation_many" field
},
];
const OrganizationsData = [
{
name: 'Alfred Kinsey',
name: 'Karl Landsteiner',
},
{
name: 'Thomas Hunt Morgan',
},
{
name: 'Thomas Hunt Morgan',
},
];
const TagsData = [
{
// type code here for "relation_one" field
name: 'Neils Bohr',
},
{
// type code here for "relation_one" field
name: 'Emil Fischer',
},
{
// type code here for "relation_one" field
name: 'Jean Piaget',
},
];
const FeedbackData = [
{
name: 'Rudolf Virchow',
// type code here for "relation_one" field
title: 'That damn diabetes',
description: 'Let me tell ya',
},
{
name: 'Werner Heisenberg',
// type code here for "relation_one" field
title: 'Like a red-headed stepchild',
description: 'Yup',
},
{
name: 'Enrico Fermi',
// type code here for "relation_one" field
title: 'Always the last one to the party',
description: 'Come on now',
},
];
@ -274,28 +242,6 @@ async function associateUserWithOrganization() {
if (User2?.setOrganization) {
await User2.setOrganization(relatedOrganization2);
}
const relatedOrganization3 = await Organizations.findOne({
offset: Math.floor(Math.random() * (await Organizations.count())),
});
const User3 = await Users.findOne({
order: [['id', 'ASC']],
offset: 3,
});
if (User3?.setOrganization) {
await User3.setOrganization(relatedOrganization3);
}
const relatedOrganization4 = await Organizations.findOne({
offset: Math.floor(Math.random() * (await Organizations.count())),
});
const User4 = await Users.findOne({
order: [['id', 'ASC']],
offset: 4,
});
if (User4?.setOrganization) {
await User4.setOrganization(relatedOrganization4);
}
}
// Similar logic for "relation_many"
@ -333,28 +279,6 @@ async function associateCategoryWithOrganization() {
if (Category2?.setOrganization) {
await Category2.setOrganization(relatedOrganization2);
}
const relatedOrganization3 = await Organizations.findOne({
offset: Math.floor(Math.random() * (await Organizations.count())),
});
const Category3 = await Categories.findOne({
order: [['id', 'ASC']],
offset: 3,
});
if (Category3?.setOrganization) {
await Category3.setOrganization(relatedOrganization3);
}
const relatedOrganization4 = await Organizations.findOne({
offset: Math.floor(Math.random() * (await Organizations.count())),
});
const Category4 = await Categories.findOne({
order: [['id', 'ASC']],
offset: 4,
});
if (Category4?.setOrganization) {
await Category4.setOrganization(relatedOrganization4);
}
}
// Similar logic for "relation_many"
@ -394,28 +318,6 @@ async function associateProjectWithOrganization() {
if (Project2?.setOrganization) {
await Project2.setOrganization(relatedOrganization2);
}
const relatedOrganization3 = await Organizations.findOne({
offset: Math.floor(Math.random() * (await Organizations.count())),
});
const Project3 = await Projects.findOne({
order: [['id', 'ASC']],
offset: 3,
});
if (Project3?.setOrganization) {
await Project3.setOrganization(relatedOrganization3);
}
const relatedOrganization4 = await Organizations.findOne({
offset: Math.floor(Math.random() * (await Organizations.count())),
});
const Project4 = await Projects.findOne({
order: [['id', 'ASC']],
offset: 4,
});
if (Project4?.setOrganization) {
await Project4.setOrganization(relatedOrganization4);
}
}
async function associateTaskWithProject() {
@ -451,28 +353,6 @@ async function associateTaskWithProject() {
if (Task2?.setProject) {
await Task2.setProject(relatedProject2);
}
const relatedProject3 = await Projects.findOne({
offset: Math.floor(Math.random() * (await Projects.count())),
});
const Task3 = await Tasks.findOne({
order: [['id', 'ASC']],
offset: 3,
});
if (Task3?.setProject) {
await Task3.setProject(relatedProject3);
}
const relatedProject4 = await Projects.findOne({
offset: Math.floor(Math.random() * (await Projects.count())),
});
const Task4 = await Tasks.findOne({
order: [['id', 'ASC']],
offset: 4,
});
if (Task4?.setProject) {
await Task4.setProject(relatedProject4);
}
}
async function associateTaskWithAssigned_to() {
@ -508,28 +388,6 @@ async function associateTaskWithAssigned_to() {
if (Task2?.setAssigned_to) {
await Task2.setAssigned_to(relatedAssigned_to2);
}
const relatedAssigned_to3 = await Users.findOne({
offset: Math.floor(Math.random() * (await Users.count())),
});
const Task3 = await Tasks.findOne({
order: [['id', 'ASC']],
offset: 3,
});
if (Task3?.setAssigned_to) {
await Task3.setAssigned_to(relatedAssigned_to3);
}
const relatedAssigned_to4 = await Users.findOne({
offset: Math.floor(Math.random() * (await Users.count())),
});
const Task4 = await Tasks.findOne({
order: [['id', 'ASC']],
offset: 4,
});
if (Task4?.setAssigned_to) {
await Task4.setAssigned_to(relatedAssigned_to4);
}
}
async function associateTaskWithCategory() {
@ -565,28 +423,6 @@ async function associateTaskWithCategory() {
if (Task2?.setCategory) {
await Task2.setCategory(relatedCategory2);
}
const relatedCategory3 = await Categories.findOne({
offset: Math.floor(Math.random() * (await Categories.count())),
});
const Task3 = await Tasks.findOne({
order: [['id', 'ASC']],
offset: 3,
});
if (Task3?.setCategory) {
await Task3.setCategory(relatedCategory3);
}
const relatedCategory4 = await Categories.findOne({
offset: Math.floor(Math.random() * (await Categories.count())),
});
const Task4 = await Tasks.findOne({
order: [['id', 'ASC']],
offset: 4,
});
if (Task4?.setCategory) {
await Task4.setCategory(relatedCategory4);
}
}
async function associateTaskWithOrganization() {
@ -622,27 +458,77 @@ async function associateTaskWithOrganization() {
if (Task2?.setOrganization) {
await Task2.setOrganization(relatedOrganization2);
}
}
const relatedOrganization3 = await Organizations.findOne({
// Similar logic for "relation_many"
async function associateTagWithOrganization() {
const relatedOrganization0 = await Organizations.findOne({
offset: Math.floor(Math.random() * (await Organizations.count())),
});
const Task3 = await Tasks.findOne({
const Tag0 = await Tags.findOne({
order: [['id', 'ASC']],
offset: 3,
offset: 0,
});
if (Task3?.setOrganization) {
await Task3.setOrganization(relatedOrganization3);
if (Tag0?.setOrganization) {
await Tag0.setOrganization(relatedOrganization0);
}
const relatedOrganization4 = await Organizations.findOne({
const relatedOrganization1 = await Organizations.findOne({
offset: Math.floor(Math.random() * (await Organizations.count())),
});
const Task4 = await Tasks.findOne({
const Tag1 = await Tags.findOne({
order: [['id', 'ASC']],
offset: 4,
offset: 1,
});
if (Task4?.setOrganization) {
await Task4.setOrganization(relatedOrganization4);
if (Tag1?.setOrganization) {
await Tag1.setOrganization(relatedOrganization1);
}
const relatedOrganization2 = await Organizations.findOne({
offset: Math.floor(Math.random() * (await Organizations.count())),
});
const Tag2 = await Tags.findOne({
order: [['id', 'ASC']],
offset: 2,
});
if (Tag2?.setOrganization) {
await Tag2.setOrganization(relatedOrganization2);
}
}
async function associateFeedbackWithOrganization() {
const relatedOrganization0 = await Organizations.findOne({
offset: Math.floor(Math.random() * (await Organizations.count())),
});
const Feedback0 = await Feedback.findOne({
order: [['id', 'ASC']],
offset: 0,
});
if (Feedback0?.setOrganization) {
await Feedback0.setOrganization(relatedOrganization0);
}
const relatedOrganization1 = await Organizations.findOne({
offset: Math.floor(Math.random() * (await Organizations.count())),
});
const Feedback1 = await Feedback.findOne({
order: [['id', 'ASC']],
offset: 1,
});
if (Feedback1?.setOrganization) {
await Feedback1.setOrganization(relatedOrganization1);
}
const relatedOrganization2 = await Organizations.findOne({
offset: Math.floor(Math.random() * (await Organizations.count())),
});
const Feedback2 = await Feedback.findOne({
order: [['id', 'ASC']],
offset: 2,
});
if (Feedback2?.setOrganization) {
await Feedback2.setOrganization(relatedOrganization2);
}
}
@ -656,6 +542,10 @@ module.exports = {
await Organizations.bulkCreate(OrganizationsData);
await Tags.bulkCreate(TagsData);
await Feedback.bulkCreate(FeedbackData);
await Promise.all([
// Similar logic for "relation_many"
@ -678,6 +568,12 @@ module.exports = {
await associateTaskWithCategory(),
await associateTaskWithOrganization(),
// Similar logic for "relation_many"
await associateTagWithOrganization(),
await associateFeedbackWithOrganization(),
]);
},
@ -689,5 +585,9 @@ module.exports = {
await queryInterface.bulkDelete('tasks', null, {});
await queryInterface.bulkDelete('organizations', 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 = ['tags'];
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

@ -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

@ -33,6 +33,10 @@ const permissionsRoutes = require('./routes/permissions');
const organizationsRoutes = require('./routes/organizations');
const tagsRoutes = require('./routes/tags');
const feedbackRoutes = require('./routes/feedback');
const getBaseUrl = (url) => {
if (!url) return '';
return url.endsWith('/api') ? url.slice(0, -4) : url;
@ -140,6 +144,18 @@ app.use(
organizationsRoutes,
);
app.use(
'/api/tags',
passport.authenticate('jwt', { session: false }),
tagsRoutes,
);
app.use(
'/api/feedback',
passport.authenticate('jwt', { session: false }),
feedbackRoutes,
);
app.use(
'/api/openai',
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;

447
backend/src/routes/tags.js Normal file
View File

@ -0,0 +1,447 @@
const express = require('express');
const TagsService = require('../services/tags');
const TagsDBApi = require('../db/api/tags');
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('tags'));
/**
* @swagger
* components:
* schemas:
* Tags:
* type: object
* properties:
* name:
* type: string
* default: name
*/
/**
* @swagger
* tags:
* name: Tags
* description: The Tags managing API
*/
/**
* @swagger
* /api/tags:
* post:
* security:
* - bearerAuth: []
* tags: [Tags]
* 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/Tags"
* responses:
* 200:
* description: The item was successfully added
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/Tags"
* 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 TagsService.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: [Tags]
* 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/Tags"
* responses:
* 200:
* description: The items were successfully imported
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/Tags"
* 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 TagsService.bulkImport(req, res, true, link.host);
const payload = true;
res.status(200).send(payload);
}),
);
/**
* @swagger
* /api/tags/{id}:
* put:
* security:
* - bearerAuth: []
* tags: [Tags]
* 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/Tags"
* required:
* - id
* responses:
* 200:
* description: The item data was successfully updated
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/Tags"
* 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 TagsService.update(req.body.data, req.body.id, req.currentUser);
const payload = true;
res.status(200).send(payload);
}),
);
/**
* @swagger
* /api/tags/{id}:
* delete:
* security:
* - bearerAuth: []
* tags: [Tags]
* 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/Tags"
* 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 TagsService.remove(req.params.id, req.currentUser);
const payload = true;
res.status(200).send(payload);
}),
);
/**
* @swagger
* /api/tags/deleteByIds:
* post:
* security:
* - bearerAuth: []
* tags: [Tags]
* 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/Tags"
* 401:
* $ref: "#/components/responses/UnauthorizedError"
* 404:
* description: Items not found
* 500:
* description: Some server error
*/
router.post(
'/deleteByIds',
wrapAsync(async (req, res) => {
await TagsService.deleteByIds(req.body.data, req.currentUser);
const payload = true;
res.status(200).send(payload);
}),
);
/**
* @swagger
* /api/tags:
* get:
* security:
* - bearerAuth: []
* tags: [Tags]
* summary: Get all tags
* description: Get all tags
* responses:
* 200:
* description: Tags list successfully received
* content:
* application/json:
* schema:
* type: array
* items:
* $ref: "#/components/schemas/Tags"
* 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 TagsDBApi.findAll(req.query, globalAccess, {
currentUser,
});
if (filetype && filetype === 'csv') {
const fields = ['id', 'name'];
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/tags/count:
* get:
* security:
* - bearerAuth: []
* tags: [Tags]
* summary: Count all tags
* description: Count all tags
* responses:
* 200:
* description: Tags count successfully received
* content:
* application/json:
* schema:
* type: array
* items:
* $ref: "#/components/schemas/Tags"
* 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 TagsDBApi.findAll(req.query, globalAccess, {
countOnly: true,
currentUser,
});
res.status(200).send(payload);
}),
);
/**
* @swagger
* /api/tags/autocomplete:
* get:
* security:
* - bearerAuth: []
* tags: [Tags]
* summary: Find all tags that match search criteria
* description: Find all tags that match search criteria
* responses:
* 200:
* description: Tags list successfully received
* content:
* application/json:
* schema:
* type: array
* items:
* $ref: "#/components/schemas/Tags"
* 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 TagsDBApi.findAllAutocomplete(
req.query.query,
req.query.limit,
req.query.offset,
globalAccess,
organizationId,
);
res.status(200).send(payload);
});
/**
* @swagger
* /api/tags/{id}:
* get:
* security:
* - bearerAuth: []
* tags: [Tags]
* 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/Tags"
* 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 TagsDBApi.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

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

View File

@ -0,0 +1,114 @@
const db = require('../db/models');
const TagsDBApi = require('../db/api/tags');
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 TagsService {
static async create(data, currentUser) {
const transaction = await db.sequelize.transaction();
try {
await TagsDBApi.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 TagsDBApi.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 tags = await TagsDBApi.findBy({ id }, { transaction });
if (!tags) {
throw new ValidationError('tagsNotFound');
}
const updatedTags = await TagsDBApi.update(id, data, {
currentUser,
transaction,
});
await transaction.commit();
return updatedTags;
} catch (error) {
await transaction.rollback();
throw error;
}
}
static async deleteByIds(ids, currentUser) {
const transaction = await db.sequelize.transaction();
try {
await TagsDBApi.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 TagsDBApi.remove(id, {
currentUser,
transaction,
});
await transaction.commit();
} catch (error) {
await transaction.rollback();
throw error;
}
}
};

View File

@ -0,0 +1 @@
{}

View File

@ -85,7 +85,7 @@ const AsideMenuItem = ({ item, isDropdownList = false }: Props) => {
].join(' ');
return (
<li className={'px-3 py-1.5'}>
<li className="p-0 m-0 rounded-none">
{item.withDevider && <hr className={`${borders} mb-3`} />}
{item.href && (
<Link href={item.href} target={item.target} className={componentClass}>

View File

@ -65,7 +65,7 @@ export default function AsideMenuLayer({
return (
<aside
id='asideMenu'
className={`${className} zzz lg:py-2 lg:pl-2 w-60 fixed flex z-40 top-0 h-screen transition-position overflow-hidden`}
className={`${className} zzz w-60 fixed flex z-40 top-0 h-screen transition-position overflow-hidden p-0 m-0 rounded-none bg-[#f8f8f8]`}
>
<div
className={`flex-1 flex flex-col overflow-hidden dark:bg-dark-900 ${asideStyle}`}

View File

@ -3,6 +3,8 @@ import { MenuAsideItem } from '../interfaces';
import AsideMenuItem from './AsideMenuItem';
import { useAppSelector } from '../stores/hooks';
import { hasPermission } from '../helpers/userPermissions';
import BaseIcon from './BaseIcon';
import { mdiBell } from '@mdi/js';
type Props = {
menu: MenuAsideItem[];
@ -32,6 +34,10 @@ export default function AsideMenuList({
/>
);
})}
<li onClick={() => alert('Hello!')} className="flex items-center cursor-pointer px-1 py-1 text-sm hover:bg-gray-200 rounded-none">
<BaseIcon path={mdiBell} size="18" className="flex-none mx-3"/>
<span className="grow">New Item</span>
</li>
</ul>
);
}

View File

@ -42,6 +42,8 @@ export default function BaseButton({
roundedFull = false,
onClick,
}: Props) {
const buttonLabels = ["new item", "filter", "download csv", "upload csv"];
const finalColor = (label && buttonLabels.includes(label.trim().toLowerCase())) ? "danger" : color;
const corners = useAppSelector((state) => state.style.corners);
const componentClass = [
'inline-flex',
@ -54,9 +56,9 @@ export default function BaseButton({
'duration-150',
'border',
disabled ? 'cursor-not-allowed' : 'cursor-pointer',
roundedFull ? 'rounded-full' : `${corners}`,
getButtonColor(color, outline, !disabled, active),
className,
getButtonColor(finalColor, outline, !disabled, active),
getButtonColor(finalColor, outline, !disabled, active),
getButtonColor(finalColor, outline, !disabled, active),
];
if (!label && icon) {

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

@ -57,6 +57,17 @@ const KanbanCard = ({
iconClassName={'w-5'}
/>
</div>
{item.tags && item.tags.length > 0 && (
<div className="flex flex-wrap mt-2">
{item.tags.map((tag: any) => (
<span key={tag.id} className="text-xs bg-blue-200 text-blue-800 rounded px-1 py-0.5 mr-1 mb-1">
{tag.name}
</span>
))}
</div>
)}
</div>
);
};

View File

@ -90,7 +90,7 @@ const KanbanColumn = ({
const loadData = useCallback(
(page: number, filters = '') => {
const query = `?page=${page}&limit=${perPage}&field=createdAt&sort=desc&${columnFieldName}=${column.id}&${filters}`;
const query = `?page=${page}&limit=${perPage}&populate=tags&field=createdAt&sort=desc&${columnFieldName}=${column.id}&${filters}`;
setLoading(true);
Axios.get(`${entityName}${query}`)
.then((res) => {

View File

@ -0,0 +1,105 @@
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 = {
tags: any[];
loading: boolean;
onDelete: (id: string) => void;
currentPage: number;
numPages: number;
onPageChange: (page: number) => void;
};
const CardTags = ({
tags,
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_TAGS');
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 &&
tags.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={`/tags/tags-view/?id=${item.id}`}
className='text-lg font-bold leading-6 line-clamp-1'
>
{item.name}
</Link>
<div className='ml-auto '>
<ListActionsPopover
onDelete={onDelete}
itemId={item.id}
pathEdit={`/tags/tags-edit/?id=${item.id}`}
pathView={`/tags/tags-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'>Name</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>{item.name}</div>
</dd>
</div>
</dl>
</li>
))}
{!loading && tags.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 CardTags;

View File

@ -0,0 +1,90 @@
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 = {
tags: any[];
loading: boolean;
onDelete: (id: string) => void;
currentPage: number;
numPages: number;
onPageChange: (page: number) => void;
};
const ListTags = ({
tags,
loading,
onDelete,
currentPage,
numPages,
onPageChange,
}: Props) => {
const currentUser = useAppSelector((state) => state.auth.currentUser);
const hasUpdatePermission = hasPermission(currentUser, 'UPDATE_TAGS');
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 &&
tags.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={`/tags/tags-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 '}>Name</p>
<p className={'line-clamp-2'}>{item.name}</p>
</div>
</Link>
<ListActionsPopover
onDelete={onDelete}
itemId={item.id}
pathEdit={`/tags/tags-edit/?id=${item.id}`}
pathView={`/tags/tags-view/?id=${item.id}`}
hasUpdatePermission={hasUpdatePermission}
/>
</div>
</CardBox>
))}
{!loading && tags.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 ListTags;

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/tags/tagsSlice';
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 './configureTagsCols';
import _ from 'lodash';
import dataFormatter from '../../helpers/dataFormatter';
import { dataGridStyles } from '../../styles';
const perPage = 10;
const TableSampleTags = ({
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 {
tags,
loading,
count,
notify: tagsNotify,
refetch,
} = useAppSelector((state) => state.tags);
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 (tagsNotify.showNotification) {
notify(tagsNotify.typeNotification, tagsNotify.textNotification);
}
}, [tagsNotify.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, `tags`, 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={tags ?? []}
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 TableSampleTags;

View File

@ -0,0 +1,73 @@
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_TAGS');
return [
{
field: 'name',
headerName: 'Name',
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={`/tags/tags-edit/?id=${params?.row?.id}`}
pathView={`/tags/tags-view/?id=${params?.row?.id}`}
key={1}
hasUpdatePermission={hasUpdatePermission}
/>,
];
},
},
];
};

View File

@ -161,6 +161,21 @@ const CardTasks = ({
</div>
</dd>
</div>
<dt className='text-gray-500 dark:text-dark-600'>Tags</dt>
<dd className='flex items-start gap-x-2'>
<div className='flex flex-wrap gap-1'>
{item.tags && item.tags.length > 0 ? (
item.tags.map((tag) => (
<span key={tag.id || tag.name} className="bg-blue-100 text-blue-800 text-xs font-medium px-2.5 py-0.5 rounded-full">
{tag.name}
</span>
))
) : (
<span className='font-medium'>No Tags</span>
)}
</div>
</dd>
</dl>
</li>
))}

View File

@ -104,6 +104,15 @@ const ListTasks = ({
{dataFormatter.dateTimeFormatter(item.end_date)}
</p>
</div>
<div className={'flex-1 px-3'}>
<p className={'text-xs text-gray-500 '}>Tags</p>
<p className={'line-clamp-2'}>
{dataFormatter
.tagsManyListFormatter(item.tags)
.join(', ')}
</p>
</div>
</Link>
<ListActionsPopover
onDelete={onDelete}

View File

@ -166,6 +166,25 @@ export const loadColumns = async (
new Date(params.row.end_date),
},
{
field: 'tags',
headerName: 'Tags',
flex: 1,
minWidth: 120,
filterable: false,
headerClassName: 'datagrid--header',
cellClassName: 'datagrid--cell',
editable: false,
sortable: false,
type: 'singleSelect',
valueFormatter: ({ value }) =>
dataFormatter.tagsManyListFormatter(value).join(', '),
renderEditCell: (params) => (
<DataGridMultiSelect {...params} entityName={'tags'} />
),
},
{
field: 'actions',
type: 'actions',

View File

@ -32,3 +32,12 @@ li.stack-item:not(:last-child):after {
.app-sidebar-brand {
box-shadow: 0px -1px 40px rgba(112, 144, 176, 0.2);
}
/* Custom override for sidebar styling */
.app-sidebar {
background-color: #f8f8f8 !important;
margin: 0 !important;
padding: 0 !important;
border-radius: 0 !important;
}

View File

@ -103,3 +103,13 @@
@apply text-pavitra-800;
}
}
/* Custom override for sidebar in themes */
.theme-pink .app-sidebar,
.theme-green .app-sidebar {
background-color: #f8f8f8 !important;
margin: 0 !important;
padding: 0 !important;
border-radius: 0 !important;
}

View File

@ -171,4 +171,23 @@ export default {
if (!val) return '';
return { label: val.name, id: val.id };
},
tagsManyListFormatter(val) {
if (!val || !val.length) return [];
return val.map((item) => item.name);
},
tagsOneListFormatter(val) {
if (!val) return '';
return val.name;
},
tagsManyListFormatterEdit(val) {
if (!val || !val.length) return [];
return val.map((item) => {
return { id: item.id, label: item.name };
});
},
tagsOneListFormatterEdit(val) {
if (!val) return '';
return { label: val.name, id: val.id };
},
};

View File

@ -68,6 +68,22 @@ const menuAside: MenuAsideItem[] = [
icon: icon.mdiTable ? icon.mdiTable : icon.mdiTable,
permissions: 'READ_ORGANIZATIONS',
},
{
href: '/tags/tags-list',
label: 'Tags',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: icon.mdiTable ? icon.mdiTable : icon.mdiTable,
permissions: 'READ_TAGS',
},
{
href: '/feedback/feedback-list',
label: 'Feedback',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: icon.mdiTable ? icon.mdiTable : icon.mdiTable,
permissions: 'READ_FEEDBACK',
},
{
href: '/profile',
label: 'Profile',

View File

@ -29,6 +29,8 @@ const Dashboard = () => {
const [roles, setRoles] = React.useState('Loading...');
const [permissions, setPermissions] = React.useState('Loading...');
const [organizations, setOrganizations] = React.useState('Loading...');
const [tags, setTags] = React.useState('Loading...');
const [feedback, setFeedback] = React.useState('Loading...');
const [widgetsRole, setWidgetsRole] = React.useState({
role: { value: '', label: '' },
@ -49,6 +51,8 @@ const Dashboard = () => {
'roles',
'permissions',
'organizations',
'tags',
'feedback',
];
const fns = [
setUsers,
@ -58,6 +62,8 @@ const Dashboard = () => {
setRoles,
setPermissions,
setOrganizations,
setTags,
setFeedback,
];
const requests = entities.map((entity, index) => {
@ -389,6 +395,70 @@ const Dashboard = () => {
</div>
</Link>
)}
{hasPermission(currentUser, 'READ_TAGS') && (
<Link href={'/tags/tags-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'>
Tags
</div>
<div className='text-3xl leading-tight font-semibold'>
{tags}
</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>
)}
{hasPermission(currentUser, 'READ_FEEDBACK') && (
<Link href={'/feedback/feedback-list'}>
<div
className={`${
corners !== 'rounded-full' ? corners : 'rounded-3xl'
} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
<div className='flex justify-between align-center'>
<div>
<div className='text-lg leading-tight text-gray-500 dark:text-gray-400'>
Feedback
</div>
<div className='text-3xl leading-tight font-semibold'>
{feedback}
</div>
</div>
<div>
<BaseIcon
className={`${iconsColor}`}
w='w-16'
h='h-16'
size={48}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
path={icon.mdiTable || icon.mdiTable}
/>
</div>
</div>
</div>
</Link>
)}
</div>
</SectionMain>
</>

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

@ -247,6 +247,82 @@ const OrganizationsView = () => {
</CardBox>
</>
<>
<p className={'block font-bold mb-2'}>Tags organizations</p>
<CardBox
className='mb-6 border border-gray-300 rounded overflow-hidden'
hasTable
>
<div className='overflow-x-auto'>
<table>
<thead>
<tr>
<th>Name</th>
</tr>
</thead>
<tbody>
{organizations.tags_organizations &&
Array.isArray(organizations.tags_organizations) &&
organizations.tags_organizations.map((item: any) => (
<tr
key={item.id}
onClick={() =>
router.push(`/tags/tags-view/?id=${item.id}`)
}
>
<td data-label='name'>{item.name}</td>
</tr>
))}
</tbody>
</table>
</div>
{!organizations?.tags_organizations?.length && (
<div className={'text-center py-4'}>No data</div>
)}
</CardBox>
</>
<>
<p className={'block font-bold mb-2'}>Feedback organizations</p>
<CardBox
className='mb-6 border border-gray-300 rounded overflow-hidden'
hasTable
>
<div className='overflow-x-auto'>
<table>
<thead>
<tr>
<th>Title</th>
<th>Description</th>
</tr>
</thead>
<tbody>
{organizations.feedback_organizations &&
Array.isArray(organizations.feedback_organizations) &&
organizations.feedback_organizations.map((item: any) => (
<tr
key={item.id}
onClick={() =>
router.push(
`/feedback/feedback-view/?id=${item.id}`,
)
}
>
<td data-label='title'>{item.title}</td>
<td data-label='description'>{item.description}</td>
</tr>
))}
</tbody>
</table>
</div>
{!organizations?.feedback_organizations?.length && (
<div className={'text-center py-4'}>No data</div>
)}
</CardBox>
</>
<BaseDivider />
<BaseButton

View File

@ -0,0 +1,139 @@
import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js';
import Head from 'next/head';
import React, { ReactElement, useEffect, useState } from 'react';
import DatePicker from 'react-datepicker';
import 'react-datepicker/dist/react-datepicker.css';
import dayjs from 'dayjs';
import CardBox from '../../components/CardBox';
import LayoutAuthenticated from '../../layouts/Authenticated';
import SectionMain from '../../components/SectionMain';
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton';
import { getPageTitle } from '../../config';
import { Field, Form, Formik } from 'formik';
import FormField from '../../components/FormField';
import BaseDivider from '../../components/BaseDivider';
import BaseButtons from '../../components/BaseButtons';
import BaseButton from '../../components/BaseButton';
import FormCheckRadio from '../../components/FormCheckRadio';
import FormCheckRadioGroup from '../../components/FormCheckRadioGroup';
import FormFilePicker from '../../components/FormFilePicker';
import FormImagePicker from '../../components/FormImagePicker';
import { SelectField } from '../../components/SelectField';
import { SelectFieldMany } from '../../components/SelectFieldMany';
import { SwitchField } from '../../components/SwitchField';
import { RichTextField } from '../../components/RichTextField';
import { update, fetch } from '../../stores/tags/tagsSlice';
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 EditTags = () => {
const router = useRouter();
const dispatch = useAppDispatch();
const initVals = {
organizations: null,
name: '',
};
const [initialValues, setInitialValues] = useState(initVals);
const { tags } = useAppSelector((state) => state.tags);
const { currentUser } = useAppSelector((state) => state.auth);
const { tagsId } = router.query;
useEffect(() => {
dispatch(fetch({ id: tagsId }));
}, [tagsId]);
useEffect(() => {
if (typeof tags === 'object') {
setInitialValues(tags);
}
}, [tags]);
useEffect(() => {
if (typeof tags === 'object') {
const newInitialVal = { ...initVals };
Object.keys(initVals).forEach((el) => (newInitialVal[el] = tags[el]));
setInitialValues(newInitialVal);
}
}, [tags]);
const handleSubmit = async (data) => {
await dispatch(update({ id: tagsId, data }));
await router.push('/tags/tags-list');
};
return (
<>
<Head>
<title>{getPageTitle('Edit tags')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton
icon={mdiChartTimelineVariant}
title={'Edit tags'}
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='Name'>
<Field name='name' placeholder='Name' />
</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('/tags/tags-list')}
/>
</BaseButtons>
</Form>
</Formik>
</CardBox>
</SectionMain>
</>
);
};
EditTags.getLayout = function getLayout(page: ReactElement) {
return (
<LayoutAuthenticated permission={'UPDATE_TAGS'}>{page}</LayoutAuthenticated>
);
};
export default EditTags;

View File

@ -0,0 +1,137 @@
import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js';
import Head from 'next/head';
import React, { ReactElement, useEffect, useState } from 'react';
import DatePicker from 'react-datepicker';
import 'react-datepicker/dist/react-datepicker.css';
import dayjs from 'dayjs';
import CardBox from '../../components/CardBox';
import LayoutAuthenticated from '../../layouts/Authenticated';
import SectionMain from '../../components/SectionMain';
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton';
import { getPageTitle } from '../../config';
import { Field, Form, Formik } from 'formik';
import FormField from '../../components/FormField';
import BaseDivider from '../../components/BaseDivider';
import BaseButtons from '../../components/BaseButtons';
import BaseButton from '../../components/BaseButton';
import FormCheckRadio from '../../components/FormCheckRadio';
import FormCheckRadioGroup from '../../components/FormCheckRadioGroup';
import FormFilePicker from '../../components/FormFilePicker';
import FormImagePicker from '../../components/FormImagePicker';
import { SelectField } from '../../components/SelectField';
import { SelectFieldMany } from '../../components/SelectFieldMany';
import { SwitchField } from '../../components/SwitchField';
import { RichTextField } from '../../components/RichTextField';
import { update, fetch } from '../../stores/tags/tagsSlice';
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 EditTagsPage = () => {
const router = useRouter();
const dispatch = useAppDispatch();
const initVals = {
organizations: null,
name: '',
};
const [initialValues, setInitialValues] = useState(initVals);
const { tags } = useAppSelector((state) => state.tags);
const { currentUser } = useAppSelector((state) => state.auth);
const { id } = router.query;
useEffect(() => {
dispatch(fetch({ id: id }));
}, [id]);
useEffect(() => {
if (typeof tags === 'object') {
setInitialValues(tags);
}
}, [tags]);
useEffect(() => {
if (typeof tags === 'object') {
const newInitialVal = { ...initVals };
Object.keys(initVals).forEach((el) => (newInitialVal[el] = tags[el]));
setInitialValues(newInitialVal);
}
}, [tags]);
const handleSubmit = async (data) => {
await dispatch(update({ id: id, data }));
await router.push('/tags/tags-list');
};
return (
<>
<Head>
<title>{getPageTitle('Edit tags')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton
icon={mdiChartTimelineVariant}
title={'Edit tags'}
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='Name'>
<Field name='name' placeholder='Name' />
</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('/tags/tags-list')}
/>
</BaseButtons>
</Form>
</Formik>
</CardBox>
</SectionMain>
</>
);
};
EditTagsPage.getLayout = function getLayout(page: ReactElement) {
return (
<LayoutAuthenticated permission={'UPDATE_TAGS'}>{page}</LayoutAuthenticated>
);
};
export default EditTagsPage;

View File

@ -0,0 +1,160 @@
import { mdiChartTimelineVariant } from '@mdi/js';
import Head from 'next/head';
import { uniqueId } from 'lodash';
import React, { ReactElement, useState } from 'react';
import CardBox from '../../components/CardBox';
import LayoutAuthenticated from '../../layouts/Authenticated';
import SectionMain from '../../components/SectionMain';
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton';
import { getPageTitle } from '../../config';
import TableTags from '../../components/Tags/TableTags';
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/tags/tagsSlice';
import { hasPermission } from '../../helpers/userPermissions';
const TagsTablesPage = () => {
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: 'Name', title: 'name' }]);
const hasCreatePermission =
currentUser && hasPermission(currentUser, 'CREATE_TAGS');
const addFilter = () => {
const newItem = {
id: uniqueId(),
fields: {
filterValue: '',
filterValueFrom: '',
filterValueTo: '',
selectedField: '',
},
};
newItem.fields.selectedField = filters[0].title;
setFilterItems([...filterItems, newItem]);
};
const getTagsCSV = async () => {
const response = await axios({
url: '/tags?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 = 'tagsCSV.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('Tags')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton
icon={mdiChartTimelineVariant}
title='Tags'
main
>
{''}
</SectionTitleLineWithButton>
<CardBox className='mb-6' cardBoxClassName='flex flex-wrap'>
{hasCreatePermission && (
<BaseButton
className={'mr-3'}
href={'/tags/tags-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={getTagsCSV}
/>
{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>
<TableTags
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>
</>
);
};
TagsTablesPage.getLayout = function getLayout(page: ReactElement) {
return (
<LayoutAuthenticated permission={'READ_TAGS'}>{page}</LayoutAuthenticated>
);
};
export default TagsTablesPage;

View File

@ -0,0 +1,108 @@
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/tags/tagsSlice';
import { useAppDispatch } from '../../stores/hooks';
import { useRouter } from 'next/router';
import moment from 'moment';
const initialValues = {
organizations: '',
name: '',
};
const TagsNew = () => {
const router = useRouter();
const dispatch = useAppDispatch();
const handleSubmit = async (data) => {
await dispatch(create(data));
await router.push('/tags/tags-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='Name'>
<Field name='name' placeholder='Name' />
</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('/tags/tags-list')}
/>
</BaseButtons>
</Form>
</Formik>
</CardBox>
</SectionMain>
</>
);
};
TagsNew.getLayout = function getLayout(page: ReactElement) {
return (
<LayoutAuthenticated permission={'CREATE_TAGS'}>{page}</LayoutAuthenticated>
);
};
export default TagsNew;

View File

@ -0,0 +1,159 @@
import { mdiChartTimelineVariant } from '@mdi/js';
import Head from 'next/head';
import { uniqueId } from 'lodash';
import React, { ReactElement, useState } from 'react';
import CardBox from '../../components/CardBox';
import LayoutAuthenticated from '../../layouts/Authenticated';
import SectionMain from '../../components/SectionMain';
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton';
import { getPageTitle } from '../../config';
import TableTags from '../../components/Tags/TableTags';
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/tags/tagsSlice';
import { hasPermission } from '../../helpers/userPermissions';
const TagsTablesPage = () => {
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: 'Name', title: 'name' }]);
const hasCreatePermission =
currentUser && hasPermission(currentUser, 'CREATE_TAGS');
const addFilter = () => {
const newItem = {
id: uniqueId(),
fields: {
filterValue: '',
filterValueFrom: '',
filterValueTo: '',
selectedField: '',
},
};
newItem.fields.selectedField = filters[0].title;
setFilterItems([...filterItems, newItem]);
};
const getTagsCSV = async () => {
const response = await axios({
url: '/tags?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 = 'tagsCSV.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('Tags')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton
icon={mdiChartTimelineVariant}
title='Tags'
main
>
{''}
</SectionTitleLineWithButton>
<CardBox className='mb-6' cardBoxClassName='flex flex-wrap'>
{hasCreatePermission && (
<BaseButton
className={'mr-3'}
href={'/tags/tags-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={getTagsCSV}
/>
{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>
<TableTags
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>
</>
);
};
TagsTablesPage.getLayout = function getLayout(page: ReactElement) {
return (
<LayoutAuthenticated permission={'READ_TAGS'}>{page}</LayoutAuthenticated>
);
};
export default TagsTablesPage;

View File

@ -0,0 +1,91 @@
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/tags/tagsSlice';
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 TagsView = () => {
const router = useRouter();
const dispatch = useAppDispatch();
const { tags } = useAppSelector((state) => state.tags);
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 tags')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton
icon={mdiChartTimelineVariant}
title={removeLastCharacter('View tags')}
main
>
<BaseButton
color='info'
label='Edit'
href={`/tags/tags-edit/?id=${id}`}
/>
</SectionTitleLineWithButton>
<CardBox>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>organizations</p>
<p>{tags?.organizations?.name ?? 'No data'}</p>
</div>
<div className={'mb-4'}>
<p className={'block font-bold mb-2'}>Name</p>
<p>{tags?.name}</p>
</div>
<BaseDivider />
<BaseButton
color='info'
label='Back'
onClick={() => router.push('/tags/tags-list')}
/>
</CardBox>
</SectionMain>
</>
);
};
TagsView.getLayout = function getLayout(page: ReactElement) {
return (
<LayoutAuthenticated permission={'READ_TAGS'}>{page}</LayoutAuthenticated>
);
};
export default TagsView;

View File

@ -55,6 +55,8 @@ const EditTasks = () => {
end_date: new Date(),
organizations: null,
tags: [],
};
const [initialValues, setInitialValues] = useState(initVals);
@ -213,6 +215,17 @@ const EditTasks = () => {
></Field>
</FormField>
<FormField label='Tags' labelFor='tags'>
<Field
name='tags'
id='tags'
component={SelectFieldMany}
options={initialValues.tags}
itemRef={'tags'}
showField={'name'}
></Field>
</FormField>
<BaseDivider />
<BaseButtons>
<BaseButton type='submit' color='info' label='Submit' />

View File

@ -55,6 +55,8 @@ const EditTasksPage = () => {
end_date: new Date(),
organizations: null,
tags: [],
};
const [initialValues, setInitialValues] = useState(initVals);
@ -211,6 +213,17 @@ const EditTasksPage = () => {
></Field>
</FormField>
<FormField label='Tags' labelFor='tags'>
<Field
name='tags'
id='tags'
component={SelectFieldMany}
options={initialValues.tags}
itemRef={'tags'}
showField={'name'}
></Field>
</FormField>
<BaseDivider />
<BaseButtons>
<BaseButton type='submit' color='info' label='Submit' />

View File

@ -41,6 +41,7 @@ const TasksTablesPage = () => {
{ label: 'Category', title: 'category' },
{ label: 'Tags', title: 'tags' },
{
label: 'Status',
title: 'status',
@ -109,6 +110,7 @@ const TasksTablesPage = () => {
<CardBox className='mb-6' cardBoxClassName='flex flex-wrap'>
{hasCreatePermission && (
<BaseButton
small={true}
className={'mr-3'}
href={'/tasks/tasks-new'}
color='info'
@ -117,12 +119,14 @@ const TasksTablesPage = () => {
)}
<BaseButton
small={true}
className={'mr-3'}
color='info'
label='Filter'
onClick={addFilter}
/>
<BaseButton
small={true}
className={'mr-3'}
color='info'
label='Download CSV'
@ -131,6 +135,7 @@ const TasksTablesPage = () => {
{hasCreatePermission && (
<BaseButton
small={true}
color='info'
label='Upload CSV'
onClick={() => setIsModalActive(true)}

View File

@ -50,6 +50,8 @@ const initialValues = {
end_date: '',
organizations: '',
tags: [],
};
const TasksNew = () => {
@ -157,6 +159,16 @@ const TasksNew = () => {
></Field>
</FormField>
<FormField label='Tags' labelFor='tags'>
<Field
name='tags'
id='tags'
itemRef={'tags'}
options={[]}
component={SelectFieldMany}
></Field>
</FormField>
<BaseDivider />
<BaseButtons>
<BaseButton type='submit' color='info' label='Submit' />

View File

@ -41,6 +41,7 @@ const TasksTablesPage = () => {
{ label: 'Category', title: 'category' },
{ label: 'Tags', title: 'tags' },
{
label: 'Status',
title: 'status',

View File

@ -137,6 +137,41 @@ const TasksView = () => {
<p>{tasks?.organizations?.name ?? 'No data'}</p>
</div>
<>
<p className={'block font-bold mb-2'}>Tags</p>
<CardBox
className='mb-6 border border-gray-300 rounded overflow-hidden'
hasTable
>
<div className='overflow-x-auto'>
<table>
<thead>
<tr>
<th>Name</th>
</tr>
</thead>
<tbody>
{tasks.tags &&
Array.isArray(tasks.tags) &&
tasks.tags.map((item: any) => (
<tr
key={item.id}
onClick={() =>
router.push(`/tags/tags-view/?id=${item.id}`)
}
>
<td data-label='name'>{item.name}</td>
</tr>
))}
</tbody>
</table>
</div>
{!tasks?.tags?.length && (
<div className={'text-center py-4'}>No data</div>
)}
</CardBox>
</>
<BaseDivider />
<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

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

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 {
tags: any;
loading: boolean;
count: number;
refetch: boolean;
rolesWidgets: any[];
notify: {
showNotification: boolean;
textNotification: string;
typeNotification: string;
};
}
const initialState: MainState = {
tags: [],
loading: false,
count: 0,
refetch: false,
rolesWidgets: [],
notify: {
showNotification: false,
textNotification: '',
typeNotification: 'warn',
},
};
export const fetch = createAsyncThunk('tags/fetch', async (data: any) => {
const { id, query } = data;
const result = await axios.get(`tags${query || (id ? `/${id}` : '')}`);
return id
? result.data
: { rows: result.data.rows, count: result.data.count };
});
export const deleteItemsByIds = createAsyncThunk(
'tags/deleteByIds',
async (data: any, { rejectWithValue }) => {
try {
await axios.post('tags/deleteByIds', { data });
} catch (error) {
if (!error.response) {
throw error;
}
return rejectWithValue(error.response.data);
}
},
);
export const deleteItem = createAsyncThunk(
'tags/deleteTags',
async (id: string, { rejectWithValue }) => {
try {
await axios.delete(`tags/${id}`);
} catch (error) {
if (!error.response) {
throw error;
}
return rejectWithValue(error.response.data);
}
},
);
export const create = createAsyncThunk(
'tags/createTags',
async (data: any, { rejectWithValue }) => {
try {
const result = await axios.post('tags', { data });
return result.data;
} catch (error) {
if (!error.response) {
throw error;
}
return rejectWithValue(error.response.data);
}
},
);
export const uploadCsv = createAsyncThunk(
'tags/uploadCsv',
async (file: File, { rejectWithValue }) => {
try {
const data = new FormData();
data.append('file', file);
data.append('filename', file.name);
const result = await axios.post('tags/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(
'tags/updateTags',
async (payload: any, { rejectWithValue }) => {
try {
const result = await axios.put(`tags/${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 tagsSlice = createSlice({
name: 'tags',
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.tags = action.payload.rows;
state.count = action.payload.count;
} else {
state.tags = 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, 'Tags 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, `${'Tags'.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, `${'Tags'.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, `${'Tags'.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, 'Tags 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 } = tagsSlice.actions;
export default tagsSlice.reducer;

View File

@ -52,7 +52,7 @@ export const basic: StyleObject = {
};
export const white: StyleObject = {
aside: 'bg-white dark:text-white lg:rounded-2xl',
aside: 'bg-[#f8f8f8] dark:text-white',
asideScrollbars: 'aside-scrollbars-light',
asideBrand: '',
asideMenuItem: