Autosave: 20260403-073442
This commit is contained in:
parent
d6ec6bb83a
commit
77754d1430
@ -51,15 +51,11 @@ const config = {
|
||||
}
|
||||
},
|
||||
roles: {
|
||||
|
||||
super_admin: 'Super Administrator',
|
||||
|
||||
admin: 'Administrator',
|
||||
|
||||
|
||||
|
||||
user: 'Concierge Coordinator',
|
||||
|
||||
concierge: 'Concierge Coordinator',
|
||||
customer: 'Customer',
|
||||
user: 'Customer',
|
||||
},
|
||||
|
||||
project_uuid: '946cafba-a21f-40cd-bb6a-bc40952c93f4',
|
||||
|
||||
@ -1,14 +1,70 @@
|
||||
|
||||
const db = require('../models');
|
||||
const FileDBApi = require('./file');
|
||||
const crypto = require('crypto');
|
||||
const Utils = require('../utils');
|
||||
|
||||
const config = require('../../config');
|
||||
const crypto = require('crypto');
|
||||
|
||||
|
||||
const Sequelize = db.Sequelize;
|
||||
const Op = Sequelize.Op;
|
||||
|
||||
function isCustomerUser(currentUser) {
|
||||
return currentUser?.app_role?.name === config.roles.customer;
|
||||
}
|
||||
|
||||
function canManageInternalBookingRequestFields(currentUser) {
|
||||
return Boolean(currentUser?.app_role?.globalAccess);
|
||||
}
|
||||
|
||||
function generateRequestCode() {
|
||||
const dateSegment = new Date().toISOString().slice(0, 10).replace(/-/g, '');
|
||||
const randomSegment = crypto.randomBytes(2).toString('hex').toUpperCase();
|
||||
|
||||
return `BR-${dateSegment}-${randomSegment}`;
|
||||
}
|
||||
|
||||
function resolveRequestCode(data, currentUser) {
|
||||
if (canManageInternalBookingRequestFields(currentUser) && data.request_code) {
|
||||
return data.request_code;
|
||||
}
|
||||
|
||||
return generateRequestCode();
|
||||
}
|
||||
|
||||
function resolveStatusForCreate(data, currentUser) {
|
||||
if (canManageInternalBookingRequestFields(currentUser)) {
|
||||
return data.status || 'draft';
|
||||
}
|
||||
|
||||
return 'submitted';
|
||||
}
|
||||
|
||||
function resolveOrganizationId(data, currentUser) {
|
||||
if (canManageInternalBookingRequestFields(currentUser)) {
|
||||
return data.organization || currentUser.organization?.id || null;
|
||||
}
|
||||
|
||||
return currentUser.organization?.id || null;
|
||||
}
|
||||
|
||||
function resolveRequestedById(data, currentUser) {
|
||||
if (canManageInternalBookingRequestFields(currentUser)) {
|
||||
return data.requested_by || currentUser.id || null;
|
||||
}
|
||||
|
||||
return currentUser.id || null;
|
||||
}
|
||||
|
||||
function mergeWhereWithScope(where, scope) {
|
||||
if (!scope) {
|
||||
return where;
|
||||
}
|
||||
|
||||
return {
|
||||
[Op.and]: [where, scope],
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = class Booking_requestsDBApi {
|
||||
|
||||
|
||||
@ -21,15 +77,9 @@ module.exports = class Booking_requestsDBApi {
|
||||
{
|
||||
id: data.id || undefined,
|
||||
|
||||
request_code: data.request_code
|
||||
||
|
||||
null
|
||||
,
|
||||
request_code: resolveRequestCode(data, currentUser),
|
||||
|
||||
status: data.status
|
||||
||
|
||||
null
|
||||
,
|
||||
status: resolveStatusForCreate(data, currentUser),
|
||||
|
||||
check_in_at: data.check_in_at
|
||||
||
|
||||
@ -84,15 +134,15 @@ module.exports = class Booking_requestsDBApi {
|
||||
);
|
||||
|
||||
|
||||
await booking_requests.setTenant( data.tenant || null, {
|
||||
await booking_requests.setTenant( canManageInternalBookingRequestFields(currentUser) ? data.tenant || null : null, {
|
||||
transaction,
|
||||
});
|
||||
|
||||
await booking_requests.setOrganization(currentUser.organization.id || null, {
|
||||
await booking_requests.setOrganization(resolveOrganizationId(data, currentUser), {
|
||||
transaction,
|
||||
});
|
||||
|
||||
await booking_requests.setRequested_by( data.requested_by || null, {
|
||||
await booking_requests.setRequested_by(resolveRequestedById(data, currentUser), {
|
||||
transaction,
|
||||
});
|
||||
|
||||
@ -211,18 +261,22 @@ module.exports = class Booking_requestsDBApi {
|
||||
const currentUser = (options && options.currentUser) || {id: null};
|
||||
const transaction = (options && options.transaction) || undefined;
|
||||
const globalAccess = currentUser.app_role?.globalAccess;
|
||||
const canManageInternalFields = canManageInternalBookingRequestFields(currentUser);
|
||||
|
||||
const booking_requests = await db.booking_requests.findByPk(id, {}, {transaction});
|
||||
const booking_requests = await db.booking_requests.findOne({
|
||||
where: mergeWhereWithScope({ id }, isCustomerUser(currentUser) ? { requested_byId: currentUser.id } : null),
|
||||
transaction,
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
const updatePayload = {};
|
||||
|
||||
if (data.request_code !== undefined) updatePayload.request_code = data.request_code;
|
||||
if (data.request_code !== undefined && canManageInternalFields) updatePayload.request_code = data.request_code;
|
||||
|
||||
|
||||
if (data.status !== undefined) updatePayload.status = data.status;
|
||||
if (data.status !== undefined && canManageInternalFields) updatePayload.status = data.status;
|
||||
|
||||
|
||||
if (data.check_in_at !== undefined) updatePayload.check_in_at = data.check_in_at;
|
||||
@ -258,7 +312,7 @@ module.exports = class Booking_requestsDBApi {
|
||||
|
||||
|
||||
|
||||
if (data.tenant !== undefined) {
|
||||
if (data.tenant !== undefined && canManageInternalFields) {
|
||||
await booking_requests.setTenant(
|
||||
|
||||
data.tenant,
|
||||
@ -276,7 +330,7 @@ module.exports = class Booking_requestsDBApi {
|
||||
);
|
||||
}
|
||||
|
||||
if (data.requested_by !== undefined) {
|
||||
if (data.requested_by !== undefined && canManageInternalFields) {
|
||||
await booking_requests.setRequested_by(
|
||||
|
||||
data.requested_by,
|
||||
@ -333,11 +387,11 @@ module.exports = class Booking_requestsDBApi {
|
||||
const transaction = (options && options.transaction) || undefined;
|
||||
|
||||
const booking_requests = await db.booking_requests.findAll({
|
||||
where: {
|
||||
where: mergeWhereWithScope({
|
||||
id: {
|
||||
[Op.in]: ids,
|
||||
},
|
||||
},
|
||||
}, isCustomerUser(currentUser) ? { requested_byId: currentUser.id } : null),
|
||||
transaction,
|
||||
});
|
||||
|
||||
@ -361,7 +415,14 @@ module.exports = class Booking_requestsDBApi {
|
||||
const currentUser = (options && options.currentUser) || {id: null};
|
||||
const transaction = (options && options.transaction) || undefined;
|
||||
|
||||
const booking_requests = await db.booking_requests.findByPk(id, options);
|
||||
const booking_requests = await db.booking_requests.findOne({
|
||||
where: mergeWhereWithScope({ id }, isCustomerUser(currentUser) ? { requested_byId: currentUser.id } : null),
|
||||
transaction,
|
||||
});
|
||||
|
||||
if (!booking_requests) {
|
||||
return null;
|
||||
}
|
||||
|
||||
await booking_requests.update({
|
||||
deletedBy: currentUser.id
|
||||
@ -378,11 +439,12 @@ module.exports = class Booking_requestsDBApi {
|
||||
|
||||
static async findBy(where, options) {
|
||||
const transaction = (options && options.transaction) || undefined;
|
||||
const currentUser = (options && options.currentUser) || null;
|
||||
|
||||
const booking_requests = await db.booking_requests.findOne(
|
||||
{ where },
|
||||
{ transaction },
|
||||
);
|
||||
const booking_requests = await db.booking_requests.findOne({
|
||||
where: mergeWhereWithScope(where, isCustomerUser(currentUser) ? { requested_byId: currentUser.id } : null),
|
||||
transaction,
|
||||
});
|
||||
|
||||
if (!booking_requests) {
|
||||
return booking_requests;
|
||||
@ -502,6 +564,7 @@ module.exports = class Booking_requestsDBApi {
|
||||
|
||||
const user = (options && options.currentUser) || null;
|
||||
const userOrganizations = (user && user.organizations?.id) || null;
|
||||
const customerScope = isCustomerUser(user) ? { requested_byId: user.id } : null;
|
||||
|
||||
|
||||
|
||||
@ -512,11 +575,12 @@ module.exports = class Booking_requestsDBApi {
|
||||
}
|
||||
|
||||
|
||||
if (customerScope) {
|
||||
where.requested_byId = user.id;
|
||||
}
|
||||
|
||||
offset = currentPage * limit;
|
||||
|
||||
const orderBy = null;
|
||||
|
||||
const transaction = (options && options.transaction) || undefined;
|
||||
|
||||
let include = [
|
||||
|
||||
@ -1005,8 +1069,12 @@ module.exports = class Booking_requestsDBApi {
|
||||
}
|
||||
}
|
||||
|
||||
static async findAllAutocomplete(query, limit, offset, globalAccess, organizationId,) {
|
||||
static async findAllAutocomplete(query, limit, offset, globalAccess, organizationId, currentUser) {
|
||||
let where = {};
|
||||
|
||||
if (isCustomerUser(currentUser)) {
|
||||
where.requested_byId = currentUser.id;
|
||||
}
|
||||
|
||||
|
||||
if (!globalAccess && organizationId) {
|
||||
@ -1016,13 +1084,18 @@ module.exports = class Booking_requestsDBApi {
|
||||
|
||||
if (query) {
|
||||
where = {
|
||||
[Op.or]: [
|
||||
{ ['id']: Utils.uuid(query) },
|
||||
Utils.ilike(
|
||||
'booking_requests',
|
||||
'request_code',
|
||||
query,
|
||||
),
|
||||
[Op.and]: [
|
||||
where,
|
||||
{
|
||||
[Op.or]: [
|
||||
{ ['id']: Utils.uuid(query) },
|
||||
Utils.ilike(
|
||||
'booking_requests',
|
||||
'request_code',
|
||||
query,
|
||||
),
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@ -1,14 +1,36 @@
|
||||
|
||||
const db = require('../models');
|
||||
const FileDBApi = require('./file');
|
||||
const crypto = require('crypto');
|
||||
const Utils = require('../utils');
|
||||
|
||||
const config = require('../../config');
|
||||
|
||||
|
||||
const Sequelize = db.Sequelize;
|
||||
const Op = Sequelize.Op;
|
||||
|
||||
function isCustomerUser(currentUser) {
|
||||
return currentUser?.app_role?.name === config.roles.customer;
|
||||
}
|
||||
|
||||
async function customerCanAccessReservation(reservation, currentUser, transaction) {
|
||||
if (!reservation || !isCustomerUser(currentUser)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!reservation.booking_requestId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const bookingRequest = await db.booking_requests.findOne({
|
||||
where: {
|
||||
id: reservation.booking_requestId,
|
||||
requested_byId: currentUser.id,
|
||||
},
|
||||
transaction,
|
||||
});
|
||||
|
||||
return Boolean(bookingRequest);
|
||||
}
|
||||
|
||||
module.exports = class ReservationsDBApi {
|
||||
|
||||
|
||||
@ -442,16 +464,18 @@ module.exports = class ReservationsDBApi {
|
||||
|
||||
static async findBy(where, options) {
|
||||
const transaction = (options && options.transaction) || undefined;
|
||||
const currentUser = (options && options.currentUser) || null;
|
||||
|
||||
const reservations = await db.reservations.findOne(
|
||||
{ where },
|
||||
{ transaction },
|
||||
);
|
||||
const reservations = await db.reservations.findOne({ where, transaction });
|
||||
|
||||
if (!reservations) {
|
||||
return reservations;
|
||||
}
|
||||
|
||||
if (!(await customerCanAccessReservation(reservations, currentUser, transaction))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const output = reservations.get({plain: true});
|
||||
|
||||
|
||||
@ -584,6 +608,7 @@ module.exports = class ReservationsDBApi {
|
||||
|
||||
const user = (options && options.currentUser) || null;
|
||||
const userOrganizations = (user && user.organizations?.id) || null;
|
||||
const isCustomer = isCustomerUser(user);
|
||||
|
||||
|
||||
|
||||
@ -596,9 +621,6 @@ module.exports = class ReservationsDBApi {
|
||||
|
||||
offset = currentPage * limit;
|
||||
|
||||
const orderBy = null;
|
||||
|
||||
const transaction = (options && options.transaction) || undefined;
|
||||
|
||||
let include = [
|
||||
|
||||
@ -628,17 +650,20 @@ module.exports = class ReservationsDBApi {
|
||||
{
|
||||
model: db.booking_requests,
|
||||
as: 'booking_request',
|
||||
|
||||
where: filter.booking_request ? {
|
||||
[Op.or]: [
|
||||
{ id: { [Op.in]: filter.booking_request.split('|').map(term => Utils.uuid(term)) } },
|
||||
{
|
||||
request_code: {
|
||||
[Op.or]: filter.booking_request.split('|').map(term => ({ [Op.iLike]: `%${term}%` }))
|
||||
}
|
||||
},
|
||||
]
|
||||
} : {},
|
||||
required: isCustomer || Boolean(filter.booking_request),
|
||||
where: {
|
||||
...(isCustomer ? { requested_byId: user.id } : {}),
|
||||
...(filter.booking_request ? {
|
||||
[Op.or]: [
|
||||
{ id: { [Op.in]: filter.booking_request.split('|').map(term => Utils.uuid(term)) } },
|
||||
{
|
||||
request_code: {
|
||||
[Op.or]: filter.booking_request.split('|').map(term => ({ [Op.iLike]: `%${term}%` }))
|
||||
}
|
||||
},
|
||||
]
|
||||
} : {}),
|
||||
},
|
||||
|
||||
},
|
||||
|
||||
@ -1204,8 +1229,9 @@ module.exports = class ReservationsDBApi {
|
||||
}
|
||||
}
|
||||
|
||||
static async findAllAutocomplete(query, limit, offset, globalAccess, organizationId,) {
|
||||
static async findAllAutocomplete(query, limit, offset, globalAccess, organizationId, currentUser) {
|
||||
let where = {};
|
||||
const include = [];
|
||||
|
||||
|
||||
if (!globalAccess && organizationId) {
|
||||
@ -1215,20 +1241,35 @@ module.exports = class ReservationsDBApi {
|
||||
|
||||
if (query) {
|
||||
where = {
|
||||
[Op.or]: [
|
||||
{ ['id']: Utils.uuid(query) },
|
||||
Utils.ilike(
|
||||
'reservations',
|
||||
'reservation_code',
|
||||
query,
|
||||
),
|
||||
[Op.and]: [
|
||||
where,
|
||||
{
|
||||
[Op.or]: [
|
||||
{ ['id']: Utils.uuid(query) },
|
||||
Utils.ilike(
|
||||
'reservations',
|
||||
'reservation_code',
|
||||
query,
|
||||
),
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
if (isCustomerUser(currentUser)) {
|
||||
include.push({
|
||||
model: db.booking_requests,
|
||||
as: 'booking_request',
|
||||
required: true,
|
||||
where: { requested_byId: currentUser.id },
|
||||
});
|
||||
}
|
||||
|
||||
const records = await db.reservations.findAll({
|
||||
attributes: [ 'id', 'reservation_code' ],
|
||||
where,
|
||||
include,
|
||||
limit: limit ? Number(limit) : undefined,
|
||||
offset: offset ? Number(offset) : undefined,
|
||||
orderBy: [['reservation_code', 'ASC']],
|
||||
|
||||
@ -1,7 +1,5 @@
|
||||
|
||||
const db = require('../models');
|
||||
const FileDBApi = require('./file');
|
||||
const crypto = require('crypto');
|
||||
const Utils = require('../utils');
|
||||
|
||||
|
||||
@ -11,6 +9,34 @@ const config = require('../../config');
|
||||
const Sequelize = db.Sequelize;
|
||||
const Op = Sequelize.Op;
|
||||
|
||||
const BUSINESS_ROLE_NAMES = [
|
||||
config.roles.super_admin,
|
||||
config.roles.admin,
|
||||
config.roles.concierge,
|
||||
config.roles.customer,
|
||||
];
|
||||
|
||||
function appendRoleVisibilityScope(where, globalAccess, businessOnly = false) {
|
||||
const scopes = [];
|
||||
|
||||
if (!globalAccess) {
|
||||
scopes.push({ name: { [Op.ne]: config.roles.super_admin } });
|
||||
}
|
||||
|
||||
if (businessOnly) {
|
||||
scopes.push({ name: { [Op.in]: BUSINESS_ROLE_NAMES } });
|
||||
}
|
||||
|
||||
if (!scopes.length) {
|
||||
return where;
|
||||
}
|
||||
|
||||
return {
|
||||
[Op.and]: [where, ...scopes],
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
module.exports = class RolesDBApi {
|
||||
|
||||
|
||||
@ -102,8 +128,6 @@ module.exports = class RolesDBApi {
|
||||
static async update(id, data, options) {
|
||||
const currentUser = (options && options.currentUser) || {id: null};
|
||||
const transaction = (options && options.transaction) || undefined;
|
||||
const globalAccess = currentUser.app_role?.globalAccess;
|
||||
|
||||
const roles = await db.roles.findByPk(id, {}, {transaction});
|
||||
|
||||
|
||||
@ -259,17 +283,8 @@ module.exports = class RolesDBApi {
|
||||
const currentPage = +filter.page;
|
||||
|
||||
|
||||
const user = (options && options.currentUser) || null;
|
||||
const userOrganizations = (user && user.organizations?.id) || null;
|
||||
|
||||
|
||||
|
||||
|
||||
offset = currentPage * limit;
|
||||
|
||||
const orderBy = null;
|
||||
|
||||
const transaction = (options && options.transaction) || undefined;
|
||||
|
||||
let include = [
|
||||
|
||||
@ -387,9 +402,9 @@ module.exports = class RolesDBApi {
|
||||
}
|
||||
}
|
||||
|
||||
if (!globalAccess) {
|
||||
where = { name: { [Op.ne]: config.roles.super_admin } };
|
||||
}
|
||||
const businessOnly = filter.businessOnly === true || filter.businessOnly === 'true';
|
||||
|
||||
where = appendRoleVisibilityScope(where, globalAccess, businessOnly);
|
||||
|
||||
|
||||
|
||||
@ -423,14 +438,8 @@ module.exports = class RolesDBApi {
|
||||
}
|
||||
}
|
||||
|
||||
static async findAllAutocomplete(query, limit, offset, globalAccess,) {
|
||||
static async findAllAutocomplete(query, limit, offset, globalAccess, businessOnly = false) {
|
||||
let where = {};
|
||||
|
||||
if (!globalAccess) {
|
||||
where = { name: { [Op.ne]: config.roles.super_admin } };
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (query) {
|
||||
where = {
|
||||
@ -445,6 +454,8 @@ module.exports = class RolesDBApi {
|
||||
};
|
||||
}
|
||||
|
||||
where = appendRoleVisibilityScope(where, globalAccess, businessOnly === true || businessOnly === 'true');
|
||||
|
||||
const records = await db.roles.findAll({
|
||||
attributes: [ 'id', 'name' ],
|
||||
where,
|
||||
|
||||
@ -1,14 +1,26 @@
|
||||
|
||||
const db = require('../models');
|
||||
const FileDBApi = require('./file');
|
||||
const crypto = require('crypto');
|
||||
const Utils = require('../utils');
|
||||
|
||||
const config = require('../../config');
|
||||
|
||||
|
||||
const Sequelize = db.Sequelize;
|
||||
const Op = Sequelize.Op;
|
||||
|
||||
function isCustomerUser(currentUser) {
|
||||
return currentUser?.app_role?.name === config.roles.customer;
|
||||
}
|
||||
|
||||
function mergeWhereWithScope(where, scope) {
|
||||
if (!scope) {
|
||||
return where;
|
||||
}
|
||||
|
||||
return {
|
||||
[Op.and]: [where, scope],
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = class Service_requestsDBApi {
|
||||
|
||||
|
||||
@ -92,7 +104,7 @@ module.exports = class Service_requestsDBApi {
|
||||
transaction,
|
||||
});
|
||||
|
||||
await service_requests.setRequested_by( data.requested_by || null, {
|
||||
await service_requests.setRequested_by( isCustomerUser(currentUser) ? currentUser.id : data.requested_by || null, {
|
||||
transaction,
|
||||
});
|
||||
|
||||
@ -202,9 +214,10 @@ module.exports = class Service_requestsDBApi {
|
||||
static async update(id, data, options) {
|
||||
const currentUser = (options && options.currentUser) || {id: null};
|
||||
const transaction = (options && options.transaction) || undefined;
|
||||
const globalAccess = currentUser.app_role?.globalAccess;
|
||||
|
||||
const service_requests = await db.service_requests.findByPk(id, {}, {transaction});
|
||||
const service_requests = await db.service_requests.findOne({
|
||||
where: mergeWhereWithScope({ id }, isCustomerUser(currentUser) ? { requested_byId: currentUser.id } : null),
|
||||
transaction,
|
||||
});
|
||||
|
||||
|
||||
|
||||
@ -271,7 +284,7 @@ module.exports = class Service_requestsDBApi {
|
||||
if (data.requested_by !== undefined) {
|
||||
await service_requests.setRequested_by(
|
||||
|
||||
data.requested_by,
|
||||
isCustomerUser(currentUser) ? currentUser.id : data.requested_by,
|
||||
|
||||
{ transaction }
|
||||
);
|
||||
@ -317,11 +330,11 @@ module.exports = class Service_requestsDBApi {
|
||||
const transaction = (options && options.transaction) || undefined;
|
||||
|
||||
const service_requests = await db.service_requests.findAll({
|
||||
where: {
|
||||
where: mergeWhereWithScope({
|
||||
id: {
|
||||
[Op.in]: ids,
|
||||
},
|
||||
},
|
||||
}, isCustomerUser(currentUser) ? { requested_byId: currentUser.id } : null),
|
||||
transaction,
|
||||
});
|
||||
|
||||
@ -345,7 +358,14 @@ module.exports = class Service_requestsDBApi {
|
||||
const currentUser = (options && options.currentUser) || {id: null};
|
||||
const transaction = (options && options.transaction) || undefined;
|
||||
|
||||
const service_requests = await db.service_requests.findByPk(id, options);
|
||||
const service_requests = await db.service_requests.findOne({
|
||||
where: mergeWhereWithScope({ id }, isCustomerUser(currentUser) ? { requested_byId: currentUser.id } : null),
|
||||
transaction,
|
||||
});
|
||||
|
||||
if (!service_requests) {
|
||||
return null;
|
||||
}
|
||||
|
||||
await service_requests.update({
|
||||
deletedBy: currentUser.id
|
||||
@ -362,11 +382,12 @@ module.exports = class Service_requestsDBApi {
|
||||
|
||||
static async findBy(where, options) {
|
||||
const transaction = (options && options.transaction) || undefined;
|
||||
const currentUser = (options && options.currentUser) || null;
|
||||
|
||||
const service_requests = await db.service_requests.findOne(
|
||||
{ where },
|
||||
{ transaction },
|
||||
);
|
||||
const service_requests = await db.service_requests.findOne({
|
||||
where: mergeWhereWithScope(where, isCustomerUser(currentUser) ? { requested_byId: currentUser.id } : null),
|
||||
transaction,
|
||||
});
|
||||
|
||||
if (!service_requests) {
|
||||
return service_requests;
|
||||
@ -464,6 +485,7 @@ module.exports = class Service_requestsDBApi {
|
||||
|
||||
const user = (options && options.currentUser) || null;
|
||||
const userOrganizations = (user && user.organizations?.id) || null;
|
||||
const customerScope = isCustomerUser(user) ? { requested_byId: user.id } : null;
|
||||
|
||||
|
||||
|
||||
@ -474,11 +496,12 @@ module.exports = class Service_requestsDBApi {
|
||||
}
|
||||
|
||||
|
||||
if (customerScope) {
|
||||
where.requested_byId = user.id;
|
||||
}
|
||||
|
||||
offset = currentPage * limit;
|
||||
|
||||
const orderBy = null;
|
||||
|
||||
const transaction = (options && options.transaction) || undefined;
|
||||
|
||||
let include = [
|
||||
|
||||
@ -901,8 +924,12 @@ module.exports = class Service_requestsDBApi {
|
||||
}
|
||||
}
|
||||
|
||||
static async findAllAutocomplete(query, limit, offset, globalAccess, organizationId,) {
|
||||
static async findAllAutocomplete(query, limit, offset, globalAccess, organizationId, currentUser) {
|
||||
let where = {};
|
||||
|
||||
if (isCustomerUser(currentUser)) {
|
||||
where.requested_byId = currentUser.id;
|
||||
}
|
||||
|
||||
|
||||
if (!globalAccess && organizationId) {
|
||||
@ -912,13 +939,18 @@ module.exports = class Service_requestsDBApi {
|
||||
|
||||
if (query) {
|
||||
where = {
|
||||
[Op.or]: [
|
||||
{ ['id']: Utils.uuid(query) },
|
||||
Utils.ilike(
|
||||
'service_requests',
|
||||
'summary',
|
||||
query,
|
||||
),
|
||||
[Op.and]: [
|
||||
where,
|
||||
{
|
||||
[Op.or]: [
|
||||
{ ['id']: Utils.uuid(query) },
|
||||
Utils.ilike(
|
||||
'service_requests',
|
||||
'summary',
|
||||
query,
|
||||
),
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@ -134,58 +134,52 @@ notes: {
|
||||
|
||||
invoices.associate = (db) => {
|
||||
|
||||
db.invoices.belongsToMany(db.invoice_line_items, {
|
||||
db.invoices.hasMany(db.invoice_line_items, {
|
||||
as: 'line_items',
|
||||
foreignKey: {
|
||||
name: 'invoices_line_itemsId',
|
||||
name: 'invoiceId',
|
||||
},
|
||||
constraints: false,
|
||||
through: 'invoicesLine_itemsInvoice_line_items',
|
||||
});
|
||||
|
||||
db.invoices.belongsToMany(db.invoice_line_items, {
|
||||
db.invoices.hasMany(db.invoice_line_items, {
|
||||
as: 'line_items_filter',
|
||||
foreignKey: {
|
||||
name: 'invoices_line_itemsId',
|
||||
name: 'invoiceId',
|
||||
},
|
||||
constraints: false,
|
||||
through: 'invoicesLine_itemsInvoice_line_items',
|
||||
});
|
||||
|
||||
db.invoices.belongsToMany(db.payments, {
|
||||
db.invoices.hasMany(db.payments, {
|
||||
as: 'payments',
|
||||
foreignKey: {
|
||||
name: 'invoices_paymentsId',
|
||||
name: 'invoiceId',
|
||||
},
|
||||
constraints: false,
|
||||
through: 'invoicesPaymentsPayments',
|
||||
});
|
||||
|
||||
db.invoices.belongsToMany(db.payments, {
|
||||
db.invoices.hasMany(db.payments, {
|
||||
as: 'payments_filter',
|
||||
foreignKey: {
|
||||
name: 'invoices_paymentsId',
|
||||
name: 'invoiceId',
|
||||
},
|
||||
constraints: false,
|
||||
through: 'invoicesPaymentsPayments',
|
||||
});
|
||||
|
||||
db.invoices.belongsToMany(db.documents, {
|
||||
db.invoices.hasMany(db.documents, {
|
||||
as: 'documents',
|
||||
foreignKey: {
|
||||
name: 'invoices_documentsId',
|
||||
name: 'invoiceId',
|
||||
},
|
||||
constraints: false,
|
||||
through: 'invoicesDocumentsDocuments',
|
||||
});
|
||||
|
||||
db.invoices.belongsToMany(db.documents, {
|
||||
db.invoices.hasMany(db.documents, {
|
||||
as: 'documents_filter',
|
||||
foreignKey: {
|
||||
name: 'invoices_documentsId',
|
||||
name: 'invoiceId',
|
||||
},
|
||||
constraints: false,
|
||||
through: 'invoicesDocumentsDocuments',
|
||||
});
|
||||
|
||||
|
||||
|
||||
@ -1,9 +1,3 @@
|
||||
const config = require('../../config');
|
||||
const providers = config.providers;
|
||||
const crypto = require('crypto');
|
||||
const bcrypt = require('bcrypt');
|
||||
const moment = require('moment');
|
||||
|
||||
module.exports = function(sequelize, DataTypes) {
|
||||
const reservations = sequelize.define(
|
||||
'reservations',
|
||||
@ -154,94 +148,84 @@ external_notes: {
|
||||
|
||||
reservations.associate = (db) => {
|
||||
|
||||
db.reservations.belongsToMany(db.reservation_guests, {
|
||||
db.reservations.hasMany(db.reservation_guests, {
|
||||
as: 'guests',
|
||||
foreignKey: {
|
||||
name: 'reservations_guestsId',
|
||||
name: 'reservationId',
|
||||
},
|
||||
constraints: false,
|
||||
through: 'reservationsGuestsReservation_guests',
|
||||
});
|
||||
|
||||
db.reservations.belongsToMany(db.reservation_guests, {
|
||||
db.reservations.hasMany(db.reservation_guests, {
|
||||
as: 'guests_filter',
|
||||
foreignKey: {
|
||||
name: 'reservations_guestsId',
|
||||
name: 'reservationId',
|
||||
},
|
||||
constraints: false,
|
||||
through: 'reservationsGuestsReservation_guests',
|
||||
});
|
||||
|
||||
db.reservations.belongsToMany(db.service_requests, {
|
||||
db.reservations.hasMany(db.service_requests, {
|
||||
as: 'service_requests',
|
||||
foreignKey: {
|
||||
name: 'reservations_service_requestsId',
|
||||
name: 'reservationId',
|
||||
},
|
||||
constraints: false,
|
||||
through: 'reservationsService_requestsService_requests',
|
||||
});
|
||||
|
||||
db.reservations.belongsToMany(db.service_requests, {
|
||||
db.reservations.hasMany(db.service_requests, {
|
||||
as: 'service_requests_filter',
|
||||
foreignKey: {
|
||||
name: 'reservations_service_requestsId',
|
||||
name: 'reservationId',
|
||||
},
|
||||
constraints: false,
|
||||
through: 'reservationsService_requestsService_requests',
|
||||
});
|
||||
|
||||
db.reservations.belongsToMany(db.invoices, {
|
||||
db.reservations.hasMany(db.invoices, {
|
||||
as: 'invoices',
|
||||
foreignKey: {
|
||||
name: 'reservations_invoicesId',
|
||||
name: 'reservationId',
|
||||
},
|
||||
constraints: false,
|
||||
through: 'reservationsInvoicesInvoices',
|
||||
});
|
||||
|
||||
db.reservations.belongsToMany(db.invoices, {
|
||||
db.reservations.hasMany(db.invoices, {
|
||||
as: 'invoices_filter',
|
||||
foreignKey: {
|
||||
name: 'reservations_invoicesId',
|
||||
name: 'reservationId',
|
||||
},
|
||||
constraints: false,
|
||||
through: 'reservationsInvoicesInvoices',
|
||||
});
|
||||
|
||||
db.reservations.belongsToMany(db.documents, {
|
||||
db.reservations.hasMany(db.documents, {
|
||||
as: 'documents',
|
||||
foreignKey: {
|
||||
name: 'reservations_documentsId',
|
||||
name: 'reservationId',
|
||||
},
|
||||
constraints: false,
|
||||
through: 'reservationsDocumentsDocuments',
|
||||
});
|
||||
|
||||
db.reservations.belongsToMany(db.documents, {
|
||||
db.reservations.hasMany(db.documents, {
|
||||
as: 'documents_filter',
|
||||
foreignKey: {
|
||||
name: 'reservations_documentsId',
|
||||
name: 'reservationId',
|
||||
},
|
||||
constraints: false,
|
||||
through: 'reservationsDocumentsDocuments',
|
||||
});
|
||||
|
||||
db.reservations.belongsToMany(db.activity_comments, {
|
||||
db.reservations.hasMany(db.activity_comments, {
|
||||
as: 'comments',
|
||||
foreignKey: {
|
||||
name: 'reservations_commentsId',
|
||||
name: 'reservationId',
|
||||
},
|
||||
constraints: false,
|
||||
through: 'reservationsCommentsActivity_comments',
|
||||
});
|
||||
|
||||
db.reservations.belongsToMany(db.activity_comments, {
|
||||
db.reservations.hasMany(db.activity_comments, {
|
||||
as: 'comments_filter',
|
||||
foreignKey: {
|
||||
name: 'reservations_commentsId',
|
||||
name: 'reservationId',
|
||||
},
|
||||
constraints: false,
|
||||
through: 'reservationsCommentsActivity_comments',
|
||||
});
|
||||
|
||||
|
||||
|
||||
@ -1,9 +1,3 @@
|
||||
const config = require('../../config');
|
||||
const providers = config.providers;
|
||||
const crypto = require('crypto');
|
||||
const bcrypt = require('bcrypt');
|
||||
const moment = require('moment');
|
||||
|
||||
module.exports = function(sequelize, DataTypes) {
|
||||
const service_requests = sequelize.define(
|
||||
'service_requests',
|
||||
@ -172,40 +166,36 @@ currency: {
|
||||
|
||||
service_requests.associate = (db) => {
|
||||
|
||||
db.service_requests.belongsToMany(db.documents, {
|
||||
db.service_requests.hasMany(db.documents, {
|
||||
as: 'documents',
|
||||
foreignKey: {
|
||||
name: 'service_requests_documentsId',
|
||||
name: 'service_requestId',
|
||||
},
|
||||
constraints: false,
|
||||
through: 'service_requestsDocumentsDocuments',
|
||||
});
|
||||
|
||||
db.service_requests.belongsToMany(db.documents, {
|
||||
db.service_requests.hasMany(db.documents, {
|
||||
as: 'documents_filter',
|
||||
foreignKey: {
|
||||
name: 'service_requests_documentsId',
|
||||
name: 'service_requestId',
|
||||
},
|
||||
constraints: false,
|
||||
through: 'service_requestsDocumentsDocuments',
|
||||
});
|
||||
|
||||
db.service_requests.belongsToMany(db.activity_comments, {
|
||||
db.service_requests.hasMany(db.activity_comments, {
|
||||
as: 'comments',
|
||||
foreignKey: {
|
||||
name: 'service_requests_commentsId',
|
||||
name: 'service_requestId',
|
||||
},
|
||||
constraints: false,
|
||||
through: 'service_requestsCommentsActivity_comments',
|
||||
});
|
||||
|
||||
db.service_requests.belongsToMany(db.activity_comments, {
|
||||
db.service_requests.hasMany(db.activity_comments, {
|
||||
as: 'comments_filter',
|
||||
foreignKey: {
|
||||
name: 'service_requests_commentsId',
|
||||
name: 'service_requestId',
|
||||
},
|
||||
constraints: false,
|
||||
through: 'service_requestsCommentsActivity_comments',
|
||||
});
|
||||
|
||||
|
||||
|
||||
@ -1,9 +1,3 @@
|
||||
const config = require('../../config');
|
||||
const providers = config.providers;
|
||||
const crypto = require('crypto');
|
||||
const bcrypt = require('bcrypt');
|
||||
const moment = require('moment');
|
||||
|
||||
module.exports = function(sequelize, DataTypes) {
|
||||
const units = sequelize.define(
|
||||
'units',
|
||||
@ -85,22 +79,20 @@ notes: {
|
||||
|
||||
units.associate = (db) => {
|
||||
|
||||
db.units.belongsToMany(db.unit_availability_blocks, {
|
||||
db.units.hasMany(db.unit_availability_blocks, {
|
||||
as: 'availability_blocks',
|
||||
foreignKey: {
|
||||
name: 'units_availability_blocksId',
|
||||
name: 'unitId',
|
||||
},
|
||||
constraints: false,
|
||||
through: 'unitsAvailability_blocksUnit_availability_blocks',
|
||||
});
|
||||
|
||||
db.units.belongsToMany(db.unit_availability_blocks, {
|
||||
db.units.hasMany(db.unit_availability_blocks, {
|
||||
as: 'availability_blocks_filter',
|
||||
foreignKey: {
|
||||
name: 'units_availability_blocksId',
|
||||
name: 'unitId',
|
||||
},
|
||||
constraints: false,
|
||||
through: 'unitsAvailability_blocksUnit_availability_blocks',
|
||||
});
|
||||
|
||||
|
||||
|
||||
@ -0,0 +1,244 @@
|
||||
'use strict';
|
||||
|
||||
const { v4: uuid } = require('uuid');
|
||||
const { QueryTypes } = require('sequelize');
|
||||
|
||||
const BUSINESS_ROLES = {
|
||||
SUPER_ADMIN: 'Super Administrator',
|
||||
ADMIN: 'Administrator',
|
||||
CONCIERGE: 'Concierge Coordinator',
|
||||
CUSTOMER: 'Customer',
|
||||
PLATFORM_OWNER: 'Platform Owner',
|
||||
OPERATIONS_DIRECTOR: 'Operations Director',
|
||||
RESERVATIONS_LEAD: 'Reservations Lead',
|
||||
FINANCE_CONTROLLER: 'Finance Controller',
|
||||
};
|
||||
|
||||
const rolePermissionMatrix = {
|
||||
[BUSINESS_ROLES.ADMIN]: [
|
||||
'CREATE_BOOKING_REQUESTS',
|
||||
'READ_BOOKING_REQUESTS',
|
||||
'UPDATE_BOOKING_REQUESTS',
|
||||
'READ_APPROVAL_STEPS',
|
||||
'UPDATE_APPROVAL_STEPS',
|
||||
'CREATE_RESERVATIONS',
|
||||
'READ_RESERVATIONS',
|
||||
'UPDATE_RESERVATIONS',
|
||||
'CREATE_SERVICE_REQUESTS',
|
||||
'READ_SERVICE_REQUESTS',
|
||||
'UPDATE_SERVICE_REQUESTS',
|
||||
'READ_INVOICES',
|
||||
'CREATE_DOCUMENTS',
|
||||
'READ_DOCUMENTS',
|
||||
'UPDATE_DOCUMENTS',
|
||||
'READ_PROPERTIES',
|
||||
'READ_UNITS',
|
||||
'READ_NEGOTIATED_RATES',
|
||||
],
|
||||
[BUSINESS_ROLES.CONCIERGE]: [
|
||||
'CREATE_BOOKING_REQUESTS',
|
||||
'READ_BOOKING_REQUESTS',
|
||||
'UPDATE_BOOKING_REQUESTS',
|
||||
'READ_RESERVATIONS',
|
||||
'CREATE_SERVICE_REQUESTS',
|
||||
'READ_SERVICE_REQUESTS',
|
||||
'UPDATE_SERVICE_REQUESTS',
|
||||
'CREATE_DOCUMENTS',
|
||||
'READ_DOCUMENTS',
|
||||
'UPDATE_DOCUMENTS',
|
||||
'READ_PROPERTIES',
|
||||
'READ_UNITS',
|
||||
],
|
||||
[BUSINESS_ROLES.CUSTOMER]: [
|
||||
'CREATE_BOOKING_REQUESTS',
|
||||
'READ_BOOKING_REQUESTS',
|
||||
'UPDATE_BOOKING_REQUESTS',
|
||||
'READ_RESERVATIONS',
|
||||
'CREATE_SERVICE_REQUESTS',
|
||||
'READ_SERVICE_REQUESTS',
|
||||
'UPDATE_SERVICE_REQUESTS',
|
||||
'READ_DOCUMENTS',
|
||||
],
|
||||
[BUSINESS_ROLES.PLATFORM_OWNER]: [
|
||||
'CREATE_BOOKING_REQUESTS',
|
||||
'READ_BOOKING_REQUESTS',
|
||||
'UPDATE_BOOKING_REQUESTS',
|
||||
'READ_APPROVAL_STEPS',
|
||||
'UPDATE_APPROVAL_STEPS',
|
||||
'CREATE_RESERVATIONS',
|
||||
'READ_RESERVATIONS',
|
||||
'UPDATE_RESERVATIONS',
|
||||
'CREATE_SERVICE_REQUESTS',
|
||||
'READ_SERVICE_REQUESTS',
|
||||
'UPDATE_SERVICE_REQUESTS',
|
||||
'READ_INVOICES',
|
||||
'CREATE_DOCUMENTS',
|
||||
'READ_DOCUMENTS',
|
||||
'UPDATE_DOCUMENTS',
|
||||
'READ_ORGANIZATIONS',
|
||||
'READ_PROPERTIES',
|
||||
'READ_UNITS',
|
||||
'READ_NEGOTIATED_RATES',
|
||||
],
|
||||
[BUSINESS_ROLES.OPERATIONS_DIRECTOR]: [
|
||||
'CREATE_BOOKING_REQUESTS',
|
||||
'READ_BOOKING_REQUESTS',
|
||||
'UPDATE_BOOKING_REQUESTS',
|
||||
'READ_APPROVAL_STEPS',
|
||||
'UPDATE_APPROVAL_STEPS',
|
||||
'CREATE_RESERVATIONS',
|
||||
'READ_RESERVATIONS',
|
||||
'UPDATE_RESERVATIONS',
|
||||
'CREATE_SERVICE_REQUESTS',
|
||||
'READ_SERVICE_REQUESTS',
|
||||
'UPDATE_SERVICE_REQUESTS',
|
||||
'CREATE_DOCUMENTS',
|
||||
'READ_DOCUMENTS',
|
||||
'UPDATE_DOCUMENTS',
|
||||
'READ_PROPERTIES',
|
||||
'READ_UNITS',
|
||||
'READ_NEGOTIATED_RATES',
|
||||
],
|
||||
[BUSINESS_ROLES.RESERVATIONS_LEAD]: [
|
||||
'CREATE_BOOKING_REQUESTS',
|
||||
'READ_BOOKING_REQUESTS',
|
||||
'UPDATE_BOOKING_REQUESTS',
|
||||
'READ_APPROVAL_STEPS',
|
||||
'UPDATE_APPROVAL_STEPS',
|
||||
'CREATE_RESERVATIONS',
|
||||
'READ_RESERVATIONS',
|
||||
'UPDATE_RESERVATIONS',
|
||||
'CREATE_SERVICE_REQUESTS',
|
||||
'READ_SERVICE_REQUESTS',
|
||||
'UPDATE_SERVICE_REQUESTS',
|
||||
'READ_DOCUMENTS',
|
||||
'READ_PROPERTIES',
|
||||
'READ_UNITS',
|
||||
'READ_NEGOTIATED_RATES',
|
||||
],
|
||||
[BUSINESS_ROLES.FINANCE_CONTROLLER]: [
|
||||
'READ_BOOKING_REQUESTS',
|
||||
'READ_RESERVATIONS',
|
||||
'READ_INVOICES',
|
||||
'UPDATE_INVOICES',
|
||||
'READ_DOCUMENTS',
|
||||
],
|
||||
};
|
||||
|
||||
async function findRoleByName(queryInterface, name) {
|
||||
const rows = await queryInterface.sequelize.query(
|
||||
'SELECT "id", "name" FROM "roles" WHERE "name" = :name LIMIT 1',
|
||||
{
|
||||
replacements: { name },
|
||||
type: QueryTypes.SELECT,
|
||||
},
|
||||
);
|
||||
|
||||
return rows[0] || null;
|
||||
}
|
||||
|
||||
async function ensureRole(queryInterface, name, now) {
|
||||
const existingRole = await findRoleByName(queryInterface, name);
|
||||
|
||||
if (existingRole) {
|
||||
return existingRole.id;
|
||||
}
|
||||
|
||||
const id = uuid();
|
||||
await queryInterface.bulkInsert('roles', [
|
||||
{
|
||||
id,
|
||||
name,
|
||||
globalAccess: false,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
},
|
||||
]);
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
async function getPermissionIdMap(queryInterface, permissionNames) {
|
||||
const permissions = await queryInterface.sequelize.query(
|
||||
'SELECT "id", "name" FROM "permissions" WHERE "name" IN (:permissionNames)',
|
||||
{
|
||||
replacements: { permissionNames },
|
||||
type: QueryTypes.SELECT,
|
||||
},
|
||||
);
|
||||
|
||||
return new Map(permissions.map((permission) => [permission.name, permission.id]));
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
async up(queryInterface) {
|
||||
const now = new Date();
|
||||
|
||||
const customerRoleId = await ensureRole(queryInterface, BUSINESS_ROLES.CUSTOMER, now);
|
||||
|
||||
await queryInterface.bulkUpdate('roles', { globalAccess: true, updatedAt: now }, { name: BUSINESS_ROLES.SUPER_ADMIN });
|
||||
await queryInterface.sequelize.query(
|
||||
'UPDATE "roles" SET "globalAccess" = false, "updatedAt" = :updatedAt WHERE "name" IN (:roleNames)',
|
||||
{
|
||||
replacements: {
|
||||
updatedAt: now,
|
||||
roleNames: [
|
||||
BUSINESS_ROLES.ADMIN,
|
||||
BUSINESS_ROLES.CONCIERGE,
|
||||
BUSINESS_ROLES.CUSTOMER,
|
||||
BUSINESS_ROLES.PLATFORM_OWNER,
|
||||
BUSINESS_ROLES.OPERATIONS_DIRECTOR,
|
||||
BUSINESS_ROLES.RESERVATIONS_LEAD,
|
||||
BUSINESS_ROLES.FINANCE_CONTROLLER,
|
||||
],
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const roleIds = {};
|
||||
for (const roleName of Object.keys(rolePermissionMatrix)) {
|
||||
const role = await findRoleByName(queryInterface, roleName);
|
||||
if (!role) {
|
||||
throw new Error(`Role '${roleName}' was not found while aligning the business role matrix.`);
|
||||
}
|
||||
roleIds[roleName] = role.id;
|
||||
}
|
||||
roleIds[BUSINESS_ROLES.CUSTOMER] = customerRoleId;
|
||||
|
||||
const permissionNames = [...new Set(Object.values(rolePermissionMatrix).flat())];
|
||||
const permissionIdMap = await getPermissionIdMap(queryInterface, permissionNames);
|
||||
const missingPermissions = permissionNames.filter((permissionName) => !permissionIdMap.get(permissionName));
|
||||
|
||||
if (missingPermissions.length > 0) {
|
||||
throw new Error(`Missing permissions for role matrix alignment: ${missingPermissions.join(', ')}`);
|
||||
}
|
||||
|
||||
const impactedRoleIds = Object.values(roleIds);
|
||||
await queryInterface.sequelize.query(
|
||||
'DELETE FROM "rolesPermissionsPermissions" WHERE "roles_permissionsId" IN (:roleIds)',
|
||||
{
|
||||
replacements: { roleIds: impactedRoleIds },
|
||||
},
|
||||
);
|
||||
|
||||
const rows = Object.entries(rolePermissionMatrix).flatMap(([roleName, permissions]) =>
|
||||
permissions.map((permissionName) => ({
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
roles_permissionsId: roleIds[roleName],
|
||||
permissionId: permissionIdMap.get(permissionName),
|
||||
})),
|
||||
);
|
||||
|
||||
if (rows.length > 0) {
|
||||
await queryInterface.bulkInsert('rolesPermissionsPermissions', rows);
|
||||
}
|
||||
|
||||
await queryInterface.bulkUpdate('users', { app_roleId: customerRoleId, updatedAt: now }, { email: 'client@hello.com' });
|
||||
await queryInterface.bulkUpdate('users', { app_roleId: roleIds[BUSINESS_ROLES.CONCIERGE], updatedAt: now }, { email: 'john@doe.com' });
|
||||
},
|
||||
|
||||
async down() {
|
||||
// Intentionally left blank. This seeder aligns live business roles and should not blindly revert production data.
|
||||
},
|
||||
};
|
||||
@ -5,7 +5,6 @@ const Booking_requestsService = require('../services/booking_requests');
|
||||
const Booking_requestsDBApi = require('../db/api/booking_requests');
|
||||
const wrapAsync = require('../helpers').wrapAsync;
|
||||
|
||||
const config = require('../config');
|
||||
|
||||
|
||||
const router = express.Router();
|
||||
@ -409,7 +408,7 @@ router.get('/autocomplete', async (req, res) => {
|
||||
req.query.query,
|
||||
req.query.limit,
|
||||
req.query.offset,
|
||||
globalAccess, organizationId,
|
||||
globalAccess, organizationId, req.currentUser,
|
||||
);
|
||||
|
||||
res.status(200).send(payload);
|
||||
@ -450,9 +449,12 @@ router.get('/autocomplete', async (req, res) => {
|
||||
router.get('/:id', wrapAsync(async (req, res) => {
|
||||
const payload = await Booking_requestsDBApi.findBy(
|
||||
{ id: req.params.id },
|
||||
{ currentUser: req.currentUser },
|
||||
);
|
||||
|
||||
|
||||
|
||||
if (!payload) {
|
||||
return res.status(404).send('Not found');
|
||||
}
|
||||
|
||||
res.status(200).send(payload);
|
||||
}));
|
||||
|
||||
@ -5,7 +5,6 @@ const ReservationsService = require('../services/reservations');
|
||||
const ReservationsDBApi = require('../db/api/reservations');
|
||||
const wrapAsync = require('../helpers').wrapAsync;
|
||||
|
||||
const config = require('../config');
|
||||
|
||||
|
||||
const router = express.Router();
|
||||
@ -406,7 +405,7 @@ router.get('/autocomplete', async (req, res) => {
|
||||
req.query.query,
|
||||
req.query.limit,
|
||||
req.query.offset,
|
||||
globalAccess, organizationId,
|
||||
globalAccess, organizationId, req.currentUser,
|
||||
);
|
||||
|
||||
res.status(200).send(payload);
|
||||
@ -447,9 +446,12 @@ router.get('/autocomplete', async (req, res) => {
|
||||
router.get('/:id', wrapAsync(async (req, res) => {
|
||||
const payload = await ReservationsDBApi.findBy(
|
||||
{ id: req.params.id },
|
||||
{ currentUser: req.currentUser },
|
||||
);
|
||||
|
||||
|
||||
|
||||
if (!payload) {
|
||||
return res.status(404).send('Not found');
|
||||
}
|
||||
|
||||
res.status(200).send(payload);
|
||||
}));
|
||||
|
||||
@ -5,7 +5,6 @@ const RolesService = require('../services/roles');
|
||||
const RolesDBApi = require('../db/api/roles');
|
||||
const wrapAsync = require('../helpers').wrapAsync;
|
||||
|
||||
const config = require('../config');
|
||||
|
||||
|
||||
const router = express.Router();
|
||||
@ -386,6 +385,7 @@ router.get('/autocomplete', async (req, res) => {
|
||||
req.query.limit,
|
||||
req.query.offset,
|
||||
globalAccess,
|
||||
req.query.businessOnly,
|
||||
);
|
||||
|
||||
res.status(200).send(payload);
|
||||
|
||||
@ -5,7 +5,6 @@ const Service_requestsService = require('../services/service_requests');
|
||||
const Service_requestsDBApi = require('../db/api/service_requests');
|
||||
const wrapAsync = require('../helpers').wrapAsync;
|
||||
|
||||
const config = require('../config');
|
||||
|
||||
|
||||
const router = express.Router();
|
||||
@ -402,7 +401,7 @@ router.get('/autocomplete', async (req, res) => {
|
||||
req.query.query,
|
||||
req.query.limit,
|
||||
req.query.offset,
|
||||
globalAccess, organizationId,
|
||||
globalAccess, organizationId, req.currentUser,
|
||||
);
|
||||
|
||||
res.status(200).send(payload);
|
||||
@ -443,9 +442,12 @@ router.get('/autocomplete', async (req, res) => {
|
||||
router.get('/:id', wrapAsync(async (req, res) => {
|
||||
const payload = await Service_requestsDBApi.findBy(
|
||||
{ id: req.params.id },
|
||||
{ currentUser: req.currentUser },
|
||||
);
|
||||
|
||||
|
||||
|
||||
if (!payload) {
|
||||
return res.status(404).send('Not found');
|
||||
}
|
||||
|
||||
res.status(200).send(payload);
|
||||
}));
|
||||
|
||||
@ -3,8 +3,6 @@ const Booking_requestsDBApi = require('../db/api/booking_requests');
|
||||
const processFile = require("../middlewares/upload");
|
||||
const ValidationError = require('./notifications/errors/validation');
|
||||
const csv = require('csv-parser');
|
||||
const axios = require('axios');
|
||||
const config = require('../config');
|
||||
const stream = require('stream');
|
||||
|
||||
|
||||
@ -28,9 +26,9 @@ module.exports = class Booking_requestsService {
|
||||
await transaction.rollback();
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
static async bulkImport(req, res, sendInvitationEmails = true, host) {
|
||||
static async bulkImport(req, res) {
|
||||
const transaction = await db.sequelize.transaction();
|
||||
|
||||
try {
|
||||
@ -70,7 +68,7 @@ module.exports = class Booking_requestsService {
|
||||
try {
|
||||
let booking_requests = await Booking_requestsDBApi.findBy(
|
||||
{id},
|
||||
{transaction},
|
||||
{transaction, currentUser},
|
||||
);
|
||||
|
||||
if (!booking_requests) {
|
||||
@ -95,7 +93,7 @@ module.exports = class Booking_requestsService {
|
||||
await transaction.rollback();
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
static async deleteByIds(ids, currentUser) {
|
||||
const transaction = await db.sequelize.transaction();
|
||||
@ -117,7 +115,7 @@ module.exports = class Booking_requestsService {
|
||||
const transaction = await db.sequelize.transaction();
|
||||
|
||||
try {
|
||||
await Booking_requestsDBApi.remove(
|
||||
const removedBooking_requests = await Booking_requestsDBApi.remove(
|
||||
id,
|
||||
{
|
||||
currentUser,
|
||||
@ -125,6 +123,12 @@ module.exports = class Booking_requestsService {
|
||||
},
|
||||
);
|
||||
|
||||
if (!removedBooking_requests) {
|
||||
throw new ValidationError(
|
||||
'booking_requestsNotFound',
|
||||
);
|
||||
}
|
||||
|
||||
await transaction.commit();
|
||||
} catch (error) {
|
||||
await transaction.rollback();
|
||||
|
||||
@ -3,8 +3,6 @@ const Service_requestsDBApi = require('../db/api/service_requests');
|
||||
const processFile = require("../middlewares/upload");
|
||||
const ValidationError = require('./notifications/errors/validation');
|
||||
const csv = require('csv-parser');
|
||||
const axios = require('axios');
|
||||
const config = require('../config');
|
||||
const stream = require('stream');
|
||||
|
||||
|
||||
@ -28,9 +26,9 @@ module.exports = class Service_requestsService {
|
||||
await transaction.rollback();
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
static async bulkImport(req, res, sendInvitationEmails = true, host) {
|
||||
static async bulkImport(req, res) {
|
||||
const transaction = await db.sequelize.transaction();
|
||||
|
||||
try {
|
||||
@ -70,7 +68,7 @@ module.exports = class Service_requestsService {
|
||||
try {
|
||||
let service_requests = await Service_requestsDBApi.findBy(
|
||||
{id},
|
||||
{transaction},
|
||||
{transaction, currentUser},
|
||||
);
|
||||
|
||||
if (!service_requests) {
|
||||
@ -95,7 +93,7 @@ module.exports = class Service_requestsService {
|
||||
await transaction.rollback();
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
static async deleteByIds(ids, currentUser) {
|
||||
const transaction = await db.sequelize.transaction();
|
||||
@ -117,7 +115,7 @@ module.exports = class Service_requestsService {
|
||||
const transaction = await db.sequelize.transaction();
|
||||
|
||||
try {
|
||||
await Service_requestsDBApi.remove(
|
||||
const removedService_requests = await Service_requestsDBApi.remove(
|
||||
id,
|
||||
{
|
||||
currentUser,
|
||||
@ -125,6 +123,12 @@ module.exports = class Service_requestsService {
|
||||
},
|
||||
);
|
||||
|
||||
if (!removedService_requests) {
|
||||
throw new ValidationError(
|
||||
'service_requestsNotFound',
|
||||
);
|
||||
}
|
||||
|
||||
await transaction.commit();
|
||||
} catch (error) {
|
||||
await transaction.rollback();
|
||||
|
||||
@ -55,7 +55,7 @@ const TableSampleRoles = ({ filterItems, setFilterItems, filters, showGrid }) =>
|
||||
if (request !== filterRequest) setFilterRequest(request);
|
||||
const { sort, field } = sortModel[0];
|
||||
|
||||
const query = `?page=${page}&limit=${perPage}${request}&sort=${sort}&field=${field}`;
|
||||
const query = `?page=${page}&limit=${perPage}&businessOnly=true${request}&sort=${sort}&field=${field}`;
|
||||
dispatch(fetch({ limit: perPage, page, query }));
|
||||
};
|
||||
|
||||
|
||||
@ -26,7 +26,8 @@ export const SelectField = ({ options, field, form, itemRef, showField, disabled
|
||||
}
|
||||
|
||||
async function callApi(inputValue: string, loadedOptions: any[]) {
|
||||
const path = `/${itemRef}/autocomplete?limit=${PAGE_SIZE}&offset=${loadedOptions.length}${inputValue ? `&query=${inputValue}` : ''}`;
|
||||
const businessOnly = itemRef === 'roles' ? '&businessOnly=true' : '';
|
||||
const path = `/${itemRef}/autocomplete?limit=${PAGE_SIZE}&offset=${loadedOptions.length}${inputValue ? `&query=${inputValue}` : ''}${businessOnly}`;
|
||||
const { data } = await axios(path);
|
||||
return {
|
||||
options: data.map(mapResponseToValuesAndLabels),
|
||||
|
||||
@ -28,7 +28,8 @@ export const RoleSelect = ({ options, field, form, itemRef, disabled, currentUse
|
||||
};
|
||||
|
||||
async function callApi(inputValue: string, loadedOptions: any[]) {
|
||||
const path = `/${itemRef}/autocomplete?limit=${PAGE_SIZE}&offset=${loadedOptions.length}${inputValue ? `&query=${inputValue}` : ''}`;
|
||||
const businessOnly = itemRef === 'roles' ? '&businessOnly=true' : '';
|
||||
const path = `/${itemRef}/autocomplete?limit=${PAGE_SIZE}&offset=${loadedOptions.length}${inputValue ? `&query=${inputValue}` : ''}${businessOnly}`;
|
||||
const { data } = await axios(path);
|
||||
return {
|
||||
options: data.map(mapResponseToValuesAndLabels),
|
||||
|
||||
@ -9,11 +9,6 @@ const menuAside: MenuAsideItem[] = [
|
||||
icon: 'mdiShieldHomeOutline' in icon ? icon['mdiShieldHomeOutline' as keyof typeof icon] : icon.mdiViewDashboardOutline,
|
||||
label: 'Command Center',
|
||||
},
|
||||
{
|
||||
href: '/dashboard',
|
||||
icon: icon.mdiViewDashboardOutline,
|
||||
label: 'Dashboard',
|
||||
},
|
||||
{
|
||||
label: 'Operations',
|
||||
icon: icon.mdiClipboardTextOutline,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -30,17 +30,40 @@ const Booking_requestsTablesPage = () => {
|
||||
const { currentUser } = useAppSelector((state) => state.auth);
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const [filters] = useState(([{label: 'Requestcode', title: 'request_code'},{label: 'Purposeofstay', title: 'purpose_of_stay'},{label: 'Specialrequirements', title: 'special_requirements'},{label: 'Budgetcode', title: 'budget_code'},{label: 'Currency', title: 'currency'},
|
||||
{label: 'Preferredbedrooms', title: 'preferred_bedrooms', number: 'true'},{label: 'Guestcount', title: 'guest_count', number: 'true'},
|
||||
{label: 'Maxbudgetamount', title: 'max_budget_amount', number: 'true'},
|
||||
{label: 'Check-inat', title: 'check_in_at', date: 'true'},{label: 'Check-outat', title: 'check_out_at', date: 'true'},
|
||||
{label: 'Tenant', title: 'tenant'},
|
||||
{label: 'Requestedby', title: 'requested_by'},
|
||||
{label: 'Preferredproperty', title: 'preferred_property'},
|
||||
{label: 'Preferredunittype', title: 'preferred_unit_type'},
|
||||
{label: 'Travelers', title: 'travelers'},{label: 'Approvalsteps', title: 'approval_steps'},{label: 'Documents', title: 'documents'},{label: 'Comments', title: 'comments'},
|
||||
{label: 'Status', title: 'status', type: 'enum', options: ['draft','submitted','in_review','changes_requested','approved','rejected','expired','converted_to_reservation','canceled']},
|
||||
]).map((filter) => ({ ...filter, label: humanize(filter.title) })));
|
||||
const canManageInternalFields = Boolean(currentUser?.app_role?.globalAccess);
|
||||
const createButtonLabel = canManageInternalFields ? 'New request' : 'Request a stay';
|
||||
const filters = React.useMemo(() => {
|
||||
const baseFilters = [
|
||||
{ label: 'Requestcode', title: 'request_code' },
|
||||
{ label: 'Purposeofstay', title: 'purpose_of_stay' },
|
||||
{ label: 'Specialrequirements', title: 'special_requirements' },
|
||||
{ label: 'Budgetcode', title: 'budget_code' },
|
||||
{ label: 'Currency', title: 'currency' },
|
||||
{ label: 'Preferredbedrooms', title: 'preferred_bedrooms', number: 'true' },
|
||||
{ label: 'Guestcount', title: 'guest_count', number: 'true' },
|
||||
{ label: 'Maxbudgetamount', title: 'max_budget_amount', number: 'true' },
|
||||
{ label: 'Check-inat', title: 'check_in_at', date: 'true' },
|
||||
{ label: 'Check-outat', title: 'check_out_at', date: 'true' },
|
||||
{ label: 'Preferredproperty', title: 'preferred_property' },
|
||||
{ label: 'Preferredunittype', title: 'preferred_unit_type' },
|
||||
{ label: 'Travelers', title: 'travelers' },
|
||||
{ label: 'Approvalsteps', title: 'approval_steps' },
|
||||
{ label: 'Documents', title: 'documents' },
|
||||
{ label: 'Comments', title: 'comments' },
|
||||
{
|
||||
label: 'Status',
|
||||
title: 'status',
|
||||
type: 'enum',
|
||||
options: ['draft','submitted','in_review','changes_requested','approved','rejected','expired','converted_to_reservation','canceled'],
|
||||
},
|
||||
];
|
||||
|
||||
if (canManageInternalFields) {
|
||||
baseFilters.splice(10, 0, { label: 'Tenant', title: 'tenant' }, { label: 'Requestedby', title: 'requested_by' });
|
||||
}
|
||||
|
||||
return baseFilters.map((filter) => ({ ...filter, label: humanize(filter.title) }));
|
||||
}, [canManageInternalFields]);
|
||||
|
||||
const hasCreatePermission = currentUser && hasPermission(currentUser, 'CREATE_BOOKING_REQUESTS');
|
||||
|
||||
@ -109,7 +132,7 @@ const Booking_requestsTablesPage = () => {
|
||||
classAddon='mr-2 mb-2 last:mr-0'
|
||||
>
|
||||
{hasCreatePermission ? (
|
||||
<BaseButton href={'/booking_requests/booking_requests-new'} color='info' label='New request' />
|
||||
<BaseButton href={'/booking_requests/booking_requests-new'} color='info' label={createButtonLabel} />
|
||||
) : null}
|
||||
<BaseButton color='whiteDark' outline label='Add filter' onClick={addFilter} />
|
||||
<BaseButton color='whiteDark' outline label='Export CSV' onClick={getBooking_requestsCSV} />
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,167 +1,148 @@
|
||||
import { mdiChartTimelineVariant } from '@mdi/js'
|
||||
import Head from 'next/head'
|
||||
import { uniqueId } from 'lodash';
|
||||
import React, { ReactElement, useState } from 'react'
|
||||
import { uniqueId } from 'lodash'
|
||||
import Link from 'next/link'
|
||||
import React, { ReactElement, useMemo, useState } from 'react'
|
||||
import axios from 'axios'
|
||||
|
||||
import BaseButton from '../../components/BaseButton'
|
||||
import CardBox from '../../components/CardBox'
|
||||
import CardBoxModal from '../../components/CardBoxModal'
|
||||
import DragDropFilePicker from '../../components/DragDropFilePicker'
|
||||
import TableBooking_requests from '../../components/Booking_requests/TableBooking_requests'
|
||||
import { getPageTitle } from '../../config'
|
||||
import { hasPermission } from '../../helpers/userPermissions'
|
||||
import LayoutAuthenticated from '../../layouts/Authenticated'
|
||||
import SectionMain from '../../components/SectionMain'
|
||||
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'
|
||||
import { getPageTitle } from '../../config'
|
||||
import TableBooking_requests from '../../components/Booking_requests/TableBooking_requests'
|
||||
import BaseButton from '../../components/BaseButton'
|
||||
import axios from "axios";
|
||||
import Link from "next/link";
|
||||
import {useAppDispatch, useAppSelector} from "../../stores/hooks";
|
||||
import CardBoxModal from "../../components/CardBoxModal";
|
||||
import DragDropFilePicker from "../../components/DragDropFilePicker";
|
||||
import {setRefetch, uploadCsv} from '../../stores/booking_requests/booking_requestsSlice';
|
||||
|
||||
|
||||
import {hasPermission} from "../../helpers/userPermissions";
|
||||
|
||||
|
||||
import { setRefetch, uploadCsv } from '../../stores/booking_requests/booking_requestsSlice'
|
||||
import { useAppDispatch, useAppSelector } from '../../stores/hooks'
|
||||
|
||||
const Booking_requestsTablesPage = () => {
|
||||
const [filterItems, setFilterItems] = useState([]);
|
||||
const [csvFile, setCsvFile] = useState<File | null>(null);
|
||||
const [isModalActive, setIsModalActive] = useState(false);
|
||||
const [showTableView, setShowTableView] = useState(false);
|
||||
const [filterItems, setFilterItems] = useState([])
|
||||
const [csvFile, setCsvFile] = useState<File | null>(null)
|
||||
const [isModalActive, setIsModalActive] = useState(false)
|
||||
|
||||
|
||||
const { currentUser } = useAppSelector((state) => state.auth);
|
||||
|
||||
const { currentUser } = useAppSelector((state) => state.auth)
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
const canManageInternalFields = Boolean(currentUser?.app_role?.globalAccess)
|
||||
const createButtonLabel = canManageInternalFields ? 'New item' : 'Request a stay'
|
||||
const filters = useMemo(() => {
|
||||
const baseFilters = [
|
||||
{ label: 'Requestcode', title: 'request_code' },
|
||||
{ label: 'Purposeofstay', title: 'purpose_of_stay' },
|
||||
{ label: 'Specialrequirements', title: 'special_requirements' },
|
||||
{ label: 'Budgetcode', title: 'budget_code' },
|
||||
{ label: 'Currency', title: 'currency' },
|
||||
{ label: 'Preferredbedrooms', title: 'preferred_bedrooms', number: 'true' },
|
||||
{ label: 'Guestcount', title: 'guest_count', number: 'true' },
|
||||
{ label: 'Maxbudgetamount', title: 'max_budget_amount', number: 'true' },
|
||||
{ label: 'Check-inat', title: 'check_in_at', date: 'true' },
|
||||
{ label: 'Check-outat', title: 'check_out_at', date: 'true' },
|
||||
{ label: 'Preferredproperty', title: 'preferred_property' },
|
||||
{ label: 'Preferredunittype', title: 'preferred_unit_type' },
|
||||
{ label: 'Travelers', title: 'travelers' },
|
||||
{ label: 'Approvalsteps', title: 'approval_steps' },
|
||||
{ label: 'Documents', title: 'documents' },
|
||||
{ label: 'Comments', title: 'comments' },
|
||||
{
|
||||
label: 'Status',
|
||||
title: 'status',
|
||||
type: 'enum',
|
||||
options: ['draft', 'submitted', 'in_review', 'changes_requested', 'approved', 'rejected', 'expired', 'converted_to_reservation', 'canceled'],
|
||||
},
|
||||
]
|
||||
|
||||
if (canManageInternalFields) {
|
||||
baseFilters.splice(10, 0, { label: 'Tenant', title: 'tenant' }, { label: 'Requestedby', title: 'requested_by' })
|
||||
}
|
||||
|
||||
const [filters] = useState([{label: 'Requestcode', title: 'request_code'},{label: 'Purposeofstay', title: 'purpose_of_stay'},{label: 'Specialrequirements', title: 'special_requirements'},{label: 'Budgetcode', title: 'budget_code'},{label: 'Currency', title: 'currency'},
|
||||
{label: 'Preferredbedrooms', title: 'preferred_bedrooms', number: 'true'},{label: 'Guestcount', title: 'guest_count', number: 'true'},
|
||||
{label: 'Maxbudgetamount', title: 'max_budget_amount', number: 'true'},
|
||||
{label: 'Check-inat', title: 'check_in_at', date: 'true'},{label: 'Check-outat', title: 'check_out_at', date: 'true'},
|
||||
|
||||
|
||||
{label: 'Tenant', title: 'tenant'},
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
{label: 'Requestedby', title: 'requested_by'},
|
||||
|
||||
|
||||
|
||||
{label: 'Preferredproperty', title: 'preferred_property'},
|
||||
|
||||
|
||||
|
||||
{label: 'Preferredunittype', title: 'preferred_unit_type'},
|
||||
|
||||
|
||||
{label: 'Travelers', title: 'travelers'},{label: 'Approvalsteps', title: 'approval_steps'},{label: 'Documents', title: 'documents'},{label: 'Comments', title: 'comments'},
|
||||
{label: 'Status', title: 'status', type: 'enum', options: ['draft','submitted','in_review','changes_requested','approved','rejected','expired','converted_to_reservation','canceled']},
|
||||
]);
|
||||
|
||||
const hasCreatePermission = currentUser && hasPermission(currentUser, 'CREATE_BOOKING_REQUESTS');
|
||||
|
||||
return baseFilters
|
||||
}, [canManageInternalFields])
|
||||
|
||||
const addFilter = () => {
|
||||
const newItem = {
|
||||
id: uniqueId(),
|
||||
fields: {
|
||||
filterValue: '',
|
||||
filterValueFrom: '',
|
||||
filterValueTo: '',
|
||||
selectedField: '',
|
||||
},
|
||||
};
|
||||
newItem.fields.selectedField = filters[0].title;
|
||||
setFilterItems([...filterItems, newItem]);
|
||||
};
|
||||
const hasCreatePermission = currentUser && hasPermission(currentUser, 'CREATE_BOOKING_REQUESTS')
|
||||
|
||||
const getBooking_requestsCSV = async () => {
|
||||
const response = await axios({url: '/booking_requests?filetype=csv', method: 'GET',responseType: 'blob'});
|
||||
const type = response.headers['content-type']
|
||||
const blob = new Blob([response.data], { type: type })
|
||||
const link = document.createElement('a')
|
||||
link.href = window.URL.createObjectURL(blob)
|
||||
link.download = 'booking_requestsCSV.csv'
|
||||
link.click()
|
||||
};
|
||||
const addFilter = () => {
|
||||
const newItem = {
|
||||
id: uniqueId(),
|
||||
fields: {
|
||||
filterValue: '',
|
||||
filterValueFrom: '',
|
||||
filterValueTo: '',
|
||||
selectedField: filters[0].title,
|
||||
},
|
||||
}
|
||||
|
||||
const onModalConfirm = async () => {
|
||||
if (!csvFile) return;
|
||||
await dispatch(uploadCsv(csvFile));
|
||||
dispatch(setRefetch(true));
|
||||
setCsvFile(null);
|
||||
setIsModalActive(false);
|
||||
};
|
||||
setFilterItems([...filterItems, newItem])
|
||||
}
|
||||
|
||||
const onModalCancel = () => {
|
||||
setCsvFile(null);
|
||||
setIsModalActive(false);
|
||||
};
|
||||
const getBooking_requestsCSV = async () => {
|
||||
const response = await axios({ url: '/booking_requests?filetype=csv', method: 'GET', responseType: 'blob' })
|
||||
const type = response.headers['content-type']
|
||||
const blob = new Blob([response.data], { type })
|
||||
const link = document.createElement('a')
|
||||
link.href = window.URL.createObjectURL(blob)
|
||||
link.download = 'booking_requestsCSV.csv'
|
||||
link.click()
|
||||
}
|
||||
|
||||
const onModalConfirm = async () => {
|
||||
if (!csvFile) return
|
||||
await dispatch(uploadCsv(csvFile))
|
||||
dispatch(setRefetch(true))
|
||||
setCsvFile(null)
|
||||
setIsModalActive(false)
|
||||
}
|
||||
|
||||
const onModalCancel = () => {
|
||||
setCsvFile(null)
|
||||
setIsModalActive(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{getPageTitle('Booking_requests')}</title>
|
||||
<title>{getPageTitle('Booking Requests')}</title>
|
||||
</Head>
|
||||
<SectionMain>
|
||||
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Booking_requests" main>
|
||||
{''}
|
||||
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title='Booking Requests' main>
|
||||
{''}
|
||||
</SectionTitleLineWithButton>
|
||||
<CardBox className='mb-6' cardBoxClassName='flex flex-wrap'>
|
||||
|
||||
{hasCreatePermission && <BaseButton className={'mr-3'} href={'/booking_requests/booking_requests-new'} color='info' label='New Item'/>}
|
||||
|
||||
<BaseButton
|
||||
className={'mr-3'}
|
||||
color='info'
|
||||
label='Filter'
|
||||
onClick={addFilter}
|
||||
/>
|
||||
<BaseButton className={'mr-3'} color='info' label='Download CSV' onClick={getBooking_requestsCSV} />
|
||||
|
||||
{hasCreatePermission && (
|
||||
<BaseButton
|
||||
color='info'
|
||||
label='Upload CSV'
|
||||
onClick={() => setIsModalActive(true)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{hasCreatePermission && (
|
||||
<BaseButton className='mr-3' href={'/booking_requests/booking_requests-new'} color='info' label={createButtonLabel} />
|
||||
)}
|
||||
<BaseButton className='mr-3' color='info' label='Filter' onClick={addFilter} />
|
||||
<BaseButton className='mr-3' color='info' label='Download CSV' onClick={getBooking_requestsCSV} />
|
||||
{hasCreatePermission && (
|
||||
<BaseButton color='info' label='Upload CSV' onClick={() => setIsModalActive(true)} />
|
||||
)}
|
||||
<div className='md:inline-flex items-center ms-auto'>
|
||||
<div id='delete-rows-button'></div>
|
||||
|
||||
<Link href={'/booking_requests/booking_requests-list'}>
|
||||
Back to <span className='capitalize'>kanban</span>
|
||||
</Link>
|
||||
|
||||
<Link href={'/booking_requests/booking_requests-list'}>
|
||||
Back to <span className='capitalize'>kanban</span>
|
||||
</Link>
|
||||
</div>
|
||||
</CardBox>
|
||||
<CardBox className="mb-6" hasTable>
|
||||
<CardBox className='mb-6' hasTable>
|
||||
<TableBooking_requests
|
||||
filterItems={filterItems}
|
||||
setFilterItems={setFilterItems}
|
||||
filters={filters}
|
||||
showGrid={true}
|
||||
/>
|
||||
/>
|
||||
</CardBox>
|
||||
</SectionMain>
|
||||
<CardBoxModal
|
||||
title='Upload CSV'
|
||||
buttonColor='info'
|
||||
buttonLabel={'Confirm'}
|
||||
// buttonLabel={false ? 'Deleting...' : 'Confirm'}
|
||||
isActive={isModalActive}
|
||||
onConfirm={onModalConfirm}
|
||||
onCancel={onModalCancel}
|
||||
title='Upload CSV'
|
||||
buttonColor='info'
|
||||
buttonLabel={'Confirm'}
|
||||
isActive={isModalActive}
|
||||
onConfirm={onModalConfirm}
|
||||
onCancel={onModalCancel}
|
||||
>
|
||||
<DragDropFilePicker
|
||||
file={csvFile}
|
||||
setFile={setCsvFile}
|
||||
formats={'.csv'}
|
||||
/>
|
||||
<DragDropFilePicker file={csvFile} setFile={setCsvFile} formats={'.csv'} />
|
||||
</CardBoxModal>
|
||||
</>
|
||||
)
|
||||
@ -169,13 +150,9 @@ const Booking_requestsTablesPage = () => {
|
||||
|
||||
Booking_requestsTablesPage.getLayout = function getLayout(page: ReactElement) {
|
||||
return (
|
||||
<LayoutAuthenticated
|
||||
|
||||
permission={'READ_BOOKING_REQUESTS'}
|
||||
|
||||
>
|
||||
{page}
|
||||
</LayoutAuthenticated>
|
||||
<LayoutAuthenticated permission={'READ_BOOKING_REQUESTS'}>
|
||||
{page}
|
||||
</LayoutAuthenticated>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -99,6 +99,12 @@ const BookingRequestsView = () => {
|
||||
}, [dispatch, id]);
|
||||
|
||||
const canEdit = currentUser && hasPermission(currentUser, 'UPDATE_BOOKING_REQUESTS');
|
||||
const canManageInternalFields = Boolean(currentUser?.app_role?.globalAccess);
|
||||
const pageTitle = canManageInternalFields ? 'Booking Request' : 'Stay Request';
|
||||
const overviewHeading = canManageInternalFields ? 'Guest request snapshot' : 'Stay request snapshot';
|
||||
const overviewCopy = canManageInternalFields
|
||||
? 'A light summary of who requested the stay, where they prefer to stay, and what needs follow-up.'
|
||||
: 'A light summary of the requested stay, preferred accommodation, and what still needs follow-up.';
|
||||
const stayWindow =
|
||||
booking_requests?.check_in_at || booking_requests?.check_out_at
|
||||
? `${formatDate(booking_requests?.check_in_at)} → ${formatDate(booking_requests?.check_out_at)}`
|
||||
@ -107,11 +113,11 @@ const BookingRequestsView = () => {
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{getPageTitle('Booking Request')}</title>
|
||||
<title>{getPageTitle(pageTitle)}</title>
|
||||
</Head>
|
||||
|
||||
<SectionMain>
|
||||
<SectionTitleLineWithButton icon={mdiClipboardTextOutline} title="Booking Request" main>
|
||||
<SectionTitleLineWithButton icon={mdiClipboardTextOutline} title={pageTitle} main>
|
||||
<BaseButtons noWrap>
|
||||
<BaseButton href="/booking_requests/booking_requests-list" color="whiteDark" outline label="Back" />
|
||||
{canEdit && (
|
||||
@ -141,20 +147,22 @@ const BookingRequestsView = () => {
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-slate-400">Overview</p>
|
||||
<h2 className="mt-2 text-xl font-semibold text-white">Guest request snapshot</h2>
|
||||
<p className="mt-1 text-sm text-slate-400">
|
||||
A light summary of who requested the stay, where they prefer to stay, and what needs follow-up.
|
||||
</p>
|
||||
<h2 className="mt-2 text-xl font-semibold text-white">{overviewHeading}</h2>
|
||||
<p className="mt-1 text-sm text-slate-400">{overviewCopy}</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3 md:grid-cols-2">
|
||||
<DetailRow
|
||||
label="Requested By"
|
||||
value={booking_requests?.requested_by?.firstName || booking_requests?.requested_by?.email}
|
||||
/>
|
||||
<DetailRow label="Tenant" value={booking_requests?.tenant?.name} />
|
||||
{hasPermission(currentUser, 'READ_ORGANIZATIONS') && (
|
||||
<DetailRow label="Organization" value={booking_requests?.organization?.name} />
|
||||
{canManageInternalFields && (
|
||||
<>
|
||||
<DetailRow
|
||||
label="Requested By"
|
||||
value={booking_requests?.requested_by?.firstName || booking_requests?.requested_by?.email}
|
||||
/>
|
||||
<DetailRow label="Tenant" value={booking_requests?.tenant?.name} />
|
||||
{hasPermission(currentUser, 'READ_ORGANIZATIONS') && (
|
||||
<DetailRow label="Organization" value={booking_requests?.organization?.name} />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<DetailRow label="Preferred Property" value={booking_requests?.preferred_property?.name} />
|
||||
<DetailRow label="Preferred Unit Type" value={booking_requests?.preferred_unit_type?.name} />
|
||||
|
||||
@ -134,6 +134,14 @@ type MetricCard = {
|
||||
value: string;
|
||||
detail: string;
|
||||
icon: string;
|
||||
visible?: boolean;
|
||||
};
|
||||
|
||||
type HeroStat = {
|
||||
title: string;
|
||||
value: string;
|
||||
detail: string;
|
||||
visible?: boolean;
|
||||
};
|
||||
|
||||
const emptyOverview: OverviewResponse = {
|
||||
@ -286,6 +294,7 @@ type FocusItem = {
|
||||
label: string;
|
||||
value: string;
|
||||
detail: string;
|
||||
visible?: boolean;
|
||||
};
|
||||
|
||||
type ActivitySection = {
|
||||
@ -327,6 +336,29 @@ const FocusListItem = ({ label, value, detail }: FocusItem) => (
|
||||
</div>
|
||||
);
|
||||
|
||||
const ROLE_LANES = {
|
||||
superAdmin: new Set(['Super Administrator']),
|
||||
admin: new Set(['Administrator', 'Platform Owner', 'Operations Director', 'Reservations Lead', 'Finance Controller']),
|
||||
concierge: new Set(['Concierge Coordinator']),
|
||||
customer: new Set(['Customer']),
|
||||
};
|
||||
|
||||
const getRoleLane = (roleName: string, hasGlobalAccess: boolean) => {
|
||||
if (hasGlobalAccess || ROLE_LANES.superAdmin.has(roleName)) {
|
||||
return 'super_admin';
|
||||
}
|
||||
|
||||
if (ROLE_LANES.admin.has(roleName)) {
|
||||
return 'admin';
|
||||
}
|
||||
|
||||
if (ROLE_LANES.customer.has(roleName)) {
|
||||
return 'customer';
|
||||
}
|
||||
|
||||
return 'concierge';
|
||||
};
|
||||
|
||||
const ActivityCard = ({ title, href, emptyState, items }: ActivitySection) => (
|
||||
<CardBox className="h-full">
|
||||
<div className="mb-4 flex items-center justify-between gap-3">
|
||||
@ -377,23 +409,31 @@ const CommandCenterPage = () => {
|
||||
currentUser?.organizations?.name || currentUser?.organization?.name || 'your corporate workspace';
|
||||
const firstName = currentUser?.firstName || currentUser?.email || 'team';
|
||||
const roleName = currentUser?.app_role?.name || 'Team member';
|
||||
const isSuperAdmin = Boolean(currentUser?.app_role?.globalAccess || roleName === 'Super Administrator');
|
||||
const isAdmin = !isSuperAdmin && roleName === 'Administrator';
|
||||
const roleLane = getRoleLane(roleName, Boolean(currentUser?.app_role?.globalAccess));
|
||||
const isSuperAdmin = roleLane === 'super_admin';
|
||||
const isAdmin = roleLane === 'admin';
|
||||
const isCustomer = roleLane === 'customer';
|
||||
const dashboardLens = isSuperAdmin
|
||||
? 'Network oversight'
|
||||
: isAdmin
|
||||
? 'Approval and portfolio operations'
|
||||
: 'Concierge delivery';
|
||||
: isCustomer
|
||||
? 'Customer stay visibility'
|
||||
: 'Concierge delivery';
|
||||
const dashboardHeadline = isSuperAdmin
|
||||
? 'See every handoff across accounts, inventory, service, and revenue.'
|
||||
: isAdmin
|
||||
? 'Approve faster, place stays cleanly, and keep billing exposure visible.'
|
||||
: 'Move travelers from request to arrival without losing operational detail.';
|
||||
: isCustomer
|
||||
? 'Request stays, follow reservation status, and stay connected during the trip.'
|
||||
: 'Move travelers from request to arrival without losing operational detail.';
|
||||
const dashboardDescription = isSuperAdmin
|
||||
? `You share the same command center as every operating role, but your lens spans organizations, portfolio health, and revenue risk across ${organizationName}.`
|
||||
: isAdmin
|
||||
? `You work from the same live records as coordinators and leadership, with a sharper focus on approvals, reservations, and financial follow-through for ${organizationName}.`
|
||||
: `You operate from the same live workspace as administrators and leadership, focused on intake, guest movement, and service execution for ${organizationName}.`;
|
||||
? `You work from the same live records as customers, coordinators, and leadership, with a sharper focus on approvals, reservations, and financial follow-through for ${organizationName}.`
|
||||
: isCustomer
|
||||
? `You work from the same live records as concierge and administrators, focused on request status, confirmed stays, documents, and active support for ${organizationName}.`
|
||||
: `You operate from the same live workspace as customers, administrators, and leadership, focused on intake, guest movement, and service execution for ${organizationName}.`;
|
||||
|
||||
const loadOverview = useCallback(async () => {
|
||||
try {
|
||||
@ -426,26 +466,51 @@ const CommandCenterPage = () => {
|
||||
value: `${overview.bookingRequests.pendingReview}`,
|
||||
detail: `${overview.approvals.pending} approvals are waiting on sign-off.`,
|
||||
icon: mdiClipboardTextOutline,
|
||||
visible: overview.access.bookings || overview.access.approvals,
|
||||
},
|
||||
{
|
||||
title: 'Upcoming arrivals',
|
||||
value: `${overview.reservations.upcomingArrivals}`,
|
||||
detail: `${overview.reservations.inHouse} travelers are currently in house.`,
|
||||
icon: mdiCalendarCheck,
|
||||
visible: overview.access.reservations,
|
||||
},
|
||||
{
|
||||
title: 'Open service queue',
|
||||
value: `${overview.serviceRequests.open}`,
|
||||
detail: `${overview.serviceRequests.urgent} urgent requests need attention.`,
|
||||
icon: mdiRoomService,
|
||||
visible: overview.access.serviceRequests,
|
||||
},
|
||||
{
|
||||
title: 'Open balance',
|
||||
value: formatMoney(overview.invoices.openBalance, overview.invoices.recent[0]?.currency || 'USD'),
|
||||
detail: `${overview.invoices.statusCounts.overdue || 0} invoices are overdue.`,
|
||||
icon: mdiFileDocument,
|
||||
visible: overview.access.invoices,
|
||||
},
|
||||
];
|
||||
].filter((card) => card.visible);
|
||||
|
||||
const heroStats: HeroStat[] = [
|
||||
{
|
||||
title: 'Corporate accounts',
|
||||
value: `${overview.organizations.total}`,
|
||||
detail: 'Organizations in the current operating scope.',
|
||||
visible: overview.access.accounts,
|
||||
},
|
||||
{
|
||||
title: 'Live properties',
|
||||
value: `${overview.inventory.activeProperties}`,
|
||||
detail: 'Properties open for negotiated corporate stays.',
|
||||
visible: overview.access.inventory,
|
||||
},
|
||||
{
|
||||
title: 'Last refresh',
|
||||
value: formatDateTime(overview.generatedAt),
|
||||
detail: 'Live snapshot across operations and billing.',
|
||||
visible: true,
|
||||
},
|
||||
].filter((stat) => stat.visible);
|
||||
|
||||
const quickActions: ActionItem[] = [
|
||||
{
|
||||
@ -454,7 +519,7 @@ const CommandCenterPage = () => {
|
||||
href: '/booking_requests/booking_requests-new',
|
||||
icon: mdiArrowTopRight,
|
||||
visible: canCreateBookingRequest,
|
||||
priority: isSuperAdmin ? 5 : isAdmin ? 4 : 1,
|
||||
priority: isSuperAdmin ? 5 : isAdmin ? 4 : isCustomer ? 1 : 2,
|
||||
},
|
||||
{
|
||||
title: 'Review approvals',
|
||||
@ -462,7 +527,7 @@ const CommandCenterPage = () => {
|
||||
href: '/approval_steps/approval_steps-list',
|
||||
icon: mdiCheckDecagram,
|
||||
visible: canReadApprovals,
|
||||
priority: isSuperAdmin ? 3 : isAdmin ? 1 : 4,
|
||||
priority: isSuperAdmin ? 3 : isAdmin ? 1 : isCustomer ? 6 : 4,
|
||||
},
|
||||
{
|
||||
title: 'Open reservations',
|
||||
@ -470,7 +535,7 @@ const CommandCenterPage = () => {
|
||||
href: '/reservations/reservations-list',
|
||||
icon: mdiCalendarCheck,
|
||||
visible: canReadReservations,
|
||||
priority: isSuperAdmin ? 4 : isAdmin ? 2 : 2,
|
||||
priority: isSuperAdmin ? 4 : isAdmin ? 2 : isCustomer ? 2 : 1,
|
||||
},
|
||||
{
|
||||
title: 'Service queue',
|
||||
@ -478,7 +543,7 @@ const CommandCenterPage = () => {
|
||||
href: '/service_requests/service_requests-list',
|
||||
icon: mdiRoomService,
|
||||
visible: canReadServiceRequests,
|
||||
priority: isSuperAdmin ? 6 : isAdmin ? 5 : 3,
|
||||
priority: isSuperAdmin ? 6 : isAdmin ? 5 : isCustomer ? 3 : 3,
|
||||
},
|
||||
{
|
||||
title: 'Finance view',
|
||||
@ -486,7 +551,7 @@ const CommandCenterPage = () => {
|
||||
href: '/invoices/invoices-list',
|
||||
icon: mdiFileDocument,
|
||||
visible: canReadInvoices,
|
||||
priority: isSuperAdmin ? 1 : isAdmin ? 3 : 6,
|
||||
priority: isSuperAdmin ? 1 : isAdmin ? 3 : isCustomer ? 6 : 6,
|
||||
},
|
||||
{
|
||||
title: 'Portfolio view',
|
||||
@ -494,7 +559,7 @@ const CommandCenterPage = () => {
|
||||
href: '/properties/properties-list',
|
||||
icon: mdiHomeCity,
|
||||
visible: overview.access.inventory,
|
||||
priority: isSuperAdmin ? 2 : isAdmin ? 6 : 5,
|
||||
priority: isSuperAdmin ? 2 : isAdmin ? 6 : isCustomer ? 6 : 5,
|
||||
},
|
||||
]
|
||||
.filter((item) => item.visible)
|
||||
@ -504,35 +569,43 @@ const CommandCenterPage = () => {
|
||||
? 'Leadership lane'
|
||||
: isAdmin
|
||||
? 'Approval and portfolio lane'
|
||||
: 'Concierge execution lane';
|
||||
: isCustomer
|
||||
? 'Customer request lane'
|
||||
: 'Concierge execution lane';
|
||||
|
||||
const laneDescription = isSuperAdmin
|
||||
? 'The same shared records, viewed through account health, supply, and revenue exposure.'
|
||||
: isAdmin
|
||||
? 'The same shared records, centered on approvals, placement quality, and billing follow-through.'
|
||||
: 'The same shared records, centered on intake speed, arrivals, and service coordination.';
|
||||
: isCustomer
|
||||
? 'The same shared records, centered on stay requests, reservation status, and live service support.'
|
||||
: 'The same shared records, centered on intake speed, arrivals, and service coordination.';
|
||||
|
||||
const focusItems: FocusItem[] = isSuperAdmin
|
||||
const focusItems: FocusItem[] = (isSuperAdmin
|
||||
? [
|
||||
{
|
||||
label: 'Organizations in scope',
|
||||
value: `${overview.organizations.total}`,
|
||||
detail: 'Corporate accounts currently visible in your network view.',
|
||||
visible: overview.access.accounts,
|
||||
},
|
||||
{
|
||||
label: 'Active properties',
|
||||
value: `${overview.inventory.activeProperties}`,
|
||||
detail: 'Supply currently available for corporate allocation.',
|
||||
visible: overview.access.inventory,
|
||||
},
|
||||
{
|
||||
label: 'Open balance',
|
||||
value: formatMoney(overview.invoices.openBalance, overview.invoices.recent[0]?.currency || 'USD'),
|
||||
detail: 'Outstanding invoice exposure across the current operating scope.',
|
||||
visible: overview.access.invoices,
|
||||
},
|
||||
{
|
||||
label: 'Urgent service items',
|
||||
value: `${overview.serviceRequests.urgent}`,
|
||||
detail: 'High-risk stays or property issues that may affect service quality.',
|
||||
visible: overview.access.serviceRequests,
|
||||
},
|
||||
]
|
||||
: isAdmin
|
||||
@ -541,52 +614,88 @@ const CommandCenterPage = () => {
|
||||
label: 'Approvals waiting',
|
||||
value: `${overview.approvals.pending}`,
|
||||
detail: 'Requests that still need client or internal sign-off.',
|
||||
visible: overview.access.approvals,
|
||||
},
|
||||
{
|
||||
label: 'Approved and ready',
|
||||
value: `${overview.bookingRequests.approvedReady}`,
|
||||
detail: 'Demand ready to quote or convert into reservations.',
|
||||
visible: overview.access.bookings,
|
||||
},
|
||||
{
|
||||
label: 'Arrivals ahead',
|
||||
value: `${overview.reservations.upcomingArrivals}`,
|
||||
detail: 'Upcoming arrivals that may need final placement review.',
|
||||
visible: overview.access.reservations,
|
||||
},
|
||||
{
|
||||
label: 'Open balance',
|
||||
value: formatMoney(overview.invoices.openBalance, overview.invoices.recent[0]?.currency || 'USD'),
|
||||
detail: 'Outstanding billing that may need follow-up before closeout.',
|
||||
visible: overview.access.invoices,
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
label: 'New demand',
|
||||
value: `${overview.bookingRequests.pendingReview}`,
|
||||
detail: 'Requests currently entering review or awaiting next action.',
|
||||
},
|
||||
{
|
||||
label: 'Guests in house',
|
||||
value: `${overview.reservations.inHouse}`,
|
||||
detail: 'Travelers currently active across your operating scope.',
|
||||
},
|
||||
{
|
||||
label: 'Service queue',
|
||||
value: `${overview.serviceRequests.open}`,
|
||||
detail: 'Open guest or property requests needing execution.',
|
||||
},
|
||||
{
|
||||
label: 'Departures ahead',
|
||||
value: `${overview.reservations.upcomingDepartures}`,
|
||||
detail: 'Upcoming departures that may require extensions or billing checks.',
|
||||
},
|
||||
];
|
||||
: isCustomer
|
||||
? [
|
||||
{
|
||||
label: 'Requests in motion',
|
||||
value: `${overview.bookingRequests.pendingReview}`,
|
||||
detail: 'Stay requests that are still moving through review or confirmation.',
|
||||
visible: overview.access.bookings,
|
||||
},
|
||||
{
|
||||
label: 'Upcoming stays',
|
||||
value: `${overview.reservations.upcomingArrivals}`,
|
||||
detail: 'Confirmed arrivals coming up soon in your current scope.',
|
||||
visible: overview.access.reservations,
|
||||
},
|
||||
{
|
||||
label: 'Guests in house',
|
||||
value: `${overview.reservations.inHouse}`,
|
||||
detail: 'Active stays that may still need service coordination.',
|
||||
visible: overview.access.reservations,
|
||||
},
|
||||
{
|
||||
label: 'Support queue',
|
||||
value: `${overview.serviceRequests.open}`,
|
||||
detail: 'Open service requests tied to live reservations or traveler needs.',
|
||||
visible: overview.access.serviceRequests,
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
label: 'New demand',
|
||||
value: `${overview.bookingRequests.pendingReview}`,
|
||||
detail: 'Requests currently entering review or awaiting next action.',
|
||||
visible: overview.access.bookings,
|
||||
},
|
||||
{
|
||||
label: 'Guests in house',
|
||||
value: `${overview.reservations.inHouse}`,
|
||||
detail: 'Travelers currently active across your operating scope.',
|
||||
visible: overview.access.reservations,
|
||||
},
|
||||
{
|
||||
label: 'Service queue',
|
||||
value: `${overview.serviceRequests.open}`,
|
||||
detail: 'Open guest or property requests needing execution.',
|
||||
visible: overview.access.serviceRequests,
|
||||
},
|
||||
{
|
||||
label: 'Departures ahead',
|
||||
value: `${overview.reservations.upcomingDepartures}`,
|
||||
detail: 'Upcoming departures that may require extensions or billing checks.',
|
||||
visible: overview.access.reservations,
|
||||
},
|
||||
])
|
||||
.filter((item) => item.visible);
|
||||
|
||||
const connectedWorkflow = [
|
||||
{
|
||||
title: 'Demand intake',
|
||||
owner: 'Concierge Coordinator',
|
||||
title: 'Request submission',
|
||||
owner: 'Customer',
|
||||
value: `${overview.bookingRequests.pendingReview}`,
|
||||
detail: 'Booking requests enter here, with traveler needs, dates, and preferred placement captured once for everyone.',
|
||||
detail: 'Customers initiate stay demand once, keeping dates, traveler context, and special requirements attached to the same shared record.',
|
||||
href: '/booking_requests/booking_requests-list',
|
||||
visible: canReadBookings || canCreateBookingRequest,
|
||||
},
|
||||
@ -691,7 +800,7 @@ const CommandCenterPage = () => {
|
||||
<SectionTitleLineWithButton
|
||||
icon={mdiViewDashboardOutline}
|
||||
title="Command Center"
|
||||
subtitle="One shared operating dashboard with a different lens for each role."
|
||||
subtitle="One shared operating dashboard with role-specific modules and visibility."
|
||||
main
|
||||
>
|
||||
<BaseButton color="whiteDark" icon={mdiRefresh} label="Refresh" onClick={loadOverview} />
|
||||
@ -768,30 +877,24 @@ const CommandCenterPage = () => {
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3 sm:grid-cols-3 lg:grid-cols-1">
|
||||
<div className="rounded-2xl border border-white/10 bg-white/5 p-4">
|
||||
<div className="text-xs uppercase tracking-[0.2em] text-slate-400">Corporate accounts</div>
|
||||
<div className="mt-2 text-3xl font-semibold text-white">{overview.organizations.total}</div>
|
||||
<div className="mt-2 text-sm text-slate-400">Organizations in the current operating scope.</div>
|
||||
</div>
|
||||
<div className="rounded-2xl border border-white/10 bg-white/5 p-4">
|
||||
<div className="text-xs uppercase tracking-[0.2em] text-slate-400">Live properties</div>
|
||||
<div className="mt-2 text-3xl font-semibold text-white">{overview.inventory.activeProperties}</div>
|
||||
<div className="mt-2 text-sm text-slate-400">Properties open for negotiated corporate stays.</div>
|
||||
</div>
|
||||
<div className="rounded-2xl border border-white/10 bg-white/5 p-4">
|
||||
<div className="text-xs uppercase tracking-[0.2em] text-slate-400">Last refresh</div>
|
||||
<div className="mt-2 text-xl font-semibold text-white">{formatDateTime(overview.generatedAt)}</div>
|
||||
<div className="mt-2 text-sm text-slate-400">Live snapshot across operations and billing.</div>
|
||||
</div>
|
||||
{heroStats.map((stat) => (
|
||||
<div key={stat.title} className="rounded-2xl border border-white/10 bg-white/5 p-4">
|
||||
<div className="text-xs uppercase tracking-[0.2em] text-slate-400">{stat.title}</div>
|
||||
<div className="mt-2 text-3xl font-semibold text-white">{stat.value}</div>
|
||||
<div className="mt-2 text-sm text-slate-400">{stat.detail}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="mb-8 grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
||||
{metricCards.map((card) => (
|
||||
<MetricSummaryCard key={card.title} {...card} />
|
||||
))}
|
||||
</div>
|
||||
{metricCards.length ? (
|
||||
<div className="mb-8 grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
||||
{metricCards.map((card) => (
|
||||
<MetricSummaryCard key={card.title} {...card} />
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="mb-8 grid gap-6 xl:grid-cols-[1.1fr_0.9fr]">
|
||||
<CardBox>
|
||||
@ -838,38 +941,44 @@ const CommandCenterPage = () => {
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{focusItems.map((item) => (
|
||||
<FocusListItem key={item.label} {...item} />
|
||||
))}
|
||||
{focusItems.length ? (
|
||||
focusItems.map((item) => <FocusListItem key={item.label} {...item} />)
|
||||
) : (
|
||||
<div className="rounded-xl border border-dashed border-white/10 px-4 py-5 text-sm text-slate-400">
|
||||
This lane will populate as more role permissions are enabled.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardBox>
|
||||
</div>
|
||||
|
||||
<CardBox className="mb-8">
|
||||
<div className="mb-5 flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-white">Interconnected operating model</h3>
|
||||
<p className="text-sm text-slate-400">
|
||||
All three account types land in the same command center, but each role acts on a different part of the same live workflow.
|
||||
</p>
|
||||
{connectedWorkflow.length ? (
|
||||
<CardBox className="mb-8">
|
||||
<div className="mb-5 flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-white">Interconnected operating model</h3>
|
||||
<p className="text-sm text-slate-400">
|
||||
Customers, concierge, administrators, and super admins share the same command center, but each role acts on a different part of the same live workflow.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-4">
|
||||
{connectedWorkflow.map((step) => (
|
||||
<Link
|
||||
key={step.title}
|
||||
href={step.href}
|
||||
className="rounded-xl border border-white/10 bg-white/5 px-4 py-4 transition hover:bg-white/10"
|
||||
>
|
||||
<div className="text-xs font-semibold uppercase tracking-[0.18em] text-slate-400">{step.owner}</div>
|
||||
<div className="mt-2 text-lg font-semibold text-white">{step.title}</div>
|
||||
<div className="mt-3 text-2xl font-semibold text-white">{step.value}</div>
|
||||
<p className="mt-2 text-sm leading-6 text-slate-400">{step.detail}</p>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</CardBox>
|
||||
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-4">
|
||||
{connectedWorkflow.map((step) => (
|
||||
<Link
|
||||
key={step.title}
|
||||
href={step.href}
|
||||
className="rounded-xl border border-white/10 bg-white/5 px-4 py-4 transition hover:bg-white/10"
|
||||
>
|
||||
<div className="text-xs font-semibold uppercase tracking-[0.18em] text-slate-400">{step.owner}</div>
|
||||
<div className="mt-2 text-lg font-semibold text-white">{step.title}</div>
|
||||
<div className="mt-3 text-2xl font-semibold text-white">{step.value}</div>
|
||||
<p className="mt-2 text-sm leading-6 text-slate-400">{step.detail}</p>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</CardBox>
|
||||
) : null}
|
||||
|
||||
<div className="grid gap-6 xl:grid-cols-2 2xl:grid-cols-4">
|
||||
{activitySections.map((section) => (
|
||||
|
||||
@ -64,7 +64,7 @@ export default function HomePage() {
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<BaseButton href="/login" color="whiteDark" label="Login" />
|
||||
<BaseButton href="/dashboard" color="info" label="Admin interface" />
|
||||
<BaseButton href="/command-center" color="info" label="Open workspace" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -157,10 +157,10 @@ export default function HomePage() {
|
||||
</div>
|
||||
<div className="rounded-2xl border border-white/10 bg-white/5 p-5">
|
||||
<div className="text-xs uppercase tracking-[0.2em] text-slate-400">Administration</div>
|
||||
<div className="mt-2 text-2xl font-semibold text-white">Admin dashboard</div>
|
||||
<div className="mt-2 text-sm leading-6 text-slate-300">Access entity management, permissions, and full system configuration.</div>
|
||||
<div className="mt-2 text-2xl font-semibold text-white">Role-based workspace</div>
|
||||
<div className="mt-2 text-sm leading-6 text-slate-300">Land in one shared command center, then branch into the modules your role actually owns.</div>
|
||||
<div className="mt-5">
|
||||
<BaseButton href="/dashboard" color="whiteDark" label="Open admin" />
|
||||
<BaseButton href="/command-center" color="whiteDark" label="Open workspace" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -65,7 +65,7 @@ export default function Login() {
|
||||
// Redirect to dashboard if user is logged in
|
||||
useEffect(() => {
|
||||
if (currentUser?.id) {
|
||||
router.push('/dashboard');
|
||||
router.push('/command-center');
|
||||
}
|
||||
}, [currentUser?.id, router]);
|
||||
// Show error message if there is one
|
||||
@ -183,12 +183,18 @@ export default function Login() {
|
||||
onClick={(e) => setLogin(e.target)}>admin@flatlogic.com</code>{' / '}
|
||||
<code className={`${textColor}`}>946cafba</code>{' / '}
|
||||
to login as Admin</p>
|
||||
<p className='mb-2'>Use <code
|
||||
className={`cursor-pointer ${textColor} `}
|
||||
data-password="bc40952c93f4"
|
||||
onClick={(e) => setLogin(e.target)}>john@doe.com</code>{' / '}
|
||||
<code className={`${textColor}`}>bc40952c93f4</code>{' / '}
|
||||
to login as Concierge</p>
|
||||
<p>Use <code
|
||||
className={`cursor-pointer ${textColor} `}
|
||||
data-password="bc40952c93f4"
|
||||
onClick={(e) => setLogin(e.target)}>client@hello.com</code>{' / '}
|
||||
<code className={`${textColor}`}>bc40952c93f4</code>{' / '}
|
||||
to login as User</p>
|
||||
to login as Customer</p>
|
||||
</div>
|
||||
<div>
|
||||
<BaseIcon
|
||||
|
||||
@ -61,7 +61,7 @@ const RolesTablesPage = () => {
|
||||
};
|
||||
|
||||
const getRolesCSV = async () => {
|
||||
const response = await axios({url: '/roles?filetype=csv', method: 'GET',responseType: 'blob'});
|
||||
const response = await axios({url: '/roles?filetype=csv&businessOnly=true', method: 'GET',responseType: 'blob'});
|
||||
const type = response.headers['content-type']
|
||||
const blob = new Blob([response.data], { type: type })
|
||||
const link = document.createElement('a')
|
||||
|
||||
@ -61,7 +61,7 @@ const RolesTablesPage = () => {
|
||||
};
|
||||
|
||||
const getRolesCSV = async () => {
|
||||
const response = await axios({url: '/roles?filetype=csv', method: 'GET',responseType: 'blob'});
|
||||
const response = await axios({url: '/roles?filetype=csv&businessOnly=true', method: 'GET',responseType: 'blob'});
|
||||
const type = response.headers['content-type']
|
||||
const blob = new Blob([response.data], { type: type })
|
||||
const link = document.createElement('a')
|
||||
|
||||
@ -74,7 +74,7 @@ const SearchView = () => {
|
||||
<BaseButton
|
||||
color='info'
|
||||
label='Back'
|
||||
onClick={() => router.push('/dashboard')}
|
||||
onClick={() => router.push('/command-center')}
|
||||
/>
|
||||
</CardBox>
|
||||
</SectionMain>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user