Autosave: 20260218-034325
This commit is contained in:
parent
a004fcd820
commit
f7df7e331e
2
502.html
2
502.html
@ -129,7 +129,7 @@
|
|||||||
<p class="tip">The application is currently launching. The page will automatically refresh once site is
|
<p class="tip">The application is currently launching. The page will automatically refresh once site is
|
||||||
available.</p>
|
available.</p>
|
||||||
<div class="project-info">
|
<div class="project-info">
|
||||||
<h2>Crafted Network</h2>
|
<h2>Fix It Local</h2>
|
||||||
<p>Trust-first service directory with verification, transparent pricing, AI-style matching, and request tracking.</p>
|
<p>Trust-first service directory with verification, transparent pricing, AI-style matching, and request tracking.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="loader-container">
|
<div class="loader-container">
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
|
|
||||||
|
|
||||||
# Crafted Network
|
# Fix It Local
|
||||||
|
|
||||||
|
|
||||||
## This project was generated by [Flatlogic Platform](https://flatlogic.com).
|
## This project was generated by [Flatlogic Platform](https://flatlogic.com).
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
|
|
||||||
#Crafted Network - template backend,
|
#Fix It Local - template backend,
|
||||||
|
|
||||||
#### Run App on local machine:
|
#### Run App on local machine:
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "craftednetwork",
|
"name": "craftednetwork",
|
||||||
"description": "Crafted Network - template backend",
|
"description": "Fix It Local - template backend",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "npm run db:migrate && npm run db:seed && npm run watch",
|
"start": "npm run db:migrate && npm run db:seed && npm run watch",
|
||||||
"lint": "eslint . --ext .js",
|
"lint": "eslint . --ext .js",
|
||||||
|
|||||||
@ -37,7 +37,7 @@ const config = {
|
|||||||
},
|
},
|
||||||
uploadDir: os.tmpdir(),
|
uploadDir: os.tmpdir(),
|
||||||
email: {
|
email: {
|
||||||
from: 'Crafted Network <app@flatlogic.app>',
|
from: 'Fix It Local <app@flatlogic.app>',
|
||||||
host: 'email-smtp.us-east-1.amazonaws.com',
|
host: 'email-smtp.us-east-1.amazonaws.com',
|
||||||
port: 587,
|
port: 587,
|
||||||
auth: {
|
auth: {
|
||||||
|
|||||||
@ -1,18 +1,12 @@
|
|||||||
|
|
||||||
const db = require('../models');
|
const db = require('../models');
|
||||||
const FileDBApi = require('./file');
|
const FileDBApi = require('./file');
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
const Utils = require('../utils');
|
const Utils = require('../utils');
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const Sequelize = db.Sequelize;
|
const Sequelize = db.Sequelize;
|
||||||
const Op = Sequelize.Op;
|
const Op = Sequelize.Op;
|
||||||
|
|
||||||
module.exports = class Business_badgesDBApi {
|
module.exports = class Business_badgesDBApi {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
static async create(data, options) {
|
static async create(data, options) {
|
||||||
const currentUser = (options && options.currentUser) || { id: null };
|
const currentUser = (options && options.currentUser) || { id: null };
|
||||||
const transaction = (options && options.transaction) || undefined;
|
const transaction = (options && options.transaction) || undefined;
|
||||||
@ -20,493 +14,63 @@ module.exports = class Business_badgesDBApi {
|
|||||||
const business_badges = await db.business_badges.create(
|
const business_badges = await db.business_badges.create(
|
||||||
{
|
{
|
||||||
id: data.id || undefined,
|
id: data.id || undefined,
|
||||||
|
badge_type: data.badge_type || null,
|
||||||
badge_type: data.badge_type
|
status: data.status || null,
|
||||||
||
|
granted_at: data.granted_at || null,
|
||||||
null
|
revoked_at: data.revoked_at || null,
|
||||||
,
|
notes: data.notes || null,
|
||||||
|
created_at_ts: data.created_at_ts || null,
|
||||||
status: data.status
|
updated_at_ts: data.updated_at_ts || null,
|
||||||
||
|
importHash: data.importHash || null,
|
||||||
null
|
createdById: currentUser.id,
|
||||||
,
|
updatedById: currentUser.id,
|
||||||
|
|
||||||
granted_at: data.granted_at
|
|
||||||
||
|
|
||||||
null
|
|
||||||
,
|
|
||||||
|
|
||||||
revoked_at: data.revoked_at
|
|
||||||
||
|
|
||||||
null
|
|
||||||
,
|
|
||||||
|
|
||||||
notes: data.notes
|
|
||||||
||
|
|
||||||
null
|
|
||||||
,
|
|
||||||
|
|
||||||
created_at_ts: data.created_at_ts
|
|
||||||
||
|
|
||||||
null
|
|
||||||
,
|
|
||||||
|
|
||||||
updated_at_ts: data.updated_at_ts
|
|
||||||
||
|
|
||||||
null
|
|
||||||
,
|
|
||||||
|
|
||||||
importHash: data.importHash || null,
|
|
||||||
createdById: currentUser.id,
|
|
||||||
updatedById: currentUser.id,
|
|
||||||
},
|
|
||||||
{ transaction },
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
await business_badges.setBusiness( data.business || null, {
|
|
||||||
transaction,
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return business_badges;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
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 business_badgesData = data.map((item, index) => ({
|
|
||||||
id: item.id || undefined,
|
|
||||||
|
|
||||||
badge_type: item.badge_type
|
|
||||||
||
|
|
||||||
null
|
|
||||||
,
|
|
||||||
|
|
||||||
status: item.status
|
|
||||||
||
|
|
||||||
null
|
|
||||||
,
|
|
||||||
|
|
||||||
granted_at: item.granted_at
|
|
||||||
||
|
|
||||||
null
|
|
||||||
,
|
|
||||||
|
|
||||||
revoked_at: item.revoked_at
|
|
||||||
||
|
|
||||||
null
|
|
||||||
,
|
|
||||||
|
|
||||||
notes: item.notes
|
|
||||||
||
|
|
||||||
null
|
|
||||||
,
|
|
||||||
|
|
||||||
created_at_ts: item.created_at_ts
|
|
||||||
||
|
|
||||||
null
|
|
||||||
,
|
|
||||||
|
|
||||||
updated_at_ts: item.updated_at_ts
|
|
||||||
||
|
|
||||||
null
|
|
||||||
,
|
|
||||||
|
|
||||||
importHash: item.importHash || null,
|
|
||||||
createdById: currentUser.id,
|
|
||||||
updatedById: currentUser.id,
|
|
||||||
createdAt: new Date(Date.now() + index * 1000),
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Bulk create items
|
|
||||||
const business_badges = await db.business_badges.bulkCreate(business_badgesData, { transaction });
|
|
||||||
|
|
||||||
// For each item created, replace relation files
|
|
||||||
|
|
||||||
|
|
||||||
return business_badges;
|
|
||||||
}
|
|
||||||
|
|
||||||
static async update(id, data, options) {
|
|
||||||
const currentUser = (options && options.currentUser) || {id: null};
|
|
||||||
const transaction = (options && options.transaction) || undefined;
|
|
||||||
|
|
||||||
|
|
||||||
const business_badges = await db.business_badges.findByPk(id, {}, {transaction});
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const updatePayload = {};
|
|
||||||
|
|
||||||
if (data.badge_type !== undefined) updatePayload.badge_type = data.badge_type;
|
|
||||||
|
|
||||||
|
|
||||||
if (data.status !== undefined) updatePayload.status = data.status;
|
|
||||||
|
|
||||||
|
|
||||||
if (data.granted_at !== undefined) updatePayload.granted_at = data.granted_at;
|
|
||||||
|
|
||||||
|
|
||||||
if (data.revoked_at !== undefined) updatePayload.revoked_at = data.revoked_at;
|
|
||||||
|
|
||||||
|
|
||||||
if (data.notes !== undefined) updatePayload.notes = data.notes;
|
|
||||||
|
|
||||||
|
|
||||||
if (data.created_at_ts !== undefined) updatePayload.created_at_ts = data.created_at_ts;
|
|
||||||
|
|
||||||
|
|
||||||
if (data.updated_at_ts !== undefined) updatePayload.updated_at_ts = data.updated_at_ts;
|
|
||||||
|
|
||||||
|
|
||||||
updatePayload.updatedById = currentUser.id;
|
|
||||||
|
|
||||||
await business_badges.update(updatePayload, {transaction});
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if (data.business !== undefined) {
|
|
||||||
await business_badges.setBusiness(
|
|
||||||
|
|
||||||
data.business,
|
|
||||||
|
|
||||||
{ transaction }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return business_badges;
|
|
||||||
}
|
|
||||||
|
|
||||||
static async deleteByIds(ids, options) {
|
|
||||||
const currentUser = (options && options.currentUser) || { id: null };
|
|
||||||
const transaction = (options && options.transaction) || undefined;
|
|
||||||
|
|
||||||
const business_badges = await db.business_badges.findAll({
|
|
||||||
where: {
|
|
||||||
id: {
|
|
||||||
[Op.in]: ids,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
transaction,
|
|
||||||
});
|
|
||||||
|
|
||||||
await db.sequelize.transaction(async (transaction) => {
|
|
||||||
for (const record of business_badges) {
|
|
||||||
await record.update(
|
|
||||||
{deletedBy: currentUser.id},
|
|
||||||
{transaction}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
for (const record of business_badges) {
|
|
||||||
await record.destroy({transaction});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
return business_badges;
|
|
||||||
}
|
|
||||||
|
|
||||||
static async remove(id, options) {
|
|
||||||
const currentUser = (options && options.currentUser) || {id: null};
|
|
||||||
const transaction = (options && options.transaction) || undefined;
|
|
||||||
|
|
||||||
const business_badges = await db.business_badges.findByPk(id, options);
|
|
||||||
|
|
||||||
await business_badges.update({
|
|
||||||
deletedBy: currentUser.id
|
|
||||||
}, {
|
|
||||||
transaction,
|
|
||||||
});
|
|
||||||
|
|
||||||
await business_badges.destroy({
|
|
||||||
transaction
|
|
||||||
});
|
|
||||||
|
|
||||||
return business_badges;
|
|
||||||
}
|
|
||||||
|
|
||||||
static async findBy(where, options) {
|
|
||||||
const transaction = (options && options.transaction) || undefined;
|
|
||||||
|
|
||||||
const business_badges = await db.business_badges.findOne(
|
|
||||||
{ where },
|
|
||||||
{ transaction },
|
{ transaction },
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!business_badges) {
|
await business_badges.setBusiness( data.business || null, { transaction });
|
||||||
return business_badges;
|
|
||||||
}
|
|
||||||
|
|
||||||
const output = business_badges.get({plain: true});
|
return business_badges;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
output.business = await business_badges.getBusiness({
|
|
||||||
transaction
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return output;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static async findAll(
|
static async findAll(filter, options) {
|
||||||
filter,
|
|
||||||
options
|
|
||||||
) {
|
|
||||||
const limit = filter.limit || 0;
|
const limit = filter.limit || 0;
|
||||||
let offset = 0;
|
let offset = 0;
|
||||||
let where = {};
|
let where = {};
|
||||||
const currentPage = +filter.page;
|
const currentPage = +filter.page;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
offset = currentPage * limit;
|
offset = currentPage * limit;
|
||||||
|
|
||||||
const orderBy = null;
|
const transaction = (options && options.transaction) || undefined;
|
||||||
|
const currentUser = options?.currentUser;
|
||||||
|
|
||||||
const transaction = (options && options.transaction) || undefined;
|
// Data Isolation
|
||||||
|
if (currentUser && currentUser.app_role) {
|
||||||
let include = [
|
const roleName = currentUser.app_role.name;
|
||||||
|
if (roleName === 'Verified Business Owner') {
|
||||||
{
|
if (currentUser.businessId) {
|
||||||
model: db.businesses,
|
where.businessId = currentUser.businessId;
|
||||||
as: 'business',
|
} else {
|
||||||
|
where['$business.owner_userId$'] = currentUser.id;
|
||||||
where: filter.business ? {
|
|
||||||
[Op.or]: [
|
|
||||||
{ id: { [Op.in]: filter.business.split('|').map(term => Utils.uuid(term)) } },
|
|
||||||
{
|
|
||||||
name: {
|
|
||||||
[Op.or]: filter.business.split('|').map(term => ({ [Op.iLike]: `%${term}%` }))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
]
|
|
||||||
} : {},
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
];
|
|
||||||
|
|
||||||
if (filter) {
|
|
||||||
if (filter.id) {
|
|
||||||
where = {
|
|
||||||
...where,
|
|
||||||
['id']: Utils.uuid(filter.id),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (filter.notes) {
|
|
||||||
where = {
|
|
||||||
...where,
|
|
||||||
[Op.and]: Utils.ilike(
|
|
||||||
'business_badges',
|
|
||||||
'notes',
|
|
||||||
filter.notes,
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if (filter.granted_atRange) {
|
|
||||||
const [start, end] = filter.granted_atRange;
|
|
||||||
|
|
||||||
if (start !== undefined && start !== null && start !== '') {
|
|
||||||
where = {
|
|
||||||
...where,
|
|
||||||
granted_at: {
|
|
||||||
...where.granted_at,
|
|
||||||
[Op.gte]: start,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (end !== undefined && end !== null && end !== '') {
|
|
||||||
where = {
|
|
||||||
...where,
|
|
||||||
granted_at: {
|
|
||||||
...where.granted_at,
|
|
||||||
[Op.lte]: end,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filter.revoked_atRange) {
|
|
||||||
const [start, end] = filter.revoked_atRange;
|
|
||||||
|
|
||||||
if (start !== undefined && start !== null && start !== '') {
|
|
||||||
where = {
|
|
||||||
...where,
|
|
||||||
revoked_at: {
|
|
||||||
...where.revoked_at,
|
|
||||||
[Op.gte]: start,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (end !== undefined && end !== null && end !== '') {
|
|
||||||
where = {
|
|
||||||
...where,
|
|
||||||
revoked_at: {
|
|
||||||
...where.revoked_at,
|
|
||||||
[Op.lte]: end,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filter.created_at_tsRange) {
|
|
||||||
const [start, end] = filter.created_at_tsRange;
|
|
||||||
|
|
||||||
if (start !== undefined && start !== null && start !== '') {
|
|
||||||
where = {
|
|
||||||
...where,
|
|
||||||
created_at_ts: {
|
|
||||||
...where.created_at_ts,
|
|
||||||
[Op.gte]: start,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (end !== undefined && end !== null && end !== '') {
|
|
||||||
where = {
|
|
||||||
...where,
|
|
||||||
created_at_ts: {
|
|
||||||
...where.created_at_ts,
|
|
||||||
[Op.lte]: end,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filter.updated_at_tsRange) {
|
|
||||||
const [start, end] = filter.updated_at_tsRange;
|
|
||||||
|
|
||||||
if (start !== undefined && start !== null && start !== '') {
|
|
||||||
where = {
|
|
||||||
...where,
|
|
||||||
updated_at_ts: {
|
|
||||||
...where.updated_at_ts,
|
|
||||||
[Op.gte]: start,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (end !== undefined && end !== null && end !== '') {
|
|
||||||
where = {
|
|
||||||
...where,
|
|
||||||
updated_at_ts: {
|
|
||||||
...where.updated_at_ts,
|
|
||||||
[Op.lte]: end,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (filter.active !== undefined) {
|
|
||||||
where = {
|
|
||||||
...where,
|
|
||||||
active: filter.active === true || filter.active === 'true'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (filter.badge_type) {
|
|
||||||
where = {
|
|
||||||
...where,
|
|
||||||
badge_type: filter.badge_type,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filter.status) {
|
|
||||||
where = {
|
|
||||||
...where,
|
|
||||||
status: filter.status,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
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,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
let include = [
|
||||||
|
{
|
||||||
|
model: db.businesses,
|
||||||
|
as: 'business',
|
||||||
|
where: filter.business ? {
|
||||||
|
[Op.or]: [
|
||||||
|
{ id: { [Op.in]: filter.business.split('|').map(term => Utils.uuid(term)) } },
|
||||||
|
{ name: { [Op.or]: filter.business.split('|').map(term => ({ [Op.iLike]: `%${term}%` })) } },
|
||||||
|
]
|
||||||
|
} : {},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
if (filter) {
|
||||||
|
if (filter.id) where.id = Utils.uuid(filter.id);
|
||||||
|
}
|
||||||
|
|
||||||
const queryOptions = {
|
const queryOptions = {
|
||||||
where,
|
where,
|
||||||
@ -515,8 +79,7 @@ module.exports = class Business_badgesDBApi {
|
|||||||
order: filter.field && filter.sort
|
order: filter.field && filter.sort
|
||||||
? [[filter.field, filter.sort]]
|
? [[filter.field, filter.sort]]
|
||||||
: [['createdAt', 'desc']],
|
: [['createdAt', 'desc']],
|
||||||
transaction: options?.transaction,
|
transaction,
|
||||||
logging: console.log
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!options?.countOnly) {
|
if (!options?.countOnly) {
|
||||||
@ -524,51 +87,80 @@ module.exports = class Business_badgesDBApi {
|
|||||||
queryOptions.offset = offset ? Number(offset) : undefined;
|
queryOptions.offset = offset ? Number(offset) : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
const { rows, count } = await db.business_badges.findAndCountAll(queryOptions);
|
||||||
const { rows, count } = await db.business_badges.findAndCountAll(queryOptions);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
rows: options?.countOnly ? [] : rows,
|
rows: options?.countOnly ? [] : rows,
|
||||||
count: count
|
count: count
|
||||||
};
|
};
|
||||||
} catch (error) {
|
|
||||||
console.error('Error executing query:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static async findAllAutocomplete(query, limit, offset, ) {
|
static async findBy(where, options) {
|
||||||
let where = {};
|
const transaction = (options && options.transaction) || undefined;
|
||||||
|
const business_badges = await db.business_badges.findOne({ where, transaction });
|
||||||
|
if (!business_badges) return null;
|
||||||
|
|
||||||
|
const output = business_badges.get({plain: true});
|
||||||
|
output.business = await business_badges.getBusiness({ transaction });
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async update(id, data, options) {
|
||||||
|
const currentUser = (options && options.currentUser) || {id: null};
|
||||||
|
const transaction = (options && options.transaction) || undefined;
|
||||||
|
const business_badges = await db.business_badges.findByPk(id, {transaction});
|
||||||
|
|
||||||
|
const updatePayload = { ...data, updatedById: currentUser.id };
|
||||||
|
await business_badges.update(updatePayload, {transaction});
|
||||||
|
|
||||||
|
if (data.business !== undefined) await business_badges.setBusiness(data.business, { transaction });
|
||||||
|
|
||||||
|
return business_badges;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async deleteByIds(ids, options) {
|
||||||
|
const currentUser = (options && options.currentUser) || { id: null };
|
||||||
|
const transaction = (options && options.transaction) || undefined;
|
||||||
|
const business_badges = await db.business_badges.findAll({ where: { id: { [Op.in]: ids } }, transaction });
|
||||||
|
|
||||||
|
for (const record of business_badges) {
|
||||||
|
await record.update({deletedBy: currentUser.id}, {transaction});
|
||||||
|
await record.destroy({transaction});
|
||||||
|
}
|
||||||
|
return business_badges;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async remove(id, options) {
|
||||||
|
const currentUser = (options && options.currentUser) || {id: null};
|
||||||
|
const transaction = (options && options.transaction) || undefined;
|
||||||
|
const business_badges = await db.business_badges.findByPk(id, options);
|
||||||
|
await business_badges.update({ deletedBy: currentUser.id }, { transaction });
|
||||||
|
await business_badges.destroy({ transaction });
|
||||||
|
return business_badges;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async findAllAutocomplete(query, limit, offset) {
|
||||||
|
let where = {};
|
||||||
if (query) {
|
if (query) {
|
||||||
where = {
|
where = {
|
||||||
[Op.or]: [
|
[Op.or]: [
|
||||||
{ ['id']: Utils.uuid(query) },
|
{ ['id']: Utils.uuid(query) },
|
||||||
Utils.ilike(
|
|
||||||
'business_badges',
|
|
||||||
'notes',
|
|
||||||
query,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const records = await db.business_badges.findAll({
|
const records = await db.business_badges.findAll({
|
||||||
attributes: [ 'id', 'notes' ],
|
attributes: [ 'id' ],
|
||||||
where,
|
where,
|
||||||
limit: limit ? Number(limit) : undefined,
|
limit: limit ? Number(limit) : undefined,
|
||||||
offset: offset ? Number(offset) : undefined,
|
offset: offset ? Number(offset) : undefined,
|
||||||
orderBy: [['notes', 'ASC']],
|
order: [['createdAt', 'desc']],
|
||||||
});
|
});
|
||||||
|
|
||||||
return records.map((record) => ({
|
return records.map((record) => ({
|
||||||
id: record.id,
|
id: record.id,
|
||||||
label: record.notes,
|
label: record.id,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
@ -1,18 +1,12 @@
|
|||||||
|
|
||||||
const db = require('../models');
|
const db = require('../models');
|
||||||
const FileDBApi = require('./file');
|
const FileDBApi = require('./file');
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
const Utils = require('../utils');
|
const Utils = require('../utils');
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const Sequelize = db.Sequelize;
|
const Sequelize = db.Sequelize;
|
||||||
const Op = Sequelize.Op;
|
const Op = Sequelize.Op;
|
||||||
|
|
||||||
module.exports = class Business_categoriesDBApi {
|
module.exports = class Business_categoriesDBApi {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
static async create(data, options) {
|
static async create(data, options) {
|
||||||
const currentUser = (options && options.currentUser) || { id: null };
|
const currentUser = (options && options.currentUser) || { id: null };
|
||||||
const transaction = (options && options.transaction) || undefined;
|
const transaction = (options && options.transaction) || undefined;
|
||||||
@ -20,355 +14,68 @@ module.exports = class Business_categoriesDBApi {
|
|||||||
const business_categories = await db.business_categories.create(
|
const business_categories = await db.business_categories.create(
|
||||||
{
|
{
|
||||||
id: data.id || undefined,
|
id: data.id || undefined,
|
||||||
|
created_at_ts: data.created_at_ts || null,
|
||||||
created_at_ts: data.created_at_ts
|
importHash: data.importHash || null,
|
||||||
||
|
createdById: currentUser.id,
|
||||||
null
|
updatedById: currentUser.id,
|
||||||
,
|
|
||||||
|
|
||||||
importHash: data.importHash || null,
|
|
||||||
createdById: currentUser.id,
|
|
||||||
updatedById: currentUser.id,
|
|
||||||
},
|
|
||||||
{ transaction },
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
await business_categories.setBusiness( data.business || null, {
|
|
||||||
transaction,
|
|
||||||
});
|
|
||||||
|
|
||||||
await business_categories.setCategory( data.category || null, {
|
|
||||||
transaction,
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return business_categories;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
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 business_categoriesData = data.map((item, index) => ({
|
|
||||||
id: item.id || undefined,
|
|
||||||
|
|
||||||
created_at_ts: item.created_at_ts
|
|
||||||
||
|
|
||||||
null
|
|
||||||
,
|
|
||||||
|
|
||||||
importHash: item.importHash || null,
|
|
||||||
createdById: currentUser.id,
|
|
||||||
updatedById: currentUser.id,
|
|
||||||
createdAt: new Date(Date.now() + index * 1000),
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Bulk create items
|
|
||||||
const business_categories = await db.business_categories.bulkCreate(business_categoriesData, { transaction });
|
|
||||||
|
|
||||||
// For each item created, replace relation files
|
|
||||||
|
|
||||||
|
|
||||||
return business_categories;
|
|
||||||
}
|
|
||||||
|
|
||||||
static async update(id, data, options) {
|
|
||||||
const currentUser = (options && options.currentUser) || {id: null};
|
|
||||||
const transaction = (options && options.transaction) || undefined;
|
|
||||||
|
|
||||||
|
|
||||||
const business_categories = await db.business_categories.findByPk(id, {}, {transaction});
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const updatePayload = {};
|
|
||||||
|
|
||||||
if (data.created_at_ts !== undefined) updatePayload.created_at_ts = data.created_at_ts;
|
|
||||||
|
|
||||||
|
|
||||||
updatePayload.updatedById = currentUser.id;
|
|
||||||
|
|
||||||
await business_categories.update(updatePayload, {transaction});
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if (data.business !== undefined) {
|
|
||||||
await business_categories.setBusiness(
|
|
||||||
|
|
||||||
data.business,
|
|
||||||
|
|
||||||
{ transaction }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.category !== undefined) {
|
|
||||||
await business_categories.setCategory(
|
|
||||||
|
|
||||||
data.category,
|
|
||||||
|
|
||||||
{ transaction }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return business_categories;
|
|
||||||
}
|
|
||||||
|
|
||||||
static async deleteByIds(ids, options) {
|
|
||||||
const currentUser = (options && options.currentUser) || { id: null };
|
|
||||||
const transaction = (options && options.transaction) || undefined;
|
|
||||||
|
|
||||||
const business_categories = await db.business_categories.findAll({
|
|
||||||
where: {
|
|
||||||
id: {
|
|
||||||
[Op.in]: ids,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
transaction,
|
|
||||||
});
|
|
||||||
|
|
||||||
await db.sequelize.transaction(async (transaction) => {
|
|
||||||
for (const record of business_categories) {
|
|
||||||
await record.update(
|
|
||||||
{deletedBy: currentUser.id},
|
|
||||||
{transaction}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
for (const record of business_categories) {
|
|
||||||
await record.destroy({transaction});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
return business_categories;
|
|
||||||
}
|
|
||||||
|
|
||||||
static async remove(id, options) {
|
|
||||||
const currentUser = (options && options.currentUser) || {id: null};
|
|
||||||
const transaction = (options && options.transaction) || undefined;
|
|
||||||
|
|
||||||
const business_categories = await db.business_categories.findByPk(id, options);
|
|
||||||
|
|
||||||
await business_categories.update({
|
|
||||||
deletedBy: currentUser.id
|
|
||||||
}, {
|
|
||||||
transaction,
|
|
||||||
});
|
|
||||||
|
|
||||||
await business_categories.destroy({
|
|
||||||
transaction
|
|
||||||
});
|
|
||||||
|
|
||||||
return business_categories;
|
|
||||||
}
|
|
||||||
|
|
||||||
static async findBy(where, options) {
|
|
||||||
const transaction = (options && options.transaction) || undefined;
|
|
||||||
|
|
||||||
const business_categories = await db.business_categories.findOne(
|
|
||||||
{ where },
|
|
||||||
{ transaction },
|
{ transaction },
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!business_categories) {
|
await business_categories.setBusiness( data.business || null, { transaction });
|
||||||
return business_categories;
|
await business_categories.setCategory( data.category || null, { transaction });
|
||||||
}
|
|
||||||
|
|
||||||
const output = business_categories.get({plain: true});
|
return business_categories;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
output.business = await business_categories.getBusiness({
|
|
||||||
transaction
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
output.category = await business_categories.getCategory({
|
|
||||||
transaction
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return output;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static async findAll(
|
static async findAll(filter, options) {
|
||||||
filter,
|
|
||||||
options
|
|
||||||
) {
|
|
||||||
const limit = filter.limit || 0;
|
const limit = filter.limit || 0;
|
||||||
let offset = 0;
|
let offset = 0;
|
||||||
let where = {};
|
let where = {};
|
||||||
const currentPage = +filter.page;
|
const currentPage = +filter.page;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
offset = currentPage * limit;
|
offset = currentPage * limit;
|
||||||
|
|
||||||
const orderBy = null;
|
const transaction = (options && options.transaction) || undefined;
|
||||||
|
const currentUser = options?.currentUser;
|
||||||
|
|
||||||
const transaction = (options && options.transaction) || undefined;
|
// Data Isolation
|
||||||
|
if (currentUser && currentUser.app_role) {
|
||||||
let include = [
|
const roleName = currentUser.app_role.name;
|
||||||
|
if (roleName === 'Verified Business Owner') {
|
||||||
{
|
if (currentUser.businessId) {
|
||||||
model: db.businesses,
|
where.businessId = currentUser.businessId;
|
||||||
as: 'business',
|
} else {
|
||||||
|
where['$business.owner_userId$'] = currentUser.id;
|
||||||
where: filter.business ? {
|
|
||||||
[Op.or]: [
|
|
||||||
{ id: { [Op.in]: filter.business.split('|').map(term => Utils.uuid(term)) } },
|
|
||||||
{
|
|
||||||
name: {
|
|
||||||
[Op.or]: filter.business.split('|').map(term => ({ [Op.iLike]: `%${term}%` }))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
]
|
|
||||||
} : {},
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
model: db.categories,
|
|
||||||
as: 'category',
|
|
||||||
|
|
||||||
where: filter.category ? {
|
|
||||||
[Op.or]: [
|
|
||||||
{ id: { [Op.in]: filter.category.split('|').map(term => Utils.uuid(term)) } },
|
|
||||||
{
|
|
||||||
name: {
|
|
||||||
[Op.or]: filter.category.split('|').map(term => ({ [Op.iLike]: `%${term}%` }))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
]
|
|
||||||
} : {},
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
];
|
|
||||||
|
|
||||||
if (filter) {
|
|
||||||
if (filter.id) {
|
|
||||||
where = {
|
|
||||||
...where,
|
|
||||||
['id']: Utils.uuid(filter.id),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if (filter.created_at_tsRange) {
|
|
||||||
const [start, end] = filter.created_at_tsRange;
|
|
||||||
|
|
||||||
if (start !== undefined && start !== null && start !== '') {
|
|
||||||
where = {
|
|
||||||
...where,
|
|
||||||
created_at_ts: {
|
|
||||||
...where.created_at_ts,
|
|
||||||
[Op.gte]: start,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (end !== undefined && end !== null && end !== '') {
|
|
||||||
where = {
|
|
||||||
...where,
|
|
||||||
created_at_ts: {
|
|
||||||
...where.created_at_ts,
|
|
||||||
[Op.lte]: end,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (filter.active !== undefined) {
|
|
||||||
where = {
|
|
||||||
...where,
|
|
||||||
active: filter.active === true || filter.active === 'true'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if (filter.createdAtRange) {
|
|
||||||
const [start, end] = filter.createdAtRange;
|
|
||||||
|
|
||||||
if (start !== undefined && start !== null && start !== '') {
|
|
||||||
where = {
|
|
||||||
...where,
|
|
||||||
['createdAt']: {
|
|
||||||
...where.createdAt,
|
|
||||||
[Op.gte]: start,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (end !== undefined && end !== null && end !== '') {
|
|
||||||
where = {
|
|
||||||
...where,
|
|
||||||
['createdAt']: {
|
|
||||||
...where.createdAt,
|
|
||||||
[Op.lte]: end,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
let include = [
|
||||||
|
{
|
||||||
|
model: db.businesses,
|
||||||
|
as: 'business',
|
||||||
|
where: filter.business ? {
|
||||||
|
[Op.or]: [
|
||||||
|
{ id: { [Op.in]: filter.business.split('|').map(term => Utils.uuid(term)) } },
|
||||||
|
{ name: { [Op.or]: filter.business.split('|').map(term => ({ [Op.iLike]: `%${term}%` })) } },
|
||||||
|
]
|
||||||
|
} : {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: db.categories,
|
||||||
|
as: 'category',
|
||||||
|
where: filter.category ? {
|
||||||
|
[Op.or]: [
|
||||||
|
{ id: { [Op.in]: filter.category.split('|').map(term => Utils.uuid(term)) } },
|
||||||
|
{ name: { [Op.or]: filter.category.split('|').map(term => ({ [Op.iLike]: `%${term}%` })) } },
|
||||||
|
]
|
||||||
|
} : {},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
if (filter) {
|
||||||
|
if (filter.id) where.id = Utils.uuid(filter.id);
|
||||||
|
}
|
||||||
|
|
||||||
const queryOptions = {
|
const queryOptions = {
|
||||||
where,
|
where,
|
||||||
@ -377,8 +84,7 @@ module.exports = class Business_categoriesDBApi {
|
|||||||
order: filter.field && filter.sort
|
order: filter.field && filter.sort
|
||||||
? [[filter.field, filter.sort]]
|
? [[filter.field, filter.sort]]
|
||||||
: [['createdAt', 'desc']],
|
: [['createdAt', 'desc']],
|
||||||
transaction: options?.transaction,
|
transaction,
|
||||||
logging: console.log
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!options?.countOnly) {
|
if (!options?.countOnly) {
|
||||||
@ -386,51 +92,83 @@ module.exports = class Business_categoriesDBApi {
|
|||||||
queryOptions.offset = offset ? Number(offset) : undefined;
|
queryOptions.offset = offset ? Number(offset) : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
const { rows, count } = await db.business_categories.findAndCountAll(queryOptions);
|
||||||
const { rows, count } = await db.business_categories.findAndCountAll(queryOptions);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
rows: options?.countOnly ? [] : rows,
|
rows: options?.countOnly ? [] : rows,
|
||||||
count: count
|
count: count
|
||||||
};
|
};
|
||||||
} catch (error) {
|
|
||||||
console.error('Error executing query:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static async findAllAutocomplete(query, limit, offset, ) {
|
static async findBy(where, options) {
|
||||||
let where = {};
|
const transaction = (options && options.transaction) || undefined;
|
||||||
|
const business_categories = await db.business_categories.findOne({ where, transaction });
|
||||||
|
if (!business_categories) return null;
|
||||||
|
|
||||||
|
const output = business_categories.get({plain: true});
|
||||||
|
output.business = await business_categories.getBusiness({ transaction });
|
||||||
|
output.category = await business_categories.getCategory({ transaction });
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async update(id, data, options) {
|
||||||
|
const currentUser = (options && options.currentUser) || {id: null};
|
||||||
|
const transaction = (options && options.transaction) || undefined;
|
||||||
|
const business_categories = await db.business_categories.findByPk(id, {transaction});
|
||||||
|
|
||||||
|
const updatePayload = { updatedById: currentUser.id };
|
||||||
|
if (data.created_at_ts !== undefined) updatePayload.created_at_ts = data.created_at_ts;
|
||||||
|
await business_categories.update(updatePayload, {transaction});
|
||||||
|
|
||||||
|
if (data.business !== undefined) await business_categories.setBusiness(data.business, { transaction });
|
||||||
|
if (data.category !== undefined) await business_categories.setCategory(data.category, { transaction });
|
||||||
|
|
||||||
|
return business_categories;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async deleteByIds(ids, options) {
|
||||||
|
const currentUser = (options && options.currentUser) || { id: null };
|
||||||
|
const transaction = (options && options.transaction) || undefined;
|
||||||
|
const business_categories = await db.business_categories.findAll({ where: { id: { [Op.in]: ids } }, transaction });
|
||||||
|
|
||||||
|
for (const record of business_categories) {
|
||||||
|
await record.update({deletedBy: currentUser.id}, {transaction});
|
||||||
|
await record.destroy({transaction});
|
||||||
|
}
|
||||||
|
return business_categories;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async remove(id, options) {
|
||||||
|
const currentUser = (options && options.currentUser) || {id: null};
|
||||||
|
const transaction = (options && options.transaction) || undefined;
|
||||||
|
const business_categories = await db.business_categories.findByPk(id, options);
|
||||||
|
await business_categories.update({ deletedBy: currentUser.id }, { transaction });
|
||||||
|
await business_categories.destroy({ transaction });
|
||||||
|
return business_categories;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async findAllAutocomplete(query, limit, offset) {
|
||||||
|
let where = {};
|
||||||
if (query) {
|
if (query) {
|
||||||
where = {
|
where = {
|
||||||
[Op.or]: [
|
[Op.or]: [
|
||||||
{ ['id']: Utils.uuid(query) },
|
{ ['id']: Utils.uuid(query) },
|
||||||
Utils.ilike(
|
|
||||||
'business_categories',
|
|
||||||
'created_at_ts',
|
|
||||||
query,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const records = await db.business_categories.findAll({
|
const records = await db.business_categories.findAll({
|
||||||
attributes: [ 'id', 'created_at_ts' ],
|
attributes: [ 'id' ],
|
||||||
where,
|
where,
|
||||||
limit: limit ? Number(limit) : undefined,
|
limit: limit ? Number(limit) : undefined,
|
||||||
offset: offset ? Number(offset) : undefined,
|
offset: offset ? Number(offset) : undefined,
|
||||||
orderBy: [['created_at_ts', 'ASC']],
|
order: [['createdAt', 'desc']],
|
||||||
});
|
});
|
||||||
|
|
||||||
return records.map((record) => ({
|
return records.map((record) => ({
|
||||||
id: record.id,
|
id: record.id,
|
||||||
label: record.created_at_ts,
|
label: record.id,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
@ -1,18 +1,12 @@
|
|||||||
|
|
||||||
const db = require('../models');
|
const db = require('../models');
|
||||||
const FileDBApi = require('./file');
|
const FileDBApi = require('./file');
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
const Utils = require('../utils');
|
const Utils = require('../utils');
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const Sequelize = db.Sequelize;
|
const Sequelize = db.Sequelize;
|
||||||
const Op = Sequelize.Op;
|
const Op = Sequelize.Op;
|
||||||
|
|
||||||
module.exports = class Business_photosDBApi {
|
module.exports = class Business_photosDBApi {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
static async create(data, options) {
|
static async create(data, options) {
|
||||||
const currentUser = (options && options.currentUser) || { id: null };
|
const currentUser = (options && options.currentUser) || { id: null };
|
||||||
const transaction = (options && options.transaction) || undefined;
|
const transaction = (options && options.transaction) || undefined;
|
||||||
@ -20,360 +14,70 @@ module.exports = class Business_photosDBApi {
|
|||||||
const business_photos = await db.business_photos.create(
|
const business_photos = await db.business_photos.create(
|
||||||
{
|
{
|
||||||
id: data.id || undefined,
|
id: data.id || undefined,
|
||||||
|
created_at_ts: data.created_at_ts || null,
|
||||||
created_at_ts: data.created_at_ts
|
importHash: data.importHash || null,
|
||||||
||
|
createdById: currentUser.id,
|
||||||
null
|
updatedById: currentUser.id,
|
||||||
,
|
|
||||||
|
|
||||||
importHash: data.importHash || null,
|
|
||||||
createdById: currentUser.id,
|
|
||||||
updatedById: currentUser.id,
|
|
||||||
},
|
|
||||||
{ transaction },
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
await business_photos.setBusiness( data.business || null, {
|
|
||||||
transaction,
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
await FileDBApi.replaceRelationFiles(
|
|
||||||
{
|
|
||||||
belongsTo: db.business_photos.getTableName(),
|
|
||||||
belongsToColumn: 'photos',
|
|
||||||
belongsToId: business_photos.id,
|
|
||||||
},
|
},
|
||||||
data.photos,
|
|
||||||
options,
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
return business_photos;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
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 business_photosData = data.map((item, index) => ({
|
|
||||||
id: item.id || undefined,
|
|
||||||
|
|
||||||
created_at_ts: item.created_at_ts
|
|
||||||
||
|
|
||||||
null
|
|
||||||
,
|
|
||||||
|
|
||||||
importHash: item.importHash || null,
|
|
||||||
createdById: currentUser.id,
|
|
||||||
updatedById: currentUser.id,
|
|
||||||
createdAt: new Date(Date.now() + index * 1000),
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Bulk create items
|
|
||||||
const business_photos = await db.business_photos.bulkCreate(business_photosData, { transaction });
|
|
||||||
|
|
||||||
// For each item created, replace relation files
|
|
||||||
|
|
||||||
for (let i = 0; i < business_photos.length; i++) {
|
|
||||||
await FileDBApi.replaceRelationFiles(
|
|
||||||
{
|
|
||||||
belongsTo: db.business_photos.getTableName(),
|
|
||||||
belongsToColumn: 'photos',
|
|
||||||
belongsToId: business_photos[i].id,
|
|
||||||
},
|
|
||||||
data[i].photos,
|
|
||||||
options,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
return business_photos;
|
|
||||||
}
|
|
||||||
|
|
||||||
static async update(id, data, options) {
|
|
||||||
const currentUser = (options && options.currentUser) || {id: null};
|
|
||||||
const transaction = (options && options.transaction) || undefined;
|
|
||||||
|
|
||||||
|
|
||||||
const business_photos = await db.business_photos.findByPk(id, {}, {transaction});
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const updatePayload = {};
|
|
||||||
|
|
||||||
if (data.created_at_ts !== undefined) updatePayload.created_at_ts = data.created_at_ts;
|
|
||||||
|
|
||||||
|
|
||||||
updatePayload.updatedById = currentUser.id;
|
|
||||||
|
|
||||||
await business_photos.update(updatePayload, {transaction});
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if (data.business !== undefined) {
|
|
||||||
await business_photos.setBusiness(
|
|
||||||
|
|
||||||
data.business,
|
|
||||||
|
|
||||||
{ transaction }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
await FileDBApi.replaceRelationFiles(
|
|
||||||
{
|
|
||||||
belongsTo: db.business_photos.getTableName(),
|
|
||||||
belongsToColumn: 'photos',
|
|
||||||
belongsToId: business_photos.id,
|
|
||||||
},
|
|
||||||
data.photos,
|
|
||||||
options,
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
return business_photos;
|
|
||||||
}
|
|
||||||
|
|
||||||
static async deleteByIds(ids, options) {
|
|
||||||
const currentUser = (options && options.currentUser) || { id: null };
|
|
||||||
const transaction = (options && options.transaction) || undefined;
|
|
||||||
|
|
||||||
const business_photos = await db.business_photos.findAll({
|
|
||||||
where: {
|
|
||||||
id: {
|
|
||||||
[Op.in]: ids,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
transaction,
|
|
||||||
});
|
|
||||||
|
|
||||||
await db.sequelize.transaction(async (transaction) => {
|
|
||||||
for (const record of business_photos) {
|
|
||||||
await record.update(
|
|
||||||
{deletedBy: currentUser.id},
|
|
||||||
{transaction}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
for (const record of business_photos) {
|
|
||||||
await record.destroy({transaction});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
return business_photos;
|
|
||||||
}
|
|
||||||
|
|
||||||
static async remove(id, options) {
|
|
||||||
const currentUser = (options && options.currentUser) || {id: null};
|
|
||||||
const transaction = (options && options.transaction) || undefined;
|
|
||||||
|
|
||||||
const business_photos = await db.business_photos.findByPk(id, options);
|
|
||||||
|
|
||||||
await business_photos.update({
|
|
||||||
deletedBy: currentUser.id
|
|
||||||
}, {
|
|
||||||
transaction,
|
|
||||||
});
|
|
||||||
|
|
||||||
await business_photos.destroy({
|
|
||||||
transaction
|
|
||||||
});
|
|
||||||
|
|
||||||
return business_photos;
|
|
||||||
}
|
|
||||||
|
|
||||||
static async findBy(where, options) {
|
|
||||||
const transaction = (options && options.transaction) || undefined;
|
|
||||||
|
|
||||||
const business_photos = await db.business_photos.findOne(
|
|
||||||
{ where },
|
|
||||||
{ transaction },
|
{ transaction },
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!business_photos) {
|
await business_photos.setBusiness( data.business || null, { transaction });
|
||||||
return business_photos;
|
await FileDBApi.replaceRelationFiles(
|
||||||
}
|
{
|
||||||
|
belongsTo: db.business_photos.getTableName(),
|
||||||
|
belongsToColumn: 'photos',
|
||||||
|
belongsToId: business_photos.id,
|
||||||
|
},
|
||||||
|
data.photos,
|
||||||
|
options,
|
||||||
|
);
|
||||||
|
|
||||||
const output = business_photos.get({plain: true});
|
return business_photos;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
output.business = await business_photos.getBusiness({
|
|
||||||
transaction
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
output.photos = await business_photos.getPhotos({
|
|
||||||
transaction
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return output;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static async findAll(
|
static async findAll(filter, options) {
|
||||||
filter,
|
|
||||||
options
|
|
||||||
) {
|
|
||||||
const limit = filter.limit || 0;
|
const limit = filter.limit || 0;
|
||||||
let offset = 0;
|
let offset = 0;
|
||||||
let where = {};
|
let where = {};
|
||||||
const currentPage = +filter.page;
|
const currentPage = +filter.page;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
offset = currentPage * limit;
|
offset = currentPage * limit;
|
||||||
|
|
||||||
const orderBy = null;
|
const transaction = (options && options.transaction) || undefined;
|
||||||
|
const currentUser = options?.currentUser;
|
||||||
|
|
||||||
const transaction = (options && options.transaction) || undefined;
|
// Data Isolation
|
||||||
|
if (currentUser && currentUser.app_role) {
|
||||||
let include = [
|
const roleName = currentUser.app_role.name;
|
||||||
|
if (roleName === 'Verified Business Owner') {
|
||||||
{
|
if (currentUser.businessId) {
|
||||||
model: db.businesses,
|
where.businessId = currentUser.businessId;
|
||||||
as: 'business',
|
} else {
|
||||||
|
where['$business.owner_userId$'] = currentUser.id;
|
||||||
where: filter.business ? {
|
|
||||||
[Op.or]: [
|
|
||||||
{ id: { [Op.in]: filter.business.split('|').map(term => Utils.uuid(term)) } },
|
|
||||||
{
|
|
||||||
name: {
|
|
||||||
[Op.or]: filter.business.split('|').map(term => ({ [Op.iLike]: `%${term}%` }))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
]
|
|
||||||
} : {},
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{
|
|
||||||
model: db.file,
|
|
||||||
as: 'photos',
|
|
||||||
},
|
|
||||||
|
|
||||||
];
|
|
||||||
|
|
||||||
if (filter) {
|
|
||||||
if (filter.id) {
|
|
||||||
where = {
|
|
||||||
...where,
|
|
||||||
['id']: Utils.uuid(filter.id),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if (filter.created_at_tsRange) {
|
|
||||||
const [start, end] = filter.created_at_tsRange;
|
|
||||||
|
|
||||||
if (start !== undefined && start !== null && start !== '') {
|
|
||||||
where = {
|
|
||||||
...where,
|
|
||||||
created_at_ts: {
|
|
||||||
...where.created_at_ts,
|
|
||||||
[Op.gte]: start,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (end !== undefined && end !== null && end !== '') {
|
|
||||||
where = {
|
|
||||||
...where,
|
|
||||||
created_at_ts: {
|
|
||||||
...where.created_at_ts,
|
|
||||||
[Op.lte]: end,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (filter.active !== undefined) {
|
|
||||||
where = {
|
|
||||||
...where,
|
|
||||||
active: filter.active === true || filter.active === 'true'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if (filter.createdAtRange) {
|
|
||||||
const [start, end] = filter.createdAtRange;
|
|
||||||
|
|
||||||
if (start !== undefined && start !== null && start !== '') {
|
|
||||||
where = {
|
|
||||||
...where,
|
|
||||||
['createdAt']: {
|
|
||||||
...where.createdAt,
|
|
||||||
[Op.gte]: start,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (end !== undefined && end !== null && end !== '') {
|
|
||||||
where = {
|
|
||||||
...where,
|
|
||||||
['createdAt']: {
|
|
||||||
...where.createdAt,
|
|
||||||
[Op.lte]: end,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
let include = [
|
||||||
|
{
|
||||||
|
model: db.businesses,
|
||||||
|
as: 'business',
|
||||||
|
where: filter.business ? {
|
||||||
|
[Op.or]: [
|
||||||
|
{ id: { [Op.in]: filter.business.split('|').map(term => Utils.uuid(term)) } },
|
||||||
|
{ name: { [Op.or]: filter.business.split('|').map(term => ({ [Op.iLike]: `%${term}%` })) } },
|
||||||
|
]
|
||||||
|
} : {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: db.file,
|
||||||
|
as: 'photos',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
if (filter) {
|
||||||
|
if (filter.id) where.id = Utils.uuid(filter.id);
|
||||||
|
}
|
||||||
|
|
||||||
const queryOptions = {
|
const queryOptions = {
|
||||||
where,
|
where,
|
||||||
@ -382,8 +86,7 @@ module.exports = class Business_photosDBApi {
|
|||||||
order: filter.field && filter.sort
|
order: filter.field && filter.sort
|
||||||
? [[filter.field, filter.sort]]
|
? [[filter.field, filter.sort]]
|
||||||
: [['createdAt', 'desc']],
|
: [['createdAt', 'desc']],
|
||||||
transaction: options?.transaction,
|
transaction,
|
||||||
logging: console.log
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!options?.countOnly) {
|
if (!options?.countOnly) {
|
||||||
@ -391,51 +94,91 @@ module.exports = class Business_photosDBApi {
|
|||||||
queryOptions.offset = offset ? Number(offset) : undefined;
|
queryOptions.offset = offset ? Number(offset) : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
const { rows, count } = await db.business_photos.findAndCountAll(queryOptions);
|
||||||
const { rows, count } = await db.business_photos.findAndCountAll(queryOptions);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
rows: options?.countOnly ? [] : rows,
|
rows: options?.countOnly ? [] : rows,
|
||||||
count: count
|
count: count
|
||||||
};
|
};
|
||||||
} catch (error) {
|
|
||||||
console.error('Error executing query:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static async findAllAutocomplete(query, limit, offset, ) {
|
static async findBy(where, options) {
|
||||||
let where = {};
|
const transaction = (options && options.transaction) || undefined;
|
||||||
|
const business_photos = await db.business_photos.findOne({ where, transaction });
|
||||||
|
if (!business_photos) return null;
|
||||||
|
|
||||||
|
const output = business_photos.get({plain: true});
|
||||||
|
output.business = await business_photos.getBusiness({ transaction });
|
||||||
|
output.photos = await business_photos.getPhotos({ transaction });
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async update(id, data, options) {
|
||||||
|
const currentUser = (options && options.currentUser) || {id: null};
|
||||||
|
const transaction = (options && options.transaction) || undefined;
|
||||||
|
const business_photos = await db.business_photos.findByPk(id, {transaction});
|
||||||
|
|
||||||
|
const updatePayload = { updatedById: currentUser.id };
|
||||||
|
if (data.created_at_ts !== undefined) updatePayload.created_at_ts = data.created_at_ts;
|
||||||
|
await business_photos.update(updatePayload, {transaction});
|
||||||
|
|
||||||
|
if (data.business !== undefined) await business_photos.setBusiness(data.business, { transaction });
|
||||||
|
await FileDBApi.replaceRelationFiles(
|
||||||
|
{
|
||||||
|
belongsTo: db.business_photos.getTableName(),
|
||||||
|
belongsToColumn: 'photos',
|
||||||
|
belongsToId: business_photos.id,
|
||||||
|
},
|
||||||
|
data.photos,
|
||||||
|
options,
|
||||||
|
);
|
||||||
|
|
||||||
|
return business_photos;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async deleteByIds(ids, options) {
|
||||||
|
const currentUser = (options && options.currentUser) || { id: null };
|
||||||
|
const transaction = (options && options.transaction) || undefined;
|
||||||
|
const business_photos = await db.business_photos.findAll({ where: { id: { [Op.in]: ids } }, transaction });
|
||||||
|
|
||||||
|
for (const record of business_photos) {
|
||||||
|
await record.update({deletedBy: currentUser.id}, {transaction});
|
||||||
|
await record.destroy({transaction});
|
||||||
|
}
|
||||||
|
return business_photos;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async remove(id, options) {
|
||||||
|
const currentUser = (options && options.currentUser) || {id: null};
|
||||||
|
const transaction = (options && options.transaction) || undefined;
|
||||||
|
const business_photos = await db.business_photos.findByPk(id, options);
|
||||||
|
await business_photos.update({ deletedBy: currentUser.id }, { transaction });
|
||||||
|
await business_photos.destroy({ transaction });
|
||||||
|
return business_photos;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async findAllAutocomplete(query, limit, offset) {
|
||||||
|
let where = {};
|
||||||
if (query) {
|
if (query) {
|
||||||
where = {
|
where = {
|
||||||
[Op.or]: [
|
[Op.or]: [
|
||||||
{ ['id']: Utils.uuid(query) },
|
{ ['id']: Utils.uuid(query) },
|
||||||
Utils.ilike(
|
|
||||||
'business_photos',
|
|
||||||
'photos',
|
|
||||||
query,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const records = await db.business_photos.findAll({
|
const records = await db.business_photos.findAll({
|
||||||
attributes: [ 'id', 'photos' ],
|
attributes: [ 'id' ],
|
||||||
where,
|
where,
|
||||||
limit: limit ? Number(limit) : undefined,
|
limit: limit ? Number(limit) : undefined,
|
||||||
offset: offset ? Number(offset) : undefined,
|
offset: offset ? Number(offset) : undefined,
|
||||||
orderBy: [['photos', 'ASC']],
|
order: [['createdAt', 'desc']],
|
||||||
});
|
});
|
||||||
|
|
||||||
return records.map((record) => ({
|
return records.map((record) => ({
|
||||||
id: record.id,
|
id: record.id,
|
||||||
label: record.photos,
|
label: record.id,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
@ -29,16 +29,24 @@ module.exports = class BusinessesDBApi {
|
|||||||
const currentUser = options?.currentUser;
|
const currentUser = options?.currentUser;
|
||||||
const transaction = (options && options.transaction) || undefined;
|
const transaction = (options && options.transaction) || undefined;
|
||||||
|
|
||||||
// Data Isolation for Crafted Network™
|
// Data Isolation
|
||||||
if (currentUser && currentUser.app_role) {
|
if (currentUser && currentUser.app_role) {
|
||||||
const roleName = currentUser.app_role.name;
|
const roleName = currentUser.app_role.name;
|
||||||
if (roleName === 'Verified Business Owner') {
|
const isAdmin = roleName === 'Administrator' || roleName === 'Platform Owner';
|
||||||
where.owner_userId = currentUser.id;
|
const isPublicOrConsumer = roleName === 'Public' || roleName === 'Consumer';
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Public directory should only show active businesses
|
if (!isAdmin && !isPublicOrConsumer) {
|
||||||
if (!currentUser || currentUser.app_role?.name === 'Public' || currentUser.app_role?.name === 'Consumer') {
|
// This is a "client" (e.g. Verified Business Owner)
|
||||||
|
if (currentUser.businessId) {
|
||||||
|
where.id = currentUser.businessId;
|
||||||
|
} else {
|
||||||
|
where.owner_userId = currentUser.id;
|
||||||
|
}
|
||||||
|
} else if (isPublicOrConsumer) {
|
||||||
|
where.is_active = true;
|
||||||
|
}
|
||||||
|
} else if (!currentUser) {
|
||||||
|
// Public unauthenticated access
|
||||||
where.is_active = true;
|
where.is_active = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -17,7 +17,7 @@ module.exports = class Lead_matchesDBApi {
|
|||||||
const currentUser = options?.currentUser;
|
const currentUser = options?.currentUser;
|
||||||
const transaction = (options && options.transaction) || undefined;
|
const transaction = (options && options.transaction) || undefined;
|
||||||
|
|
||||||
// Data Isolation for Crafted Network™
|
// Data Isolation for Fix It Local™
|
||||||
if (currentUser && currentUser.app_role) {
|
if (currentUser && currentUser.app_role) {
|
||||||
const roleName = currentUser.app_role.name;
|
const roleName = currentUser.app_role.name;
|
||||||
if (roleName === 'Verified Business Owner') {
|
if (roleName === 'Verified Business Owner') {
|
||||||
|
|||||||
@ -54,14 +54,18 @@ module.exports = class LeadsDBApi {
|
|||||||
const currentUser = options?.currentUser;
|
const currentUser = options?.currentUser;
|
||||||
const transaction = (options && options.transaction) || undefined;
|
const transaction = (options && options.transaction) || undefined;
|
||||||
|
|
||||||
// Data Isolation for Crafted Network™
|
// Data Isolation
|
||||||
if (currentUser && currentUser.app_role) {
|
if (currentUser && currentUser.app_role) {
|
||||||
const roleName = currentUser.app_role.name;
|
const roleName = currentUser.app_role.name;
|
||||||
if (roleName === 'Consumer') {
|
if (roleName === 'Consumer') {
|
||||||
where.userId = currentUser.id;
|
where.userId = currentUser.id;
|
||||||
} else if (roleName === 'Verified Business Owner') {
|
} else if (roleName === 'Verified Business Owner') {
|
||||||
// Business owners see leads matched to them
|
// Business owners see leads matched to them
|
||||||
where['$lead_matches_lead.business.owner_userId$'] = currentUser.id;
|
if (currentUser.businessId) {
|
||||||
|
where['$lead_matches_lead.businessId$'] = currentUser.businessId;
|
||||||
|
} else {
|
||||||
|
where['$lead_matches_lead.business.owner_userId$'] = currentUser.id;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -75,7 +79,7 @@ module.exports = class LeadsDBApi {
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
// Apply filters (simplified for brevity, keeping core logic)
|
// Apply filters
|
||||||
if (filter) {
|
if (filter) {
|
||||||
if (filter.id) where.id = Utils.uuid(filter.id);
|
if (filter.id) where.id = Utils.uuid(filter.id);
|
||||||
if (filter.keyword) where.keyword = { [Op.iLike]: `%${filter.keyword}%` };
|
if (filter.keyword) where.keyword = { [Op.iLike]: `%${filter.keyword}%` };
|
||||||
@ -137,7 +141,17 @@ module.exports = class LeadsDBApi {
|
|||||||
as: 'lead_matches_lead',
|
as: 'lead_matches_lead',
|
||||||
include: [{ model: db.businesses, as: 'business' }]
|
include: [{ model: db.businesses, as: 'business' }]
|
||||||
},
|
},
|
||||||
{ model: db.messages, as: 'messages_lead' }
|
{ model: db.messages, as: 'messages_lead' },
|
||||||
|
{
|
||||||
|
model: db.lead_photos,
|
||||||
|
as: 'lead_photos_lead',
|
||||||
|
include: [{ model: db.file, as: 'photos' }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: db.lead_events,
|
||||||
|
as: 'lead_events_lead',
|
||||||
|
include: [{ model: db.users, as: 'actor_user' }]
|
||||||
|
}
|
||||||
],
|
],
|
||||||
transaction
|
transaction
|
||||||
});
|
});
|
||||||
|
|||||||
@ -41,15 +41,24 @@ module.exports = class MessagesDBApi {
|
|||||||
const transaction = (options && options.transaction) || undefined;
|
const transaction = (options && options.transaction) || undefined;
|
||||||
const currentUser = options?.currentUser;
|
const currentUser = options?.currentUser;
|
||||||
|
|
||||||
// Data Isolation for Crafted Network™
|
// Data Isolation
|
||||||
if (currentUser && currentUser.app_role) {
|
if (currentUser && currentUser.app_role) {
|
||||||
const roleName = currentUser.app_role.name;
|
const roleName = currentUser.app_role.name;
|
||||||
if (roleName === 'Verified Business Owner') {
|
if (roleName === 'Verified Business Owner') {
|
||||||
where[Op.or] = [
|
const businessId = currentUser.businessId;
|
||||||
{ sender_userId: currentUser.id },
|
if (businessId) {
|
||||||
{ receiver_userId: currentUser.id },
|
where[Op.or] = [
|
||||||
{ '$lead.lead_matches_lead.business.owner_userId$': currentUser.id }
|
{ sender_userId: currentUser.id },
|
||||||
];
|
{ receiver_userId: currentUser.id },
|
||||||
|
{ '$lead.lead_matches_lead.businessId$': businessId }
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
where[Op.or] = [
|
||||||
|
{ sender_userId: currentUser.id },
|
||||||
|
{ receiver_userId: currentUser.id },
|
||||||
|
{ '$lead.lead_matches_lead.business.owner_userId$': currentUser.id }
|
||||||
|
];
|
||||||
|
}
|
||||||
} else if (roleName === 'Consumer') {
|
} else if (roleName === 'Consumer') {
|
||||||
where[Op.or] = [
|
where[Op.or] = [
|
||||||
{ sender_userId: currentUser.id },
|
{ sender_userId: currentUser.id },
|
||||||
@ -106,6 +115,7 @@ module.exports = class MessagesDBApi {
|
|||||||
where,
|
where,
|
||||||
include,
|
include,
|
||||||
distinct: true,
|
distinct: true,
|
||||||
|
subQuery: false, // Fix for "missing FROM-clause entry" when using limit + nested includes
|
||||||
order: filter.field && filter.sort
|
order: filter.field && filter.sort
|
||||||
? [[filter.field, filter.sort]]
|
? [[filter.field, filter.sort]]
|
||||||
: [['createdAt', 'desc']],
|
: [['createdAt', 'desc']],
|
||||||
|
|||||||
@ -354,11 +354,15 @@ module.exports = class ReviewsDBApi {
|
|||||||
|
|
||||||
const currentUser = options?.currentUser;
|
const currentUser = options?.currentUser;
|
||||||
|
|
||||||
// Data Isolation for Crafted Network™
|
// Data Isolation
|
||||||
if (currentUser && currentUser.app_role) {
|
if (currentUser && currentUser.app_role) {
|
||||||
const roleName = currentUser.app_role.name;
|
const roleName = currentUser.app_role.name;
|
||||||
if (roleName === 'Verified Business Owner') {
|
if (roleName === 'Verified Business Owner') {
|
||||||
where['$business.owner_userId$'] = currentUser.id;
|
if (currentUser.businessId) {
|
||||||
|
where.businessId = currentUser.businessId;
|
||||||
|
} else {
|
||||||
|
where['$business.owner_userId$'] = currentUser.id;
|
||||||
|
}
|
||||||
} else if (roleName === 'Consumer') {
|
} else if (roleName === 'Consumer') {
|
||||||
where.userId = currentUser.id;
|
where.userId = currentUser.id;
|
||||||
}
|
}
|
||||||
@ -652,4 +656,4 @@ module.exports = class ReviewsDBApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
const db = require('../models');
|
const db = require('../models');
|
||||||
const FileDBApi = require('./file');
|
const FileDBApi = require('./file');
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
@ -307,6 +306,20 @@ module.exports = class Service_pricesDBApi {
|
|||||||
|
|
||||||
const transaction = (options && options.transaction) || undefined;
|
const transaction = (options && options.transaction) || undefined;
|
||||||
|
|
||||||
|
const currentUser = options?.currentUser;
|
||||||
|
|
||||||
|
// Data Isolation
|
||||||
|
if (currentUser && currentUser.app_role) {
|
||||||
|
const roleName = currentUser.app_role.name;
|
||||||
|
if (roleName === 'Verified Business Owner') {
|
||||||
|
if (currentUser.businessId) {
|
||||||
|
where.businessId = currentUser.businessId;
|
||||||
|
} else {
|
||||||
|
where['$business.owner_userId$'] = currentUser.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let include = [
|
let include = [
|
||||||
|
|
||||||
{
|
{
|
||||||
@ -591,5 +604,4 @@ module.exports = class Service_pricesDBApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
const db = require('../models');
|
const db = require('../models');
|
||||||
const FileDBApi = require('./file');
|
const FileDBApi = require('./file');
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
@ -85,6 +84,7 @@ module.exports = class UsersDBApi {
|
|||||||
null
|
null
|
||||||
,
|
,
|
||||||
|
|
||||||
|
businessId: data.data.businessId || null,
|
||||||
importHash: data.data.importHash || null,
|
importHash: data.data.importHash || null,
|
||||||
createdById: currentUser.id,
|
createdById: currentUser.id,
|
||||||
updatedById: currentUser.id,
|
updatedById: currentUser.id,
|
||||||
@ -298,6 +298,7 @@ module.exports = class UsersDBApi {
|
|||||||
|
|
||||||
if (data.provider !== undefined) updatePayload.provider = data.provider;
|
if (data.provider !== undefined) updatePayload.provider = data.provider;
|
||||||
|
|
||||||
|
if (data.businessId !== undefined) updatePayload.businessId = data.businessId;
|
||||||
|
|
||||||
updatePayload.updatedById = currentUser.id;
|
updatePayload.updatedById = currentUser.id;
|
||||||
|
|
||||||
@ -983,5 +984,4 @@ module.exports = class UsersDBApi {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1,18 +1,12 @@
|
|||||||
|
|
||||||
const db = require('../models');
|
const db = require('../models');
|
||||||
const FileDBApi = require('./file');
|
const FileDBApi = require('./file');
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
const Utils = require('../utils');
|
const Utils = require('../utils');
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const Sequelize = db.Sequelize;
|
const Sequelize = db.Sequelize;
|
||||||
const Op = Sequelize.Op;
|
const Op = Sequelize.Op;
|
||||||
|
|
||||||
module.exports = class Verification_evidencesDBApi {
|
module.exports = class Verification_evidencesDBApi {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
static async create(data, options) {
|
static async create(data, options) {
|
||||||
const currentUser = (options && options.currentUser) || { id: null };
|
const currentUser = (options && options.currentUser) || { id: null };
|
||||||
const transaction = (options && options.transaction) || undefined;
|
const transaction = (options && options.transaction) || undefined;
|
||||||
@ -20,404 +14,73 @@ module.exports = class Verification_evidencesDBApi {
|
|||||||
const verification_evidences = await db.verification_evidences.create(
|
const verification_evidences = await db.verification_evidences.create(
|
||||||
{
|
{
|
||||||
id: data.id || undefined,
|
id: data.id || undefined,
|
||||||
|
evidence_type: data.evidence_type || null,
|
||||||
evidence_type: data.evidence_type
|
url: data.url || null,
|
||||||
||
|
created_at_ts: data.created_at_ts || null,
|
||||||
null
|
importHash: data.importHash || null,
|
||||||
,
|
createdById: currentUser.id,
|
||||||
|
updatedById: currentUser.id,
|
||||||
url: data.url
|
|
||||||
||
|
|
||||||
null
|
|
||||||
,
|
|
||||||
|
|
||||||
created_at_ts: data.created_at_ts
|
|
||||||
||
|
|
||||||
null
|
|
||||||
,
|
|
||||||
|
|
||||||
importHash: data.importHash || null,
|
|
||||||
createdById: currentUser.id,
|
|
||||||
updatedById: currentUser.id,
|
|
||||||
},
|
|
||||||
{ transaction },
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
await verification_evidences.setSubmission( data.submission || null, {
|
|
||||||
transaction,
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
await FileDBApi.replaceRelationFiles(
|
|
||||||
{
|
|
||||||
belongsTo: db.verification_evidences.getTableName(),
|
|
||||||
belongsToColumn: 'files',
|
|
||||||
belongsToId: verification_evidences.id,
|
|
||||||
},
|
},
|
||||||
data.files,
|
|
||||||
options,
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
return verification_evidences;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
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 verification_evidencesData = data.map((item, index) => ({
|
|
||||||
id: item.id || undefined,
|
|
||||||
|
|
||||||
evidence_type: item.evidence_type
|
|
||||||
||
|
|
||||||
null
|
|
||||||
,
|
|
||||||
|
|
||||||
url: item.url
|
|
||||||
||
|
|
||||||
null
|
|
||||||
,
|
|
||||||
|
|
||||||
created_at_ts: item.created_at_ts
|
|
||||||
||
|
|
||||||
null
|
|
||||||
,
|
|
||||||
|
|
||||||
importHash: item.importHash || null,
|
|
||||||
createdById: currentUser.id,
|
|
||||||
updatedById: currentUser.id,
|
|
||||||
createdAt: new Date(Date.now() + index * 1000),
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Bulk create items
|
|
||||||
const verification_evidences = await db.verification_evidences.bulkCreate(verification_evidencesData, { transaction });
|
|
||||||
|
|
||||||
// For each item created, replace relation files
|
|
||||||
|
|
||||||
for (let i = 0; i < verification_evidences.length; i++) {
|
|
||||||
await FileDBApi.replaceRelationFiles(
|
|
||||||
{
|
|
||||||
belongsTo: db.verification_evidences.getTableName(),
|
|
||||||
belongsToColumn: 'files',
|
|
||||||
belongsToId: verification_evidences[i].id,
|
|
||||||
},
|
|
||||||
data[i].files,
|
|
||||||
options,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
return verification_evidences;
|
|
||||||
}
|
|
||||||
|
|
||||||
static async update(id, data, options) {
|
|
||||||
const currentUser = (options && options.currentUser) || {id: null};
|
|
||||||
const transaction = (options && options.transaction) || undefined;
|
|
||||||
|
|
||||||
|
|
||||||
const verification_evidences = await db.verification_evidences.findByPk(id, {}, {transaction});
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const updatePayload = {};
|
|
||||||
|
|
||||||
if (data.evidence_type !== undefined) updatePayload.evidence_type = data.evidence_type;
|
|
||||||
|
|
||||||
|
|
||||||
if (data.url !== undefined) updatePayload.url = data.url;
|
|
||||||
|
|
||||||
|
|
||||||
if (data.created_at_ts !== undefined) updatePayload.created_at_ts = data.created_at_ts;
|
|
||||||
|
|
||||||
|
|
||||||
updatePayload.updatedById = currentUser.id;
|
|
||||||
|
|
||||||
await verification_evidences.update(updatePayload, {transaction});
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if (data.submission !== undefined) {
|
|
||||||
await verification_evidences.setSubmission(
|
|
||||||
|
|
||||||
data.submission,
|
|
||||||
|
|
||||||
{ transaction }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
await FileDBApi.replaceRelationFiles(
|
|
||||||
{
|
|
||||||
belongsTo: db.verification_evidences.getTableName(),
|
|
||||||
belongsToColumn: 'files',
|
|
||||||
belongsToId: verification_evidences.id,
|
|
||||||
},
|
|
||||||
data.files,
|
|
||||||
options,
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
return verification_evidences;
|
|
||||||
}
|
|
||||||
|
|
||||||
static async deleteByIds(ids, options) {
|
|
||||||
const currentUser = (options && options.currentUser) || { id: null };
|
|
||||||
const transaction = (options && options.transaction) || undefined;
|
|
||||||
|
|
||||||
const verification_evidences = await db.verification_evidences.findAll({
|
|
||||||
where: {
|
|
||||||
id: {
|
|
||||||
[Op.in]: ids,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
transaction,
|
|
||||||
});
|
|
||||||
|
|
||||||
await db.sequelize.transaction(async (transaction) => {
|
|
||||||
for (const record of verification_evidences) {
|
|
||||||
await record.update(
|
|
||||||
{deletedBy: currentUser.id},
|
|
||||||
{transaction}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
for (const record of verification_evidences) {
|
|
||||||
await record.destroy({transaction});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
return verification_evidences;
|
|
||||||
}
|
|
||||||
|
|
||||||
static async remove(id, options) {
|
|
||||||
const currentUser = (options && options.currentUser) || {id: null};
|
|
||||||
const transaction = (options && options.transaction) || undefined;
|
|
||||||
|
|
||||||
const verification_evidences = await db.verification_evidences.findByPk(id, options);
|
|
||||||
|
|
||||||
await verification_evidences.update({
|
|
||||||
deletedBy: currentUser.id
|
|
||||||
}, {
|
|
||||||
transaction,
|
|
||||||
});
|
|
||||||
|
|
||||||
await verification_evidences.destroy({
|
|
||||||
transaction
|
|
||||||
});
|
|
||||||
|
|
||||||
return verification_evidences;
|
|
||||||
}
|
|
||||||
|
|
||||||
static async findBy(where, options) {
|
|
||||||
const transaction = (options && options.transaction) || undefined;
|
|
||||||
|
|
||||||
const verification_evidences = await db.verification_evidences.findOne(
|
|
||||||
{ where },
|
|
||||||
{ transaction },
|
{ transaction },
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!verification_evidences) {
|
await verification_evidences.setSubmission( data.submission || null, { transaction });
|
||||||
return verification_evidences;
|
await FileDBApi.replaceRelationFiles(
|
||||||
}
|
{
|
||||||
|
belongsTo: db.verification_evidences.getTableName(),
|
||||||
|
belongsToColumn: 'files',
|
||||||
|
belongsToId: verification_evidences.id,
|
||||||
|
},
|
||||||
|
data.files,
|
||||||
|
options,
|
||||||
|
);
|
||||||
|
|
||||||
const output = verification_evidences.get({plain: true});
|
return verification_evidences;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
output.submission = await verification_evidences.getSubmission({
|
|
||||||
transaction
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
output.files = await verification_evidences.getFiles({
|
|
||||||
transaction
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return output;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static async findAll(
|
static async findAll(filter, options) {
|
||||||
filter,
|
|
||||||
options
|
|
||||||
) {
|
|
||||||
const limit = filter.limit || 0;
|
const limit = filter.limit || 0;
|
||||||
let offset = 0;
|
let offset = 0;
|
||||||
let where = {};
|
let where = {};
|
||||||
const currentPage = +filter.page;
|
const currentPage = +filter.page;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
offset = currentPage * limit;
|
offset = currentPage * limit;
|
||||||
|
|
||||||
const orderBy = null;
|
const transaction = (options && options.transaction) || undefined;
|
||||||
|
const currentUser = options?.currentUser;
|
||||||
|
|
||||||
const transaction = (options && options.transaction) || undefined;
|
// Data Isolation
|
||||||
|
if (currentUser && currentUser.app_role) {
|
||||||
let include = [
|
const roleName = currentUser.app_role.name;
|
||||||
|
if (roleName === 'Verified Business Owner') {
|
||||||
{
|
if (currentUser.businessId) {
|
||||||
model: db.verification_submissions,
|
where['$submission.businessId$'] = currentUser.businessId;
|
||||||
as: 'submission',
|
} else {
|
||||||
|
where['$submission.business.owner_userId$'] = currentUser.id;
|
||||||
where: filter.submission ? {
|
|
||||||
[Op.or]: [
|
|
||||||
{ id: { [Op.in]: filter.submission.split('|').map(term => Utils.uuid(term)) } },
|
|
||||||
{
|
|
||||||
notes: {
|
|
||||||
[Op.or]: filter.submission.split('|').map(term => ({ [Op.iLike]: `%${term}%` }))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
]
|
|
||||||
} : {},
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{
|
|
||||||
model: db.file,
|
|
||||||
as: 'files',
|
|
||||||
},
|
|
||||||
|
|
||||||
];
|
|
||||||
|
|
||||||
if (filter) {
|
|
||||||
if (filter.id) {
|
|
||||||
where = {
|
|
||||||
...where,
|
|
||||||
['id']: Utils.uuid(filter.id),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (filter.url) {
|
|
||||||
where = {
|
|
||||||
...where,
|
|
||||||
[Op.and]: Utils.ilike(
|
|
||||||
'verification_evidences',
|
|
||||||
'url',
|
|
||||||
filter.url,
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if (filter.created_at_tsRange) {
|
|
||||||
const [start, end] = filter.created_at_tsRange;
|
|
||||||
|
|
||||||
if (start !== undefined && start !== null && start !== '') {
|
|
||||||
where = {
|
|
||||||
...where,
|
|
||||||
created_at_ts: {
|
|
||||||
...where.created_at_ts,
|
|
||||||
[Op.gte]: start,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (end !== undefined && end !== null && end !== '') {
|
|
||||||
where = {
|
|
||||||
...where,
|
|
||||||
created_at_ts: {
|
|
||||||
...where.created_at_ts,
|
|
||||||
[Op.lte]: end,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (filter.active !== undefined) {
|
|
||||||
where = {
|
|
||||||
...where,
|
|
||||||
active: filter.active === true || filter.active === 'true'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (filter.evidence_type) {
|
|
||||||
where = {
|
|
||||||
...where,
|
|
||||||
evidence_type: filter.evidence_type,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
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,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
let include = [
|
||||||
|
{
|
||||||
|
model: db.verification_submissions,
|
||||||
|
as: 'submission',
|
||||||
|
include: [{ model: db.businesses, as: 'business' }],
|
||||||
|
where: filter.submission ? {
|
||||||
|
[Op.or]: [
|
||||||
|
{ id: { [Op.in]: filter.submission.split('|').map(term => Utils.uuid(term)) } },
|
||||||
|
{ notes: { [Op.or]: filter.submission.split('|').map(term => ({ [Op.iLike]: `%${term}%` })) } },
|
||||||
|
]
|
||||||
|
} : {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: db.file,
|
||||||
|
as: 'files',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
if (filter) {
|
||||||
|
if (filter.id) where.id = Utils.uuid(filter.id);
|
||||||
|
}
|
||||||
|
|
||||||
const queryOptions = {
|
const queryOptions = {
|
||||||
where,
|
where,
|
||||||
@ -426,8 +89,7 @@ module.exports = class Verification_evidencesDBApi {
|
|||||||
order: filter.field && filter.sort
|
order: filter.field && filter.sort
|
||||||
? [[filter.field, filter.sort]]
|
? [[filter.field, filter.sort]]
|
||||||
: [['createdAt', 'desc']],
|
: [['createdAt', 'desc']],
|
||||||
transaction: options?.transaction,
|
transaction,
|
||||||
logging: console.log
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!options?.countOnly) {
|
if (!options?.countOnly) {
|
||||||
@ -435,51 +97,90 @@ module.exports = class Verification_evidencesDBApi {
|
|||||||
queryOptions.offset = offset ? Number(offset) : undefined;
|
queryOptions.offset = offset ? Number(offset) : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
const { rows, count } = await db.verification_evidences.findAndCountAll(queryOptions);
|
||||||
const { rows, count } = await db.verification_evidences.findAndCountAll(queryOptions);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
rows: options?.countOnly ? [] : rows,
|
rows: options?.countOnly ? [] : rows,
|
||||||
count: count
|
count: count
|
||||||
};
|
};
|
||||||
} catch (error) {
|
|
||||||
console.error('Error executing query:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static async findAllAutocomplete(query, limit, offset, ) {
|
static async findBy(where, options) {
|
||||||
let where = {};
|
const transaction = (options && options.transaction) || undefined;
|
||||||
|
const verification_evidences = await db.verification_evidences.findOne({ where, transaction });
|
||||||
|
if (!verification_evidences) return null;
|
||||||
|
|
||||||
|
const output = verification_evidences.get({plain: true});
|
||||||
|
output.submission = await verification_evidences.getSubmission({ transaction });
|
||||||
|
output.files = await verification_evidences.getFiles({ transaction });
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async update(id, data, options) {
|
||||||
|
const currentUser = (options && options.currentUser) || {id: null};
|
||||||
|
const transaction = (options && options.transaction) || undefined;
|
||||||
|
const verification_evidences = await db.verification_evidences.findByPk(id, {transaction});
|
||||||
|
|
||||||
|
const updatePayload = { ...data, updatedById: currentUser.id };
|
||||||
|
await verification_evidences.update(updatePayload, {transaction});
|
||||||
|
|
||||||
|
if (data.submission !== undefined) await verification_evidences.setSubmission(data.submission, { transaction });
|
||||||
|
await FileDBApi.replaceRelationFiles(
|
||||||
|
{
|
||||||
|
belongsTo: db.verification_evidences.getTableName(),
|
||||||
|
belongsToColumn: 'files',
|
||||||
|
belongsToId: verification_evidences.id,
|
||||||
|
},
|
||||||
|
data.files,
|
||||||
|
options,
|
||||||
|
);
|
||||||
|
|
||||||
|
return verification_evidences;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async deleteByIds(ids, options) {
|
||||||
|
const currentUser = (options && options.currentUser) || { id: null };
|
||||||
|
const transaction = (options && options.transaction) || undefined;
|
||||||
|
const verification_evidences = await db.verification_evidences.findAll({ where: { id: { [Op.in]: ids } }, transaction });
|
||||||
|
|
||||||
|
for (const record of verification_evidences) {
|
||||||
|
await record.update({deletedBy: currentUser.id}, {transaction});
|
||||||
|
await record.destroy({transaction});
|
||||||
|
}
|
||||||
|
return verification_evidences;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async remove(id, options) {
|
||||||
|
const currentUser = (options && options.currentUser) || {id: null};
|
||||||
|
const transaction = (options && options.transaction) || undefined;
|
||||||
|
const verification_evidences = await db.verification_evidences.findByPk(id, options);
|
||||||
|
await verification_evidences.update({ deletedBy: currentUser.id }, { transaction });
|
||||||
|
await verification_evidences.destroy({ transaction });
|
||||||
|
return verification_evidences;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async findAllAutocomplete(query, limit, offset) {
|
||||||
|
let where = {};
|
||||||
if (query) {
|
if (query) {
|
||||||
where = {
|
where = {
|
||||||
[Op.or]: [
|
[Op.or]: [
|
||||||
{ ['id']: Utils.uuid(query) },
|
{ ['id']: Utils.uuid(query) },
|
||||||
Utils.ilike(
|
|
||||||
'verification_evidences',
|
|
||||||
'url',
|
|
||||||
query,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const records = await db.verification_evidences.findAll({
|
const records = await db.verification_evidences.findAll({
|
||||||
attributes: [ 'id', 'url' ],
|
attributes: [ 'id' ],
|
||||||
where,
|
where,
|
||||||
limit: limit ? Number(limit) : undefined,
|
limit: limit ? Number(limit) : undefined,
|
||||||
offset: offset ? Number(offset) : undefined,
|
offset: offset ? Number(offset) : undefined,
|
||||||
orderBy: [['url', 'ASC']],
|
order: [['createdAt', 'desc']],
|
||||||
});
|
});
|
||||||
|
|
||||||
return records.map((record) => ({
|
return records.map((record) => ({
|
||||||
id: record.id,
|
id: record.id,
|
||||||
label: record.url,
|
label: record.id,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
const db = require('../models');
|
const db = require('../models');
|
||||||
const FileDBApi = require('./file');
|
const FileDBApi = require('./file');
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
@ -298,6 +297,20 @@ module.exports = class Verification_submissionsDBApi {
|
|||||||
|
|
||||||
const transaction = (options && options.transaction) || undefined;
|
const transaction = (options && options.transaction) || undefined;
|
||||||
|
|
||||||
|
const currentUser = options?.currentUser;
|
||||||
|
|
||||||
|
// Data Isolation
|
||||||
|
if (currentUser && currentUser.app_role) {
|
||||||
|
const roleName = currentUser.app_role.name;
|
||||||
|
if (roleName === 'Verified Business Owner') {
|
||||||
|
if (currentUser.businessId) {
|
||||||
|
where.businessId = currentUser.businessId;
|
||||||
|
} else {
|
||||||
|
where['$business.owner_userId$'] = currentUser.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let include = [
|
let include = [
|
||||||
|
|
||||||
{
|
{
|
||||||
@ -524,5 +537,4 @@ module.exports = class Verification_submissionsDBApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
|
||||||
|
module.exports = {
|
||||||
|
/**
|
||||||
|
* @param {import("sequelize").QueryInterface} queryInterface
|
||||||
|
* @param {import("sequelize").Sequelize} Sequelize
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async up(queryInterface, Sequelize) {
|
||||||
|
await queryInterface.addColumn('users', 'businessId', {
|
||||||
|
type: Sequelize.DataTypes.UUID,
|
||||||
|
references: {
|
||||||
|
model: 'businesses',
|
||||||
|
key: 'id',
|
||||||
|
},
|
||||||
|
allowNull: true,
|
||||||
|
onUpdate: 'CASCADE',
|
||||||
|
onDelete: 'SET NULL',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async down(queryInterface, Sequelize) {
|
||||||
|
await queryInterface.removeColumn('users', 'businessId');
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -0,0 +1,55 @@
|
|||||||
|
module.exports = {
|
||||||
|
/**
|
||||||
|
* @param {import("sequelize").QueryInterface} queryInterface
|
||||||
|
* @param {import("sequelize").Sequelize} Sequelize
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async up(queryInterface, Sequelize) {
|
||||||
|
const roles = await queryInterface.sequelize.query(
|
||||||
|
`SELECT id FROM roles WHERE name = 'Trust & Safety Lead'`,
|
||||||
|
{ type: queryInterface.sequelize.QueryTypes.SELECT }
|
||||||
|
);
|
||||||
|
if (!roles || roles.length === 0) return;
|
||||||
|
const roleId = roles[0].id;
|
||||||
|
|
||||||
|
const permissionsToGrant = [
|
||||||
|
'READ_VERIFICATION_SUBMISSIONS',
|
||||||
|
'UPDATE_VERIFICATION_SUBMISSIONS',
|
||||||
|
'READ_VERIFICATION_EVIDENCES',
|
||||||
|
'READ_REVIEWS',
|
||||||
|
'UPDATE_REVIEWS',
|
||||||
|
'DELETE_REVIEWS',
|
||||||
|
'READ_BUSINESSES',
|
||||||
|
'READ_USERS'
|
||||||
|
];
|
||||||
|
|
||||||
|
const permissions = await queryInterface.sequelize.query(
|
||||||
|
`SELECT id, name FROM permissions WHERE name IN (${permissionsToGrant.map(p => `'${p}'`).join(',')})`,
|
||||||
|
{ type: queryInterface.sequelize.QueryTypes.SELECT }
|
||||||
|
);
|
||||||
|
|
||||||
|
const rolesPermissions = [];
|
||||||
|
for (const p of permissions) {
|
||||||
|
const existing = await queryInterface.sequelize.query(
|
||||||
|
`SELECT * FROM "rolesPermissionsPermissions" WHERE "roles_permissionsId" = '${roleId}' AND "permissionId" = '${p.id}'`,
|
||||||
|
{ type: queryInterface.sequelize.QueryTypes.SELECT }
|
||||||
|
);
|
||||||
|
if (existing.length === 0) {
|
||||||
|
rolesPermissions.push({
|
||||||
|
roles_permissionsId: roleId,
|
||||||
|
permissionId: p.id,
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rolesPermissions.length > 0) {
|
||||||
|
await queryInterface.bulkInsert('rolesPermissionsPermissions', rolesPermissions);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async down(queryInterface, Sequelize) {
|
||||||
|
// Optionally implement down migration
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -0,0 +1,56 @@
|
|||||||
|
module.exports = {
|
||||||
|
up: async (queryInterface, Sequelize) => {
|
||||||
|
await queryInterface.createTable('listing_events', {
|
||||||
|
id: {
|
||||||
|
type: Sequelize.UUID,
|
||||||
|
defaultValue: Sequelize.UUIDV4,
|
||||||
|
primaryKey: true,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
businessId: {
|
||||||
|
type: Sequelize.UUID,
|
||||||
|
allowNull: false,
|
||||||
|
references: {
|
||||||
|
model: 'businesses',
|
||||||
|
key: 'id',
|
||||||
|
},
|
||||||
|
onUpdate: 'CASCADE',
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
},
|
||||||
|
event_type: {
|
||||||
|
type: Sequelize.ENUM('VIEW', 'CALL_CLICK', 'WEBSITE_CLICK'),
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
userId: {
|
||||||
|
type: Sequelize.UUID,
|
||||||
|
allowNull: true,
|
||||||
|
references: {
|
||||||
|
model: 'users',
|
||||||
|
key: 'id',
|
||||||
|
},
|
||||||
|
onUpdate: 'CASCADE',
|
||||||
|
onDelete: 'SET NULL',
|
||||||
|
},
|
||||||
|
metadata: {
|
||||||
|
type: Sequelize.JSONB,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
createdAt: {
|
||||||
|
type: Sequelize.DATE,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
updatedAt: {
|
||||||
|
type: Sequelize.DATE,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
deletedAt: {
|
||||||
|
type: Sequelize.DATE,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
down: async (queryInterface, Sequelize) => {
|
||||||
|
await queryInterface.dropTable('listing_events');
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -0,0 +1,91 @@
|
|||||||
|
module.exports = {
|
||||||
|
up: async (queryInterface, Sequelize) => {
|
||||||
|
await queryInterface.createTable('plans', {
|
||||||
|
id: {
|
||||||
|
type: Sequelize.UUID,
|
||||||
|
defaultValue: Sequelize.UUIDV4,
|
||||||
|
primaryKey: true,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: Sequelize.STRING,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
price: {
|
||||||
|
type: Sequelize.DECIMAL(10, 2),
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: 0,
|
||||||
|
},
|
||||||
|
features: {
|
||||||
|
type: Sequelize.JSONB,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
createdAt: {
|
||||||
|
type: Sequelize.DATE,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
updatedAt: {
|
||||||
|
type: Sequelize.DATE,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await queryInterface.addColumn('businesses', 'planId', {
|
||||||
|
type: Sequelize.UUID,
|
||||||
|
allowNull: true,
|
||||||
|
references: {
|
||||||
|
model: 'plans',
|
||||||
|
key: 'id',
|
||||||
|
},
|
||||||
|
onUpdate: 'CASCADE',
|
||||||
|
onDelete: 'SET NULL',
|
||||||
|
});
|
||||||
|
|
||||||
|
await queryInterface.addColumn('businesses', 'renewal_date', {
|
||||||
|
type: Sequelize.DATE,
|
||||||
|
allowNull: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Seed basic plans
|
||||||
|
const now = new Date();
|
||||||
|
const plans = [
|
||||||
|
{
|
||||||
|
id: '00000000-0000-0000-0000-000000000001',
|
||||||
|
name: 'Basic (Free)',
|
||||||
|
price: 0,
|
||||||
|
features: JSON.stringify(['Limited Leads', 'Standard Listing']),
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '00000000-0000-0000-0000-000000000002',
|
||||||
|
name: 'Professional',
|
||||||
|
price: 49.99,
|
||||||
|
features: JSON.stringify(['Unlimited Leads', 'Priority Support', 'Enhanced Profile']),
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '00000000-0000-0000-0000-000000000003',
|
||||||
|
name: 'Enterprise',
|
||||||
|
price: 199.99,
|
||||||
|
features: JSON.stringify(['Custom Branding', 'API Access', 'Dedicated Manager']),
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
await queryInterface.bulkInsert('plans', plans);
|
||||||
|
|
||||||
|
// Assign free plan to existing businesses
|
||||||
|
await queryInterface.sequelize.query(
|
||||||
|
`UPDATE businesses SET "planId" = '00000000-0000-0000-0000-000000000001', "renewal_date" = '${new Date(now.getFullYear(), now.getMonth() + 1, now.getDate()).toISOString()}'`
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
down: async (queryInterface, Sequelize) => {
|
||||||
|
await queryInterface.removeColumn('businesses', 'planId');
|
||||||
|
await queryInterface.removeColumn('businesses', 'renewal_date');
|
||||||
|
await queryInterface.dropTable('plans');
|
||||||
|
},
|
||||||
|
};
|
||||||
10
backend/src/db/migrations/20260218040000-fix-client-role.js
Normal file
10
backend/src/db/migrations/20260218040000-fix-client-role.js
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
module.exports = {
|
||||||
|
async up(queryInterface) {
|
||||||
|
const [roles] = await queryInterface.sequelize.query("SELECT id FROM roles WHERE name = 'Verified Business Owner' LIMIT 1;");
|
||||||
|
if (roles && roles.length > 0) {
|
||||||
|
const vboRoleId = roles[0].id;
|
||||||
|
await queryInterface.sequelize.query(`UPDATE users SET "app_roleId" = '${vboRoleId}' WHERE email = 'client@hello.com';`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async down(queryInterface) {}
|
||||||
|
};
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
module.exports = {
|
||||||
|
async up(queryInterface) {
|
||||||
|
const createdAt = new Date();
|
||||||
|
const updatedAt = new Date();
|
||||||
|
|
||||||
|
const [roles] = await queryInterface.sequelize.query("SELECT id FROM roles WHERE name = 'Verified Business Owner' LIMIT 1;");
|
||||||
|
const [permissions] = await queryInterface.sequelize.query("SELECT id FROM permissions WHERE name = 'CREATE_BUSINESSES' LIMIT 1;");
|
||||||
|
|
||||||
|
if (roles && roles.length > 0 && permissions && permissions.length > 0) {
|
||||||
|
const vboRoleId = roles[0].id;
|
||||||
|
const createPermId = permissions[0].id;
|
||||||
|
|
||||||
|
// Check if permission already exists for this role
|
||||||
|
const [existing] = await queryInterface.sequelize.query(`SELECT * FROM "rolesPermissionsPermissions" WHERE "roles_permissionsId" = '${vboRoleId}' AND "permissionId" = '${createPermId}';`);
|
||||||
|
|
||||||
|
if (existing.length === 0) {
|
||||||
|
await queryInterface.bulkInsert("rolesPermissionsPermissions", [
|
||||||
|
{ createdAt, updatedAt, roles_permissionsId: vboRoleId, permissionId: createPermId }
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async down(queryInterface) {}
|
||||||
|
};
|
||||||
@ -179,6 +179,16 @@ google_place_id: {
|
|||||||
allowNull: false,
|
allowNull: false,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
planId: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
renewal_date: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
|
||||||
created_at_ts: {
|
created_at_ts: {
|
||||||
type: DataTypes.DATE,
|
type: DataTypes.DATE,
|
||||||
|
|
||||||
@ -312,8 +322,15 @@ updated_at_ts: {
|
|||||||
constraints: false,
|
constraints: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
db.businesses.belongsTo(db.plans, {
|
||||||
|
as: 'plan',
|
||||||
|
foreignKey: 'planId',
|
||||||
|
});
|
||||||
|
|
||||||
|
db.businesses.hasMany(db.listing_events, {
|
||||||
|
as: 'listing_events',
|
||||||
|
foreignKey: 'businessId',
|
||||||
|
});
|
||||||
|
|
||||||
db.businesses.belongsTo(db.users, {
|
db.businesses.belongsTo(db.users, {
|
||||||
as: 'createdBy',
|
as: 'createdBy',
|
||||||
@ -327,4 +344,4 @@ updated_at_ts: {
|
|||||||
|
|
||||||
|
|
||||||
return businesses;
|
return businesses;
|
||||||
};
|
};
|
||||||
|
|||||||
47
backend/src/db/models/listing_events.js
Normal file
47
backend/src/db/models/listing_events.js
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
module.exports = function(sequelize, DataTypes) {
|
||||||
|
const listing_events = sequelize.define(
|
||||||
|
'listing_events',
|
||||||
|
{
|
||||||
|
id: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
defaultValue: DataTypes.UUIDV4,
|
||||||
|
primaryKey: true,
|
||||||
|
},
|
||||||
|
businessId: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
event_type: {
|
||||||
|
type: DataTypes.ENUM('VIEW', 'CALL_CLICK', 'WEBSITE_CLICK'),
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
userId: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
metadata: {
|
||||||
|
type: DataTypes.JSONB,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamps: true,
|
||||||
|
paranoid: true,
|
||||||
|
freezeTableName: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
listing_events.associate = (db) => {
|
||||||
|
db.listing_events.belongsTo(db.businesses, {
|
||||||
|
as: 'business',
|
||||||
|
foreignKey: 'businessId',
|
||||||
|
});
|
||||||
|
|
||||||
|
db.listing_events.belongsTo(db.users, {
|
||||||
|
as: 'user',
|
||||||
|
foreignKey: 'userId',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return listing_events;
|
||||||
|
};
|
||||||
37
backend/src/db/models/plans.js
Normal file
37
backend/src/db/models/plans.js
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
module.exports = function(sequelize, DataTypes) {
|
||||||
|
const plans = sequelize.define(
|
||||||
|
'plans',
|
||||||
|
{
|
||||||
|
id: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
defaultValue: DataTypes.UUIDV4,
|
||||||
|
primaryKey: true,
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
price: {
|
||||||
|
type: DataTypes.DECIMAL(10, 2),
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: 0,
|
||||||
|
},
|
||||||
|
features: {
|
||||||
|
type: DataTypes.JSONB,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamps: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
plans.associate = (db) => {
|
||||||
|
db.plans.hasMany(db.businesses, {
|
||||||
|
as: 'businesses',
|
||||||
|
foreignKey: 'planId',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return plans;
|
||||||
|
};
|
||||||
@ -104,6 +104,11 @@ provider: {
|
|||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
businessId: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
|
||||||
importHash: {
|
importHash: {
|
||||||
type: DataTypes.STRING(255),
|
type: DataTypes.STRING(255),
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
@ -257,7 +262,13 @@ provider: {
|
|||||||
constraints: false,
|
constraints: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
db.users.belongsTo(db.businesses, {
|
||||||
|
as: 'business',
|
||||||
|
foreignKey: {
|
||||||
|
name: 'businessId',
|
||||||
|
},
|
||||||
|
constraints: false,
|
||||||
|
});
|
||||||
|
|
||||||
db.users.hasMany(db.file, {
|
db.users.hasMany(db.file, {
|
||||||
as: 'avatar',
|
as: 'avatar',
|
||||||
@ -322,5 +333,4 @@ function trimStringFields(users) {
|
|||||||
: null;
|
: null;
|
||||||
|
|
||||||
return users;
|
return users;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -18,7 +18,7 @@ const pexelsRoutes = require('./routes/pexels');
|
|||||||
|
|
||||||
const openaiRoutes = require('./routes/openai');
|
const openaiRoutes = require('./routes/openai');
|
||||||
|
|
||||||
|
const dashboardRoutes = require('./routes/dashboard');
|
||||||
|
|
||||||
const usersRoutes = require('./routes/users');
|
const usersRoutes = require('./routes/users');
|
||||||
|
|
||||||
@ -77,8 +77,8 @@ const options = {
|
|||||||
openapi: "3.0.0",
|
openapi: "3.0.0",
|
||||||
info: {
|
info: {
|
||||||
version: "1.0.0",
|
version: "1.0.0",
|
||||||
title: "Crafted Network",
|
title: "Fix It Local",
|
||||||
description: "Crafted Network Online REST API for Testing and Prototyping application. You can perform all major operations with your entities - create, delete and etc.",
|
description: "Fix It Local Online REST API for Testing and Prototyping application. You can perform all major operations with your entities - create, delete and etc.",
|
||||||
},
|
},
|
||||||
servers: [
|
servers: [
|
||||||
{
|
{
|
||||||
@ -117,6 +117,15 @@ app.use(cors({origin: true}));
|
|||||||
require('./auth/auth');
|
require('./auth/auth');
|
||||||
|
|
||||||
app.use(bodyParser.json());
|
app.use(bodyParser.json());
|
||||||
|
|
||||||
|
const optionalAuth = (req, res, next) => {
|
||||||
|
passport.authenticate('jwt', { session: false }, (err, user, info) => {
|
||||||
|
if (user) {
|
||||||
|
req.currentUser = user;
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
})(req, res, next);
|
||||||
|
};
|
||||||
|
|
||||||
app.use('/api/auth', authRoutes);
|
app.use('/api/auth', authRoutes);
|
||||||
app.use('/api/file', fileRoutes);
|
app.use('/api/file', fileRoutes);
|
||||||
@ -132,19 +141,19 @@ app.use('/api/permissions', passport.authenticate('jwt', {session: false}), perm
|
|||||||
|
|
||||||
app.use('/api/refresh_tokens', passport.authenticate('jwt', {session: false}), refresh_tokensRoutes);
|
app.use('/api/refresh_tokens', passport.authenticate('jwt', {session: false}), refresh_tokensRoutes);
|
||||||
|
|
||||||
app.use('/api/categories', categoriesRoutes);
|
app.use('/api/categories', optionalAuth, categoriesRoutes);
|
||||||
|
|
||||||
app.use('/api/locations', locationsRoutes);
|
app.use('/api/locations', optionalAuth, locationsRoutes);
|
||||||
|
|
||||||
app.use('/api/businesses', businessesRoutes);
|
app.use('/api/businesses', optionalAuth, businessesRoutes);
|
||||||
|
|
||||||
app.use('/api/business_photos', business_photosRoutes);
|
app.use('/api/business_photos', optionalAuth, business_photosRoutes);
|
||||||
|
|
||||||
app.use('/api/business_categories', business_categoriesRoutes);
|
app.use('/api/business_categories', optionalAuth, business_categoriesRoutes);
|
||||||
|
|
||||||
app.use('/api/service_prices', service_pricesRoutes);
|
app.use('/api/service_prices', optionalAuth, service_pricesRoutes);
|
||||||
|
|
||||||
app.use('/api/business_badges', business_badgesRoutes);
|
app.use('/api/business_badges', optionalAuth, business_badgesRoutes);
|
||||||
|
|
||||||
app.use('/api/verification_submissions', passport.authenticate('jwt', {session: false}), verification_submissionsRoutes);
|
app.use('/api/verification_submissions', passport.authenticate('jwt', {session: false}), verification_submissionsRoutes);
|
||||||
|
|
||||||
@ -160,7 +169,7 @@ app.use('/api/messages', passport.authenticate('jwt', {session: false}), message
|
|||||||
|
|
||||||
app.use('/api/lead_events', passport.authenticate('jwt', {session: false}), lead_eventsRoutes);
|
app.use('/api/lead_events', passport.authenticate('jwt', {session: false}), lead_eventsRoutes);
|
||||||
|
|
||||||
app.use('/api/reviews', reviewsRoutes);
|
app.use('/api/reviews', optionalAuth, reviewsRoutes);
|
||||||
|
|
||||||
app.use('/api/disputes', passport.authenticate('jwt', {session: false}), disputesRoutes);
|
app.use('/api/disputes', passport.authenticate('jwt', {session: false}), disputesRoutes);
|
||||||
|
|
||||||
@ -181,6 +190,10 @@ app.use(
|
|||||||
openaiRoutes,
|
openaiRoutes,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
app.use(
|
||||||
|
'/api/dashboard',
|
||||||
|
dashboardRoutes);
|
||||||
|
|
||||||
app.use(
|
app.use(
|
||||||
'/api/search',
|
'/api/search',
|
||||||
searchRoutes);
|
searchRoutes);
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
|
|
||||||
const BusinessesService = require('../services/businesses');
|
const BusinessesService = require('../services/businesses');
|
||||||
@ -405,7 +404,6 @@ router.get('/count', wrapAsync(async (req, res) => {
|
|||||||
const currentUser = req.currentUser;
|
const currentUser = req.currentUser;
|
||||||
const payload = await BusinessesDBApi.findAll(
|
const payload = await BusinessesDBApi.findAll(
|
||||||
req.query,
|
req.query,
|
||||||
null,
|
|
||||||
{ countOnly: true, currentUser }
|
{ countOnly: true, currentUser }
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -493,4 +491,4 @@ router.get('/:id', wrapAsync(async (req, res) => {
|
|||||||
|
|
||||||
router.use('/', require('../helpers').commonErrorHandler);
|
router.use('/', require('../helpers').commonErrorHandler);
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
31
backend/src/routes/dashboard.js
Normal file
31
backend/src/routes/dashboard.js
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const DashboardService = require('../services/dashboard');
|
||||||
|
const { wrapAsync } = require('../helpers');
|
||||||
|
const db = require('../db/models');
|
||||||
|
const passport = require('passport');
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
router.get('/business-metrics', passport.authenticate('jwt', { session: false }), wrapAsync(async (req, res) => {
|
||||||
|
const payload = await DashboardService.getBusinessMetrics(req.currentUser);
|
||||||
|
res.status(200).send(payload);
|
||||||
|
}));
|
||||||
|
|
||||||
|
router.post('/record-event', (req, res, next) => {
|
||||||
|
passport.authenticate('jwt', { session: false }, (err, user) => {
|
||||||
|
req.currentUser = user || null;
|
||||||
|
next();
|
||||||
|
})(req, res, next);
|
||||||
|
}, wrapAsync(async (req, res) => {
|
||||||
|
const { businessId, event_type, metadata } = req.body;
|
||||||
|
|
||||||
|
const event = await db.listing_events.create({
|
||||||
|
businessId,
|
||||||
|
event_type,
|
||||||
|
userId: req.currentUser ? req.currentUser.id : null,
|
||||||
|
metadata
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(200).send(event);
|
||||||
|
}));
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
@ -25,6 +25,14 @@ module.exports = class BusinessesService {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Link business to user if they don't have one set yet
|
||||||
|
if (currentUser.app_role?.name === 'Verified Business Owner' && !currentUser.businessId) {
|
||||||
|
await db.users.update({ businessId: business.id }, {
|
||||||
|
where: { id: currentUser.id },
|
||||||
|
transaction
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
await transaction.commit();
|
await transaction.commit();
|
||||||
return business;
|
return business;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -49,6 +57,14 @@ module.exports = class BusinessesService {
|
|||||||
is_claimed: true,
|
is_claimed: true,
|
||||||
}, { transaction });
|
}, { transaction });
|
||||||
|
|
||||||
|
// Link business to user if they don't have one set yet
|
||||||
|
if (!currentUser.businessId) {
|
||||||
|
await db.users.update({ businessId: business.id }, {
|
||||||
|
where: { id: currentUser.id },
|
||||||
|
transaction
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
await transaction.commit();
|
await transaction.commit();
|
||||||
return business;
|
return business;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -65,7 +81,7 @@ module.exports = class BusinessesService {
|
|||||||
const bufferStream = new stream.PassThrough();
|
const bufferStream = new stream.PassThrough();
|
||||||
const results = [];
|
const results = [];
|
||||||
|
|
||||||
await bufferStream.end(Buffer.from(req.file.buffer, "utf-8")); // convert Buffer to Stream
|
await bufferStream.end(Buffer.from(req.file.buffer, "utf-8"));
|
||||||
|
|
||||||
await new Promise((resolve, reject) => {
|
await new Promise((resolve, reject) => {
|
||||||
bufferStream
|
bufferStream
|
||||||
@ -108,7 +124,7 @@ module.exports = class BusinessesService {
|
|||||||
|
|
||||||
// Ownership check for Verified Business Owner
|
// Ownership check for Verified Business Owner
|
||||||
if (currentUser.app_role?.name === 'Verified Business Owner') {
|
if (currentUser.app_role?.name === 'Verified Business Owner') {
|
||||||
if (business.owner_userId !== currentUser.id) {
|
if (business.owner_userId !== currentUser.id && business.id !== currentUser.businessId) {
|
||||||
throw new ForbiddenError('forbidden');
|
throw new ForbiddenError('forbidden');
|
||||||
}
|
}
|
||||||
// Prevent transferring ownership
|
// Prevent transferring ownership
|
||||||
@ -143,7 +159,10 @@ module.exports = class BusinessesService {
|
|||||||
const records = await db.businesses.findAll({
|
const records = await db.businesses.findAll({
|
||||||
where: {
|
where: {
|
||||||
id: { [db.Sequelize.Op.in]: ids },
|
id: { [db.Sequelize.Op.in]: ids },
|
||||||
owner_userId: { [db.Sequelize.Op.ne]: currentUser.id }
|
[db.Sequelize.Op.and]: [
|
||||||
|
{ owner_userId: { [db.Sequelize.Op.ne]: currentUser.id } },
|
||||||
|
{ id: { [db.Sequelize.Op.ne]: currentUser.businessId || null } }
|
||||||
|
]
|
||||||
},
|
},
|
||||||
transaction
|
transaction
|
||||||
});
|
});
|
||||||
@ -169,7 +188,9 @@ module.exports = class BusinessesService {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
let business = await db.businesses.findByPk(id, { transaction });
|
let business = await db.businesses.findByPk(id, { transaction });
|
||||||
if (currentUser.app_role?.name === 'Verified Business Owner' && business.owner_userId !== currentUser.id) {
|
if (!business) throw new ValidationError('businessesNotFound');
|
||||||
|
|
||||||
|
if (currentUser.app_role?.name === 'Verified Business Owner' && business.owner_userId !== currentUser.id && business.id !== currentUser.businessId) {
|
||||||
throw new ForbiddenError('forbidden');
|
throw new ForbiddenError('forbidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -189,4 +210,4 @@ module.exports = class BusinessesService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|||||||
179
backend/src/services/dashboard.js
Normal file
179
backend/src/services/dashboard.js
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
const { Op } = require('sequelize');
|
||||||
|
const db = require('../db/models');
|
||||||
|
const moment = require('moment');
|
||||||
|
|
||||||
|
module.exports = class DashboardService {
|
||||||
|
static async getBusinessMetrics(currentUser) {
|
||||||
|
// 1. Get businesses owned by current user
|
||||||
|
const businesses = await db.businesses.findAll({
|
||||||
|
where: { owner_userId: currentUser.id },
|
||||||
|
attributes: ['id', 'name', 'planId', 'renewal_date', 'reliability_score', 'description', 'phone', 'website', 'address', 'hours_json'],
|
||||||
|
include: [{ model: db.plans, as: 'plan' }]
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!businesses.length) {
|
||||||
|
return { no_business: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
const businessIds = businesses.map(b => b.id);
|
||||||
|
const last24h = moment().subtract(24, 'hours').toDate();
|
||||||
|
const last30d = moment().subtract(30, 'days').toDate();
|
||||||
|
const last7d = moment().subtract(7, 'days').toDate();
|
||||||
|
|
||||||
|
// --- Action Queue ---
|
||||||
|
// New leads last 24h
|
||||||
|
const newLeads24h = await db.lead_matches.count({
|
||||||
|
where: {
|
||||||
|
businessId: { [Op.in]: businessIds },
|
||||||
|
createdAt: { [Op.gte]: last24h }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Leads needing response (status NEW in lead_matches OR unread messages)
|
||||||
|
const leadsNeedingResponse = await db.lead_matches.count({
|
||||||
|
where: {
|
||||||
|
businessId: { [Op.in]: businessIds },
|
||||||
|
[Op.or]: [
|
||||||
|
{ status: 'SENT' }, // SENT means it's new for the business
|
||||||
|
{ leadId: { [Op.in]: db.sequelize.literal(`(SELECT "leadId" FROM messages WHERE "receiver_userId" = '${currentUser.id}' AND "read_at" IS NULL)`) } }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const verificationPending = await db.verification_submissions.count({
|
||||||
|
where: {
|
||||||
|
businessId: { [Op.in]: businessIds },
|
||||||
|
status: 'PENDING'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Lead Pipeline Snapshot ---
|
||||||
|
const pipelineStats = await db.lead_matches.findAll({
|
||||||
|
where: { businessId: { [Op.in]: businessIds } },
|
||||||
|
attributes: [
|
||||||
|
'status',
|
||||||
|
[db.sequelize.fn('COUNT', db.sequelize.col('id')), 'count']
|
||||||
|
],
|
||||||
|
group: ['status']
|
||||||
|
});
|
||||||
|
|
||||||
|
const pipeline = pipelineStats.reduce((acc, curr) => {
|
||||||
|
acc[curr.status] = parseInt(curr.get('count'));
|
||||||
|
return acc;
|
||||||
|
}, { SENT: 0, VIEWED: 0, RESPONDED: 0, SCHEDULED: 0, COMPLETED: 0, DECLINED: 0 });
|
||||||
|
|
||||||
|
const won30d = await db.lead_matches.count({
|
||||||
|
where: {
|
||||||
|
businessId: { [Op.in]: businessIds },
|
||||||
|
status: 'COMPLETED',
|
||||||
|
updatedAt: { [Op.gte]: last30d }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const lost30d = await db.lead_matches.count({
|
||||||
|
where: {
|
||||||
|
businessId: { [Op.in]: businessIds },
|
||||||
|
status: 'DECLINED',
|
||||||
|
updatedAt: { [Op.gte]: last30d }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const winRate30d = (won30d + lost30d) > 0 ? (won30d / (won30d + lost30d)) * 100 : 0;
|
||||||
|
|
||||||
|
// --- Recent Messages ---
|
||||||
|
// Join messages with lead_matches to ensure they belong to this business
|
||||||
|
const recentMessages = await db.messages.findAll({
|
||||||
|
where: {
|
||||||
|
[Op.or]: [
|
||||||
|
{ sender_userId: currentUser.id },
|
||||||
|
{ receiver_userId: currentUser.id }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
limit: 5,
|
||||||
|
order: [['createdAt', 'DESC']],
|
||||||
|
include: [
|
||||||
|
{ model: db.leads, as: 'lead' },
|
||||||
|
{ model: db.users, as: 'sender_user', attributes: ['firstName', 'lastName'] }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Performance ---
|
||||||
|
const getEventCount = async (type, since) => {
|
||||||
|
return await db.listing_events.count({
|
||||||
|
where: {
|
||||||
|
businessId: { [Op.in]: businessIds },
|
||||||
|
event_type: type,
|
||||||
|
createdAt: { [Op.gte]: since }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const views7d = await getEventCount('VIEW', last7d);
|
||||||
|
const views30d = await getEventCount('VIEW', last30d);
|
||||||
|
const calls7d = await getEventCount('CALL_CLICK', last7d);
|
||||||
|
const calls30d = await getEventCount('CALL_CLICK', last30d);
|
||||||
|
const website7d = await getEventCount('WEBSITE_CLICK', last7d);
|
||||||
|
const website30d = await getEventCount('WEBSITE_CLICK', last30d);
|
||||||
|
|
||||||
|
const totalClicks30d = calls30d + website30d;
|
||||||
|
const conversionRate = views30d > 0 ? (totalClicks30d / views30d) * 100 : 0;
|
||||||
|
|
||||||
|
// --- Health Score ---
|
||||||
|
const firstBusiness = businesses[0];
|
||||||
|
let healthScore = 0;
|
||||||
|
const missingFields = [];
|
||||||
|
const fieldsToCheck = [
|
||||||
|
{ name: 'name', weight: 10, label: 'Name' },
|
||||||
|
{ name: 'description', weight: 20, label: 'Description' },
|
||||||
|
{ name: 'phone', weight: 10, label: 'Phone' },
|
||||||
|
{ name: 'website', weight: 10, label: 'Website' },
|
||||||
|
{ name: 'address', weight: 10, label: 'Address' },
|
||||||
|
{ name: 'hours_json', weight: 10, label: 'Business Hours' },
|
||||||
|
];
|
||||||
|
|
||||||
|
fieldsToCheck.forEach(f => {
|
||||||
|
if (firstBusiness[f.name] && firstBusiness[f.name] !== '') {
|
||||||
|
healthScore += f.weight;
|
||||||
|
} else {
|
||||||
|
missingFields.push(f.label);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add weights for photos/categories/prices
|
||||||
|
const photoCount = await db.business_photos.count({ where: { businessId: firstBusiness.id } });
|
||||||
|
if (photoCount > 0) healthScore += 15; else missingFields.push('Photos');
|
||||||
|
|
||||||
|
const categoryCount = await db.business_categories.count({ where: { businessId: firstBusiness.id } });
|
||||||
|
if (categoryCount > 0) healthScore += 10; else missingFields.push('Categories');
|
||||||
|
|
||||||
|
const priceCount = await db.service_prices.count({ where: { businessId: firstBusiness.id } });
|
||||||
|
if (priceCount > 0) healthScore += 5; else missingFields.push('Service Prices');
|
||||||
|
|
||||||
|
return {
|
||||||
|
businesses,
|
||||||
|
action_queue: {
|
||||||
|
newLeads24h,
|
||||||
|
leadsNeedingResponse,
|
||||||
|
verificationPending,
|
||||||
|
missingFields
|
||||||
|
},
|
||||||
|
pipeline: {
|
||||||
|
NEW: pipeline.SENT,
|
||||||
|
CONTACTED: pipeline.VIEWED + pipeline.RESPONDED,
|
||||||
|
SCHEDULED: pipeline.SCHEDULED,
|
||||||
|
WON: pipeline.COMPLETED,
|
||||||
|
LOST: pipeline.DECLINED,
|
||||||
|
winRate30d
|
||||||
|
},
|
||||||
|
recentMessages,
|
||||||
|
performance: {
|
||||||
|
views7d,
|
||||||
|
views30d,
|
||||||
|
calls7d,
|
||||||
|
calls30d,
|
||||||
|
website7d,
|
||||||
|
website30d,
|
||||||
|
conversionRate
|
||||||
|
},
|
||||||
|
healthScore: Math.min(healthScore, 100)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -2,6 +2,7 @@ const db = require('../db/models');
|
|||||||
const LeadsDBApi = require('../db/api/leads');
|
const LeadsDBApi = require('../db/api/leads');
|
||||||
const processFile = require("../middlewares/upload");
|
const processFile = require("../middlewares/upload");
|
||||||
const ValidationError = require('./notifications/errors/validation');
|
const ValidationError = require('./notifications/errors/validation');
|
||||||
|
const ForbiddenError = require('./notifications/errors/forbidden');
|
||||||
const csv = require('csv-parser');
|
const csv = require('csv-parser');
|
||||||
const axios = require('axios');
|
const axios = require('axios');
|
||||||
const config = require('../config');
|
const config = require('../config');
|
||||||
@ -70,6 +71,18 @@ module.exports = class LeadsService {
|
|||||||
try {
|
try {
|
||||||
let leads = await LeadsDBApi.findBy({id}, {transaction});
|
let leads = await LeadsDBApi.findBy({id}, {transaction});
|
||||||
if (!leads) { throw new ValidationError('leadsNotFound'); }
|
if (!leads) { throw new ValidationError('leadsNotFound'); }
|
||||||
|
|
||||||
|
// Ownership check for Verified Business Owner
|
||||||
|
if (currentUser.app_role?.name === 'Verified Business Owner') {
|
||||||
|
const match = await db.lead_matches.findOne({
|
||||||
|
where: { leadId: id, businessId: currentUser.businessId },
|
||||||
|
transaction
|
||||||
|
});
|
||||||
|
if (!match) {
|
||||||
|
throw new ForbiddenError('forbidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const updatedLeads = await LeadsDBApi.update(id, data, { currentUser, transaction });
|
const updatedLeads = await LeadsDBApi.update(id, data, { currentUser, transaction });
|
||||||
await transaction.commit();
|
await transaction.commit();
|
||||||
return updatedLeads;
|
return updatedLeads;
|
||||||
@ -82,6 +95,20 @@ module.exports = class LeadsService {
|
|||||||
static async deleteByIds(ids, currentUser) {
|
static async deleteByIds(ids, currentUser) {
|
||||||
const transaction = await db.sequelize.transaction();
|
const transaction = await db.sequelize.transaction();
|
||||||
try {
|
try {
|
||||||
|
// Ownership check for Verified Business Owner
|
||||||
|
if (currentUser.app_role?.name === 'Verified Business Owner') {
|
||||||
|
const count = await db.lead_matches.count({
|
||||||
|
where: {
|
||||||
|
leadId: { [db.Sequelize.Op.in]: ids },
|
||||||
|
businessId: currentUser.businessId
|
||||||
|
},
|
||||||
|
transaction
|
||||||
|
});
|
||||||
|
if (count !== ids.length) {
|
||||||
|
throw new ForbiddenError('forbidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await LeadsDBApi.deleteByIds(ids, { currentUser, transaction });
|
await LeadsDBApi.deleteByIds(ids, { currentUser, transaction });
|
||||||
await transaction.commit();
|
await transaction.commit();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -93,6 +120,17 @@ module.exports = class LeadsService {
|
|||||||
static async remove(id, currentUser) {
|
static async remove(id, currentUser) {
|
||||||
const transaction = await db.sequelize.transaction();
|
const transaction = await db.sequelize.transaction();
|
||||||
try {
|
try {
|
||||||
|
// Ownership check for Verified Business Owner
|
||||||
|
if (currentUser.app_role?.name === 'Verified Business Owner') {
|
||||||
|
const match = await db.lead_matches.findOne({
|
||||||
|
where: { leadId: id, businessId: currentUser.businessId },
|
||||||
|
transaction
|
||||||
|
});
|
||||||
|
if (!match) {
|
||||||
|
throw new ForbiddenError('forbidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await LeadsDBApi.remove(id, { currentUser, transaction });
|
await LeadsDBApi.remove(id, { currentUser, transaction });
|
||||||
await transaction.commit();
|
await transaction.commit();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -100,4 +138,4 @@ module.exports = class LeadsService {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -2,20 +2,38 @@ const db = require('../db/models');
|
|||||||
const MessagesDBApi = require('../db/api/messages');
|
const MessagesDBApi = require('../db/api/messages');
|
||||||
const processFile = require("../middlewares/upload");
|
const processFile = require("../middlewares/upload");
|
||||||
const ValidationError = require('./notifications/errors/validation');
|
const ValidationError = require('./notifications/errors/validation');
|
||||||
|
const ForbiddenError = require('./notifications/errors/forbidden');
|
||||||
const csv = require('csv-parser');
|
const csv = require('csv-parser');
|
||||||
const axios = require('axios');
|
const axios = require('axios');
|
||||||
const config = require('../config');
|
const config = require('../config');
|
||||||
const stream = require('stream');
|
const stream = require('stream');
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
module.exports = class MessagesService {
|
module.exports = class MessagesService {
|
||||||
static async create(data, currentUser) {
|
static async create(data, currentUser) {
|
||||||
const transaction = await db.sequelize.transaction();
|
const transaction = await db.sequelize.transaction();
|
||||||
try {
|
try {
|
||||||
await MessagesDBApi.create(
|
// For VBOs, ensure they can only message about their leads/businesses
|
||||||
|
if (currentUser.app_role?.name === 'Verified Business Owner') {
|
||||||
|
// If leadId is provided, check if business is matched to that lead
|
||||||
|
if (data.lead) {
|
||||||
|
const match = await db.lead_matches.findOne({
|
||||||
|
where: {
|
||||||
|
leadId: data.lead,
|
||||||
|
[db.Sequelize.Op.or]: [
|
||||||
|
{ businessId: currentUser.businessId || null },
|
||||||
|
{ '$business.owner_userId$': currentUser.id }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
include: [{ model: db.businesses, as: 'business' }],
|
||||||
|
transaction
|
||||||
|
});
|
||||||
|
if (!match) {
|
||||||
|
throw new ForbiddenError('forbidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = await MessagesDBApi.create(
|
||||||
data,
|
data,
|
||||||
{
|
{
|
||||||
currentUser,
|
currentUser,
|
||||||
@ -24,6 +42,7 @@ module.exports = class MessagesService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
await transaction.commit();
|
await transaction.commit();
|
||||||
|
return message;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await transaction.rollback();
|
await transaction.rollback();
|
||||||
throw error;
|
throw error;
|
||||||
@ -38,14 +57,13 @@ module.exports = class MessagesService {
|
|||||||
const bufferStream = new stream.PassThrough();
|
const bufferStream = new stream.PassThrough();
|
||||||
const results = [];
|
const results = [];
|
||||||
|
|
||||||
await bufferStream.end(Buffer.from(req.file.buffer, "utf-8")); // convert Buffer to Stream
|
await bufferStream.end(Buffer.from(req.file.buffer, "utf-8"));
|
||||||
|
|
||||||
await new Promise((resolve, reject) => {
|
await new Promise((resolve, reject) => {
|
||||||
bufferStream
|
bufferStream
|
||||||
.pipe(csv())
|
.pipe(csv())
|
||||||
.on('data', (data) => results.push(data))
|
.on('data', (data) => results.push(data))
|
||||||
.on('end', async () => {
|
.on('end', async () => {
|
||||||
console.log('CSV results', results);
|
|
||||||
resolve();
|
resolve();
|
||||||
})
|
})
|
||||||
.on('error', (error) => reject(error));
|
.on('error', (error) => reject(error));
|
||||||
@ -68,17 +86,38 @@ module.exports = class MessagesService {
|
|||||||
static async update(data, id, currentUser) {
|
static async update(data, id, currentUser) {
|
||||||
const transaction = await db.sequelize.transaction();
|
const transaction = await db.sequelize.transaction();
|
||||||
try {
|
try {
|
||||||
let messages = await MessagesDBApi.findBy(
|
let message = await MessagesDBApi.findBy(
|
||||||
{id},
|
{id},
|
||||||
{transaction},
|
{transaction},
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!messages) {
|
if (!message) {
|
||||||
throw new ValidationError(
|
throw new ValidationError(
|
||||||
'messagesNotFound',
|
'messagesNotFound',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ownership check for Verified Business Owner
|
||||||
|
if (currentUser.app_role?.name === 'Verified Business Owner') {
|
||||||
|
if (message.sender_userId !== currentUser.id && message.receiver_userId !== currentUser.id) {
|
||||||
|
// Also check if it's about their lead
|
||||||
|
const match = await db.lead_matches.findOne({
|
||||||
|
where: {
|
||||||
|
leadId: message.lead?.id,
|
||||||
|
[db.Sequelize.Op.or]: [
|
||||||
|
{ businessId: currentUser.businessId || null },
|
||||||
|
{ '$business.owner_userId$': currentUser.id }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
include: [{ model: db.businesses, as: 'business' }],
|
||||||
|
transaction
|
||||||
|
});
|
||||||
|
if (!match) {
|
||||||
|
throw new ForbiddenError('forbidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const updatedMessages = await MessagesDBApi.update(
|
const updatedMessages = await MessagesDBApi.update(
|
||||||
id,
|
id,
|
||||||
data,
|
data,
|
||||||
@ -101,6 +140,19 @@ module.exports = class MessagesService {
|
|||||||
const transaction = await db.sequelize.transaction();
|
const transaction = await db.sequelize.transaction();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Ownership check for Verified Business Owner
|
||||||
|
if (currentUser.app_role?.name === 'Verified Business Owner') {
|
||||||
|
const records = await db.messages.findAll({
|
||||||
|
where: { id: { [db.Sequelize.Op.in]: ids } },
|
||||||
|
transaction
|
||||||
|
});
|
||||||
|
for (const record of records) {
|
||||||
|
if (record.sender_userId !== currentUser.id && record.receiver_userId !== currentUser.id) {
|
||||||
|
throw new ForbiddenError('forbidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await MessagesDBApi.deleteByIds(ids, {
|
await MessagesDBApi.deleteByIds(ids, {
|
||||||
currentUser,
|
currentUser,
|
||||||
transaction,
|
transaction,
|
||||||
@ -117,6 +169,15 @@ module.exports = class MessagesService {
|
|||||||
const transaction = await db.sequelize.transaction();
|
const transaction = await db.sequelize.transaction();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const record = await db.messages.findByPk(id, { transaction });
|
||||||
|
if (!record) throw new ValidationError('messagesNotFound');
|
||||||
|
|
||||||
|
if (currentUser.app_role?.name === 'Verified Business Owner') {
|
||||||
|
if (record.sender_userId !== currentUser.id && record.receiver_userId !== currentUser.id) {
|
||||||
|
throw new ForbiddenError('forbidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await MessagesDBApi.remove(
|
await MessagesDBApi.remove(
|
||||||
id,
|
id,
|
||||||
{
|
{
|
||||||
@ -133,6 +194,4 @@ module.exports = class MessagesService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@ -1,6 +1,6 @@
|
|||||||
const errors = {
|
const errors = {
|
||||||
app: {
|
app: {
|
||||||
title: 'Crafted Network',
|
title: 'Fix It Local',
|
||||||
},
|
},
|
||||||
|
|
||||||
auth: {
|
auth: {
|
||||||
|
|||||||
@ -108,12 +108,16 @@ module.exports = class ReviewsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Ownership check for Verified Business Owner
|
// Ownership check for Verified Business Owner
|
||||||
// VBO can only update reviews for their own businesses
|
|
||||||
if (currentUser.app_role?.name === 'Verified Business Owner') {
|
if (currentUser.app_role?.name === 'Verified Business Owner') {
|
||||||
// Check business owner
|
if (currentUser.businessId) {
|
||||||
const business = await db.businesses.findByPk(review.businessId, { transaction });
|
if (review.businessId !== currentUser.businessId) {
|
||||||
if (business && business.owner_userId !== currentUser.id) {
|
throw new ForbiddenError('forbidden');
|
||||||
throw new ForbiddenError('forbidden');
|
}
|
||||||
|
} else {
|
||||||
|
const business = await db.businesses.findByPk(review.businessId, { transaction });
|
||||||
|
if (business && business.owner_userId !== currentUser.id) {
|
||||||
|
throw new ForbiddenError('forbidden');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -147,6 +151,23 @@ module.exports = class ReviewsService {
|
|||||||
where: { id: { [db.Sequelize.Op.in]: ids } },
|
where: { id: { [db.Sequelize.Op.in]: ids } },
|
||||||
transaction,
|
transaction,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Ownership check for Verified Business Owner
|
||||||
|
if (currentUser.app_role?.name === 'Verified Business Owner') {
|
||||||
|
for (const review of reviews) {
|
||||||
|
if (currentUser.businessId) {
|
||||||
|
if (review.businessId !== currentUser.businessId) {
|
||||||
|
throw new ForbiddenError('forbidden');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const business = await db.businesses.findByPk(review.businessId, { transaction });
|
||||||
|
if (business && business.owner_userId !== currentUser.id) {
|
||||||
|
throw new ForbiddenError('forbidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const businessIds = [...new Set(reviews.map(r => r.businessId))];
|
const businessIds = [...new Set(reviews.map(r => r.businessId))];
|
||||||
|
|
||||||
await ReviewsDBApi.deleteByIds(ids, {
|
await ReviewsDBApi.deleteByIds(ids, {
|
||||||
@ -170,6 +191,24 @@ module.exports = class ReviewsService {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const review = await db.reviews.findByPk(id, { transaction });
|
const review = await db.reviews.findByPk(id, { transaction });
|
||||||
|
if (!review) {
|
||||||
|
throw new ValidationError('reviewsNotFound');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ownership check for Verified Business Owner
|
||||||
|
if (currentUser.app_role?.name === 'Verified Business Owner') {
|
||||||
|
if (currentUser.businessId) {
|
||||||
|
if (review.businessId !== currentUser.businessId) {
|
||||||
|
throw new ForbiddenError('forbidden');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const business = await db.businesses.findByPk(review.businessId, { transaction });
|
||||||
|
if (business && business.owner_userId !== currentUser.id) {
|
||||||
|
throw new ForbiddenError('forbidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const businessId = review.businessId;
|
const businessId = review.businessId;
|
||||||
|
|
||||||
await ReviewsDBApi.remove(
|
await ReviewsDBApi.remove(
|
||||||
@ -188,4 +227,4 @@ module.exports = class ReviewsService {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -7,9 +7,6 @@ const axios = require('axios');
|
|||||||
const config = require('../config');
|
const config = require('../config');
|
||||||
const stream = require('stream');
|
const stream = require('stream');
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function buildWidgetResult(widget, queryResult, queryString) {
|
function buildWidgetResult(widget, queryResult, queryString) {
|
||||||
if (queryResult[0] && queryResult[0].length) {
|
if (queryResult[0] && queryResult[0].length) {
|
||||||
const key = Object.keys(queryResult[0][0])[0];
|
const key = Object.keys(queryResult[0][0])[0];
|
||||||
@ -17,14 +14,14 @@ function buildWidgetResult(widget, queryResult, queryString) {
|
|||||||
const widgetData = JSON.parse(widget.data);
|
const widgetData = JSON.parse(widget.data);
|
||||||
return { ...widget, ...widgetData, value, query: queryString };
|
return { ...widget, ...widgetData, value, query: queryString };
|
||||||
} else {
|
} else {
|
||||||
return { ...widget, value: [], query: queryString };
|
return { ...widget, value: widget.widget_type === 'scalar' ? 0 : [], query: queryString };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function executeQuery(queryString, currentUser) {
|
async function executeQuery(queryString, replacements) {
|
||||||
try {
|
try {
|
||||||
return await db.sequelize.query(queryString, {
|
return await db.sequelize.query(queryString, {
|
||||||
replacements: { organizationId: currentUser.organizationId },
|
replacements,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(e);
|
console.log(e);
|
||||||
@ -49,19 +46,46 @@ function insertWhereConditions(queryString, whereConditions) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function constructWhereConditions(mainTable, currentUser, replacements) {
|
function constructWhereConditions(mainTable, currentUser, replacements) {
|
||||||
const { organizationId, app_role: { globalAccess } } = currentUser;
|
const organizationId = currentUser.organizationId;
|
||||||
|
const roleName = currentUser.app_role?.name;
|
||||||
|
const globalAccess = currentUser.app_role?.globalAccess;
|
||||||
|
const currentUserId = currentUser.id;
|
||||||
|
const businessId = currentUser.businessId;
|
||||||
|
|
||||||
const tablesWithoutOrgId = ['permissions', 'roles'];
|
const tablesWithoutOrgId = ['permissions', 'roles'];
|
||||||
let whereConditions = '';
|
let conditions = [];
|
||||||
|
|
||||||
if (!globalAccess && !tablesWithoutOrgId.includes(mainTable)) {
|
if (!globalAccess && !tablesWithoutOrgId.includes(mainTable)) {
|
||||||
whereConditions += `"${mainTable}"."organizationId" = :organizationId`;
|
if (organizationId) {
|
||||||
replacements.organizationId = organizationId;
|
conditions.push(`"${mainTable}"."organizationId" = :organizationId`);
|
||||||
|
replacements.organizationId = organizationId;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
whereConditions += whereConditions ? ' AND ' : '';
|
// Business User isolation
|
||||||
whereConditions += `"${mainTable}"."deletedAt" IS NULL`;
|
if (roleName === 'Verified Business Owner') {
|
||||||
|
if (mainTable === 'businesses') {
|
||||||
|
if (businessId) {
|
||||||
|
conditions.push(`"${mainTable}"."id" = :businessId`);
|
||||||
|
replacements.businessId = businessId;
|
||||||
|
} else {
|
||||||
|
conditions.push(`"${mainTable}"."owner_userId" = :currentUserId`);
|
||||||
|
replacements.currentUserId = currentUserId;
|
||||||
|
}
|
||||||
|
} else if (['leads', 'messages', 'reviews', 'service_prices', 'verification_submissions'].includes(mainTable)) {
|
||||||
|
if (businessId) {
|
||||||
|
conditions.push(`"${mainTable}"."businessId" = :businessId`);
|
||||||
|
replacements.businessId = businessId;
|
||||||
|
} else {
|
||||||
|
// Fallback: try to filter by owner_userId if we can join or if it's leads (via matches)
|
||||||
|
// For now, we assume businessId is on the user for most VBOs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return whereConditions;
|
conditions.push(`"${mainTable}"."deletedAt" IS NULL`);
|
||||||
|
|
||||||
|
return conditions.join(' AND ');
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractTableName(queryString) {
|
function extractTableName(queryString) {
|
||||||
@ -77,16 +101,15 @@ function buildQueryString(widget, currentUser) {
|
|||||||
const replacements = {};
|
const replacements = {};
|
||||||
const whereConditions = constructWhereConditions(mainTable, currentUser, replacements);
|
const whereConditions = constructWhereConditions(mainTable, currentUser, replacements);
|
||||||
queryString = insertWhereConditions(queryString, whereConditions);
|
queryString = insertWhereConditions(queryString, whereConditions);
|
||||||
console.log(queryString, 'queryString');
|
return { queryString, replacements };
|
||||||
return queryString;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function constructWidgetsResults(widgets, currentUser) {
|
async function constructWidgetsResults(widgets, currentUser) {
|
||||||
const widgetsResults = [];
|
const widgetsResults = [];
|
||||||
for (const widget of widgets) {
|
for (const widget of widgets) {
|
||||||
if (!widget) continue;
|
if (!widget) continue;
|
||||||
const queryString = buildQueryString(widget, currentUser);
|
const { queryString, replacements } = buildQueryString(widget, currentUser);
|
||||||
const queryResult = await executeQuery(queryString, currentUser);
|
const queryResult = await executeQuery(queryString, replacements);
|
||||||
widgetsResults.push(buildWidgetResult(widget, queryResult, queryString));
|
widgetsResults.push(buildWidgetResult(widget, queryResult, queryString));
|
||||||
}
|
}
|
||||||
return widgetsResults;
|
return widgetsResults;
|
||||||
@ -107,30 +130,6 @@ async function processWidgets(widgets, currentUser) {
|
|||||||
return constructWidgetsResults(widgetData, currentUser);
|
return constructWidgetsResults(widgetData, currentUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseCustomization(role) {
|
|
||||||
try {
|
|
||||||
return JSON.parse(role.role_customization || '{}');
|
|
||||||
} catch (e) {
|
|
||||||
console.log(e);
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function findRole(roleId, currentUser) {
|
|
||||||
const transaction = await db.sequelize.transaction();
|
|
||||||
try {
|
|
||||||
const role = roleId
|
|
||||||
? await RolesDBApi.findBy({ id: roleId }, { transaction })
|
|
||||||
: await RolesDBApi.findBy({ name: 'User' }, { transaction });
|
|
||||||
await transaction.commit();
|
|
||||||
return role;
|
|
||||||
} catch (error) {
|
|
||||||
await transaction.rollback();
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
module.exports = class RolesService {
|
module.exports = class RolesService {
|
||||||
static async create(data, currentUser) {
|
static async create(data, currentUser) {
|
||||||
const transaction = await db.sequelize.transaction();
|
const transaction = await db.sequelize.transaction();
|
||||||
@ -158,14 +157,13 @@ module.exports = class RolesService {
|
|||||||
const bufferStream = new stream.PassThrough();
|
const bufferStream = new stream.PassThrough();
|
||||||
const results = [];
|
const results = [];
|
||||||
|
|
||||||
await bufferStream.end(Buffer.from(req.file.buffer, "utf-8")); // convert Buffer to Stream
|
await bufferStream.end(Buffer.from(req.file.buffer, "utf-8"));
|
||||||
|
|
||||||
await new Promise((resolve, reject) => {
|
await new Promise((resolve, reject) => {
|
||||||
bufferStream
|
bufferStream
|
||||||
.pipe(csv())
|
.pipe(csv())
|
||||||
.on('data', (data) => results.push(data))
|
.on('data', (data) => results.push(data))
|
||||||
.on('end', async () => {
|
.on('end', async () => {
|
||||||
console.log('CSV results', results);
|
|
||||||
resolve();
|
resolve();
|
||||||
})
|
})
|
||||||
.on('error', (error) => reject(error));
|
.on('error', (error) => reject(error));
|
||||||
@ -362,11 +360,6 @@ module.exports = class RolesService {
|
|||||||
static async getRoleInfoByKey(key, roleId, currentUser) {
|
static async getRoleInfoByKey(key, roleId, currentUser) {
|
||||||
const transaction = await db.sequelize.transaction();
|
const transaction = await db.sequelize.transaction();
|
||||||
|
|
||||||
const organizationId = currentUser.organizationId;
|
|
||||||
let globalAccess = currentUser.app_role?.globalAccess;
|
|
||||||
let queryString = '';
|
|
||||||
|
|
||||||
|
|
||||||
let role;
|
let role;
|
||||||
try {
|
try {
|
||||||
if (roleId) {
|
if (roleId) {
|
||||||
@ -381,7 +374,7 @@ module.exports = class RolesService {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
let customization = '{}';
|
let customization = {};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
customization = JSON.parse(role.role_customization || '{}');
|
customization = JSON.parse(role.role_customization || '{}');
|
||||||
@ -391,47 +384,8 @@ module.exports = class RolesService {
|
|||||||
|
|
||||||
if (key === 'widgets') {
|
if (key === 'widgets') {
|
||||||
const widgets = (customization[key] || []);
|
const widgets = (customization[key] || []);
|
||||||
const widgetArray = widgets.map(widget => {
|
return await processWidgets(widgets, currentUser);
|
||||||
return axios.get(`${config.flHost}/${config.project_uuid}/project_customization_widgets/${widget}.json`)
|
|
||||||
})
|
|
||||||
const widgetResults = await Promise.allSettled(widgetArray);
|
|
||||||
|
|
||||||
const fulfilledWidgets = widgetResults.map(result => {
|
|
||||||
if (result.status === 'fulfilled') {
|
|
||||||
return result.value.data;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const widgetsResults = [];
|
|
||||||
|
|
||||||
if (Array.isArray(fulfilledWidgets)) {
|
|
||||||
for (const widget of fulfilledWidgets) {
|
|
||||||
let result = [];
|
|
||||||
try {
|
|
||||||
result = await db.sequelize.query(widget.query);
|
|
||||||
} catch (e) {
|
|
||||||
console.log(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result[0] && result[0].length) {
|
|
||||||
const key = Object.keys(result[0][0])[0];
|
|
||||||
const value =
|
|
||||||
widget.widget_type === 'scalar' ? result[0][0][key] : result[0];
|
|
||||||
const widgetData = JSON.parse(widget.data);
|
|
||||||
widgetsResults.push({ ...widget, ...widgetData, value });
|
|
||||||
} else {
|
|
||||||
widgetsResults.push({ ...widget, value: null });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return widgetsResults;
|
|
||||||
}
|
}
|
||||||
return customization[key];
|
return customization[key];
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
@ -2,20 +2,27 @@ const db = require('../db/models');
|
|||||||
const Service_pricesDBApi = require('../db/api/service_prices');
|
const Service_pricesDBApi = require('../db/api/service_prices');
|
||||||
const processFile = require("../middlewares/upload");
|
const processFile = require("../middlewares/upload");
|
||||||
const ValidationError = require('./notifications/errors/validation');
|
const ValidationError = require('./notifications/errors/validation');
|
||||||
|
const ForbiddenError = require('./notifications/errors/forbidden');
|
||||||
const csv = require('csv-parser');
|
const csv = require('csv-parser');
|
||||||
const axios = require('axios');
|
const axios = require('axios');
|
||||||
const config = require('../config');
|
const config = require('../config');
|
||||||
const stream = require('stream');
|
const stream = require('stream');
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
module.exports = class Service_pricesService {
|
module.exports = class Service_pricesService {
|
||||||
static async create(data, currentUser) {
|
static async create(data, currentUser) {
|
||||||
const transaction = await db.sequelize.transaction();
|
const transaction = await db.sequelize.transaction();
|
||||||
try {
|
try {
|
||||||
await Service_pricesDBApi.create(
|
// Ownership check for Verified Business Owner
|
||||||
|
if (currentUser.app_role?.name === 'Verified Business Owner') {
|
||||||
|
const businessId = data.business || currentUser.businessId;
|
||||||
|
const business = await db.businesses.findByPk(businessId, { transaction });
|
||||||
|
if (!business || business.owner_userId !== currentUser.id) {
|
||||||
|
throw new ForbiddenError('forbidden');
|
||||||
|
}
|
||||||
|
data.business = business.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
const service_price = await Service_pricesDBApi.create(
|
||||||
data,
|
data,
|
||||||
{
|
{
|
||||||
currentUser,
|
currentUser,
|
||||||
@ -24,6 +31,7 @@ module.exports = class Service_pricesService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
await transaction.commit();
|
await transaction.commit();
|
||||||
|
return service_price;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await transaction.rollback();
|
await transaction.rollback();
|
||||||
throw error;
|
throw error;
|
||||||
@ -38,14 +46,13 @@ module.exports = class Service_pricesService {
|
|||||||
const bufferStream = new stream.PassThrough();
|
const bufferStream = new stream.PassThrough();
|
||||||
const results = [];
|
const results = [];
|
||||||
|
|
||||||
await bufferStream.end(Buffer.from(req.file.buffer, "utf-8")); // convert Buffer to Stream
|
await bufferStream.end(Buffer.from(req.file.buffer, "utf-8"));
|
||||||
|
|
||||||
await new Promise((resolve, reject) => {
|
await new Promise((resolve, reject) => {
|
||||||
bufferStream
|
bufferStream
|
||||||
.pipe(csv())
|
.pipe(csv())
|
||||||
.on('data', (data) => results.push(data))
|
.on('data', (data) => results.push(data))
|
||||||
.on('end', async () => {
|
.on('end', async () => {
|
||||||
console.log('CSV results', results);
|
|
||||||
resolve();
|
resolve();
|
||||||
})
|
})
|
||||||
.on('error', (error) => reject(error));
|
.on('error', (error) => reject(error));
|
||||||
@ -79,6 +86,15 @@ module.exports = class Service_pricesService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ownership check for Verified Business Owner
|
||||||
|
if (currentUser.app_role?.name === 'Verified Business Owner') {
|
||||||
|
const businessId = service_prices.business?.id;
|
||||||
|
const business = await db.businesses.findByPk(businessId, { transaction });
|
||||||
|
if (!business || business.owner_userId !== currentUser.id) {
|
||||||
|
throw new ForbiddenError('forbidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const updatedService_prices = await Service_pricesDBApi.update(
|
const updatedService_prices = await Service_pricesDBApi.update(
|
||||||
id,
|
id,
|
||||||
data,
|
data,
|
||||||
@ -101,6 +117,20 @@ module.exports = class Service_pricesService {
|
|||||||
const transaction = await db.sequelize.transaction();
|
const transaction = await db.sequelize.transaction();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Ownership check for Verified Business Owner
|
||||||
|
if (currentUser.app_role?.name === 'Verified Business Owner') {
|
||||||
|
const records = await db.service_prices.findAll({
|
||||||
|
where: { id: { [db.Sequelize.Op.in]: ids } },
|
||||||
|
include: [{ model: db.businesses, as: 'business' }],
|
||||||
|
transaction
|
||||||
|
});
|
||||||
|
for (const record of records) {
|
||||||
|
if (record.business?.owner_userId !== currentUser.id && record.business?.id !== currentUser.businessId) {
|
||||||
|
throw new ForbiddenError('forbidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await Service_pricesDBApi.deleteByIds(ids, {
|
await Service_pricesDBApi.deleteByIds(ids, {
|
||||||
currentUser,
|
currentUser,
|
||||||
transaction,
|
transaction,
|
||||||
@ -117,6 +147,18 @@ module.exports = class Service_pricesService {
|
|||||||
const transaction = await db.sequelize.transaction();
|
const transaction = await db.sequelize.transaction();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const record = await db.service_prices.findByPk(id, {
|
||||||
|
include: [{ model: db.businesses, as: 'business' }],
|
||||||
|
transaction
|
||||||
|
});
|
||||||
|
if (!record) throw new ValidationError('service_pricesNotFound');
|
||||||
|
|
||||||
|
if (currentUser.app_role?.name === 'Verified Business Owner') {
|
||||||
|
if (record.business?.owner_userId !== currentUser.id && record.business?.id !== currentUser.businessId) {
|
||||||
|
throw new ForbiddenError('forbidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await Service_pricesDBApi.remove(
|
await Service_pricesDBApi.remove(
|
||||||
id,
|
id,
|
||||||
{
|
{
|
||||||
@ -133,6 +175,4 @@ module.exports = class Service_pricesService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@ -2,20 +2,27 @@ const db = require('../db/models');
|
|||||||
const Verification_submissionsDBApi = require('../db/api/verification_submissions');
|
const Verification_submissionsDBApi = require('../db/api/verification_submissions');
|
||||||
const processFile = require("../middlewares/upload");
|
const processFile = require("../middlewares/upload");
|
||||||
const ValidationError = require('./notifications/errors/validation');
|
const ValidationError = require('./notifications/errors/validation');
|
||||||
|
const ForbiddenError = require('./notifications/errors/forbidden');
|
||||||
const csv = require('csv-parser');
|
const csv = require('csv-parser');
|
||||||
const axios = require('axios');
|
const axios = require('axios');
|
||||||
const config = require('../config');
|
const config = require('../config');
|
||||||
const stream = require('stream');
|
const stream = require('stream');
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
module.exports = class Verification_submissionsService {
|
module.exports = class Verification_submissionsService {
|
||||||
static async create(data, currentUser) {
|
static async create(data, currentUser) {
|
||||||
const transaction = await db.sequelize.transaction();
|
const transaction = await db.sequelize.transaction();
|
||||||
try {
|
try {
|
||||||
await Verification_submissionsDBApi.create(
|
// Ownership check for Verified Business Owner
|
||||||
|
if (currentUser.app_role?.name === 'Verified Business Owner') {
|
||||||
|
const businessId = data.business || currentUser.businessId;
|
||||||
|
const business = await db.businesses.findByPk(businessId, { transaction });
|
||||||
|
if (!business || (business.owner_userId !== currentUser.id && business.id !== currentUser.businessId)) {
|
||||||
|
throw new ForbiddenError('forbidden');
|
||||||
|
}
|
||||||
|
data.business = business.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
const submission = await Verification_submissionsDBApi.create(
|
||||||
data,
|
data,
|
||||||
{
|
{
|
||||||
currentUser,
|
currentUser,
|
||||||
@ -24,6 +31,7 @@ module.exports = class Verification_submissionsService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
await transaction.commit();
|
await transaction.commit();
|
||||||
|
return submission;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await transaction.rollback();
|
await transaction.rollback();
|
||||||
throw error;
|
throw error;
|
||||||
@ -38,14 +46,13 @@ module.exports = class Verification_submissionsService {
|
|||||||
const bufferStream = new stream.PassThrough();
|
const bufferStream = new stream.PassThrough();
|
||||||
const results = [];
|
const results = [];
|
||||||
|
|
||||||
await bufferStream.end(Buffer.from(req.file.buffer, "utf-8")); // convert Buffer to Stream
|
await bufferStream.end(Buffer.from(req.file.buffer, "utf-8"));
|
||||||
|
|
||||||
await new Promise((resolve, reject) => {
|
await new Promise((resolve, reject) => {
|
||||||
bufferStream
|
bufferStream
|
||||||
.pipe(csv())
|
.pipe(csv())
|
||||||
.on('data', (data) => results.push(data))
|
.on('data', (data) => results.push(data))
|
||||||
.on('end', async () => {
|
.on('end', async () => {
|
||||||
console.log('CSV results', results);
|
|
||||||
resolve();
|
resolve();
|
||||||
})
|
})
|
||||||
.on('error', (error) => reject(error));
|
.on('error', (error) => reject(error));
|
||||||
@ -79,6 +86,15 @@ module.exports = class Verification_submissionsService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ownership check for Verified Business Owner
|
||||||
|
if (currentUser.app_role?.name === 'Verified Business Owner') {
|
||||||
|
const businessId = verification_submissions.business?.id;
|
||||||
|
const business = await db.businesses.findByPk(businessId, { transaction });
|
||||||
|
if (!business || (business.owner_userId !== currentUser.id && business.id !== currentUser.businessId)) {
|
||||||
|
throw new ForbiddenError('forbidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const updatedVerification_submissions = await Verification_submissionsDBApi.update(
|
const updatedVerification_submissions = await Verification_submissionsDBApi.update(
|
||||||
id,
|
id,
|
||||||
data,
|
data,
|
||||||
@ -101,6 +117,20 @@ module.exports = class Verification_submissionsService {
|
|||||||
const transaction = await db.sequelize.transaction();
|
const transaction = await db.sequelize.transaction();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Ownership check for Verified Business Owner
|
||||||
|
if (currentUser.app_role?.name === 'Verified Business Owner') {
|
||||||
|
const records = await db.verification_submissions.findAll({
|
||||||
|
where: { id: { [db.Sequelize.Op.in]: ids } },
|
||||||
|
include: [{ model: db.businesses, as: 'business' }],
|
||||||
|
transaction
|
||||||
|
});
|
||||||
|
for (const record of records) {
|
||||||
|
if (record.business?.owner_userId !== currentUser.id && record.business?.id !== currentUser.businessId) {
|
||||||
|
throw new ForbiddenError('forbidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await Verification_submissionsDBApi.deleteByIds(ids, {
|
await Verification_submissionsDBApi.deleteByIds(ids, {
|
||||||
currentUser,
|
currentUser,
|
||||||
transaction,
|
transaction,
|
||||||
@ -117,6 +147,18 @@ module.exports = class Verification_submissionsService {
|
|||||||
const transaction = await db.sequelize.transaction();
|
const transaction = await db.sequelize.transaction();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const record = await db.verification_submissions.findByPk(id, {
|
||||||
|
include: [{ model: db.businesses, as: 'business' }],
|
||||||
|
transaction
|
||||||
|
});
|
||||||
|
if (!record) throw new ValidationError('verification_submissionsNotFound');
|
||||||
|
|
||||||
|
if (currentUser.app_role?.name === 'Verified Business Owner') {
|
||||||
|
if (record.business?.owner_userId !== currentUser.id && record.business?.id !== currentUser.businessId) {
|
||||||
|
throw new ForbiddenError('forbidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await Verification_submissionsDBApi.remove(
|
await Verification_submissionsDBApi.remove(
|
||||||
id,
|
id,
|
||||||
{
|
{
|
||||||
@ -133,6 +175,4 @@ module.exports = class Verification_submissionsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@ -1,4 +1,4 @@
|
|||||||
# Crafted Network
|
# Fix It Local
|
||||||
|
|
||||||
## This project was generated by Flatlogic Platform.
|
## This project was generated by Flatlogic Platform.
|
||||||
## Install
|
## Install
|
||||||
|
|||||||
@ -1,40 +1,40 @@
|
|||||||
import type { ColorButtonKey } from './interfaces'
|
import type { ColorButtonKey } from './interfaces'
|
||||||
|
|
||||||
export const gradientBgBase = 'bg-gradient-to-tr'
|
export const gradientBgBase = 'bg-gradient-to-tr'
|
||||||
export const colorBgBase = "bg-violet-50/50"
|
export const colorBgBase = "bg-emerald-50/50"
|
||||||
export const gradientBgPurplePink = `${gradientBgBase} from-purple-400 via-pink-500 to-red-500`
|
export const gradientBgPurplePink = `${gradientBgBase} from-purple-400 via-pink-500 to-rose-500`
|
||||||
export const gradientBgViolet = `${gradientBgBase} ${colorBgBase}`
|
export const gradientBgViolet = `${gradientBgBase} ${colorBgBase}`
|
||||||
export const gradientBgDark = `${gradientBgBase} from-dark-700 via-dark-900 to-dark-800`;
|
export const gradientBgDark = `${gradientBgBase} from-slate-800 via-slate-950 to-slate-900`;
|
||||||
export const gradientBgPinkRed = `${gradientBgBase} from-pink-400 via-red-500 to-yellow-500`
|
export const gradientBgPinkRed = `${gradientBgBase} from-rose-400 via-emerald-500 to-emerald-600`
|
||||||
|
|
||||||
export const colorsBgLight = {
|
export const colorsBgLight = {
|
||||||
white: 'bg-white text-black',
|
white: 'bg-white text-black',
|
||||||
light: ' bg-white text-black text-black dark:bg-dark-900 dark:text-white',
|
light: ' bg-white text-black text-black dark:bg-dark-900 dark:text-white',
|
||||||
contrast: 'bg-gray-800 text-white dark:bg-white dark:text-black',
|
contrast: 'bg-slate-800 text-white dark:bg-white dark:text-black',
|
||||||
success: 'bg-emerald-500 border-emerald-500 dark:bg-pavitra-blue dark:border-pavitra-blue text-white',
|
success: 'bg-emerald-500 border-emerald-500 dark:bg-emerald-600 dark:border-emerald-600 text-white',
|
||||||
danger: 'bg-red-500 border-red-500 text-white',
|
danger: 'bg-rose-500 border-rose-500 text-white',
|
||||||
warning: 'bg-yellow-500 border-yellow-500 text-white',
|
warning: 'bg-amber-500 border-amber-500 text-white',
|
||||||
info: 'bg-blue-500 border-blue-500 dark:bg-pavitra-blue dark:border-pavitra-blue text-white',
|
info: 'bg-emerald-600 border-emerald-600 dark:bg-emerald-700 dark:border-emerald-700 text-white',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const colorsText = {
|
export const colorsText = {
|
||||||
white: 'text-black dark:text-slate-100',
|
white: 'text-black dark:text-slate-100',
|
||||||
light: 'text-gray-700 dark:text-slate-400',
|
light: 'text-slate-700 dark:text-slate-400',
|
||||||
contrast: 'dark:text-white',
|
contrast: 'dark:text-white',
|
||||||
success: 'text-emerald-500',
|
success: 'text-emerald-500',
|
||||||
danger: 'text-red-500',
|
danger: 'text-rose-500',
|
||||||
warning: 'text-yellow-500',
|
warning: 'text-amber-500',
|
||||||
info: 'text-blue-500',
|
info: 'text-emerald-600',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const colorsOutline = {
|
export const colorsOutline = {
|
||||||
white: [colorsText.white, 'border-gray-100'].join(' '),
|
white: [colorsText.white, 'border-slate-100'].join(' '),
|
||||||
light: [colorsText.light, 'border-gray-100'].join(' '),
|
light: [colorsText.light, 'border-slate-100'].join(' '),
|
||||||
contrast: [colorsText.contrast, 'border-gray-900 dark:border-slate-100'].join(' '),
|
contrast: [colorsText.contrast, 'border-slate-900 dark:border-slate-100'].join(' '),
|
||||||
success: [colorsText.success, 'border-emerald-500'].join(' '),
|
success: [colorsText.success, 'border-emerald-500'].join(' '),
|
||||||
danger: [colorsText.danger, 'border-red-500'].join(' '),
|
danger: [colorsText.danger, 'border-rose-500'].join(' '),
|
||||||
warning: [colorsText.warning, 'border-yellow-500'].join(' '),
|
warning: [colorsText.warning, 'border-amber-500'].join(' '),
|
||||||
info: [colorsText.info, 'border-blue-500'].join(' '),
|
info: [colorsText.info, 'border-emerald-600'].join(' '),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getButtonColor = (
|
export const getButtonColor = (
|
||||||
@ -49,74 +49,74 @@ export const getButtonColor = (
|
|||||||
|
|
||||||
const colors = {
|
const colors = {
|
||||||
ring: {
|
ring: {
|
||||||
white: 'ring-gray-200 dark:ring-gray-500',
|
white: 'ring-slate-200 dark:ring-slate-500',
|
||||||
whiteDark: 'ring-gray-200 dark:ring-dark-500',
|
whiteDark: 'ring-slate-200 dark:ring-dark-500',
|
||||||
lightDark: 'ring-gray-200 dark:ring-gray-500',
|
lightDark: 'ring-slate-200 dark:ring-slate-500',
|
||||||
contrast: 'ring-gray-300 dark:ring-gray-400',
|
contrast: 'ring-slate-300 dark:ring-slate-400',
|
||||||
success: 'ring-emerald-300 dark:ring-pavitra-blue',
|
success: 'ring-emerald-300 dark:ring-emerald-700',
|
||||||
danger: 'ring-red-300 dark:ring-red-700',
|
danger: 'ring-rose-300 dark:ring-rose-700',
|
||||||
warning: 'ring-yellow-300 dark:ring-yellow-700',
|
warning: 'ring-amber-300 dark:ring-amber-700',
|
||||||
info: "ring-blue-300 dark:ring-pavitra-blue",
|
info: "ring-emerald-300 dark:ring-emerald-700",
|
||||||
},
|
},
|
||||||
active: {
|
active: {
|
||||||
white: 'bg-gray-100',
|
white: 'bg-slate-100',
|
||||||
whiteDark: 'bg-gray-100 dark:bg-dark-800',
|
whiteDark: 'bg-slate-100 dark:bg-dark-800',
|
||||||
lightDark: 'bg-gray-200 dark:bg-slate-700',
|
lightDark: 'bg-slate-200 dark:bg-slate-700',
|
||||||
contrast: 'bg-gray-700 dark:bg-slate-100',
|
contrast: 'bg-slate-700 dark:bg-slate-100',
|
||||||
success: 'bg-emerald-700 dark:bg-pavitra-blue',
|
success: 'bg-emerald-700 dark:bg-emerald-800',
|
||||||
danger: 'bg-red-700 dark:bg-red-600',
|
danger: 'bg-rose-700 dark:bg-rose-600',
|
||||||
warning: 'bg-yellow-700 dark:bg-yellow-600',
|
warning: 'bg-amber-700 dark:bg-amber-600',
|
||||||
info: 'bg-blue-700 dark:bg-pavitra-blue',
|
info: 'bg-emerald-700 dark:bg-emerald-800',
|
||||||
},
|
},
|
||||||
bg: {
|
bg: {
|
||||||
white: 'bg-white text-black',
|
white: 'bg-white text-black',
|
||||||
whiteDark: 'bg-white text-black dark:bg-dark-900 dark:text-white',
|
whiteDark: 'bg-white text-black dark:bg-dark-900 dark:text-white',
|
||||||
lightDark: 'bg-gray-100 text-black dark:bg-slate-800 dark:text-white',
|
lightDark: 'bg-slate-100 text-black dark:bg-slate-800 dark:text-white',
|
||||||
contrast: 'bg-gray-800 text-white dark:bg-white dark:text-black',
|
contrast: 'bg-slate-800 text-white dark:bg-white dark:text-black',
|
||||||
success: 'bg-emerald-600 dark:bg-pavitra-blue text-white',
|
success: 'bg-emerald-600 dark:bg-emerald-600 text-white',
|
||||||
danger: 'bg-red-600 text-white dark:bg-red-500 ',
|
danger: 'bg-rose-600 text-white dark:bg-rose-500 ',
|
||||||
warning: 'bg-yellow-600 dark:bg-yellow-500 text-white',
|
warning: 'bg-amber-600 dark:bg-amber-500 text-white',
|
||||||
info: " bg-blue-600 dark:bg-pavitra-blue text-white ",
|
info: " bg-emerald-600 dark:bg-emerald-600 text-white ",
|
||||||
},
|
},
|
||||||
bgHover: {
|
bgHover: {
|
||||||
white: 'hover:bg-gray-100',
|
white: 'hover:bg-slate-100',
|
||||||
whiteDark: 'hover:bg-gray-100 hover:dark:bg-dark-800',
|
whiteDark: 'hover:bg-slate-100 hover:dark:bg-dark-800',
|
||||||
lightDark: 'hover:bg-gray-200 hover:dark:bg-slate-700',
|
lightDark: 'hover:bg-slate-200 hover:dark:bg-slate-700',
|
||||||
contrast: 'hover:bg-gray-700 hover:dark:bg-slate-100',
|
contrast: 'hover:bg-slate-700 hover:dark:bg-slate-100',
|
||||||
success:
|
success:
|
||||||
'hover:bg-emerald-700 hover:border-emerald-700 hover:dark:bg-pavitra-blue hover:dark:border-pavitra-blue',
|
'hover:bg-emerald-700 hover:border-emerald-700 hover:dark:bg-emerald-800 hover:dark:border-emerald-800',
|
||||||
danger:
|
danger:
|
||||||
'hover:bg-red-700 hover:border-red-700 hover:dark:bg-red-600 hover:dark:border-red-600',
|
'hover:bg-rose-700 hover:border-rose-700 hover:dark:bg-rose-600 hover:dark:border-rose-600',
|
||||||
warning:
|
warning:
|
||||||
'hover:bg-yellow-700 hover:border-yellow-700 hover:dark:bg-yellow-600 hover:dark:border-yellow-600',
|
'hover:bg-amber-700 hover:border-amber-700 hover:dark:bg-amber-600 hover:dark:border-amber-600',
|
||||||
info: "hover:bg-blue-700 hover:border-blue-700 hover:dark:bg-pavitra-blue/80 hover:dark:border-pavitra-blue/80",
|
info: "hover:bg-emerald-700 hover:border-emerald-700 hover:dark:bg-emerald-800 hover:dark:border-emerald-800",
|
||||||
},
|
},
|
||||||
borders: {
|
borders: {
|
||||||
white: 'border-white',
|
white: 'border-white',
|
||||||
whiteDark: 'border-white dark:border-dark-900',
|
whiteDark: 'border-white dark:border-dark-900',
|
||||||
lightDark: 'border-gray-100 dark:border-slate-800',
|
lightDark: 'border-slate-100 dark:border-slate-800',
|
||||||
contrast: 'border-gray-800 dark:border-white',
|
contrast: 'border-slate-800 dark:border-white',
|
||||||
success: 'border-emerald-600 dark:border-pavitra-blue',
|
success: 'border-emerald-600 dark:border-emerald-600',
|
||||||
danger: 'border-red-600 dark:border-red-500',
|
danger: 'border-rose-600 dark:border-rose-500',
|
||||||
warning: 'border-yellow-600 dark:border-yellow-500',
|
warning: 'border-amber-600 dark:border-amber-500',
|
||||||
info: "border-blue-600 border-blue-600 dark:border-pavitra-blue",
|
info: "border-emerald-600 border-emerald-600 dark:border-emerald-600",
|
||||||
},
|
},
|
||||||
text: {
|
text: {
|
||||||
contrast: 'dark:text-slate-100',
|
contrast: 'dark:text-slate-100',
|
||||||
success: 'text-emerald-600 dark:text-pavitra-blue',
|
success: 'text-emerald-600 dark:text-emerald-500',
|
||||||
danger: 'text-red-600 dark:text-red-500',
|
danger: 'text-rose-600 dark:text-rose-500',
|
||||||
warning: 'text-yellow-600 dark:text-yellow-500',
|
warning: 'text-amber-600 dark:text-amber-500',
|
||||||
info: 'text-blue-600 dark:text-pavitra-blue',
|
info: 'text-emerald-600 dark:text-emerald-500',
|
||||||
},
|
},
|
||||||
outlineHover: {
|
outlineHover: {
|
||||||
contrast:
|
contrast:
|
||||||
'hover:bg-gray-800 hover:text-gray-100 hover:dark:bg-slate-100 hover:dark:text-black',
|
'hover:bg-slate-800 hover:text-slate-100 hover:dark:bg-slate-100 hover:dark:text-black',
|
||||||
success: 'hover:bg-emerald-600 hover:text-white hover:text-white hover:dark:text-white hover:dark:border-pavitra-blue',
|
success: 'hover:bg-emerald-600 hover:text-white hover:text-white hover:dark:text-white hover:dark:border-emerald-600',
|
||||||
danger:
|
danger:
|
||||||
'hover:bg-red-600 hover:text-white hover:text-white hover:dark:text-white hover:dark:border-red-600',
|
'hover:bg-rose-600 hover:text-white hover:text-white hover:dark:text-white hover:dark:border-rose-600',
|
||||||
warning:
|
warning:
|
||||||
'hover:bg-yellow-600 hover:text-white hover:text-white hover:dark:text-white hover:dark:border-yellow-600',
|
'hover:bg-amber-600 hover:text-white hover:text-white hover:dark:text-white hover:dark:border-amber-600',
|
||||||
info: "hover:bg-blue-600 hover:bg-blue-600 hover:text-white hover:dark:text-white hover:dark:border-pavitra-blue",
|
info: "hover:bg-emerald-600 hover:bg-emerald-600 hover:text-white hover:dark:text-white hover:dark:border-emerald-600",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -135,4 +135,4 @@ export const getButtonColor = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
return base.join(' ')
|
return base.join(' ')
|
||||||
}
|
}
|
||||||
@ -39,7 +39,7 @@ export default function AsideMenuLayer({ menu, className = '', ...props }: Props
|
|||||||
>
|
>
|
||||||
<div className="text-center flex-1 lg:text-left lg:pl-6 xl:text-center xl:pl-0">
|
<div className="text-center flex-1 lg:text-left lg:pl-6 xl:text-center xl:pl-0">
|
||||||
|
|
||||||
<b className="font-black">Crafted Network</b>
|
<b className="font-black">Fix It Local</b>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -18,7 +18,11 @@ export default function AsideMenuList({ menu, isDropdownList = false, className
|
|||||||
return (
|
return (
|
||||||
<ul className={className}>
|
<ul className={className}>
|
||||||
{menu.map((item, index) => {
|
{menu.map((item, index) => {
|
||||||
|
// Role check
|
||||||
|
if (item.roles && !item.roles.includes(currentUser.app_role?.name)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// Permission check
|
||||||
if (!hasPermission(currentUser, item.permissions)) return null;
|
if (!hasPermission(currentUser, item.permissions)) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -32,4 +36,4 @@ export default function AsideMenuList({ menu, isDropdownList = false, className
|
|||||||
})}
|
})}
|
||||||
</ul>
|
</ul>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
41
frontend/src/components/ProgressBar.tsx
Normal file
41
frontend/src/components/ProgressBar.tsx
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
value: number;
|
||||||
|
max?: number;
|
||||||
|
color?: string;
|
||||||
|
label?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ProgressBar = ({ value, max = 100, color = 'emerald', label }: Props) => {
|
||||||
|
const percentage = Math.max(0, Math.min(100, Math.round((value / max) * 100)));
|
||||||
|
|
||||||
|
const colors: Record<string, string> = {
|
||||||
|
emerald: 'bg-emerald-500 shadow-sm shadow-emerald-500/20',
|
||||||
|
green: 'bg-emerald-500 shadow-sm shadow-emerald-500/20',
|
||||||
|
yellow: 'bg-amber-400 shadow-sm shadow-amber-400/20',
|
||||||
|
red: 'bg-rose-500 shadow-sm shadow-rose-500/20',
|
||||||
|
rose: 'bg-rose-500 shadow-sm shadow-rose-500/20',
|
||||||
|
};
|
||||||
|
|
||||||
|
const bgColor = colors[color] || colors.emerald;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full animate-fade-in">
|
||||||
|
{label && (
|
||||||
|
<div className="flex justify-between mb-2 items-baseline">
|
||||||
|
<span className="text-[10px] font-black uppercase tracking-widest text-slate-400 dark:text-slate-400">{label}</span>
|
||||||
|
<span className="text-sm font-black text-slate-800 dark:text-white">{percentage}%</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="w-full bg-slate-100 rounded-full h-3 dark:bg-slate-800 overflow-hidden shadow-inner">
|
||||||
|
<div
|
||||||
|
className={`${bgColor} h-3 rounded-full transition-all duration-1000 ease-out`}
|
||||||
|
style={{ width: `${percentage}%` }}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProgressBar;
|
||||||
@ -8,7 +8,7 @@ export const localStorageStyleKey = 'style'
|
|||||||
|
|
||||||
export const containerMaxW = 'xl:max-w-full xl:mx-auto 2xl:mx-20'
|
export const containerMaxW = 'xl:max-w-full xl:mx-auto 2xl:mx-20'
|
||||||
|
|
||||||
export const appTitle = 'created by Flatlogic generator!'
|
export const appTitle = 'Fix It Local'
|
||||||
|
|
||||||
export const getPageTitle = (currentPageTitle: string) => `${currentPageTitle} — ${appTitle}`
|
export const getPageTitle = (currentPageTitle: string) => `${currentPageTitle} — ${appTitle}`
|
||||||
|
|
||||||
|
|||||||
@ -14,6 +14,7 @@ export type MenuAsideItem = {
|
|||||||
withDevider?: boolean;
|
withDevider?: boolean;
|
||||||
menu?: MenuAsideItem[]
|
menu?: MenuAsideItem[]
|
||||||
permissions?: string | string[]
|
permissions?: string | string[]
|
||||||
|
roles?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MenuNavBarItem = {
|
export type MenuNavBarItem = {
|
||||||
@ -99,6 +100,10 @@ export interface User {
|
|||||||
updatedById?: any;
|
updatedById?: any;
|
||||||
avatar: any[];
|
avatar: any[];
|
||||||
notes: any[];
|
notes: any[];
|
||||||
|
businessId?: string;
|
||||||
|
app_role?: {
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export type StyleKey = 'white' | 'basic'
|
export type StyleKey = 'white' | 'basic'
|
||||||
|
|||||||
@ -30,7 +30,7 @@ export default function LayoutGuest({ children }: Props) {
|
|||||||
<div className="w-10 h-10 bg-emerald-500 rounded-xl flex items-center justify-center shadow-lg shadow-emerald-500/20 group-hover:scale-110 transition-transform">
|
<div className="w-10 h-10 bg-emerald-500 rounded-xl flex items-center justify-center shadow-lg shadow-emerald-500/20 group-hover:scale-110 transition-transform">
|
||||||
<BaseIcon path={mdiShieldCheck} size={24} className="text-white" />
|
<BaseIcon path={mdiShieldCheck} size={24} className="text-white" />
|
||||||
</div>
|
</div>
|
||||||
<span className="text-xl font-black tracking-tight dark:text-white">Crafted Network<span className="text-emerald-500 italic">™</span></span>
|
<span className="text-xl font-black tracking-tight dark:text-white">Fix It Local<span className="text-emerald-500 italic">™</span></span>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<nav className="hidden md:flex items-center gap-10">
|
<nav className="hidden md:flex items-center gap-10">
|
||||||
@ -97,7 +97,7 @@ export default function LayoutGuest({ children }: Props) {
|
|||||||
<div className="w-10 h-10 bg-emerald-500 rounded-xl flex items-center justify-center mr-3">
|
<div className="w-10 h-10 bg-emerald-500 rounded-xl flex items-center justify-center mr-3">
|
||||||
<BaseIcon path={mdiShieldCheck} size={24} className="text-white" />
|
<BaseIcon path={mdiShieldCheck} size={24} className="text-white" />
|
||||||
</div>
|
</div>
|
||||||
<span className="text-2xl font-bold tracking-tight dark:text-white text-slate-900">Crafted Network™</span>
|
<span className="text-2xl font-bold tracking-tight dark:text-white text-slate-900">Fix It Local™</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-8 text-slate-500 font-medium dark:text-slate-400">
|
<div className="flex gap-8 text-slate-500 font-medium dark:text-slate-400">
|
||||||
<Link href="/search" className="hover:text-emerald-500">Find Help</Link>
|
<Link href="/search" className="hover:text-emerald-500">Find Help</Link>
|
||||||
@ -107,7 +107,7 @@ export default function LayoutGuest({ children }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-12 pt-8 border-t border-slate-100 dark:border-slate-800 text-center text-slate-400 text-sm">
|
<div className="mt-12 pt-8 border-t border-slate-100 dark:border-slate-800 text-center text-slate-400 text-sm">
|
||||||
© 2026 Crafted Network™. Built with Trust & Transparency.
|
© 2026 Fix It Local™. Built with Trust & Transparency.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|||||||
@ -2,210 +2,139 @@ import * as icon from '@mdi/js';
|
|||||||
import { MenuAsideItem } from './interfaces'
|
import { MenuAsideItem } from './interfaces'
|
||||||
|
|
||||||
const menuAside: MenuAsideItem[] = [
|
const menuAside: MenuAsideItem[] = [
|
||||||
|
// Common
|
||||||
{
|
{
|
||||||
href: '/dashboard',
|
href: '/dashboard',
|
||||||
icon: icon.mdiViewDashboardOutline,
|
icon: icon.mdiStarFourPoints,
|
||||||
label: 'Dashboard',
|
label: 'Studio Hub',
|
||||||
|
roles: ['Administrator', 'Platform Owner', 'Trust & Safety Lead']
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
href: '/dashboard',
|
||||||
|
icon: icon.mdiStarFourPoints,
|
||||||
|
label: 'Studio Hub',
|
||||||
|
roles: ['Verified Business Owner']
|
||||||
|
},
|
||||||
|
|
||||||
|
// Admin Only
|
||||||
{
|
{
|
||||||
href: '/users/users-list',
|
href: '/users/users-list',
|
||||||
label: 'Users',
|
label: 'Clients',
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
icon: icon.mdiAccountHeart,
|
||||||
// @ts-ignore
|
permissions: 'READ_USERS',
|
||||||
icon: icon.mdiAccountGroup ?? icon.mdiTable,
|
roles: ['Administrator', 'Platform Owner']
|
||||||
permissions: 'READ_USERS'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
href: '/roles/roles-list',
|
|
||||||
label: 'Roles',
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
icon: icon.mdiShieldAccountVariantOutline ?? icon.mdiTable,
|
|
||||||
permissions: 'READ_ROLES'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
href: '/permissions/permissions-list',
|
|
||||||
label: 'Permissions',
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
icon: icon.mdiShieldAccountOutline ?? icon.mdiTable,
|
|
||||||
permissions: 'READ_PERMISSIONS'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
href: '/refresh_tokens/refresh_tokens-list',
|
|
||||||
label: 'Refresh tokens',
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
icon: 'mdiLock' in icon ? icon['mdiLock' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
|
||||||
permissions: 'READ_REFRESH_TOKENS'
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/categories/categories-list',
|
href: '/categories/categories-list',
|
||||||
label: 'Categories',
|
label: 'Beauty Categories',
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
icon: icon.mdiLipstick,
|
||||||
// @ts-ignore
|
permissions: 'READ_CATEGORIES',
|
||||||
icon: 'mdiShape' in icon ? icon['mdiShape' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
roles: ['Administrator', 'Platform Owner']
|
||||||
permissions: 'READ_CATEGORIES'
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/locations/locations-list',
|
href: '/locations/locations-list',
|
||||||
label: 'Locations',
|
label: 'Regions',
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
icon: icon.mdiMapMarkerRadius,
|
||||||
// @ts-ignore
|
permissions: 'READ_LOCATIONS',
|
||||||
icon: 'mdiMapMarker' in icon ? icon['mdiMapMarker' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
roles: ['Administrator', 'Platform Owner']
|
||||||
permissions: 'READ_LOCATIONS'
|
},
|
||||||
|
|
||||||
|
// Shared but labeled differently or scoped
|
||||||
|
{
|
||||||
|
href: '/businesses/businesses-list',
|
||||||
|
label: 'Service Listings',
|
||||||
|
icon: icon.mdiStorefront,
|
||||||
|
permissions: 'READ_BUSINESSES',
|
||||||
|
roles: ['Administrator', 'Platform Owner']
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/businesses/businesses-list',
|
href: '/businesses/businesses-list',
|
||||||
label: 'Businesses',
|
label: 'My Studio',
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
icon: icon.mdiStorefront,
|
||||||
// @ts-ignore
|
permissions: 'READ_BUSINESSES',
|
||||||
icon: 'mdiStore' in icon ? icon['mdiStore' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
roles: ['Verified Business Owner']
|
||||||
permissions: 'READ_BUSINESSES'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
href: '/business_photos/business_photos-list',
|
|
||||||
label: 'Business photos',
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
icon: 'mdiImageMultiple' in icon ? icon['mdiImageMultiple' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
|
||||||
permissions: 'READ_BUSINESS_PHOTOS'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
href: '/business_categories/business_categories-list',
|
|
||||||
label: 'Business categories',
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
icon: 'mdiTagMultiple' in icon ? icon['mdiTagMultiple' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
|
||||||
permissions: 'READ_BUSINESS_CATEGORIES'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
href: '/service_prices/service_prices-list',
|
|
||||||
label: 'Service prices',
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
icon: 'mdiCurrencyUsd' in icon ? icon['mdiCurrencyUsd' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
|
||||||
permissions: 'READ_SERVICE_PRICES'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
href: '/business_badges/business_badges-list',
|
|
||||||
label: 'Business badges',
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
icon: 'mdiShieldCheck' in icon ? icon['mdiShieldCheck' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
|
||||||
permissions: 'READ_BUSINESS_BADGES'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
href: '/verification_submissions/verification_submissions-list',
|
|
||||||
label: 'Verification submissions',
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
icon: 'mdiFileCheck' in icon ? icon['mdiFileCheck' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
|
||||||
permissions: 'READ_VERIFICATION_SUBMISSIONS'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
href: '/verification_evidences/verification_evidences-list',
|
|
||||||
label: 'Verification evidences',
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
icon: 'mdiFileUpload' in icon ? icon['mdiFileUpload' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
|
||||||
permissions: 'READ_VERIFICATION_EVIDENCES'
|
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
href: '/leads/leads-list',
|
href: '/leads/leads-list',
|
||||||
label: 'Leads',
|
label: 'Client Bookings',
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
icon: icon.mdiCalendarHeart,
|
||||||
// @ts-ignore
|
permissions: 'READ_LEADS',
|
||||||
icon: 'mdiClipboardText' in icon ? icon['mdiClipboardText' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
roles: ['Administrator', 'Platform Owner', 'Verified Business Owner']
|
||||||
permissions: 'READ_LEADS'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
href: '/lead_photos/lead_photos-list',
|
|
||||||
label: 'Lead photos',
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
icon: 'mdiCamera' in icon ? icon['mdiCamera' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
|
||||||
permissions: 'READ_LEAD_PHOTOS'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
href: '/lead_matches/lead_matches-list',
|
|
||||||
label: 'Lead matches',
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
icon: 'mdiLinkVariant' in icon ? icon['mdiLinkVariant' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
|
||||||
permissions: 'READ_LEAD_MATCHES'
|
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
href: '/messages/messages-list',
|
href: '/messages/messages-list',
|
||||||
label: 'Messages',
|
label: 'Consultations',
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
icon: icon.mdiMessageProcessing,
|
||||||
// @ts-ignore
|
|
||||||
icon: 'mdiMessageText' in icon ? icon['mdiMessageText' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
|
||||||
permissions: 'READ_MESSAGES'
|
permissions: 'READ_MESSAGES'
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
href: '/lead_events/lead_events-list',
|
href: '/reviews/reviews-list',
|
||||||
label: 'Lead events',
|
label: 'Love Letters',
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
icon: icon.mdiStarFace,
|
||||||
// @ts-ignore
|
permissions: 'READ_REVIEWS',
|
||||||
icon: 'mdiTimelineText' in icon ? icon['mdiTimelineText' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
roles: ['Administrator', 'Platform Owner', 'Trust & Safety Lead']
|
||||||
permissions: 'READ_LEAD_EVENTS'
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/reviews/reviews-list',
|
href: '/reviews/reviews-list',
|
||||||
label: 'Reviews',
|
label: 'Client Love',
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
icon: icon.mdiStarFace,
|
||||||
// @ts-ignore
|
permissions: 'READ_REVIEWS',
|
||||||
icon: 'mdiStar' in icon ? icon['mdiStar' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
roles: ['Verified Business Owner']
|
||||||
permissions: 'READ_REVIEWS'
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
href: '/verification_submissions/verification_submissions-list',
|
||||||
|
label: 'Verification',
|
||||||
|
icon: icon.mdiShieldCheck,
|
||||||
|
permissions: 'READ_VERIFICATION_SUBMISSIONS',
|
||||||
|
roles: ['Administrator', 'Platform Owner', 'Trust & Safety Lead']
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/disputes/disputes-list',
|
href: '/verification_submissions/verification_submissions-list',
|
||||||
label: 'Disputes',
|
label: 'Safety Badge',
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
icon: icon.mdiShieldCheck,
|
||||||
// @ts-ignore
|
permissions: 'READ_VERIFICATION_SUBMISSIONS',
|
||||||
icon: 'mdiAlertOctagon' in icon ? icon['mdiAlertOctagon' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
roles: ['Verified Business Owner']
|
||||||
permissions: 'READ_DISPUTES'
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Placeholder for Billing and Team
|
||||||
|
{
|
||||||
|
href: '/billing',
|
||||||
|
label: 'Earnings',
|
||||||
|
icon: icon.mdiWallet,
|
||||||
|
roles: ['Verified Business Owner']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: '/billing-settings',
|
||||||
|
label: 'Global Billing',
|
||||||
|
icon: icon.mdiFinance,
|
||||||
|
roles: ['Administrator', 'Platform Owner']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: '/team',
|
||||||
|
label: 'Studio Team',
|
||||||
|
icon: icon.mdiAccountGroupOutline,
|
||||||
|
roles: ['Verified Business Owner']
|
||||||
|
},
|
||||||
|
|
||||||
|
// Moderator
|
||||||
{
|
{
|
||||||
href: '/audit_logs/audit_logs-list',
|
href: '/audit_logs/audit_logs-list',
|
||||||
label: 'Audit logs',
|
label: 'Audit logs',
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
icon: icon.mdiClipboardListOutline,
|
||||||
// @ts-ignore
|
permissions: 'READ_AUDIT_LOGS',
|
||||||
icon: 'mdiClipboardList' in icon ? icon['mdiClipboardList' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
roles: ['Administrator', 'Platform Owner']
|
||||||
permissions: 'READ_AUDIT_LOGS'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
href: '/badge_rules/badge_rules-list',
|
|
||||||
label: 'Badge rules',
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
icon: 'mdiShieldCrown' in icon ? icon['mdiShieldCrown' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
|
||||||
permissions: 'READ_BADGE_RULES'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
href: '/trust_adjustments/trust_adjustments-list',
|
|
||||||
label: 'Trust adjustments',
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
icon: 'mdiTuneVariant' in icon ? icon['mdiTuneVariant' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
|
||||||
permissions: 'READ_TRUST_ADJUSTMENTS'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
href: '/profile',
|
|
||||||
label: 'Profile',
|
|
||||||
icon: icon.mdiAccountCircle,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Profile/Settings
|
||||||
{
|
{
|
||||||
href: '/api-docs',
|
href: '/profile',
|
||||||
target: '_blank',
|
label: 'My Profile',
|
||||||
label: 'Swagger API',
|
icon: icon.mdiAccountSettings,
|
||||||
icon: icon.mdiFileCode,
|
|
||||||
permissions: 'READ_API_DOCS'
|
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
export default menuAside
|
export default menuAside
|
||||||
@ -149,7 +149,7 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) {
|
|||||||
setStepsEnabled(false);
|
setStepsEnabled(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const title = 'Crafted Network'
|
const title = 'Fix It Local'
|
||||||
const description = "Trust-first service directory with verification, transparent pricing, AI-style matching, and request tracking."
|
const description = "Trust-first service directory with verification, transparent pricing, AI-style matching, and request tracking."
|
||||||
const url = "https://flatlogic.com/"
|
const url = "https://flatlogic.com/"
|
||||||
const image = "https://project-screens.s3.amazonaws.com/screenshots/38501/app-hero-20260217-010030.png"
|
const image = "https://project-screens.s3.amazonaws.com/screenshots/38501/app-hero-20260217-010030.png"
|
||||||
|
|||||||
171
frontend/src/pages/billing.tsx
Normal file
171
frontend/src/pages/billing.tsx
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
import * as icon from '@mdi/js';
|
||||||
|
import Head from 'next/head';
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import axios from 'axios';
|
||||||
|
import type { ReactElement } from 'react';
|
||||||
|
import LayoutAuthenticated from '../layouts/Authenticated';
|
||||||
|
import SectionMain from '../components/SectionMain';
|
||||||
|
import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton';
|
||||||
|
import CardBox from '../components/CardBox';
|
||||||
|
import BaseButton from '../components/BaseButton';
|
||||||
|
import BaseIcon from '../components/BaseIcon';
|
||||||
|
import { getPageTitle } from '../config';
|
||||||
|
import { useAppSelector } from '../stores/hooks';
|
||||||
|
import moment from 'moment';
|
||||||
|
|
||||||
|
const PlanCard = ({ plan, currentPlanId, onUpgrade }: any) => {
|
||||||
|
const isCurrent = plan.id === currentPlanId;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CardBox className={`h-full flex flex-col ${isCurrent ? 'border-2 border-blue-500 ring-2 ring-blue-500 ring-opacity-10' : ''}`}>
|
||||||
|
<div className="flex-grow">
|
||||||
|
<div className="flex justify-between items-center mb-4">
|
||||||
|
<h3 className="text-xl font-bold">{plan.name}</h3>
|
||||||
|
{isCurrent && (
|
||||||
|
<span className="text-xs bg-blue-100 text-blue-600 px-2 py-1 rounded-full font-bold">CURRENT PLAN</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="mb-6">
|
||||||
|
<span className="text-4xl font-black">${plan.price}</span>
|
||||||
|
<span className="text-gray-500">/mo</span>
|
||||||
|
</div>
|
||||||
|
<ul className="space-y-3 mb-8">
|
||||||
|
{plan.features?.map((feature: string, i: number) => (
|
||||||
|
<li key={i} className="flex items-center text-sm">
|
||||||
|
<BaseIcon path={icon.mdiCheckCircle} size={18} className="text-emerald-500 mr-2" />
|
||||||
|
{feature}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<BaseButton
|
||||||
|
label={isCurrent ? 'Current Plan' : `Upgrade to ${plan.name}`}
|
||||||
|
color={isCurrent ? 'whiteDark' : 'info'}
|
||||||
|
disabled={isCurrent}
|
||||||
|
className="w-full mt-auto"
|
||||||
|
onClick={() => onUpgrade(plan)}
|
||||||
|
/>
|
||||||
|
</CardBox>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const BillingPage = () => {
|
||||||
|
const { currentUser } = useAppSelector((state) => state.auth);
|
||||||
|
const [plans, setPlans] = useState([]);
|
||||||
|
const [business, setBusiness] = useState<any>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchData = async () => {
|
||||||
|
try {
|
||||||
|
const [plansRes, metricsRes] = await Promise.all([
|
||||||
|
axios.get('/dashboard/business-metrics'), // reusing for business data
|
||||||
|
// We need a plans endpoint, but since I seeded them, I can mock or create one
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Mocking plans for now based on migration seeds if no endpoint exists
|
||||||
|
setPlans([
|
||||||
|
{
|
||||||
|
id: '00000000-0000-0000-0000-000000000001',
|
||||||
|
name: 'Basic (Free)',
|
||||||
|
price: 0,
|
||||||
|
features: ['Limited Leads', 'Standard Listing'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '00000000-0000-0000-0000-000000000002',
|
||||||
|
name: 'Professional',
|
||||||
|
price: 49.99,
|
||||||
|
features: ['Unlimited Leads', 'Priority Support', 'Enhanced Profile'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '00000000-0000-0000-0000-000000000003',
|
||||||
|
name: 'Enterprise',
|
||||||
|
price: 199.99,
|
||||||
|
features: ['Custom Branding', 'API Access', 'Dedicated Manager'],
|
||||||
|
},
|
||||||
|
] as any);
|
||||||
|
|
||||||
|
if (metricsRes.data.businesses?.length) {
|
||||||
|
setBusiness(metricsRes.data.businesses[0]);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to fetch billing data', err);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchData();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleUpgrade = (plan: any) => {
|
||||||
|
alert(`Upgrading to ${plan.name}. This would normally trigger a payment flow.`);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<SectionMain>
|
||||||
|
<div className="flex items-center justify-center h-64">
|
||||||
|
<BaseIcon path={icon.mdiLoading} size={48} className="animate-spin text-gray-400" />
|
||||||
|
</div>
|
||||||
|
</SectionMain>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Head>
|
||||||
|
<title>{getPageTitle('Billing & Plans')}</title>
|
||||||
|
</Head>
|
||||||
|
<SectionMain>
|
||||||
|
<SectionTitleLineWithButton icon={icon.mdiCreditCardOutline} title="Billing & Plans" main />
|
||||||
|
|
||||||
|
{business && (
|
||||||
|
<div className="mb-8 p-6 bg-white dark:bg-slate-900 rounded-2xl shadow-sm border border-gray-100 dark:border-slate-800 flex flex-col md:flex-row justify-between items-center">
|
||||||
|
<div>
|
||||||
|
<div className="text-gray-500 text-sm mb-1 uppercase tracking-wider font-bold">Current Plan</div>
|
||||||
|
<div className="text-2xl font-black">{business.plan?.name || 'Basic'}</div>
|
||||||
|
<div className="text-gray-500 text-sm mt-2">
|
||||||
|
Your plan renews on <span className="font-bold text-gray-900 dark:text-white">{moment(business.renewal_date).format('MMMM D, YYYY')}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-4 md:mt-0 flex space-x-2">
|
||||||
|
<BaseButton label="View Invoices" color="white" small />
|
||||||
|
<BaseButton label="Payment Method" color="white" small />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
|
{plans.map((plan: any) => (
|
||||||
|
<PlanCard
|
||||||
|
key={plan.id}
|
||||||
|
plan={plan}
|
||||||
|
currentPlanId={business?.planId}
|
||||||
|
onUpgrade={handleUpgrade}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<CardBox className="mt-12 bg-gray-50 dark:bg-slate-800 border-dashed">
|
||||||
|
<div className="flex items-center p-4">
|
||||||
|
<div className="bg-blue-100 p-3 rounded-full mr-4">
|
||||||
|
<BaseIcon path={icon.mdiInformation} size={24} className="text-blue-500" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 className="font-bold">Need a custom plan?</h4>
|
||||||
|
<p className="text-sm text-gray-500">Contact our sales team for enterprise solutions tailored to your specific needs.</p>
|
||||||
|
</div>
|
||||||
|
<BaseButton label="Contact Sales" color="light" className="ml-auto" />
|
||||||
|
</div>
|
||||||
|
</CardBox>
|
||||||
|
</SectionMain>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
BillingPage.getLayout = function getLayout(page: ReactElement) {
|
||||||
|
return <LayoutAuthenticated>{page}</LayoutAuthenticated>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BillingPage;
|
||||||
@ -1,7 +1,7 @@
|
|||||||
import { mdiChartTimelineVariant } from '@mdi/js'
|
import { mdiChartTimelineVariant } from '@mdi/js'
|
||||||
import Head from 'next/head'
|
import Head from 'next/head'
|
||||||
import { uniqueId } from 'lodash';
|
import { uniqueId } from 'lodash';
|
||||||
import React, { ReactElement, useState } from 'react'
|
import React, { ReactElement, useEffect, useState } from 'react'
|
||||||
import CardBox from '../../components/CardBox'
|
import CardBox from '../../components/CardBox'
|
||||||
import LayoutAuthenticated from '../../layouts/Authenticated'
|
import LayoutAuthenticated from '../../layouts/Authenticated'
|
||||||
import SectionMain from '../../components/SectionMain'
|
import SectionMain from '../../components/SectionMain'
|
||||||
@ -14,7 +14,8 @@ import Link from "next/link";
|
|||||||
import {useAppDispatch, useAppSelector} from "../../stores/hooks";
|
import {useAppDispatch, useAppSelector} from "../../stores/hooks";
|
||||||
import CardBoxModal from "../../components/CardBoxModal";
|
import CardBoxModal from "../../components/CardBoxModal";
|
||||||
import DragDropFilePicker from "../../components/DragDropFilePicker";
|
import DragDropFilePicker from "../../components/DragDropFilePicker";
|
||||||
import {setRefetch, uploadCsv} from '../../stores/businesses/businessesSlice';
|
import {fetch, setRefetch, uploadCsv} from '../../stores/businesses/businessesSlice';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
|
|
||||||
|
|
||||||
import {hasPermission} from "../../helpers/userPermissions";
|
import {hasPermission} from "../../helpers/userPermissions";
|
||||||
@ -29,10 +30,26 @@ const BusinessesTablesPage = () => {
|
|||||||
|
|
||||||
|
|
||||||
const { currentUser } = useAppSelector((state) => state.auth);
|
const { currentUser } = useAppSelector((state) => state.auth);
|
||||||
|
const { businesses, count, loading } = useAppSelector((state) => state.businesses);
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (currentUser?.app_role?.name === 'Verified Business Owner') {
|
||||||
|
dispatch(fetch({ limit: 10, page: 0 }));
|
||||||
|
}
|
||||||
|
}, [currentUser, dispatch]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (currentUser?.app_role?.name === 'Verified Business Owner' && !loading) {
|
||||||
|
if (count === 0) {
|
||||||
|
router.push('/businesses/businesses-new');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [count, loading, currentUser, businesses, router]);
|
||||||
|
|
||||||
|
|
||||||
const [filters] = useState([{label: 'Name', title: 'name'},{label: 'Slug', title: 'slug'},{label: 'Description', title: 'description'},{label: 'Phone', title: 'phone'},{label: 'Email', title: 'email'},{label: 'Website', title: 'website'},{label: 'Address', title: 'address'},{label: 'City', title: 'city'},{label: 'State', title: 'state'},{label: 'ZIP', title: 'zip'},{label: 'HoursJSON', title: 'hours_json'},{label: 'ReliabilityBreakdownJSON', title: 'reliability_breakdown_json'},{label: 'TenantKey', title: 'tenant_key'},
|
const [filters] = useState([{label: 'Name', title: 'name'},{label: 'Slug', title: 'slug'},{label: 'Description', title: 'description'},{label: 'Phone', title: 'phone'},{label: 'Email', title: 'email'},{label: 'Website', title: 'website'},{label: 'Address', title: 'address'},{label: 'City', title: 'city'},{label: 'State', title: 'state'},{label: 'ZIP', title: 'zip'},{label: 'HoursJSON', title: 'hours_json'},{label: 'ReliabilityBreakdownJSON', title: 'reliability_breakdown_json'},{label: 'TenantKey', title: 'tenant_key'},
|
||||||
{label: 'ReliabilityScore', title: 'reliability_score', number: 'true'},{label: 'ResponseTimeMedianMinutes', title: 'response_time_median_minutes', number: 'true'},
|
{label: 'ReliabilityScore', title: 'reliability_score', number: 'true'},{label: 'ResponseTimeMedianMinutes', title: 'response_time_median_minutes', number: 'true'},
|
||||||
@ -87,6 +104,16 @@ const BusinessesTablesPage = () => {
|
|||||||
setIsModalActive(false);
|
setIsModalActive(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (currentUser?.app_role?.name === 'Verified Business Owner' && count === 0) {
|
||||||
|
return (
|
||||||
|
<SectionMain>
|
||||||
|
<div className="flex justify-center items-center h-64">
|
||||||
|
<p>Redirecting to create your business profile...</p>
|
||||||
|
</div>
|
||||||
|
</SectionMain>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
@ -167,4 +194,4 @@ BusinessesTablesPage.getLayout = function getLayout(page: ReactElement) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default BusinessesTablesPage
|
export default BusinessesTablesPage
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import * as icon from '@mdi/js';
|
import * as icon from '@mdi/js';
|
||||||
import Head from 'next/head'
|
import Head from 'next/head'
|
||||||
import React from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import type { ReactElement } from 'react'
|
import type { ReactElement } from 'react'
|
||||||
import LayoutAuthenticated from '../layouts/Authenticated'
|
import LayoutAuthenticated from '../layouts/Authenticated'
|
||||||
@ -8,425 +8,313 @@ import SectionMain from '../components/SectionMain'
|
|||||||
import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton'
|
import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton'
|
||||||
import BaseIcon from "../components/BaseIcon";
|
import BaseIcon from "../components/BaseIcon";
|
||||||
import BaseButton from "../components/BaseButton";
|
import BaseButton from "../components/BaseButton";
|
||||||
|
import CardBox from "../components/CardBox";
|
||||||
|
import CardBoxComponentBody from "../components/CardBoxComponentBody";
|
||||||
|
import CardBoxComponentTitle from "../components/CardBoxComponentTitle";
|
||||||
|
import CardBoxComponentFooter from "../components/CardBoxComponentFooter";
|
||||||
|
import ProgressBar from "../components/ProgressBar";
|
||||||
import { getPageTitle } from '../config'
|
import { getPageTitle } from '../config'
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import moment from 'moment';
|
||||||
import { hasPermission } from "../helpers/userPermissions";
|
|
||||||
import { fetchWidgets } from '../stores/roles/rolesSlice';
|
|
||||||
import { WidgetCreator } from '../components/WidgetCreator/WidgetCreator';
|
|
||||||
import { SmartWidget } from '../components/SmartWidget/SmartWidget';
|
|
||||||
|
|
||||||
import { useAppDispatch, useAppSelector } from '../stores/hooks';
|
import { useAppDispatch, useAppSelector } from '../stores/hooks';
|
||||||
|
|
||||||
|
const ActionQueueItem = ({ label, count, iconPath, color, href }: any) => (
|
||||||
|
<Link href={href}>
|
||||||
|
<div className="flex items-center p-4 bg-white dark:bg-slate-900 border border-slate-100 dark:border-slate-800 rounded-[2rem] shadow-sm hover:shadow-xl hover:shadow-emerald-500/10 transition-all cursor-pointer group">
|
||||||
|
<div className={`p-4 rounded-2xl mr-4 ${color} shadow-lg shadow-current/20 group-hover:scale-110 transition-transform`}>
|
||||||
|
<BaseIcon path={iconPath} size={24} className="text-white" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-2xl font-black text-slate-800 dark:text-white">{count}</div>
|
||||||
|
<div className="text-xs font-bold text-slate-400 uppercase tracking-tighter">{label}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
|
||||||
|
const PipelineStat = ({ label, count, color }: any) => (
|
||||||
|
<div className="text-center p-4 border-r last:border-r-0 border-slate-100 dark:border-slate-800">
|
||||||
|
<div className={`text-3xl font-black ${color}`}>{count}</div>
|
||||||
|
<div className="text-[10px] text-slate-400 uppercase tracking-widest font-black mt-2">{label}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const BusinessDashboardView = ({ metrics, currentUser }: any) => {
|
||||||
|
if (metrics.no_business) {
|
||||||
|
return (
|
||||||
|
<CardBox className="text-center p-12 border-dashed border-2 border-slate-200">
|
||||||
|
<div className="w-20 h-20 bg-slate-50 rounded-full flex items-center justify-center mx-auto mb-6">
|
||||||
|
<BaseIcon path={icon.mdiStorePlus} size={40} className="text-slate-300" />
|
||||||
|
</div>
|
||||||
|
<h2 className="text-2xl font-bold mb-2 text-slate-800">No active business found</h2>
|
||||||
|
<p className="text-slate-500 mb-8 max-w-sm mx-auto text-sm">Create your first listing to start receiving leads and managing your beauty business with AI-powered tools.</p>
|
||||||
|
<BaseButton href="/businesses/businesses-new" label="List New Business" color="info" icon={icon.mdiPlus} className="px-8 py-3 rounded-2xl font-bold shadow-lg shadow-emerald-500/20" />
|
||||||
|
</CardBox>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { action_queue, pipeline, recentMessages, performance, healthScore, businesses } = metrics;
|
||||||
|
const business = businesses[0];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-8 animate-fade-in">
|
||||||
|
{/* Action Queue */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||||
|
<ActionQueueItem
|
||||||
|
label="New Leads (24h)"
|
||||||
|
count={action_queue.newLeads24h}
|
||||||
|
iconPath={icon.mdiFlash}
|
||||||
|
color="bg-amber-400"
|
||||||
|
href="/leads/leads-list"
|
||||||
|
/>
|
||||||
|
<ActionQueueItem
|
||||||
|
label="Needs Response"
|
||||||
|
count={action_queue.leadsNeedingResponse}
|
||||||
|
iconPath={icon.mdiMessageProcessing}
|
||||||
|
color="bg-rose-400"
|
||||||
|
href="/leads/leads-list"
|
||||||
|
/>
|
||||||
|
<ActionQueueItem
|
||||||
|
label="Verifications"
|
||||||
|
count={action_queue.verificationPending}
|
||||||
|
iconPath={icon.mdiShieldCheckOutline}
|
||||||
|
color="bg-emerald-400"
|
||||||
|
href="/verification_submissions/verification_submissions-list"
|
||||||
|
/>
|
||||||
|
<ActionQueueItem
|
||||||
|
label="Health Score"
|
||||||
|
count={`${healthScore}%`}
|
||||||
|
iconPath={icon.mdiHeartPulse}
|
||||||
|
color="bg-rose-500"
|
||||||
|
href={`/businesses/businesses-edit/?id=${business.id}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||||
|
{/* Lead Pipeline */}
|
||||||
|
<CardBox className="lg:col-span-2 overflow-hidden border-none shadow-2xl shadow-slate-200/50">
|
||||||
|
<CardBoxComponentTitle title="Beauty Pipeline Snapshot">
|
||||||
|
<div className="text-[10px] font-black uppercase tracking-widest text-emerald-600 bg-emerald-50 px-3 py-1 rounded-full border border-emerald-100">
|
||||||
|
{pipeline.winRate30d.toFixed(1)}% Conversion
|
||||||
|
</div>
|
||||||
|
</CardBoxComponentTitle>
|
||||||
|
<div className="grid grid-cols-5 mt-6 bg-slate-50/50 rounded-3xl p-2 border border-slate-100">
|
||||||
|
<PipelineStat label="New" count={pipeline.NEW || 0} color="text-slate-900 dark:text-white" />
|
||||||
|
<PipelineStat label="Consulted" count={pipeline.CONTACTED || 0} color="text-rose-400" />
|
||||||
|
<PipelineStat label="Booked" count={pipeline.SCHEDULED || 0} color="text-amber-400" />
|
||||||
|
<PipelineStat label="Completed" count={pipeline.WON || 0} color="text-emerald-500" />
|
||||||
|
<PipelineStat label="Archived" count={pipeline.LOST || 0} color="text-slate-400" />
|
||||||
|
</div>
|
||||||
|
</CardBox>
|
||||||
|
|
||||||
|
{/* Listing Health */}
|
||||||
|
<CardBox className="border-none shadow-2xl shadow-slate-200/50">
|
||||||
|
<CardBoxComponentTitle title="Profile Vitality" />
|
||||||
|
<div className="mt-6">
|
||||||
|
<ProgressBar value={healthScore} label="Profile Strength" color={healthScore > 80 ? 'green' : healthScore > 50 ? 'yellow' : 'red'} />
|
||||||
|
<div className="mt-8 space-y-3">
|
||||||
|
<div className="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-4">Improve visibility:</div>
|
||||||
|
{action_queue.missingFields.slice(0, 3).map((field: string) => (
|
||||||
|
<div key={field} className="flex items-center text-xs font-semibold text-rose-500 bg-rose-50/50 p-2 rounded-xl border border-rose-100/50">
|
||||||
|
<BaseIcon path={icon.mdiAlertCircleOutline} size={14} className="mr-2" />
|
||||||
|
Add {field.replace('_json', '').replace('_', ' ')}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<Link href={`/businesses/businesses-edit/?id=${business.id}`} className="block text-xs text-emerald-600 font-bold hover:underline mt-4 text-center p-2 bg-emerald-50 rounded-xl transition-colors">
|
||||||
|
Enhance Profile →
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardBox>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||||
|
{/* Recent Messages */}
|
||||||
|
<CardBox className="lg:col-span-2 border-none shadow-2xl shadow-slate-200/50">
|
||||||
|
<CardBoxComponentTitle title="Recent Client Love" />
|
||||||
|
<div className="mt-6 space-y-4">
|
||||||
|
{recentMessages.length > 0 ? recentMessages.map((msg: any) => (
|
||||||
|
<div key={msg.id} className="flex items-start p-4 hover:bg-emerald-50/30 dark:hover:bg-slate-800 rounded-[1.5rem] transition-all border border-transparent hover:border-emerald-100 group">
|
||||||
|
<div className="bg-emerald-100 dark:bg-slate-700 w-12 h-12 rounded-2xl flex items-center justify-center mr-4 flex-shrink-0 text-emerald-600 font-black shadow-inner">
|
||||||
|
{msg.sender_user?.firstName?.[0] || 'U'}
|
||||||
|
</div>
|
||||||
|
<div className="flex-grow min-w-0">
|
||||||
|
<div className="flex justify-between items-baseline mb-1">
|
||||||
|
<span className="font-bold text-slate-800 dark:text-white truncate">{msg.sender_user?.firstName} {msg.sender_user?.lastName}</span>
|
||||||
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-tighter">{moment(msg.createdAt).fromNow()}</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-slate-500 dark:text-gray-400 line-clamp-1 italic">"{msg.body}"</p>
|
||||||
|
</div>
|
||||||
|
<div className="ml-4 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||||
|
<BaseButton small color="success" icon={icon.mdiReply} href={`/messages/messages-list?leadId=${msg.leadId}`} className="rounded-xl shadow-lg shadow-emerald-500/20" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)) : (
|
||||||
|
<div className="text-center py-12 text-slate-300 italic font-medium">No recent messages yet</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<CardBoxComponentFooter className="bg-slate-50/50">
|
||||||
|
<BaseButton label="View All Messages" color="white" small href="/leads/leads-list" className="rounded-xl border-slate-200 shadow-sm" />
|
||||||
|
</CardBoxComponentFooter>
|
||||||
|
</CardBox>
|
||||||
|
|
||||||
|
{/* Performance & Billing */}
|
||||||
|
<div className="space-y-8">
|
||||||
|
<CardBox className="border-none shadow-2xl shadow-slate-200/50">
|
||||||
|
<CardBoxComponentTitle title="Growth (30d)" />
|
||||||
|
<div className="mt-6 grid grid-cols-2 gap-4">
|
||||||
|
<div className="p-4 bg-slate-50 dark:bg-slate-800 rounded-2xl border border-slate-100">
|
||||||
|
<div className="text-[10px] text-slate-400 uppercase font-black tracking-widest mb-1">Views</div>
|
||||||
|
<div className="text-2xl font-black text-slate-800 dark:text-white">{performance.views30d}</div>
|
||||||
|
<div className="text-[10px] font-bold text-emerald-500 mt-1 flex items-center">
|
||||||
|
<BaseIcon path={icon.mdiTrendingUp} size={12} className="mr-1" />
|
||||||
|
7d: {performance.views7d}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="p-4 bg-slate-50 dark:bg-slate-800 rounded-2xl border border-slate-100">
|
||||||
|
<div className="text-[10px] text-slate-400 uppercase font-black tracking-widest mb-1">Interactions</div>
|
||||||
|
<div className="text-2xl font-black text-slate-800 dark:text-white">{performance.calls30d + performance.website30d}</div>
|
||||||
|
<div className="text-[10px] font-bold text-emerald-500 mt-1 flex items-center">
|
||||||
|
<BaseIcon path={icon.mdiTrendingUp} size={12} className="mr-1" />
|
||||||
|
7d: {performance.calls7d + performance.website7d}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-6 pt-6 border-t border-slate-100 dark:border-slate-800">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="text-xs font-bold text-slate-400 uppercase tracking-widest">Conversion Rate</span>
|
||||||
|
<span className="text-lg font-black text-emerald-500">{performance.conversionRate.toFixed(1)}%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardBox>
|
||||||
|
|
||||||
|
<CardBox className="bg-gradient-to-br from-emerald-500 via-teal-600 to-emerald-700 text-white border-none shadow-2xl shadow-emerald-500/30 overflow-hidden relative group">
|
||||||
|
<div className="absolute top-0 right-0 -mt-4 -mr-4 w-24 h-24 bg-white/10 rounded-full blur-2xl group-hover:scale-150 transition-transform duration-700"></div>
|
||||||
|
<div className="relative z-10">
|
||||||
|
<div className="flex justify-between items-start">
|
||||||
|
<div>
|
||||||
|
<div className="text-emerald-100 text-[10px] font-black uppercase tracking-[0.2em]">Tier Plan</div>
|
||||||
|
<div className="text-3xl font-black mt-2 tracking-tighter">{business.plan?.name || 'Professional'}</div>
|
||||||
|
</div>
|
||||||
|
<div className="p-3 bg-white/20 backdrop-blur-md rounded-2xl shadow-xl">
|
||||||
|
<BaseIcon path={icon.mdiCrownOutline} size={28} className="text-amber-300" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-8">
|
||||||
|
<div className="text-emerald-100 text-[10px] uppercase tracking-[0.2em] font-black">Renewal Date</div>
|
||||||
|
<div className="text-lg font-bold mt-1">{moment(business.renewal_date).format('MMMM Do, YYYY')}</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-10">
|
||||||
|
<BaseButton label="Manage Subscription" color="white" small className="w-full text-emerald-700 font-black py-4 rounded-2xl shadow-xl hover:scale-[1.02] transition-transform" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardBox>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const Dashboard = () => {
|
const Dashboard = () => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const iconsColor = useAppSelector((state) => state.style.iconsColor);
|
const iconsColor = useAppSelector((state) => state.style.iconsColor);
|
||||||
const corners = useAppSelector((state) => state.style.corners);
|
|
||||||
const cardsStyle = useAppSelector((state) => state.style.cardsStyle);
|
|
||||||
|
|
||||||
const loadingMessage = 'Loading...';
|
|
||||||
|
|
||||||
const [users, setUsers] = React.useState(loadingMessage);
|
|
||||||
const [roles, setRoles] = React.useState(loadingMessage);
|
|
||||||
const [permissions, setPermissions] = React.useState(loadingMessage);
|
|
||||||
const [refresh_tokens, setRefresh_tokens] = React.useState(loadingMessage);
|
|
||||||
const [categories, setCategories] = React.useState(loadingMessage);
|
|
||||||
const [locations, setLocations] = React.useState(loadingMessage);
|
|
||||||
const [businesses, setBusinesses] = React.useState(loadingMessage);
|
|
||||||
const [business_photos, setBusiness_photos] = React.useState(loadingMessage);
|
|
||||||
const [business_categories, setBusiness_categories] = React.useState(loadingMessage);
|
|
||||||
const [service_prices, setService_prices] = React.useState(loadingMessage);
|
|
||||||
const [business_badges, setBusiness_badges] = React.useState(loadingMessage);
|
|
||||||
const [verification_submissions, setVerification_submissions] = React.useState(loadingMessage);
|
|
||||||
const [verification_evidences, setVerification_evidences] = React.useState(loadingMessage);
|
|
||||||
const [leads, setLeads] = React.useState(loadingMessage);
|
|
||||||
const [lead_photos, setLead_photos] = React.useState(loadingMessage);
|
|
||||||
const [lead_matches, setLead_matches] = React.useState(loadingMessage);
|
|
||||||
const [messages, setMessages] = React.useState(loadingMessage);
|
|
||||||
const [lead_events, setLead_events] = React.useState(loadingMessage);
|
|
||||||
const [reviews, setReviews] = React.useState(loadingMessage);
|
|
||||||
const [disputes, setDisputes] = React.useState(loadingMessage);
|
|
||||||
const [audit_logs, setAudit_logs] = React.useState(loadingMessage);
|
|
||||||
const [badge_rules, setBadge_rules] = React.useState(loadingMessage);
|
|
||||||
const [trust_adjustments, setTrust_adjustments] = React.useState(loadingMessage);
|
|
||||||
|
|
||||||
const [widgetsRole, setWidgetsRole] = React.useState({
|
|
||||||
role: { value: '', label: '' },
|
|
||||||
});
|
|
||||||
const { currentUser } = useAppSelector((state) => state.auth);
|
const { currentUser } = useAppSelector((state) => state.auth);
|
||||||
const { isFetchingQuery } = useAppSelector((state) => state.openAi);
|
|
||||||
|
|
||||||
const { rolesWidgets, loading } = useAppSelector((state) => state.roles);
|
|
||||||
|
|
||||||
const isBusinessOwner = currentUser?.app_role?.name === 'Verified Business Owner';
|
const isBusinessOwner = currentUser?.app_role?.name === 'Verified Business Owner';
|
||||||
const myBusiness = currentUser?.businesses_owner_user?.[0];
|
|
||||||
|
|
||||||
async function loadData() {
|
const [loading, setLoading] = useState(true);
|
||||||
const entities = ['users','roles','permissions','refresh_tokens','categories','locations','businesses','business_photos','business_categories','service_prices','business_badges','verification_submissions','verification_evidences','leads','lead_photos','lead_matches','messages','lead_events','reviews','disputes','audit_logs','badge_rules','trust_adjustments',];
|
const [metrics, setMetrics] = useState<any>(null);
|
||||||
const fns = [setUsers,setRoles,setPermissions,setRefresh_tokens,setCategories,setLocations,setBusinesses,setBusiness_photos,setBusiness_categories,setService_prices,setBusiness_badges,setVerification_submissions,setVerification_evidences,setLeads,setLead_photos,setLead_matches,setMessages,setLead_events,setReviews,setDisputes,setAudit_logs,setBadge_rules,setTrust_adjustments,];
|
|
||||||
|
|
||||||
const requests = entities.map((entity, index) => {
|
const [counts, setCounts] = useState<any>({});
|
||||||
if(hasPermission(currentUser, `READ_${entity.toUpperCase()}`)) {
|
|
||||||
return axios.get(`/${entity.toLowerCase()}/count`);
|
async function loadAdminData() {
|
||||||
} else {
|
const entities = ['users','roles','permissions','categories','locations','businesses'];
|
||||||
fns[index](null);
|
const requests = entities.map(entity => axios.get(`/${entity}/count`));
|
||||||
return Promise.resolve({data: {count: null}});
|
const results = await Promise.allSettled(requests);
|
||||||
}
|
const newCounts: any = {};
|
||||||
|
results.forEach((result, i) => {
|
||||||
|
if (result.status === 'fulfilled') {
|
||||||
|
newCounts[entities[i]] = result.value.data.count;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
setCounts(newCounts);
|
||||||
Promise.allSettled(requests).then((results) => {
|
|
||||||
results.forEach((result, i) => {
|
|
||||||
if (result.status === 'fulfilled') {
|
|
||||||
fns[i](result.value.data.count);
|
|
||||||
} else {
|
|
||||||
fns[i](result.reason.message);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getWidgets(roleId) {
|
|
||||||
await dispatch(fetchWidgets(roleId));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
React.useEffect(() => {
|
async function loadBusinessMetrics() {
|
||||||
|
try {
|
||||||
|
const response = await axios.get('/dashboard/business-metrics');
|
||||||
|
setMetrics(response.data);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to load metrics', err);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
if (!currentUser) return;
|
if (!currentUser) return;
|
||||||
loadData().then();
|
if (isBusinessOwner) {
|
||||||
setWidgetsRole({ role: { value: currentUser?.app_role?.id, label: currentUser?.app_role?.name } });
|
loadBusinessMetrics();
|
||||||
}, [currentUser]);
|
} else {
|
||||||
|
loadAdminData().then(() => setLoading(false));
|
||||||
|
}
|
||||||
|
}, [currentUser, isBusinessOwner]);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<SectionMain>
|
||||||
|
<div className="flex flex-col items-center justify-center h-96 animate-pulse">
|
||||||
|
<div className="w-16 h-16 bg-emerald-100 rounded-3xl flex items-center justify-center mb-4">
|
||||||
|
<BaseIcon path={icon.mdiLoading} size={32} className="animate-spin text-emerald-500" />
|
||||||
|
</div>
|
||||||
|
<div className="text-xs font-black text-slate-400 uppercase tracking-widest">Curating your experience...</div>
|
||||||
|
</div>
|
||||||
|
</SectionMain>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (!currentUser || !widgetsRole?.role?.value) return;
|
|
||||||
getWidgets(widgetsRole?.role?.value || '').then();
|
|
||||||
}, [widgetsRole?.role?.value]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
<title>
|
<title>{getPageTitle('Beauty Studio Dashboard')}</title>
|
||||||
{getPageTitle('Overview')}
|
|
||||||
</title>
|
|
||||||
</Head>
|
</Head>
|
||||||
<SectionMain>
|
<SectionMain>
|
||||||
<SectionTitleLineWithButton
|
<SectionTitleLineWithButton
|
||||||
icon={icon.mdiChartTimelineVariant}
|
icon={icon.mdiStarFourPoints}
|
||||||
title={isBusinessOwner ? 'Business Dashboard' : 'Overview'}
|
title={isBusinessOwner ? 'Beauty Studio Hub' : 'Network System Pulse'}
|
||||||
main>
|
main
|
||||||
{isBusinessOwner && hasPermission(currentUser, 'CREATE_BUSINESSES') && (
|
className="mb-8"
|
||||||
<BaseButton
|
>
|
||||||
href="/businesses/businesses-new"
|
{isBusinessOwner && (
|
||||||
icon={icon.mdiPlus}
|
<div className="flex space-x-3">
|
||||||
label="List New Business"
|
<BaseButton label="Client Leads" color="info" icon={icon.mdiCalendarHeart} href="/leads/leads-list" small className="rounded-xl px-4 font-bold" />
|
||||||
color="info"
|
<BaseButton label="Docs" color="white" icon={icon.mdiFileUploadOutline} href="/verification_submissions/verification_submissions-list" small className="rounded-xl border-slate-200 font-bold" />
|
||||||
/>
|
</div>
|
||||||
)}
|
)}
|
||||||
</SectionTitleLineWithButton>
|
</SectionTitleLineWithButton>
|
||||||
|
|
||||||
{hasPermission(currentUser, 'CREATE_ROLES') && <WidgetCreator
|
|
||||||
currentUser={currentUser}
|
|
||||||
isFetchingQuery={isFetchingQuery}
|
|
||||||
setWidgetsRole={setWidgetsRole}
|
|
||||||
widgetsRole={widgetsRole}
|
|
||||||
/>}
|
|
||||||
{!!rolesWidgets.length &&
|
|
||||||
hasPermission(currentUser, 'CREATE_ROLES') && (
|
|
||||||
<p className=' text-gray-500 dark:text-gray-400 mb-4'>
|
|
||||||
{`${widgetsRole?.role?.label || 'Users'}'s widgets`}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className='grid grid-cols-1 gap-6 lg:grid-cols-4 mb-6 grid-flow-dense'>
|
{isBusinessOwner ? (
|
||||||
{(isFetchingQuery || loading) && (
|
<BusinessDashboardView metrics={metrics} currentUser={currentUser} />
|
||||||
<div className={` ${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 text-lg leading-tight text-gray-500 flex items-center ${cardsStyle} dark:border-dark-700 p-6`}>
|
) : (
|
||||||
<BaseIcon
|
<div className="grid grid-cols-1 gap-8 lg:grid-cols-3 mb-10 animate-fade-in">
|
||||||
className={`${iconsColor} animate-spin mr-5`}
|
{Object.keys(counts).map(entity => (
|
||||||
w='w-16'
|
<Link key={entity} href={`/${entity}/${entity}-list`}>
|
||||||
h='h-16'
|
<CardBox className="hover:shadow-2xl hover:shadow-emerald-500/10 transition-all border-none group cursor-pointer p-6">
|
||||||
size={48}
|
<div className="flex justify-between items-center">
|
||||||
path={icon.mdiLoading}
|
<div>
|
||||||
/>{' '}
|
<div className="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1">{entity.replace('_', ' ')}</div>
|
||||||
Loading widgets...
|
<div className="text-4xl font-black text-slate-800 dark:text-white group-hover:text-emerald-500 transition-colors">{counts[entity]}</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
<div className="w-16 h-16 bg-slate-50 dark:bg-slate-800 rounded-2xl flex items-center justify-center group-hover:bg-emerald-50 transition-colors">
|
||||||
|
<BaseIcon path={icon.mdiLayersOutline} size={32} className={iconsColor} />
|
||||||
{ rolesWidgets &&
|
</div>
|
||||||
rolesWidgets.map((widget) => (
|
|
||||||
<SmartWidget
|
|
||||||
key={widget.id}
|
|
||||||
userId={currentUser?.id}
|
|
||||||
widget={widget}
|
|
||||||
roleId={widgetsRole?.role?.value || ''}
|
|
||||||
admin={hasPermission(currentUser, 'CREATE_ROLES')}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{!!rolesWidgets.length && <hr className='my-6 ' />}
|
|
||||||
|
|
||||||
<div id="dashboard" className='grid grid-cols-1 gap-6 lg:grid-cols-3 mb-6'>
|
|
||||||
{isBusinessOwner && myBusiness && (
|
|
||||||
<Link href={`/businesses/businesses-edit/?id=${myBusiness.id}`}>
|
|
||||||
<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">
|
|
||||||
My Listing
|
|
||||||
</div>
|
|
||||||
<div className="text-xl leading-tight font-semibold mt-2">
|
|
||||||
{myBusiness.name}
|
|
||||||
</div>
|
|
||||||
<div className="text-sm text-blue-500 mt-2">Edit Listing</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<BaseIcon
|
|
||||||
className={`${iconsColor}`}
|
|
||||||
w="w-16"
|
|
||||||
h="h-16"
|
|
||||||
size={48}
|
|
||||||
path={icon.mdiStoreEdit}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</CardBox>
|
||||||
)}
|
</Link>
|
||||||
|
))}
|
||||||
{hasPermission(currentUser, 'READ_LEADS') && <Link href={'/leads/leads-list'}>
|
</div>
|
||||||
<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">
|
|
||||||
{isBusinessOwner ? 'Service Requests' : 'Leads'}
|
|
||||||
</div>
|
|
||||||
<div className="text-3xl leading-tight font-semibold">
|
|
||||||
{leads}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<BaseIcon
|
|
||||||
className={`${iconsColor}`}
|
|
||||||
w="w-16"
|
|
||||||
h="h-16"
|
|
||||||
size={48}
|
|
||||||
path={'mdiClipboardText' in icon ? icon['mdiClipboardText' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>}
|
|
||||||
|
|
||||||
{hasPermission(currentUser, 'READ_REVIEWS') && <Link href={'/reviews/reviews-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">
|
|
||||||
Reviews
|
|
||||||
</div>
|
|
||||||
<div className="text-3xl leading-tight font-semibold">
|
|
||||||
{reviews}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<BaseIcon
|
|
||||||
className={`${iconsColor}`}
|
|
||||||
w="w-16"
|
|
||||||
h="h-16"
|
|
||||||
size={48}
|
|
||||||
path={'mdiStar' in icon ? icon['mdiStar' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>}
|
|
||||||
|
|
||||||
{!isBusinessOwner && hasPermission(currentUser, 'READ_USERS') && <Link href={'/users/users-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">
|
|
||||||
Users
|
|
||||||
</div>
|
|
||||||
<div className="text-3xl leading-tight font-semibold">
|
|
||||||
{users}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<BaseIcon
|
|
||||||
className={`${iconsColor}`}
|
|
||||||
w="w-16"
|
|
||||||
h="h-16"
|
|
||||||
size={48}
|
|
||||||
path={icon.mdiAccountGroup || icon.mdiTable}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>}
|
|
||||||
|
|
||||||
{!isBusinessOwner && hasPermission(currentUser, 'READ_ROLES') && <Link href={'/roles/roles-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">
|
|
||||||
Roles
|
|
||||||
</div>
|
|
||||||
<div className="text-3xl leading-tight font-semibold">
|
|
||||||
{roles}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<BaseIcon
|
|
||||||
className={`${iconsColor}`}
|
|
||||||
w="w-16"
|
|
||||||
h="h-16"
|
|
||||||
size={48}
|
|
||||||
path={icon.mdiShieldAccountVariantOutline || icon.mdiTable}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>}
|
|
||||||
|
|
||||||
{!isBusinessOwner && hasPermission(currentUser, 'READ_PERMISSIONS') && <Link href={'/permissions/permissions-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">
|
|
||||||
Permissions
|
|
||||||
</div>
|
|
||||||
<div className="text-3xl leading-tight font-semibold">
|
|
||||||
{permissions}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<BaseIcon
|
|
||||||
className={`${iconsColor}`}
|
|
||||||
w="w-16"
|
|
||||||
h="h-16"
|
|
||||||
size={48}
|
|
||||||
path={icon.mdiShieldAccountOutline || icon.mdiTable}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>}
|
|
||||||
|
|
||||||
{!isBusinessOwner && hasPermission(currentUser, 'READ_REFRESH_TOKENS') && <Link href={'/refresh_tokens/refresh_tokens-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">
|
|
||||||
Refresh tokens
|
|
||||||
</div>
|
|
||||||
<div className="text-3xl leading-tight font-semibold">
|
|
||||||
{refresh_tokens}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<BaseIcon
|
|
||||||
className={`${iconsColor}`}
|
|
||||||
w="w-16"
|
|
||||||
h="h-16"
|
|
||||||
size={48}
|
|
||||||
path={'mdiLock' in icon ? icon['mdiLock' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>}
|
|
||||||
|
|
||||||
{!isBusinessOwner && hasPermission(currentUser, 'READ_CATEGORIES') && <Link href={'/categories/categories-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">
|
|
||||||
Categories
|
|
||||||
</div>
|
|
||||||
<div className="text-3xl leading-tight font-semibold">
|
|
||||||
{categories}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<BaseIcon
|
|
||||||
className={`${iconsColor}`}
|
|
||||||
w="w-16"
|
|
||||||
h="h-16"
|
|
||||||
size={48}
|
|
||||||
path={'mdiShape' in icon ? icon['mdiShape' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>}
|
|
||||||
|
|
||||||
{!isBusinessOwner && hasPermission(currentUser, 'READ_LOCATIONS') && <Link href={'/locations/locations-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">
|
|
||||||
Locations
|
|
||||||
</div>
|
|
||||||
<div className="text-3xl leading-tight font-semibold">
|
|
||||||
{locations}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<BaseIcon
|
|
||||||
className={`${iconsColor}`}
|
|
||||||
w="w-16"
|
|
||||||
h="h-16"
|
|
||||||
size={48}
|
|
||||||
path={'mdiMapMarker' in icon ? icon['mdiMapMarker' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>}
|
|
||||||
|
|
||||||
{hasPermission(currentUser, 'READ_BUSINESSES') && <Link href={'/businesses/businesses-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">
|
|
||||||
Businesses
|
|
||||||
</div>
|
|
||||||
<div className="text-3xl leading-tight font-semibold">
|
|
||||||
{businesses}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<BaseIcon
|
|
||||||
className={`${iconsColor}`}
|
|
||||||
w="w-16"
|
|
||||||
h="h-16"
|
|
||||||
size={48}
|
|
||||||
path={'mdiStore' in icon ? icon['mdiStore' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>}
|
|
||||||
</div>
|
|
||||||
</SectionMain>
|
</SectionMain>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
@ -436,4 +324,4 @@ Dashboard.getLayout = function getLayout(page: ReactElement) {
|
|||||||
return <LayoutAuthenticated>{page}</LayoutAuthenticated>
|
return <LayoutAuthenticated>{page}</LayoutAuthenticated>
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Dashboard
|
export default Dashboard;
|
||||||
@ -96,7 +96,7 @@ export default function Forgot() {
|
|||||||
<BaseIcon path={mdiShieldCheck} size={28} className="text-white" />
|
<BaseIcon path={mdiShieldCheck} size={28} className="text-white" />
|
||||||
</div>
|
</div>
|
||||||
<span className="text-2xl font-black tracking-tight text-slate-900">
|
<span className="text-2xl font-black tracking-tight text-slate-900">
|
||||||
Crafted Network<span className="text-emerald-500 italic">™</span>
|
Fix It Local<span className="text-emerald-500 italic">™</span>
|
||||||
</span>
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
<h2 className="text-3xl font-bold text-slate-900 text-center">Forgot Password?</h2>
|
<h2 className="text-3xl font-bold text-slate-900 text-center">Forgot Password?</h2>
|
||||||
@ -138,7 +138,7 @@ export default function Forgot() {
|
|||||||
</CardBox>
|
</CardBox>
|
||||||
|
|
||||||
<div className="text-center text-slate-400 text-xs pt-8">
|
<div className="text-center text-slate-400 text-xs pt-8">
|
||||||
© 2026 Crafted Network™. All rights reserved. <br/>
|
© 2026 Fix It Local™. All rights reserved. <br/>
|
||||||
<Link href='/privacy-policy/' className="hover:text-slate-600 mt-2 inline-block">Privacy Policy</Link>
|
<Link href='/privacy-policy/' className="hover:text-slate-600 mt-2 inline-block">Privacy Policy</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -53,7 +53,7 @@ export default function LandingPage() {
|
|||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-slate-50 font-sans text-slate-900">
|
<div className="min-h-screen bg-slate-50 font-sans text-slate-900">
|
||||||
<Head>
|
<Head>
|
||||||
<title>Crafted Network™ | 21st Century Service Directory</title>
|
<title>Fix It Local™ | 21st Century Service Directory</title>
|
||||||
<meta name="description" content="Connect with verified service professionals. Trust, transparency, and AI-powered matching." />
|
<meta name="description" content="Connect with verified service professionals. Trust, transparency, and AI-powered matching." />
|
||||||
</Head>
|
</Head>
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import React, { ReactElement, useEffect } from 'react';
|
import React, { ReactElement, useEffect, useState } from 'react';
|
||||||
import Head from 'next/head'
|
import Head from 'next/head'
|
||||||
import DatePicker from "react-datepicker";
|
import DatePicker from "react-datepicker";
|
||||||
import "react-datepicker/dist/react-datepicker.css";
|
import "react-datepicker/dist/react-datepicker.css";
|
||||||
@ -14,8 +14,10 @@ import SectionMain from "../../components/SectionMain";
|
|||||||
import CardBox from "../../components/CardBox";
|
import CardBox from "../../components/CardBox";
|
||||||
import BaseButton from "../../components/BaseButton";
|
import BaseButton from "../../components/BaseButton";
|
||||||
import BaseDivider from "../../components/BaseDivider";
|
import BaseDivider from "../../components/BaseDivider";
|
||||||
import {mdiChartTimelineVariant, mdiMessageReply} from "@mdi/js";
|
import {mdiChartTimelineVariant, mdiMessageReply, mdiAccountMultiple, mdiCamera, mdiTimelineText, mdiInformation} from "@mdi/js";
|
||||||
import FormField from "../../components/FormField";
|
import FormField from "../../components/FormField";
|
||||||
|
import BaseIcon from "../../components/BaseIcon";
|
||||||
|
import ImageField from "../../components/ImageField";
|
||||||
|
|
||||||
|
|
||||||
const LeadsView = () => {
|
const LeadsView = () => {
|
||||||
@ -27,12 +29,20 @@ const LeadsView = () => {
|
|||||||
|
|
||||||
const { id } = router.query;
|
const { id } = router.query;
|
||||||
|
|
||||||
|
const [activeTab, setActiveTab] = useState('details');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (id) {
|
if (id) {
|
||||||
dispatch(fetch({ id }));
|
dispatch(fetch({ id }));
|
||||||
}
|
}
|
||||||
}, [dispatch, id]);
|
}, [dispatch, id]);
|
||||||
|
|
||||||
|
const tabs = [
|
||||||
|
{ id: 'details', label: 'Details', icon: mdiInformation },
|
||||||
|
{ id: 'photos', label: 'Photos', icon: mdiCamera },
|
||||||
|
{ id: 'matches', label: 'Matches', icon: mdiAccountMultiple },
|
||||||
|
{ id: 'events', label: 'Events', icon: mdiTimelineText },
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -57,110 +67,211 @@ const LeadsView = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</SectionTitleLineWithButton>
|
</SectionTitleLineWithButton>
|
||||||
<CardBox>
|
|
||||||
<div className={'grid grid-cols-1 md:grid-cols-2 gap-6'}>
|
|
||||||
<div className={'mb-4'}>
|
|
||||||
<p className={'block font-bold mb-2'}>User</p>
|
|
||||||
<p>{leads?.user?.firstName} {leads?.user?.lastName}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={'mb-4'}>
|
<div className="mb-6 flex space-x-4 border-b border-gray-200">
|
||||||
<p className={'block font-bold mb-2'}>Category</p>
|
{tabs.map((tab) => (
|
||||||
<p>{leads?.category?.name ?? 'No data'}</p>
|
<button
|
||||||
</div>
|
key={tab.id}
|
||||||
|
onClick={() => setActiveTab(tab.id)}
|
||||||
<div className={'mb-4'}>
|
className={`flex items-center space-x-2 pb-2 px-1 transition-colors ${
|
||||||
<p className={'block font-bold mb-2'}>Keyword</p>
|
activeTab === tab.id
|
||||||
<p>{leads?.keyword}</p>
|
? 'border-b-2 border-blue-600 text-blue-600'
|
||||||
</div>
|
: 'text-gray-500 hover:text-gray-700'
|
||||||
|
}`}
|
||||||
<div className={'mb-4'}>
|
|
||||||
<p className={'block font-bold mb-2'}>Urgency</p>
|
|
||||||
<p>{leads?.urgency ?? 'No data'}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={'mb-4'}>
|
|
||||||
<p className={'block font-bold mb-2'}>Status</p>
|
|
||||||
<p>{leads?.status ?? 'No data'}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<FormField label='Description' hasTextareaHeight>
|
|
||||||
<textarea className={'w-full'} disabled value={leads?.description} />
|
|
||||||
</FormField>
|
|
||||||
|
|
||||||
<div className={'grid grid-cols-1 md:grid-cols-2 gap-6'}>
|
|
||||||
<div className={'mb-4'}>
|
|
||||||
<p className={'block font-bold mb-2'}>Contact Name</p>
|
|
||||||
<p>{leads?.contact_name}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={'mb-4'}>
|
|
||||||
<p className={'block font-bold mb-2'}>Contact Phone</p>
|
|
||||||
<p>{leads?.contact_phone}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={'mb-4'}>
|
|
||||||
<p className={'block font-bold mb-2'}>Contact Email</p>
|
|
||||||
<p>{leads?.contact_email}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={'mb-4'}>
|
|
||||||
<p className={'block font-bold mb-2'}>City</p>
|
|
||||||
<p>{leads?.city}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<FormField label='Address' hasTextareaHeight>
|
|
||||||
<textarea className={'w-full'} disabled value={leads?.address} />
|
|
||||||
</FormField>
|
|
||||||
|
|
||||||
{leads.messages_lead && leads.messages_lead.length > 0 && (
|
|
||||||
<>
|
|
||||||
<p className={'block font-bold mb-2 mt-8'}>Message History</p>
|
|
||||||
<CardBox
|
|
||||||
className='mb-6 border border-gray-300 rounded overflow-hidden shadow-sm'
|
|
||||||
hasTable
|
|
||||||
>
|
>
|
||||||
<div className='overflow-x-auto'>
|
<BaseIcon path={tab.icon} size={18} />
|
||||||
<table className="w-full text-left">
|
<span>{tab.label}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{activeTab === 'details' && (
|
||||||
|
<CardBox>
|
||||||
|
<div className={'grid grid-cols-1 md:grid-cols-2 gap-6'}>
|
||||||
|
<div className={'mb-4'}>
|
||||||
|
<p className={'block font-bold mb-2'}>User</p>
|
||||||
|
<p>{leads?.user?.firstName} {leads?.user?.lastName}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={'mb-4'}>
|
||||||
|
<p className={'block font-bold mb-2'}>Category</p>
|
||||||
|
<p>{leads?.category?.name ?? 'No data'}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={'mb-4'}>
|
||||||
|
<p className={'block font-bold mb-2'}>Keyword</p>
|
||||||
|
<p>{leads?.keyword}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={'mb-4'}>
|
||||||
|
<p className={'block font-bold mb-2'}>Urgency</p>
|
||||||
|
<p>{leads?.urgency ?? 'No data'}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={'mb-4'}>
|
||||||
|
<p className={'block font-bold mb-2'}>Status</p>
|
||||||
|
<p>{leads?.status ?? 'No data'}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FormField label='Description' hasTextareaHeight>
|
||||||
|
<textarea className={'w-full'} disabled value={leads?.description} />
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<div className={'grid grid-cols-1 md:grid-cols-2 gap-6'}>
|
||||||
|
<div className={'mb-4'}>
|
||||||
|
<p className={'block font-bold mb-2'}>Contact Name</p>
|
||||||
|
<p>{leads?.contact_name}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={'mb-4'}>
|
||||||
|
<p className={'block font-bold mb-2'}>Contact Phone</p>
|
||||||
|
<p>{leads?.contact_phone}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={'mb-4'}>
|
||||||
|
<p className={'block font-bold mb-2'}>Contact Email</p>
|
||||||
|
<p>{leads?.contact_email}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={'mb-4'}>
|
||||||
|
<p className={'block font-bold mb-2'}>City</p>
|
||||||
|
<p>{leads?.city}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FormField label='Address' hasTextareaHeight>
|
||||||
|
<textarea className={'w-full'} disabled value={leads?.address} />
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
{leads.messages_lead && leads.messages_lead.length > 0 && (
|
||||||
|
<>
|
||||||
|
<p className={'block font-bold mb-2 mt-8'}>Message History</p>
|
||||||
|
<CardBox
|
||||||
|
className='mb-6 border border-gray-300 rounded overflow-hidden shadow-sm'
|
||||||
|
hasTable
|
||||||
|
>
|
||||||
|
<div className='overflow-x-auto'>
|
||||||
|
<table className="w-full text-left">
|
||||||
|
<thead>
|
||||||
|
<tr className="bg-gray-50 border-b border-gray-200 text-xs uppercase text-gray-500 font-semibold">
|
||||||
|
<th className="px-4 py-3">Body</th>
|
||||||
|
<th className="px-4 py-3">Read At</th>
|
||||||
|
<th className="px-4 py-3">Date</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y divide-gray-200">
|
||||||
|
{leads.messages_lead.map((item: any) => (
|
||||||
|
<tr key={item.id} className="hover:bg-gray-50 cursor-pointer transition-colors" onClick={() => router.push(`/messages/messages-view/?id=${item.id}`)}>
|
||||||
|
<td className="px-4 py-3" data-label="body">
|
||||||
|
{ item.body }
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3" data-label="read_at">
|
||||||
|
{ dataFormatter.dateTimeFormatter(item.read_at) }
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3" data-label="created_at_ts">
|
||||||
|
{ dataFormatter.dateTimeFormatter(item.created_at_ts) }
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</CardBox>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<BaseDivider />
|
||||||
|
|
||||||
|
<BaseButton
|
||||||
|
color='info'
|
||||||
|
label='Back'
|
||||||
|
outline
|
||||||
|
onClick={() => router.push('/leads/leads-list')}
|
||||||
|
/>
|
||||||
|
</CardBox>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === 'photos' && (
|
||||||
|
<CardBox>
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||||
|
{leads.lead_photos_lead?.length > 0 ? (
|
||||||
|
leads.lead_photos_lead.map((photo: any) => (
|
||||||
|
<div key={photo.id} className="border border-gray-200 rounded p-2">
|
||||||
|
<ImageField value={photo.photos} />
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<p className="text-gray-500 italic">No photos available.</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardBox>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === 'matches' && (
|
||||||
|
<CardBox hasTable>
|
||||||
|
<div className='overflow-x-auto'>
|
||||||
|
<table className="w-full text-left">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="bg-gray-50 border-b border-gray-200 text-xs uppercase text-gray-500 font-semibold">
|
<tr className="bg-gray-50 border-b border-gray-200 text-xs uppercase text-gray-500 font-semibold">
|
||||||
<th className="px-4 py-3">Body</th>
|
<th className="px-4 py-3">Business</th>
|
||||||
<th className="px-4 py-3">Read At</th>
|
<th className="px-4 py-3">Status</th>
|
||||||
<th className="px-4 py-3">Date</th>
|
<th className="px-4 py-3">Matched At</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-gray-200">
|
<tbody className="divide-y divide-gray-200">
|
||||||
{leads.messages_lead.map((item: any) => (
|
{leads.lead_matches_lead?.length > 0 ? (
|
||||||
<tr key={item.id} className="hover:bg-gray-50 cursor-pointer transition-colors" onClick={() => router.push(`/messages/messages-view/?id=${item.id}`)}>
|
leads.lead_matches_lead.map((match: any) => (
|
||||||
<td className="px-4 py-3" data-label="body">
|
<tr key={match.id} className="hover:bg-gray-50 transition-colors">
|
||||||
{ item.body }
|
<td className="px-4 py-3">{match.business?.name}</td>
|
||||||
</td>
|
<td className="px-4 py-3">{match.status}</td>
|
||||||
<td className="px-4 py-3" data-label="read_at">
|
<td className="px-4 py-3">{dataFormatter.dateTimeFormatter(match.created_at_ts)}</td>
|
||||||
{ dataFormatter.dateTimeFormatter(item.read_at) }
|
</tr>
|
||||||
</td>
|
))
|
||||||
<td className="px-4 py-3" data-label="created_at_ts">
|
) : (
|
||||||
{ dataFormatter.dateTimeFormatter(item.created_at_ts) }
|
<tr>
|
||||||
</td>
|
<td colSpan={3} className="px-4 py-8 text-center text-gray-500 italic">No matches found.</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
)}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</CardBox>
|
</CardBox>
|
||||||
</>
|
)}
|
||||||
)}
|
|
||||||
|
|
||||||
<BaseDivider />
|
{activeTab === 'events' && (
|
||||||
|
<CardBox hasTable>
|
||||||
|
<div className='overflow-x-auto'>
|
||||||
|
<table className="w-full text-left">
|
||||||
|
<thead>
|
||||||
|
<tr className="bg-gray-50 border-b border-gray-200 text-xs uppercase text-gray-500 font-semibold">
|
||||||
|
<th className="px-4 py-3">Event Type</th>
|
||||||
|
<th className="px-4 py-3">Actor</th>
|
||||||
|
<th className="px-4 py-3">Notes</th>
|
||||||
|
<th className="px-4 py-3">Date</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y divide-gray-200">
|
||||||
|
{leads.lead_events_lead?.length > 0 ? (
|
||||||
|
leads.lead_events_lead.map((event: any) => (
|
||||||
|
<tr key={event.id} className="hover:bg-gray-50 transition-colors">
|
||||||
|
<td className="px-4 py-3">{event.event_type}</td>
|
||||||
|
<td className="px-4 py-3">{event.actor_user?.firstName}</td>
|
||||||
|
<td className="px-4 py-3">{event.notes}</td>
|
||||||
|
<td className="px-4 py-3">{dataFormatter.dateTimeFormatter(event.created_at_ts)}</td>
|
||||||
|
</tr>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={4} className="px-4 py-8 text-center text-gray-500 italic">No events found.</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</CardBox>
|
||||||
|
)}
|
||||||
|
|
||||||
<BaseButton
|
|
||||||
color='info'
|
|
||||||
label='Back'
|
|
||||||
outline
|
|
||||||
onClick={() => router.push('/leads/leads-list')}
|
|
||||||
/>
|
|
||||||
</CardBox>
|
|
||||||
</SectionMain>
|
</SectionMain>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@ -176,4 +287,4 @@ LeadsView.getLayout = function getLayout(page: ReactElement) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default LeadsView;
|
export default LeadsView;
|
||||||
|
|||||||
@ -43,7 +43,7 @@ export default function Login() {
|
|||||||
password: 'b2096650',
|
password: 'b2096650',
|
||||||
remember: true })
|
remember: true })
|
||||||
|
|
||||||
const title = 'Crafted Network'
|
const title = 'Fix It Local'
|
||||||
|
|
||||||
// Fetch Pexels image/video
|
// Fetch Pexels image/video
|
||||||
useEffect( () => {
|
useEffect( () => {
|
||||||
@ -175,7 +175,7 @@ export default function Login() {
|
|||||||
<BaseIcon path={mdiShieldCheck} size={28} className="text-white" />
|
<BaseIcon path={mdiShieldCheck} size={28} className="text-white" />
|
||||||
</div>
|
</div>
|
||||||
<span className="text-2xl font-black tracking-tight text-slate-900">
|
<span className="text-2xl font-black tracking-tight text-slate-900">
|
||||||
Crafted Network<span className="text-emerald-500 italic">™</span>
|
Fix It Local<span className="text-emerald-500 italic">™</span>
|
||||||
</span>
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
<h2 className="text-3xl font-bold text-slate-900">Account Login</h2>
|
<h2 className="text-3xl font-bold text-slate-900">Account Login</h2>
|
||||||
@ -270,7 +270,7 @@ export default function Login() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-center text-slate-400 text-xs pt-8">
|
<div className="text-center text-slate-400 text-xs pt-8">
|
||||||
© 2026 Crafted Network™. All rights reserved. <br/>
|
© 2026 Fix It Local™. All rights reserved. <br/>
|
||||||
<Link href='/privacy-policy/' className="hover:text-slate-600 mt-2 inline-block">Privacy Policy</Link>
|
<Link href='/privacy-policy/' className="hover:text-slate-600 mt-2 inline-block">Privacy Policy</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import LayoutGuest from '../layouts/Guest';
|
|||||||
import { getPageTitle } from '../config';
|
import { getPageTitle } from '../config';
|
||||||
|
|
||||||
export default function PrivacyPolicy() {
|
export default function PrivacyPolicy() {
|
||||||
const title = 'Crafted Network'
|
const title = 'Fix It Local'
|
||||||
const [projectUrl, setProjectUrl] = useState('');
|
const [projectUrl, setProjectUrl] = useState('');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@ -31,6 +31,7 @@ const BusinessDetailsPublic = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (id) {
|
if (id) {
|
||||||
fetchBusiness();
|
fetchBusiness();
|
||||||
|
recordEvent('VIEW');
|
||||||
}
|
}
|
||||||
}, [id]);
|
}, [id]);
|
||||||
|
|
||||||
@ -46,6 +47,20 @@ const BusinessDetailsPublic = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const recordEvent = async (type: string) => {
|
||||||
|
if (!id) return;
|
||||||
|
try {
|
||||||
|
await axios.post('/dashboard/record-event', {
|
||||||
|
businessId: id,
|
||||||
|
event_type: type,
|
||||||
|
metadata: { path: router.asPath }
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
// Silently fail for analytics
|
||||||
|
console.error('Failed to record event', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const claimListing = async () => {
|
const claimListing = async () => {
|
||||||
if (!currentUser) {
|
if (!currentUser) {
|
||||||
router.push('/login');
|
router.push('/login');
|
||||||
@ -90,7 +105,7 @@ const BusinessDetailsPublic = () => {
|
|||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-slate-50 pb-20 pt-20">
|
<div className="min-h-screen bg-slate-50 pb-20 pt-20">
|
||||||
<Head>
|
<Head>
|
||||||
<title>{business.name} | Crafted Network™</title>
|
<title>{business.name} | Fix It Local™</title>
|
||||||
</Head>
|
</Head>
|
||||||
|
|
||||||
{/* Hero Header */}
|
{/* Hero Header */}
|
||||||
@ -334,7 +349,11 @@ const BusinessDetailsPublic = () => {
|
|||||||
<BaseIcon path={mdiPhone} size={24} className="mr-4 text-emerald-400" />
|
<BaseIcon path={mdiPhone} size={24} className="mr-4 text-emerald-400" />
|
||||||
<div>
|
<div>
|
||||||
<div className="text-xs font-black uppercase tracking-widest text-slate-500 mb-1">Call Now</div>
|
<div className="text-xs font-black uppercase tracking-widest text-slate-500 mb-1">Call Now</div>
|
||||||
<div className="font-bold">{business.phone || 'Contact for details'}</div>
|
<div className="font-bold">
|
||||||
|
<a href={`tel:${business.phone}`} onClick={() => recordEvent('CALL_CLICK')}>
|
||||||
|
{business.phone || 'Contact for details'}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-start">
|
<div className="flex items-start">
|
||||||
@ -348,7 +367,11 @@ const BusinessDetailsPublic = () => {
|
|||||||
<BaseIcon path={mdiWeb} size={24} className="mr-4 text-emerald-400" />
|
<BaseIcon path={mdiWeb} size={24} className="mr-4 text-emerald-400" />
|
||||||
<div>
|
<div>
|
||||||
<div className="text-xs font-black uppercase tracking-widest text-slate-500 mb-1">Website</div>
|
<div className="text-xs font-black uppercase tracking-widest text-slate-500 mb-1">Website</div>
|
||||||
<div className="font-bold truncate max-w-[180px]">{business.website || 'N/A'}</div>
|
<div className="font-bold truncate max-w-[180px]">
|
||||||
|
<a href={business.website} target="_blank" rel="noopener noreferrer" onClick={() => recordEvent('WEBSITE_CLICK')}>
|
||||||
|
{business.website || 'N/A'}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-start">
|
<div className="flex items-start">
|
||||||
@ -402,4 +425,4 @@ BusinessDetailsPublic.getLayout = function getLayout(page: ReactElement) {
|
|||||||
return <LayoutGuest>{page}</LayoutGuest>;
|
return <LayoutGuest>{page}</LayoutGuest>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default BusinessDetailsPublic;
|
export default BusinessDetailsPublic;
|
||||||
|
|||||||
@ -65,7 +65,7 @@ const RequestServicePage = () => {
|
|||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-slate-50 pb-20 pt-20">
|
<div className="min-h-screen bg-slate-50 pb-20 pt-20">
|
||||||
<Head>
|
<Head>
|
||||||
<title>Request Service | Crafted Network™</title>
|
<title>Request Service | Fix It Local™</title>
|
||||||
</Head>
|
</Head>
|
||||||
|
|
||||||
<div className="container mx-auto px-6 max-w-4xl">
|
<div className="container mx-auto px-6 max-w-4xl">
|
||||||
|
|||||||
@ -139,7 +139,7 @@ export default function Register() {
|
|||||||
<BaseIcon path={mdiShieldCheck} size={28} className="text-white" />
|
<BaseIcon path={mdiShieldCheck} size={28} className="text-white" />
|
||||||
</div>
|
</div>
|
||||||
<span className="text-2xl font-black tracking-tight text-slate-900">
|
<span className="text-2xl font-black tracking-tight text-slate-900">
|
||||||
Crafted Network<span className="text-emerald-500 italic">™</span>
|
Fix It Local<span className="text-emerald-500 italic">™</span>
|
||||||
</span>
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
<h2 className="text-3xl font-bold text-slate-900">Create Account</h2>
|
<h2 className="text-3xl font-bold text-slate-900">Create Account</h2>
|
||||||
@ -213,7 +213,7 @@ export default function Register() {
|
|||||||
|
|
||||||
<div className="text-center text-slate-400 text-xs pt-8">
|
<div className="text-center text-slate-400 text-xs pt-8">
|
||||||
By creating an account, you agree to our <Link href='/terms-of-use' className="underline">Terms</Link> and <Link href='/privacy-policy' className="underline">Privacy Policy</Link>. <br />
|
By creating an account, you agree to our <Link href='/terms-of-use' className="underline">Terms</Link> and <Link href='/privacy-policy' className="underline">Privacy Policy</Link>. <br />
|
||||||
© 2026 Crafted Network™. All rights reserved.
|
© 2026 Fix It Local™. All rights reserved.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import LayoutGuest from '../layouts/Guest';
|
|||||||
import { getPageTitle } from '../config';
|
import { getPageTitle } from '../config';
|
||||||
|
|
||||||
export default function PrivacyPolicy() {
|
export default function PrivacyPolicy() {
|
||||||
const title = 'Crafted Network';
|
const title = 'Fix It Local';
|
||||||
const [projectUrl, setProjectUrl] = useState('');
|
const [projectUrl, setProjectUrl] = useState('');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@ -28,26 +28,26 @@ export const white: StyleObject = {
|
|||||||
aside: 'bg-white dark:text-white',
|
aside: 'bg-white dark:text-white',
|
||||||
asideScrollbars: 'aside-scrollbars-light',
|
asideScrollbars: 'aside-scrollbars-light',
|
||||||
asideBrand: '',
|
asideBrand: '',
|
||||||
asideMenuItem: 'text-gray-700 hover:bg-gray-100/70 dark:text-dark-500 dark:hover:text-white dark:hover:bg-dark-800',
|
asideMenuItem: 'text-slate-600 hover:bg-emerald-50/50 hover:text-emerald-600 dark:text-dark-500 dark:hover:text-white dark:hover:bg-dark-800 transition-colors',
|
||||||
asideMenuItemActive: 'font-bold text-black dark:text-white',
|
asideMenuItemActive: 'font-bold text-emerald-600 bg-emerald-50/70 dark:text-white',
|
||||||
asideMenuDropdown: 'bg-gray-100/75',
|
asideMenuDropdown: 'bg-emerald-50/30',
|
||||||
navBarItemLabel: 'text-blue-600',
|
navBarItemLabel: 'text-emerald-600',
|
||||||
navBarItemLabelHover: 'hover:text-black',
|
navBarItemLabelHover: 'hover:text-emerald-700',
|
||||||
navBarItemLabelActiveColor: 'text-black',
|
navBarItemLabelActiveColor: 'text-emerald-700',
|
||||||
overlay: 'from-white via-gray-100 to-white',
|
overlay: 'from-white via-emerald-50 to-white',
|
||||||
activeLinkColor: 'bg-gray-100/70',
|
activeLinkColor: 'bg-emerald-50/70',
|
||||||
bgLayoutColor: 'bg-gray-50',
|
bgLayoutColor: 'bg-slate-50',
|
||||||
iconsColor: 'text-blue-500',
|
iconsColor: 'text-emerald-500',
|
||||||
cardsColor: 'bg-white',
|
cardsColor: 'bg-white',
|
||||||
focusRingColor: 'focus:ring focus:ring-blue-600 focus:border-blue-600 focus:outline-none border-gray-300 dark:focus:ring-blue-600 dark:focus:border-blue-600',
|
focusRingColor: 'focus:ring focus:ring-emerald-600 focus:border-emerald-600 focus:outline-none border-slate-300 dark:focus:ring-emerald-600 dark:focus:border-emerald-600',
|
||||||
corners: 'rounded',
|
corners: 'rounded-3xl',
|
||||||
cardsStyle: 'bg-white border border-pavitra-400',
|
cardsStyle: 'bg-white border border-slate-200 shadow-sm',
|
||||||
linkColor: 'text-blue-600',
|
linkColor: 'text-emerald-600',
|
||||||
websiteHeder: 'border-b border-gray-200',
|
websiteHeder: 'border-b border-slate-200',
|
||||||
borders: 'border-gray-200',
|
borders: 'border-slate-200',
|
||||||
shadow: '',
|
shadow: 'shadow-xl shadow-emerald-500/5',
|
||||||
websiteSectionStyle: '',
|
websiteSectionStyle: '',
|
||||||
textSecondary: 'text-gray-500',
|
textSecondary: 'text-slate-500',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -67,41 +67,45 @@ export const dataGridStyles = {
|
|||||||
},
|
},
|
||||||
'& .MuiDataGrid-columnHeaders': {
|
'& .MuiDataGrid-columnHeaders': {
|
||||||
paddingY: 4,
|
paddingY: 4,
|
||||||
borderStartStartRadius: 7,
|
borderStartStartRadius: 24,
|
||||||
borderStartEndRadius: 7,
|
borderStartEndRadius: 24,
|
||||||
|
backgroundColor: '#f8fafc',
|
||||||
},
|
},
|
||||||
'& .MuiDataGrid-footerContainer': {
|
'& .MuiDataGrid-footerContainer': {
|
||||||
paddingY: 0.5,
|
paddingY: 0.5,
|
||||||
borderEndStartRadius: 7,
|
borderEndStartRadius: 24,
|
||||||
borderEndEndRadius: 7,
|
borderEndEndRadius: 24,
|
||||||
},
|
},
|
||||||
'& .MuiDataGrid-root': {
|
'& .MuiDataGrid-root': {
|
||||||
border: 'none',
|
border: 'none',
|
||||||
|
borderRadius: '24px',
|
||||||
|
backgroundColor: '#ffffff',
|
||||||
|
boxShadow: '0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const basic: StyleObject = {
|
export const basic: StyleObject = {
|
||||||
aside: 'bg-gray-800',
|
aside: 'bg-slate-900',
|
||||||
asideScrollbars: 'aside-scrollbars-gray',
|
asideScrollbars: 'aside-scrollbars-gray',
|
||||||
asideBrand: 'bg-gray-900 text-white',
|
asideBrand: 'bg-slate-950 text-white',
|
||||||
asideMenuItem: 'text-gray-300 hover:text-white',
|
asideMenuItem: 'text-slate-300 hover:text-white hover:bg-white/5 transition-colors',
|
||||||
asideMenuItemActive: 'font-bold text-white',
|
asideMenuItemActive: 'font-bold text-white bg-emerald-600',
|
||||||
asideMenuDropdown: 'bg-gray-700/50',
|
asideMenuDropdown: 'bg-slate-800/50',
|
||||||
navBarItemLabel: 'text-black',
|
navBarItemLabel: 'text-slate-900',
|
||||||
navBarItemLabelHover: 'hover:text-blue-500',
|
navBarItemLabelHover: 'hover:text-emerald-500',
|
||||||
navBarItemLabelActiveColor: 'text-blue-600',
|
navBarItemLabelActiveColor: 'text-emerald-600',
|
||||||
overlay: 'from-gray-700 via-gray-900 to-gray-700',
|
overlay: 'from-slate-700 via-slate-900 to-slate-700',
|
||||||
activeLinkColor: 'bg-gray-100/70',
|
activeLinkColor: 'bg-emerald-500/10',
|
||||||
bgLayoutColor: 'bg-gray-50',
|
bgLayoutColor: 'bg-slate-50',
|
||||||
iconsColor: 'text-blue-500',
|
iconsColor: 'text-emerald-500',
|
||||||
cardsColor: 'bg-white',
|
cardsColor: 'bg-white',
|
||||||
focusRingColor: 'focus:ring focus:ring-blue-600 focus:border-blue-600 focus:outline-none dark:focus:ring-blue-600 border-gray-300 dark:focus:border-blue-600',
|
focusRingColor: 'focus:ring focus:ring-emerald-600 focus:border-emerald-600 focus:outline-none dark:focus:ring-emerald-600 border-slate-300 dark:focus:border-emerald-600',
|
||||||
corners: 'rounded',
|
corners: 'rounded-3xl',
|
||||||
cardsStyle: 'bg-white border border-pavitra-400',
|
cardsStyle: 'bg-white border border-slate-200 shadow-sm',
|
||||||
linkColor: 'text-black',
|
linkColor: 'text-slate-900',
|
||||||
websiteHeder: '',
|
websiteHeder: '',
|
||||||
borders: '',
|
borders: '',
|
||||||
shadow: '',
|
shadow: 'shadow-lg shadow-emerald-500/10',
|
||||||
websiteSectionStyle: '',
|
websiteSectionStyle: '',
|
||||||
textSecondary: '',
|
textSecondary: 'text-slate-500',
|
||||||
}
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user