Autosave: 20260403-073442

This commit is contained in:
Flatlogic Bot 2026-04-03 07:34:43 +00:00
parent d6ec6bb83a
commit 77754d1430
31 changed files with 1318 additions and 3525 deletions

View File

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

View File

@ -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,9 +1069,13 @@ 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) {
where.organizationId = 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,
),
],
},
],
};
}

View File

@ -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']],

View File

@ -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,15 +438,9 @@ 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 = {
[Op.or]: [
@ -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,

View File

@ -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,9 +924,13 @@ 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) {
where.organizationId = 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,
),
],
},
],
};
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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.
},
};

View File

@ -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);
}));

View File

@ -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);
}));

View File

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

View File

@ -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);
}));

View File

@ -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();

View File

@ -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();

View File

@ -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 }));
};

View File

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

View File

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

View File

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

View File

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

View File

@ -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 dispatch = useAppDispatch()
const { currentUser } = useAppSelector((state) => state.auth);
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 dispatch = useAppDispatch();
return baseFilters
}, [canManageInternalFields])
const hasCreatePermission = currentUser && hasPermission(currentUser, 'CREATE_BOOKING_REQUESTS')
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'},
const addFilter = () => {
const newItem = {
id: uniqueId(),
fields: {
filterValue: '',
filterValueFrom: '',
filterValueTo: '',
selectedField: filters[0].title,
},
}
setFilterItems([...filterItems, newItem])
}
{label: 'Tenant', title: 'tenant'},
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)
}
{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');
const addFilter = () => {
const newItem = {
id: uniqueId(),
fields: {
filterValue: '',
filterValueFrom: '',
filterValueTo: '',
selectedField: '',
},
};
newItem.fields.selectedField = filters[0].title;
setFilterItems([...filterItems, newItem]);
};
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 onModalConfirm = async () => {
if (!csvFile) return;
await dispatch(uploadCsv(csvFile));
dispatch(setRefetch(true));
setCsvFile(null);
setIsModalActive(false);
};
const onModalCancel = () => {
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>
)
}

View File

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

View File

@ -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) => (

View File

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

View File

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

View File

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

View File

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

View File

@ -74,7 +74,7 @@ const SearchView = () => {
<BaseButton
color='info'
label='Back'
onClick={() => router.push('/dashboard')}
onClick={() => router.push('/command-center')}
/>
</CardBox>
</SectionMain>