diff --git a/.perm_test_apache b/.perm_test_apache
new file mode 100644
index 0000000..e69de29
diff --git a/.perm_test_exec b/.perm_test_exec
new file mode 100644
index 0000000..e69de29
diff --git a/backend/src/db/models/booking_requests.js b/backend/src/db/models/booking_requests.js
index 6fa79e3..69034be 100644
--- a/backend/src/db/models/booking_requests.js
+++ b/backend/src/db/models/booking_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 booking_requests = sequelize.define(
'booking_requests',
@@ -14,111 +8,59 @@ module.exports = function(sequelize, DataTypes) {
primaryKey: true,
},
-request_code: {
+ request_code: {
type: DataTypes.TEXT,
-
-
-
},
-status: {
+ status: {
type: DataTypes.ENUM,
-
-
-
values: [
-
-"draft",
-
-
-"submitted",
-
-
-"in_review",
-
-
-"changes_requested",
-
-
-"approved",
-
-
-"rejected",
-
-
-"expired",
-
-
-"converted_to_reservation",
-
-
-"canceled"
-
+ 'draft',
+ 'submitted',
+ 'in_review',
+ 'changes_requested',
+ 'approved',
+ 'rejected',
+ 'expired',
+ 'converted_to_reservation',
+ 'canceled',
],
-
},
-check_in_at: {
+ check_in_at: {
type: DataTypes.DATE,
-
-
-
},
-check_out_at: {
+ check_out_at: {
type: DataTypes.DATE,
-
-
-
},
-preferred_bedrooms: {
+ preferred_bedrooms: {
type: DataTypes.INTEGER,
-
-
-
},
-guest_count: {
+ guest_count: {
type: DataTypes.INTEGER,
-
-
-
},
-purpose_of_stay: {
+ purpose_of_stay: {
type: DataTypes.TEXT,
-
-
-
},
-special_requirements: {
+ special_requirements: {
type: DataTypes.TEXT,
-
-
-
},
-budget_code: {
+ budget_code: {
type: DataTypes.TEXT,
-
-
-
},
-max_budget_amount: {
+ max_budget_amount: {
type: DataTypes.DECIMAL,
-
-
-
},
-currency: {
+ currency: {
type: DataTypes.TEXT,
-
-
-
},
importHash: {
@@ -135,157 +77,110 @@ currency: {
);
booking_requests.associate = (db) => {
-
- db.booking_requests.belongsToMany(db.booking_request_travelers, {
+ db.booking_requests.hasMany(db.booking_request_travelers, {
as: 'travelers',
foreignKey: {
- name: 'booking_requests_travelersId',
+ name: 'booking_requestId',
},
constraints: false,
- through: 'booking_requestsTravelersBooking_request_travelers',
});
- db.booking_requests.belongsToMany(db.booking_request_travelers, {
+ db.booking_requests.hasMany(db.booking_request_travelers, {
as: 'travelers_filter',
foreignKey: {
- name: 'booking_requests_travelersId',
+ name: 'booking_requestId',
},
constraints: false,
- through: 'booking_requestsTravelersBooking_request_travelers',
});
- db.booking_requests.belongsToMany(db.approval_steps, {
+ db.booking_requests.hasMany(db.approval_steps, {
as: 'approval_steps',
foreignKey: {
- name: 'booking_requests_approval_stepsId',
+ name: 'booking_requestId',
},
constraints: false,
- through: 'booking_requestsApproval_stepsApproval_steps',
});
- db.booking_requests.belongsToMany(db.approval_steps, {
+ db.booking_requests.hasMany(db.approval_steps, {
as: 'approval_steps_filter',
foreignKey: {
- name: 'booking_requests_approval_stepsId',
+ name: 'booking_requestId',
},
constraints: false,
- through: 'booking_requestsApproval_stepsApproval_steps',
});
- db.booking_requests.belongsToMany(db.documents, {
+ db.booking_requests.hasMany(db.documents, {
as: 'documents',
foreignKey: {
- name: 'booking_requests_documentsId',
+ name: 'booking_requestId',
},
constraints: false,
- through: 'booking_requestsDocumentsDocuments',
});
- db.booking_requests.belongsToMany(db.documents, {
+ db.booking_requests.hasMany(db.documents, {
as: 'documents_filter',
foreignKey: {
- name: 'booking_requests_documentsId',
+ name: 'booking_requestId',
},
constraints: false,
- through: 'booking_requestsDocumentsDocuments',
});
- db.booking_requests.belongsToMany(db.activity_comments, {
+ db.booking_requests.hasMany(db.activity_comments, {
as: 'comments',
foreignKey: {
- name: 'booking_requests_commentsId',
+ name: 'booking_requestId',
},
constraints: false,
- through: 'booking_requestsCommentsActivity_comments',
});
- db.booking_requests.belongsToMany(db.activity_comments, {
+ db.booking_requests.hasMany(db.activity_comments, {
as: 'comments_filter',
foreignKey: {
- name: 'booking_requests_commentsId',
+ name: 'booking_requestId',
},
constraints: false,
- through: 'booking_requestsCommentsActivity_comments',
});
-
-/// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
db.booking_requests.hasMany(db.booking_request_travelers, {
as: 'booking_request_travelers_booking_request',
foreignKey: {
- name: 'booking_requestId',
+ name: 'booking_requestId',
},
constraints: false,
});
-
db.booking_requests.hasMany(db.approval_steps, {
as: 'approval_steps_booking_request',
foreignKey: {
- name: 'booking_requestId',
+ name: 'booking_requestId',
},
constraints: false,
});
-
db.booking_requests.hasMany(db.reservations, {
as: 'reservations_booking_request',
foreignKey: {
- name: 'booking_requestId',
+ name: 'booking_requestId',
},
constraints: false,
});
-
-
-
-
-
-
db.booking_requests.hasMany(db.documents, {
as: 'documents_booking_request',
foreignKey: {
- name: 'booking_requestId',
+ name: 'booking_requestId',
},
constraints: false,
});
-
-
-
db.booking_requests.hasMany(db.activity_comments, {
as: 'activity_comments_booking_request',
foreignKey: {
- name: 'booking_requestId',
+ name: 'booking_requestId',
},
constraints: false,
});
-
-
-
-
-
-//end loop
-
-
-
db.booking_requests.belongsTo(db.tenants, {
as: 'tenant',
foreignKey: {
@@ -326,9 +221,6 @@ currency: {
constraints: false,
});
-
-
-
db.booking_requests.belongsTo(db.users, {
as: 'createdBy',
});
@@ -338,9 +230,5 @@ currency: {
});
};
-
-
return booking_requests;
};
-
-
diff --git a/backend/src/db/models/properties.js b/backend/src/db/models/properties.js
index 25b0587..88bf090 100644
--- a/backend/src/db/models/properties.js
+++ b/backend/src/db/models/properties.js
@@ -1,8 +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 properties = sequelize.define(
@@ -88,58 +83,52 @@ is_active: {
properties.associate = (db) => {
- db.properties.belongsToMany(db.unit_types, {
+ db.properties.hasMany(db.unit_types, {
as: 'unit_types',
foreignKey: {
- name: 'properties_unit_typesId',
+ name: 'propertyId',
},
constraints: false,
- through: 'propertiesUnit_typesUnit_types',
});
- db.properties.belongsToMany(db.unit_types, {
+ db.properties.hasMany(db.unit_types, {
as: 'unit_types_filter',
foreignKey: {
- name: 'properties_unit_typesId',
+ name: 'propertyId',
},
constraints: false,
- through: 'propertiesUnit_typesUnit_types',
});
- db.properties.belongsToMany(db.units, {
+ db.properties.hasMany(db.units, {
as: 'units',
foreignKey: {
- name: 'properties_unitsId',
+ name: 'propertyId',
},
constraints: false,
- through: 'propertiesUnitsUnits',
});
- db.properties.belongsToMany(db.units, {
+ db.properties.hasMany(db.units, {
as: 'units_filter',
foreignKey: {
- name: 'properties_unitsId',
+ name: 'propertyId',
},
constraints: false,
- through: 'propertiesUnitsUnits',
});
- db.properties.belongsToMany(db.amenities, {
+ db.properties.hasMany(db.amenities, {
as: 'amenities',
foreignKey: {
- name: 'properties_amenitiesId',
+ name: 'propertyId',
},
constraints: false,
- through: 'propertiesAmenitiesAmenities',
});
- db.properties.belongsToMany(db.amenities, {
+ db.properties.hasMany(db.amenities, {
as: 'amenities_filter',
foreignKey: {
- name: 'properties_amenitiesId',
+ name: 'propertyId',
},
constraints: false,
- through: 'propertiesAmenitiesAmenities',
});
diff --git a/backend/src/db/models/unit_types.js b/backend/src/db/models/unit_types.js
index 6aa4b23..d94ee28 100644
--- a/backend/src/db/models/unit_types.js
+++ b/backend/src/db/models/unit_types.js
@@ -1,8 +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 unit_types = sequelize.define(
@@ -99,22 +94,20 @@ minimum_stay_nights: {
unit_types.associate = (db) => {
- db.unit_types.belongsToMany(db.units, {
+ db.unit_types.hasMany(db.units, {
as: 'units',
foreignKey: {
- name: 'unit_types_unitsId',
+ name: 'unit_typeId',
},
constraints: false,
- through: 'unit_typesUnitsUnits',
});
- db.unit_types.belongsToMany(db.units, {
+ db.unit_types.hasMany(db.units, {
as: 'units_filter',
foreignKey: {
- name: 'unit_types_unitsId',
+ name: 'unit_typeId',
},
constraints: false,
- through: 'unit_typesUnitsUnits',
});
diff --git a/backend/src/index.js b/backend/src/index.js
index aab8a61..bdeb726 100644
--- a/backend/src/index.js
+++ b/backend/src/index.js
@@ -81,6 +81,8 @@ const checklist_itemsRoutes = require('./routes/checklist_items');
const job_runsRoutes = require('./routes/job_runs');
+const corporateStayPortalRoutes = require('./routes/corporate_stay_portal');
+
const getBaseUrl = (url) => {
if (!url) return '';
@@ -197,6 +199,8 @@ app.use('/api/checklist_items', passport.authenticate('jwt', {session: false}),
app.use('/api/job_runs', passport.authenticate('jwt', {session: false}), job_runsRoutes);
+app.use('/api/corporate-stay-portal', passport.authenticate('jwt', {session: false}), corporateStayPortalRoutes);
+
app.use(
'/api/openai',
passport.authenticate('jwt', { session: false }),
diff --git a/backend/src/routes/corporate_stay_portal.js b/backend/src/routes/corporate_stay_portal.js
new file mode 100644
index 0000000..7e51cb7
--- /dev/null
+++ b/backend/src/routes/corporate_stay_portal.js
@@ -0,0 +1,437 @@
+const express = require('express');
+const { Op } = require('sequelize');
+
+const db = require('../db/models');
+const wrapAsync = require('../helpers').wrapAsync;
+
+const router = express.Router();
+
+const bookingPermissions = ['READ_BOOKING_REQUESTS'];
+const approvalPermissions = ['READ_APPROVAL_STEPS'];
+const reservationPermissions = ['READ_RESERVATIONS'];
+const servicePermissions = ['READ_SERVICE_REQUESTS'];
+const financePermissions = ['READ_INVOICES'];
+const inventoryPermissions = ['READ_PROPERTIES', 'READ_UNITS'];
+const accountPermissions = ['READ_ORGANIZATIONS'];
+
+function getPermissionSet(currentUser) {
+ return new Set([
+ ...((currentUser?.custom_permissions || []).map((permission) => permission.name)),
+ ...((currentUser?.app_role_permissions || []).map((permission) => permission.name)),
+ ]);
+}
+
+function hasAnyPermission(currentUser, permissions) {
+ if (currentUser?.app_role?.globalAccess) {
+ return true;
+ }
+
+ const permissionSet = getPermissionSet(currentUser);
+ return permissions.some((permission) => permissionSet.has(permission));
+}
+
+function scopedWhere(currentUser, key, extraWhere = {}) {
+ if (currentUser?.app_role?.globalAccess || !currentUser?.organizationId) {
+ return extraWhere;
+ }
+
+ return {
+ ...extraWhere,
+ [key]: currentUser.organizationId,
+ };
+}
+
+function groupRowsToCounts(rows, field) {
+ return rows.reduce((accumulator, row) => {
+ accumulator[row[field]] = Number(row.count || 0);
+ return accumulator;
+ }, {});
+}
+
+function formatUserDisplay(user) {
+ if (!user) {
+ return null;
+ }
+
+ const fullName = [user.firstName, user.lastName].filter(Boolean).join(' ').trim();
+ return fullName || user.email || 'Assigned staff';
+}
+
+async function getGroupedCounts(model, statusField, where) {
+ const rows = await model.findAll({
+ attributes: [
+ statusField,
+ [db.sequelize.fn('COUNT', db.sequelize.col('id')), 'count'],
+ ],
+ where,
+ group: [statusField],
+ raw: true,
+ });
+
+ return groupRowsToCounts(rows, statusField);
+}
+
+router.get(
+ '/overview',
+ wrapAsync(async (req, res) => {
+ const { currentUser } = req;
+ const now = new Date();
+ const nextSevenDays = new Date(now);
+ nextSevenDays.setDate(nextSevenDays.getDate() + 7);
+
+ const access = {
+ accounts: hasAnyPermission(currentUser, accountPermissions),
+ bookings: hasAnyPermission(currentUser, bookingPermissions),
+ approvals: hasAnyPermission(currentUser, approvalPermissions),
+ reservations: hasAnyPermission(currentUser, reservationPermissions),
+ serviceRequests: hasAnyPermission(currentUser, servicePermissions),
+ invoices: hasAnyPermission(currentUser, financePermissions),
+ inventory: hasAnyPermission(currentUser, inventoryPermissions),
+ };
+
+ const response = {
+ generatedAt: now.toISOString(),
+ access,
+ organizations: {
+ total: 0,
+ },
+ bookingRequests: {
+ statusCounts: {},
+ pendingReview: 0,
+ approvedReady: 0,
+ recent: [],
+ },
+ approvals: {
+ pending: 0,
+ },
+ reservations: {
+ statusCounts: {},
+ upcomingArrivals: 0,
+ upcomingDepartures: 0,
+ inHouse: 0,
+ recent: [],
+ },
+ serviceRequests: {
+ statusCounts: {},
+ open: 0,
+ urgent: 0,
+ recent: [],
+ },
+ invoices: {
+ statusCounts: {},
+ openBalance: 0,
+ recent: [],
+ },
+ inventory: {
+ activeProperties: 0,
+ unitStatusCounts: {},
+ },
+ };
+
+ if (access.accounts) {
+ response.organizations.total = await db.organizations.count({
+ where: currentUser?.app_role?.globalAccess
+ ? {}
+ : { id: currentUser.organizationId },
+ });
+ }
+
+ if (access.bookings) {
+ const bookingWhere = scopedWhere(currentUser, 'organizationId');
+ response.bookingRequests.statusCounts = await getGroupedCounts(
+ db.booking_requests,
+ 'status',
+ bookingWhere,
+ );
+ response.bookingRequests.pendingReview = [
+ 'submitted',
+ 'in_review',
+ 'changes_requested',
+ ].reduce(
+ (sum, status) => sum + (response.bookingRequests.statusCounts[status] || 0),
+ 0,
+ );
+ response.bookingRequests.approvedReady = (
+ response.bookingRequests.statusCounts.approved || 0
+ );
+
+ const recentBookingRequests = await db.booking_requests.findAll({
+ where: bookingWhere,
+ attributes: [
+ 'id',
+ 'request_code',
+ 'status',
+ 'check_in_at',
+ 'check_out_at',
+ 'guest_count',
+ 'preferred_bedrooms',
+ 'updatedAt',
+ ],
+ include: [
+ {
+ model: db.organizations,
+ as: 'organization',
+ attributes: ['id', 'name'],
+ required: false,
+ },
+ {
+ model: db.properties,
+ as: 'preferred_property',
+ attributes: ['id', 'name', 'city'],
+ required: false,
+ },
+ {
+ model: db.unit_types,
+ as: 'preferred_unit_type',
+ attributes: ['id', 'name'],
+ required: false,
+ },
+ {
+ model: db.users,
+ as: 'requested_by',
+ attributes: ['id', 'firstName', 'lastName', 'email'],
+ required: false,
+ },
+ ],
+ order: [['updatedAt', 'DESC']],
+ limit: 5,
+ });
+
+ response.bookingRequests.recent = recentBookingRequests.map((item) => ({
+ id: item.id,
+ request_code: item.request_code,
+ status: item.status,
+ check_in_at: item.check_in_at,
+ check_out_at: item.check_out_at,
+ guest_count: item.guest_count,
+ preferred_bedrooms: item.preferred_bedrooms,
+ updatedAt: item.updatedAt,
+ organizationName: item.organization?.name || 'Unassigned account',
+ requestedBy: formatUserDisplay(item.requested_by),
+ propertyName: item.preferred_property?.name || 'Open inventory',
+ unitTypeName: item.preferred_unit_type?.name || 'Any unit type',
+ }));
+ }
+
+ if (access.approvals) {
+ response.approvals.pending = await db.approval_steps.count({
+ where: scopedWhere(currentUser, 'organizationsId', { decision: 'pending' }),
+ });
+ }
+
+ if (access.reservations) {
+ const reservationWhere = scopedWhere(currentUser, 'organizationId');
+ response.reservations.statusCounts = await getGroupedCounts(
+ db.reservations,
+ 'status',
+ reservationWhere,
+ );
+ response.reservations.upcomingArrivals = await db.reservations.count({
+ where: scopedWhere(currentUser, 'organizationId', {
+ status: { [Op.in]: ['confirmed', 'checked_in'] },
+ check_in_at: { [Op.between]: [now, nextSevenDays] },
+ }),
+ });
+ response.reservations.upcomingDepartures = await db.reservations.count({
+ where: scopedWhere(currentUser, 'organizationId', {
+ status: { [Op.in]: ['confirmed', 'checked_in'] },
+ check_out_at: { [Op.between]: [now, nextSevenDays] },
+ }),
+ });
+ response.reservations.inHouse = await db.reservations.count({
+ where: scopedWhere(currentUser, 'organizationId', {
+ status: 'checked_in',
+ }),
+ });
+
+ const recentReservations = await db.reservations.findAll({
+ where: reservationWhere,
+ attributes: [
+ 'id',
+ 'reservation_code',
+ 'status',
+ 'check_in_at',
+ 'check_out_at',
+ 'guest_count',
+ 'currency',
+ 'nightly_rate',
+ 'updatedAt',
+ ],
+ include: [
+ {
+ model: db.organizations,
+ as: 'organization',
+ attributes: ['id', 'name'],
+ required: false,
+ },
+ {
+ model: db.properties,
+ as: 'property',
+ attributes: ['id', 'name', 'city'],
+ required: false,
+ },
+ {
+ model: db.units,
+ as: 'unit',
+ attributes: ['id', 'unit_number', 'status'],
+ required: false,
+ },
+ {
+ model: db.booking_requests,
+ as: 'booking_request',
+ attributes: ['id', 'request_code'],
+ required: false,
+ },
+ ],
+ order: [['check_in_at', 'ASC']],
+ limit: 5,
+ });
+
+ response.reservations.recent = recentReservations.map((item) => ({
+ id: item.id,
+ reservation_code: item.reservation_code,
+ status: item.status,
+ check_in_at: item.check_in_at,
+ check_out_at: item.check_out_at,
+ guest_count: item.guest_count,
+ updatedAt: item.updatedAt,
+ organizationName: item.organization?.name || 'Unassigned account',
+ propertyName: item.property?.name || 'Property pending',
+ unitNumber: item.unit?.unit_number || 'TBD',
+ sourceRequestCode: item.booking_request?.request_code || null,
+ }));
+ }
+
+ if (access.serviceRequests) {
+ const serviceWhere = scopedWhere(currentUser, 'organizationsId');
+ response.serviceRequests.statusCounts = await getGroupedCounts(
+ db.service_requests,
+ 'status',
+ serviceWhere,
+ );
+ response.serviceRequests.open = Object.entries(response.serviceRequests.statusCounts)
+ .filter(([status]) => !['completed', 'canceled'].includes(status))
+ .reduce((sum, [, count]) => sum + Number(count || 0), 0);
+ response.serviceRequests.urgent = await db.service_requests.count({
+ where: scopedWhere(currentUser, 'organizationsId', {
+ priority: 'urgent',
+ status: { [Op.notIn]: ['completed', 'canceled'] },
+ }),
+ });
+
+ const recentServiceRequests = await db.service_requests.findAll({
+ where: serviceWhere,
+ attributes: [
+ 'id',
+ 'request_type',
+ 'status',
+ 'priority',
+ 'summary',
+ 'due_at',
+ 'updatedAt',
+ ],
+ include: [
+ {
+ model: db.reservations,
+ as: 'reservation',
+ attributes: ['id', 'reservation_code'],
+ required: false,
+ },
+ {
+ model: db.users,
+ as: 'assigned_to',
+ attributes: ['id', 'firstName', 'lastName', 'email'],
+ required: false,
+ },
+ ],
+ order: [['updatedAt', 'DESC']],
+ limit: 5,
+ });
+
+ response.serviceRequests.recent = recentServiceRequests.map((item) => ({
+ id: item.id,
+ request_type: item.request_type,
+ status: item.status,
+ priority: item.priority,
+ summary: item.summary,
+ due_at: item.due_at,
+ updatedAt: item.updatedAt,
+ reservationCode: item.reservation?.reservation_code || null,
+ assignedTo: formatUserDisplay(item.assigned_to),
+ }));
+ }
+
+ if (access.invoices) {
+ const invoiceWhere = scopedWhere(currentUser, 'organizationId');
+ response.invoices.statusCounts = await getGroupedCounts(
+ db.invoices,
+ 'status',
+ invoiceWhere,
+ );
+ const openBalance = await db.invoices.sum('balance_due', {
+ where: scopedWhere(currentUser, 'organizationId', {
+ status: { [Op.in]: ['issued', 'overdue', 'partially_paid'] },
+ }),
+ });
+ response.invoices.openBalance = Number(openBalance || 0);
+
+ const recentInvoices = await db.invoices.findAll({
+ where: invoiceWhere,
+ attributes: [
+ 'id',
+ 'invoice_number',
+ 'status',
+ 'total_amount',
+ 'balance_due',
+ 'currency',
+ 'due_at',
+ 'updatedAt',
+ ],
+ include: [
+ {
+ model: db.organizations,
+ as: 'organization',
+ attributes: ['id', 'name'],
+ required: false,
+ },
+ {
+ model: db.reservations,
+ as: 'reservation',
+ attributes: ['id', 'reservation_code'],
+ required: false,
+ },
+ ],
+ order: [['updatedAt', 'DESC']],
+ limit: 5,
+ });
+
+ response.invoices.recent = recentInvoices.map((item) => ({
+ id: item.id,
+ invoice_number: item.invoice_number,
+ status: item.status,
+ total_amount: Number(item.total_amount || 0),
+ balance_due: Number(item.balance_due || 0),
+ currency: item.currency || 'USD',
+ due_at: item.due_at,
+ updatedAt: item.updatedAt,
+ organizationName: item.organization?.name || 'Unassigned account',
+ reservationCode: item.reservation?.reservation_code || null,
+ }));
+ }
+
+ if (access.inventory) {
+ response.inventory.activeProperties = await db.properties.count({
+ where: scopedWhere(currentUser, 'organizationsId', { is_active: true }),
+ });
+ response.inventory.unitStatusCounts = await getGroupedCounts(
+ db.units,
+ 'status',
+ scopedWhere(currentUser, 'organizationsId'),
+ );
+ }
+
+ res.status(200).send(response);
+ }),
+);
+
+module.exports = router;
diff --git a/frontend/next.config.mjs b/frontend/next.config.mjs
index 89767ec..00d0a57 100644
--- a/frontend/next.config.mjs
+++ b/frontend/next.config.mjs
@@ -3,8 +3,22 @@
*/
const output = process.env.NODE_ENV === 'production' ? 'export' : 'standalone';
+const backendProxyTarget = process.env.BACKEND_INTERNAL_URL || 'http://127.0.0.1:3000';
+
const nextConfig = {
trailingSlash: true,
+ async rewrites() {
+ if (process.env.NODE_ENV === 'production') {
+ return [];
+ }
+
+ return [
+ {
+ source: '/api/:path*',
+ destination: `${backendProxyTarget}/api/:path*`,
+ },
+ ];
+ },
distDir: 'build',
output,
basePath: "",
diff --git a/frontend/src/components/AsideMenuItem.tsx b/frontend/src/components/AsideMenuItem.tsx
index dbb09b2..8acc175 100644
--- a/frontend/src/components/AsideMenuItem.tsx
+++ b/frontend/src/components/AsideMenuItem.tsx
@@ -15,66 +15,58 @@ type Props = {
const AsideMenuItem = ({ item, isDropdownList = false }: Props) => {
const [isLinkActive, setIsLinkActive] = useState(false)
- const [isDropdownActive, setIsDropdownActive] = useState(false)
+ const [isDropdownActive, setIsDropdownActive] = useState(!isDropdownList && !!item.menu)
const asideMenuItemStyle = useAppSelector((state) => state.style.asideMenuItemStyle)
const asideMenuDropdownStyle = useAppSelector((state) => state.style.asideMenuDropdownStyle)
const asideMenuItemActiveStyle = useAppSelector((state) => state.style.asideMenuItemActiveStyle)
- const borders = useAppSelector((state) => state.style.borders);
- const activeLinkColor = useAppSelector(
- (state) => state.style.activeLinkColor,
- );
+ const borders = useAppSelector((state) => state.style.borders)
+ const activeLinkColor = useAppSelector((state) => state.style.activeLinkColor)
const activeClassAddon = !item.color && isLinkActive ? asideMenuItemActiveStyle : ''
const { asPath, isReady } = useRouter()
useEffect(() => {
if (item.href && isReady) {
- const linkPathName = new URL(item.href, location.href).pathname + '/';
+ const linkPathName = new URL(item.href, location.href).pathname + '/'
const activePathname = new URL(asPath, location.href).pathname
- const activeView = activePathname.split('/')[1];
- const linkPathNameView = linkPathName.split('/')[1];
+ const activeView = activePathname.split('/')[1]
+ const linkPathNameView = linkPathName.split('/')[1]
- setIsLinkActive(linkPathNameView === activeView);
+ setIsLinkActive(linkPathNameView === activeView)
}
}, [item.href, isReady, asPath])
const asideMenuItemInnerContents = (
<>
{item.icon && (
-