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 ( <>
-Overview
-- A light summary of who requested the stay, where they prefer to stay, and what needs follow-up. -
+{overviewCopy}
- 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 ? ( ++ Customers, concierge, administrators, and super admins share the same command center, but each role acts on a different part of the same live workflow. +
+{step.detail}
- - ))} -{step.detail}
+ + ))} +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