From 77754d14303bff33e5d4a376c266db61d5219825 Mon Sep 17 00:00:00 2001
From: Flatlogic Bot
Date: Fri, 3 Apr 2026 07:34:43 +0000
Subject: [PATCH] Autosave: 20260403-073442
---
backend/src/config.js | 10 +-
backend/src/db/api/booking_requests.js | 147 +-
backend/src/db/api/reservations.js | 99 +-
backend/src/db/api/roles.js | 57 +-
backend/src/db/api/service_requests.js | 84 +-
backend/src/db/models/invoices.js | 30 +-
backend/src/db/models/reservations.js | 56 +-
backend/src/db/models/service_requests.js | 26 +-
backend/src/db/models/units.js | 16 +-
...260403090000-align-business-role-matrix.js | 244 ++
backend/src/routes/booking_requests.js | 10 +-
backend/src/routes/reservations.js | 10 +-
backend/src/routes/roles.js | 2 +-
backend/src/routes/service_requests.js | 10 +-
backend/src/services/booking_requests.js | 18 +-
backend/src/services/service_requests.js | 18 +-
frontend/src/components/Roles/TableRoles.tsx | 2 +-
frontend/src/components/SelectField.tsx | 3 +-
.../components/WidgetCreator/RoleSelect.tsx | 3 +-
frontend/src/menuAside.ts | 5 -
.../booking_requests-edit.tsx | 2149 ++---------------
.../booking_requests-list.tsx | 47 +-
.../booking_requests/booking_requests-new.tsx | 1209 ++--------
.../booking_requests-table.tsx | 245 +-
.../booking_requests-view.tsx | 34 +-
frontend/src/pages/command-center.tsx | 285 ++-
frontend/src/pages/index.tsx | 8 +-
frontend/src/pages/login.tsx | 10 +-
frontend/src/pages/roles/roles-list.tsx | 2 +-
frontend/src/pages/roles/roles-table.tsx | 2 +-
frontend/src/pages/search.tsx | 2 +-
31 files changed, 1318 insertions(+), 3525 deletions(-)
create mode 100644 backend/src/db/seeders/20260403090000-align-business-role-matrix.js
diff --git a/backend/src/config.js b/backend/src/config.js
index 1fb4d5e..27373ec 100644
--- a/backend/src/config.js
+++ b/backend/src/config.js
@@ -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',
diff --git a/backend/src/db/api/booking_requests.js b/backend/src/db/api/booking_requests.js
index 4cfc3a4..efde39c 100644
--- a/backend/src/db/api/booking_requests.js
+++ b/backend/src/db/api/booking_requests.js
@@ -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,
+ ),
+ ],
+ },
],
};
}
diff --git a/backend/src/db/api/reservations.js b/backend/src/db/api/reservations.js
index 1da071b..c60a506 100644
--- a/backend/src/db/api/reservations.js
+++ b/backend/src/db/api/reservations.js
@@ -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']],
diff --git a/backend/src/db/api/roles.js b/backend/src/db/api/roles.js
index 19dd56c..5c166c2 100644
--- a/backend/src/db/api/roles.js
+++ b/backend/src/db/api/roles.js
@@ -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,
diff --git a/backend/src/db/api/service_requests.js b/backend/src/db/api/service_requests.js
index dd4cda9..bbbd732 100644
--- a/backend/src/db/api/service_requests.js
+++ b/backend/src/db/api/service_requests.js
@@ -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,
+ ),
+ ],
+ },
],
};
}
diff --git a/backend/src/db/models/invoices.js b/backend/src/db/models/invoices.js
index 2db80c5..f875479 100644
--- a/backend/src/db/models/invoices.js
+++ b/backend/src/db/models/invoices.js
@@ -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',
});
diff --git a/backend/src/db/models/reservations.js b/backend/src/db/models/reservations.js
index ac03b6a..f2a6693 100644
--- a/backend/src/db/models/reservations.js
+++ b/backend/src/db/models/reservations.js
@@ -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',
});
diff --git a/backend/src/db/models/service_requests.js b/backend/src/db/models/service_requests.js
index 1fc8644..7f68342 100644
--- a/backend/src/db/models/service_requests.js
+++ b/backend/src/db/models/service_requests.js
@@ -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',
});
diff --git a/backend/src/db/models/units.js b/backend/src/db/models/units.js
index 89ef816..bf72924 100644
--- a/backend/src/db/models/units.js
+++ b/backend/src/db/models/units.js
@@ -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',
});
diff --git a/backend/src/db/seeders/20260403090000-align-business-role-matrix.js b/backend/src/db/seeders/20260403090000-align-business-role-matrix.js
new file mode 100644
index 0000000..ce5f5d0
--- /dev/null
+++ b/backend/src/db/seeders/20260403090000-align-business-role-matrix.js
@@ -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.
+ },
+};
diff --git a/backend/src/routes/booking_requests.js b/backend/src/routes/booking_requests.js
index eeff265..5b91baa 100644
--- a/backend/src/routes/booking_requests.js
+++ b/backend/src/routes/booking_requests.js
@@ -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);
}));
diff --git a/backend/src/routes/reservations.js b/backend/src/routes/reservations.js
index 02bc7b5..6311c5a 100644
--- a/backend/src/routes/reservations.js
+++ b/backend/src/routes/reservations.js
@@ -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);
}));
diff --git a/backend/src/routes/roles.js b/backend/src/routes/roles.js
index 91ceba8..fc897ea 100644
--- a/backend/src/routes/roles.js
+++ b/backend/src/routes/roles.js
@@ -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);
diff --git a/backend/src/routes/service_requests.js b/backend/src/routes/service_requests.js
index 6b5cf13..2ee2d8e 100644
--- a/backend/src/routes/service_requests.js
+++ b/backend/src/routes/service_requests.js
@@ -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);
}));
diff --git a/backend/src/services/booking_requests.js b/backend/src/services/booking_requests.js
index 6fe1160..6e9563b 100644
--- a/backend/src/services/booking_requests.js
+++ b/backend/src/services/booking_requests.js
@@ -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();
diff --git a/backend/src/services/service_requests.js b/backend/src/services/service_requests.js
index 793b010..4b1f73c 100644
--- a/backend/src/services/service_requests.js
+++ b/backend/src/services/service_requests.js
@@ -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();
diff --git a/frontend/src/components/Roles/TableRoles.tsx b/frontend/src/components/Roles/TableRoles.tsx
index 97a20d0..c433b01 100644
--- a/frontend/src/components/Roles/TableRoles.tsx
+++ b/frontend/src/components/Roles/TableRoles.tsx
@@ -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 }));
};
diff --git a/frontend/src/components/SelectField.tsx b/frontend/src/components/SelectField.tsx
index cab0629..41035ab 100644
--- a/frontend/src/components/SelectField.tsx
+++ b/frontend/src/components/SelectField.tsx
@@ -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),
diff --git a/frontend/src/components/WidgetCreator/RoleSelect.tsx b/frontend/src/components/WidgetCreator/RoleSelect.tsx
index 7cc095c..e8d226a 100644
--- a/frontend/src/components/WidgetCreator/RoleSelect.tsx
+++ b/frontend/src/components/WidgetCreator/RoleSelect.tsx
@@ -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),
diff --git a/frontend/src/menuAside.ts b/frontend/src/menuAside.ts
index d58df1b..9067aaf 100644
--- a/frontend/src/menuAside.ts
+++ b/frontend/src/menuAside.ts
@@ -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,
diff --git a/frontend/src/pages/booking_requests/booking_requests-edit.tsx b/frontend/src/pages/booking_requests/booking_requests-edit.tsx
index c723ba3..9d32008 100644
--- a/frontend/src/pages/booking_requests/booking_requests-edit.tsx
+++ b/frontend/src/pages/booking_requests/booking_requests-edit.tsx
@@ -1,2000 +1,219 @@
-import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js'
+import { mdiChartTimelineVariant } from '@mdi/js'
+import dayjs from 'dayjs'
import Head from 'next/head'
-import React, { ReactElement, useEffect, useState } from 'react'
-import DatePicker from "react-datepicker";
-import "react-datepicker/dist/react-datepicker.css";
-import dayjs from "dayjs";
+import React, { ReactElement, useEffect, useMemo } from 'react'
+import { Field, Form, Formik } from 'formik'
+import { useRouter } from 'next/router'
+import BaseButton from '../../components/BaseButton'
+import BaseButtons from '../../components/BaseButtons'
+import BaseDivider from '../../components/BaseDivider'
import CardBox from '../../components/CardBox'
-import LayoutAuthenticated from '../../layouts/Authenticated'
+import FormField from '../../components/FormField'
import SectionMain from '../../components/SectionMain'
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'
+import { SelectField } from '../../components/SelectField'
import { getPageTitle } from '../../config'
-
-import { Field, Form, Formik } from 'formik'
-import FormField from '../../components/FormField'
-import BaseDivider from '../../components/BaseDivider'
-import BaseButtons from '../../components/BaseButtons'
-import BaseButton from '../../components/BaseButton'
-import FormCheckRadio from '../../components/FormCheckRadio'
-import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'
-import FormFilePicker from '../../components/FormFilePicker'
-import FormImagePicker from '../../components/FormImagePicker'
-import { SelectField } from "../../components/SelectField";
-import { SelectFieldMany } from "../../components/SelectFieldMany";
-import { SwitchField } from '../../components/SwitchField'
-import {RichTextField} from "../../components/RichTextField";
-
-import { update, fetch } from '../../stores/booking_requests/booking_requestsSlice'
+import LayoutAuthenticated from '../../layouts/Authenticated'
+import { fetch, update } from '../../stores/booking_requests/booking_requestsSlice'
import { useAppDispatch, useAppSelector } from '../../stores/hooks'
-import { useRouter } from 'next/router'
-import {saveFile} from "../../helpers/fileSaver";
-import dataFormatter from '../../helpers/dataFormatter';
-import ImageField from "../../components/ImageField";
-import {hasPermission} from "../../helpers/userPermissions";
+const emptyValues = {
+ tenant: null,
+ organization: null,
+ requested_by: null,
+ request_code: '',
+ status: 'submitted',
+ check_in_at: '',
+ check_out_at: '',
+ preferred_property: null,
+ preferred_unit_type: null,
+ preferred_bedrooms: '',
+ guest_count: '',
+ purpose_of_stay: '',
+ special_requirements: '',
+ budget_code: '',
+ max_budget_amount: '',
+ currency: 'USD',
+}
+const formatDateTimeLocal = (value) => {
+ if (!value) {
+ return ''
+ }
+ return dayjs(value).format('YYYY-MM-DDTHH:mm')
+}
+
+const buildInitialValues = (record) => ({
+ tenant: record?.tenant || null,
+ organization: record?.organization || null,
+ requested_by: record?.requested_by || null,
+ request_code: record?.request_code || '',
+ status: record?.status || 'submitted',
+ check_in_at: formatDateTimeLocal(record?.check_in_at),
+ check_out_at: formatDateTimeLocal(record?.check_out_at),
+ preferred_property: record?.preferred_property || null,
+ preferred_unit_type: record?.preferred_unit_type || null,
+ preferred_bedrooms: record?.preferred_bedrooms ?? '',
+ guest_count: record?.guest_count ?? '',
+ purpose_of_stay: record?.purpose_of_stay || '',
+ special_requirements: record?.special_requirements || '',
+ budget_code: record?.budget_code || '',
+ max_budget_amount: record?.max_budget_amount ?? '',
+ currency: record?.currency || 'USD',
+})
const EditBooking_requestsPage = () => {
const router = useRouter()
const dispatch = useAppDispatch()
- const initVals = {
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- tenant: null,
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- organization: null,
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- requested_by: null,
-
-
-
-
-
- 'request_code': '',
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- status: '',
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- check_in_at: new Date(),
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- check_out_at: new Date(),
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- preferred_property: null,
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- preferred_unit_type: null,
-
-
-
-
-
-
-
-
-
-
-
- preferred_bedrooms: '',
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- guest_count: '',
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- purpose_of_stay: '',
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- special_requirements: '',
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 'budget_code': '',
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 'max_budget_amount': '',
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 'currency': '',
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- travelers: [],
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- approval_steps: [],
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- documents: [],
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- comments: [],
-
-
-
- }
- const [initialValues, setInitialValues] = useState(initVals)
-
- const { booking_requests } = useAppSelector((state) => state.booking_requests)
-
- const { currentUser } = useAppSelector((state) => state.auth);
-
-
+ const { currentUser } = useAppSelector((state) => state.auth)
+ const { booking_requests, loading } = useAppSelector((state) => state.booking_requests)
const { id } = router.query
- useEffect(() => {
- dispatch(fetch({ id: id }))
- }, [id])
+ const canManageInternalFields = Boolean(currentUser?.app_role?.globalAccess)
+ const pageTitle = canManageInternalFields ? 'Edit Booking Request' : 'Update Stay Request'
+ const introCopy = canManageInternalFields
+ ? 'Update the booking request and, if needed, adjust internal routing fields.'
+ : 'Update the stay details. Internal ownership, organization routing, and the workflow reference stay system-managed.'
useEffect(() => {
- if (typeof booking_requests === 'object') {
- setInitialValues(booking_requests)
+ if (id) {
+ dispatch(fetch({ id }))
}
+ }, [dispatch, id])
+
+ const initialValues = useMemo(() => {
+ if (booking_requests && typeof booking_requests === 'object' && !Array.isArray(booking_requests)) {
+ return buildInitialValues(booking_requests)
+ }
+
+ return emptyValues
}, [booking_requests])
- useEffect(() => {
- if (typeof booking_requests === 'object') {
- const newInitialVal = {...initVals};
- Object.keys(initVals).forEach(el => newInitialVal[el] = (booking_requests)[el])
- setInitialValues(newInitialVal);
- }
- }, [booking_requests])
+ const handleSubmit = async (values) => {
+ const payload = { ...values }
- const handleSubmit = async (data) => {
- await dispatch(update({ id: id, data }))
+ if (!canManageInternalFields) {
+ delete payload.tenant
+ delete payload.organization
+ delete payload.requested_by
+ delete payload.request_code
+ delete payload.status
+ }
+
+ await dispatch(update({ id, data: payload }))
await router.push('/booking_requests/booking_requests-list')
}
return (
<>
- {getPageTitle('Edit Booking Request')}
+ {getPageTitle(pageTitle)}
+
-
- {''}
+
+ {''}
-
- handleSubmit(values)}
- >
+
+
+
+ {introCopy}
+
+
+
@@ -2006,13 +225,9 @@ const EditBooking_requestsPage = () => {
EditBooking_requestsPage.getLayout = function getLayout(page: ReactElement) {
return (
-
- {page}
-
+
+ {page}
+
)
}
diff --git a/frontend/src/pages/booking_requests/booking_requests-list.tsx b/frontend/src/pages/booking_requests/booking_requests-list.tsx
index e249c3c..3941e8c 100644
--- a/frontend/src/pages/booking_requests/booking_requests-list.tsx
+++ b/frontend/src/pages/booking_requests/booking_requests-list.tsx
@@ -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 ? (
-
+
) : null}
diff --git a/frontend/src/pages/booking_requests/booking_requests-new.tsx b/frontend/src/pages/booking_requests/booking_requests-new.tsx
index 6283eb1..0eceb68 100644
--- a/frontend/src/pages/booking_requests/booking_requests-new.tsx
+++ b/frontend/src/pages/booking_requests/booking_requests-new.tsx
@@ -1,1086 +1,177 @@
-import { mdiAccount, mdiChartTimelineVariant, mdiMail, mdiUpload } from '@mdi/js'
+import { mdiChartTimelineVariant } from '@mdi/js'
import Head from 'next/head'
import React, { ReactElement } from 'react'
+import { Field, Form, Formik } from 'formik'
+import { useRouter } from 'next/router'
+
+import BaseButton from '../../components/BaseButton'
+import BaseButtons from '../../components/BaseButtons'
+import BaseDivider from '../../components/BaseDivider'
import CardBox from '../../components/CardBox'
-import LayoutAuthenticated from '../../layouts/Authenticated'
+import FormField from '../../components/FormField'
import SectionMain from '../../components/SectionMain'
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'
-import { getPageTitle } from '../../config'
-
-import { Field, Form, Formik } from 'formik'
-import FormField from '../../components/FormField'
-import BaseDivider from '../../components/BaseDivider'
-import BaseButtons from '../../components/BaseButtons'
-import BaseButton from '../../components/BaseButton'
-import FormCheckRadio from '../../components/FormCheckRadio'
-import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'
-import FormFilePicker from '../../components/FormFilePicker'
-import FormImagePicker from '../../components/FormImagePicker'
-import { SwitchField } from '../../components/SwitchField'
-
import { SelectField } from '../../components/SelectField'
-import { SelectFieldMany } from "../../components/SelectFieldMany";
-import {RichTextField} from "../../components/RichTextField";
-
+import { getPageTitle } from '../../config'
+import LayoutAuthenticated from '../../layouts/Authenticated'
import { create } from '../../stores/booking_requests/booking_requestsSlice'
-import { useAppDispatch } from '../../stores/hooks'
-import { useRouter } from 'next/router'
-import moment from 'moment';
+import { useAppDispatch, useAppSelector } from '../../stores/hooks'
const initialValues = {
-
-
-
-
-
-
-
-
-
-
-
-
-
- tenant: '',
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- organization: '',
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- requested_by: '',
-
-
-
-
- request_code: '',
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- status: 'draft',
-
-
-
-
-
-
-
-
-
-
-
-
- check_in_at: '',
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- check_out_at: '',
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- preferred_property: '',
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- preferred_unit_type: '',
-
-
-
-
-
-
-
- preferred_bedrooms: '',
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- guest_count: '',
-
-
-
-
-
-
-
-
-
-
-
-
-
- purpose_of_stay: '',
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- special_requirements: '',
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- budget_code: '',
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- max_budget_amount: '',
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- currency: '',
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- travelers: [],
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- approval_steps: [],
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- documents: [],
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- comments: [],
-
-
+ tenant: '',
+ organization: '',
+ requested_by: '',
+ request_code: '',
+ status: 'submitted',
+ check_in_at: '',
+ check_out_at: '',
+ preferred_property: '',
+ preferred_unit_type: '',
+ preferred_bedrooms: '',
+ guest_count: '',
+ purpose_of_stay: '',
+ special_requirements: '',
+ budget_code: '',
+ max_budget_amount: '',
+ currency: 'USD',
}
-
const Booking_requestsNew = () => {
- const router = useRouter()
const dispatch = useAppDispatch()
+ const router = useRouter()
+ const { currentUser } = useAppSelector((state) => state.auth)
-
-
+ const canManageInternalFields = Boolean(currentUser?.app_role?.globalAccess)
+ const pageTitle = canManageInternalFields ? 'New Booking Request' : 'Request a Stay'
+ const introCopy = canManageInternalFields
+ ? 'Create a booking request and optionally manage internal routing fields directly.'
+ : 'Tell us when you need accommodation and what kind of stay you need. Your organization, requester, and reference code will be attached automatically.'
- const handleSubmit = async (data) => {
- await dispatch(create(data))
+ const handleSubmit = async (values) => {
+ const payload = {
+ ...values,
+ ...(canManageInternalFields ? {} : { status: 'submitted' }),
+ }
+
+ if (!canManageInternalFields) {
+ delete payload.tenant
+ delete payload.organization
+ delete payload.requested_by
+ delete payload.request_code
+ }
+
+ await dispatch(create(payload))
await router.push('/booking_requests/booking_requests-list')
}
+
return (
<>
- {getPageTitle('New Booking Request')}
+ {getPageTitle(pageTitle)}
+
-
- {''}
+
+ {''}
-
- handleSubmit(values)}
- >
+
+
+
+ {introCopy}
+
+
+
@@ -1092,13 +183,9 @@ const Booking_requestsNew = () => {
Booking_requestsNew.getLayout = function getLayout(page: ReactElement) {
return (
-
- {page}
-
+
+ {page}
+
)
}
diff --git a/frontend/src/pages/booking_requests/booking_requests-table.tsx b/frontend/src/pages/booking_requests/booking_requests-table.tsx
index 385cd3c..73f62ea 100644
--- a/frontend/src/pages/booking_requests/booking_requests-table.tsx
+++ b/frontend/src/pages/booking_requests/booking_requests-table.tsx
@@ -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(null);
- const [isModalActive, setIsModalActive] = useState(false);
- const [showTableView, setShowTableView] = useState(false);
+ const [filterItems, setFilterItems] = useState([])
+ const [csvFile, setCsvFile] = useState(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 (
<>
- {getPageTitle('Booking_requests')}
+ {getPageTitle('Booking Requests')}
-
- {''}
+
+ {''}
-
- {hasCreatePermission && }
-
-
-
-
- {hasCreatePermission && (
- setIsModalActive(true)}
- />
- )}
-
+ {hasCreatePermission && (
+
+ )}
+
+
+ {hasCreatePermission && (
+ setIsModalActive(true)} />
+ )}
-
-
- Back to
kanban
-
-
+
+ Back to
kanban
+
-
+
+ />
-
+
>
)
@@ -169,13 +150,9 @@ const Booking_requestsTablesPage = () => {
Booking_requestsTablesPage.getLayout = function getLayout(page: ReactElement) {
return (
-
- {page}
-
+
+ {page}
+
)
}
diff --git a/frontend/src/pages/booking_requests/booking_requests-view.tsx b/frontend/src/pages/booking_requests/booking_requests-view.tsx
index 206430a..a0c5e4d 100644
--- a/frontend/src/pages/booking_requests/booking_requests-view.tsx
+++ b/frontend/src/pages/booking_requests/booking_requests-view.tsx
@@ -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 (
<>
- {getPageTitle('Booking Request')}
+ {getPageTitle(pageTitle)}
-
+
{canEdit && (
@@ -141,20 +147,22 @@ const BookingRequestsView = () => {
Overview
-
Guest request snapshot
-
- A light summary of who requested the stay, where they prefer to stay, and what needs follow-up.
-
+
{overviewHeading}
+
{overviewCopy}
-
-
- {hasPermission(currentUser, 'READ_ORGANIZATIONS') && (
-
+ {canManageInternalFields && (
+ <>
+
+
+ {hasPermission(currentUser, 'READ_ORGANIZATIONS') && (
+
+ )}
+ >
)}
diff --git a/frontend/src/pages/command-center.tsx b/frontend/src/pages/command-center.tsx
index cd84744..24bffb4 100644
--- a/frontend/src/pages/command-center.tsx
+++ b/frontend/src/pages/command-center.tsx
@@ -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) => (
);
+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) => (
@@ -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 = () => {
@@ -768,30 +877,24 @@ const CommandCenterPage = () => {
-
-
Corporate accounts
-
{overview.organizations.total}
-
Organizations in the current operating scope.
-
-
-
Live properties
-
{overview.inventory.activeProperties}
-
Properties open for negotiated corporate stays.
-
-
-
Last refresh
-
{formatDateTime(overview.generatedAt)}
-
Live snapshot across operations and billing.
-
+ {heroStats.map((stat) => (
+
+
{stat.title}
+
{stat.value}
+
{stat.detail}
+
+ ))}
-
- {metricCards.map((card) => (
-
- ))}
-
+ {metricCards.length ? (
+
+ {metricCards.map((card) => (
+
+ ))}
+
+ ) : null}
@@ -838,38 +941,44 @@ const CommandCenterPage = () => {
- {focusItems.map((item) => (
-
- ))}
+ {focusItems.length ? (
+ focusItems.map((item) =>
)
+ ) : (
+
+ This lane will populate as more role permissions are enabled.
+
+ )}
-
-
-
-
Interconnected operating model
-
- All three account types land in the same command center, but each role acts on a different part of the same live workflow.
-
+ {connectedWorkflow.length ? (
+
+
+
+
Interconnected operating model
+
+ Customers, concierge, administrators, and super admins share the same command center, but each role acts on a different part of the same live workflow.
+
+
-
-
- {connectedWorkflow.map((step) => (
-
-
{step.owner}
-
{step.title}
-
{step.value}
-
{step.detail}
-
- ))}
-
-
+
+ {connectedWorkflow.map((step) => (
+
+
{step.owner}
+
{step.title}
+
{step.value}
+
{step.detail}
+
+ ))}
+
+
+ ) : null}
{activitySections.map((section) => (
diff --git a/frontend/src/pages/index.tsx b/frontend/src/pages/index.tsx
index 0f0ba31..09b8ebf 100644
--- a/frontend/src/pages/index.tsx
+++ b/frontend/src/pages/index.tsx
@@ -64,7 +64,7 @@ export default function HomePage() {
-
+
@@ -157,10 +157,10 @@ export default function HomePage() {
Administration
-
Admin dashboard
-
Access entity management, permissions, and full system configuration.
+
Role-based workspace
+
Land in one shared command center, then branch into the modules your role actually owns.
-
+
diff --git a/frontend/src/pages/login.tsx b/frontend/src/pages/login.tsx
index 64c2ef0..d7cf2e2 100644
--- a/frontend/src/pages/login.tsx
+++ b/frontend/src/pages/login.tsx
@@ -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{' / '}
946cafba{' / '}
to login as Admin
+ Use setLogin(e.target)}>john@doe.com{' / '}
+ bc40952c93f4{' / '}
+ to login as Concierge
Use setLogin(e.target)}>client@hello.com{' / '}
bc40952c93f4{' / '}
- to login as User
+ to login as Customer
{
};
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')
diff --git a/frontend/src/pages/roles/roles-table.tsx b/frontend/src/pages/roles/roles-table.tsx
index ac487a8..19427d8 100644
--- a/frontend/src/pages/roles/roles-table.tsx
+++ b/frontend/src/pages/roles/roles-table.tsx
@@ -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')
diff --git a/frontend/src/pages/search.tsx b/frontend/src/pages/search.tsx
index 005eb07..fe98237 100644
--- a/frontend/src/pages/search.tsx
+++ b/frontend/src/pages/search.tsx
@@ -74,7 +74,7 @@ const SearchView = () => {
router.push('/dashboard')}
+ onClick={() => router.push('/command-center')}
/>