Autosave: 20260404-155315

This commit is contained in:
Flatlogic Bot 2026-04-04 15:53:16 +00:00
parent 95c088fa21
commit d8c8294cc5
13 changed files with 460 additions and 178 deletions

View File

@ -570,7 +570,7 @@ module.exports = class Booking_requestsDBApi {
if (userOrganizations) {
if (options?.currentUser?.organizationsId) {
where.organizationsId = options.currentUser.organizationsId;
where.organizationId = options.currentUser.organizationsId;
}
}
@ -1036,7 +1036,7 @@ module.exports = class Booking_requestsDBApi {
if (globalAccess) {
delete where.organizationsId;
delete where.organizationId;
}

View File

@ -607,15 +607,18 @@ module.exports = class ReservationsDBApi {
const user = (options && options.currentUser) || null;
const userOrganizations = (user && user.organizations?.id) || null;
const userOrganizationId =
user?.organization?.id ||
user?.organizations?.id ||
user?.organizationId ||
user?.organizationsId ||
null;
const isCustomer = isCustomerUser(user);
if (userOrganizations) {
if (options?.currentUser?.organizationsId) {
where.organizationsId = options.currentUser.organizationsId;
}
if (!globalAccess && userOrganizationId) {
where.organizationId = userOrganizationId;
}
@ -1196,7 +1199,7 @@ module.exports = class ReservationsDBApi {
if (globalAccess) {
delete where.organizationsId;
delete where.organizationId;
}

View File

@ -49,11 +49,13 @@ const CurrentWorkspaceChip = () => {
currentUser?.organizationsId,
],
)
const canViewOrganizations = hasPermission(currentUser, 'READ_ORGANIZATIONS')
const canViewTenants = hasPermission(currentUser, 'READ_TENANTS')
useEffect(() => {
let isMounted = true
if (!organizationId) {
if (!organizationId || !canViewTenants) {
setLinkedTenantSummary(emptyOrganizationTenantSummary)
setIsLoadingTenants(false)
return () => {
@ -89,7 +91,7 @@ const CurrentWorkspaceChip = () => {
return () => {
isMounted = false
}
}, [organizationId])
}, [canViewTenants, organizationId])
useEffect(() => {
setIsPopoverActive(false)
@ -139,11 +141,18 @@ const CurrentWorkspaceChip = () => {
const appRoleName = currentUser?.app_role?.name || 'User'
const linkedTenants = Array.isArray(linkedTenantSummary.rows) ? linkedTenantSummary.rows : []
const linkedTenantCount = linkedTenantSummary.count || 0
const tenantLabel = isLoadingTenants
? 'Loading tenants…'
: `${linkedTenantCount} tenant${linkedTenantCount === 1 ? '' : 's'}`
const canViewOrganizations = hasPermission(currentUser, 'READ_ORGANIZATIONS')
const canViewTenants = hasPermission(currentUser, 'READ_TENANTS')
const tenantLabel = !organizationId
? 'No workspace linked'
: !canViewTenants
? 'Tenant access restricted'
: isLoadingTenants
? 'Loading tenants…'
: `${linkedTenantCount} tenant${linkedTenantCount === 1 ? '' : 's'}`
const tenantEmptyStateMessage = !organizationId
? 'No organization is attached to this account yet.'
: !canViewTenants
? 'Your current role can open the workspace, but it does not include tenant-read access.'
: 'No tenant link surfaced yet for this workspace.'
const organizationHref =
canViewOrganizations && organizationId ? getOrganizationViewHref(organizationId) : '/profile'
const hasOrganizationMetadata = Boolean(organizationSlug || organizationDomain)
@ -340,7 +349,7 @@ const CurrentWorkspaceChip = () => {
))
) : (
<div className="rounded-xl border border-dashed border-gray-200 bg-gray-50 px-3 py-3 text-sm text-gray-600 dark:border-dark-700 dark:bg-dark-800 dark:text-gray-300">
No tenant link surfaced yet for this workspace.
{tenantEmptyStateMessage}
</div>
)}
</div>
@ -464,7 +473,7 @@ const CurrentWorkspaceChip = () => {
</div>
) : (
<div className="rounded-xl border border-dashed border-gray-200 bg-gray-50 px-4 py-3 text-sm text-gray-600 dark:border-dark-700 dark:bg-dark-800 dark:text-gray-300">
No tenant link surfaced yet for this workspace.
{tenantEmptyStateMessage}
</div>
)}
</div>

View File

@ -41,11 +41,13 @@ const MyOrgTenantSummary = ({ className = '' }: MyOrgTenantSummaryProps) => {
currentUser?.organization?.name ||
currentUser?.organizationName ||
'No organization assigned yet'
const canViewOrganizations = hasPermission(currentUser, 'READ_ORGANIZATIONS')
const canViewTenants = hasPermission(currentUser, 'READ_TENANTS')
useEffect(() => {
let isMounted = true
if (!organizationId) {
if (!organizationId || !canViewTenants) {
setLinkedTenantSummary(emptyOrganizationTenantSummary)
setIsLoadingTenants(false)
return () => {
@ -81,7 +83,7 @@ const MyOrgTenantSummary = ({ className = '' }: MyOrgTenantSummaryProps) => {
return () => {
isMounted = false
}
}, [organizationId])
}, [canViewTenants, organizationId])
if (!currentUser) {
return null
@ -89,8 +91,18 @@ const MyOrgTenantSummary = ({ className = '' }: MyOrgTenantSummaryProps) => {
const appRoleName = currentUser?.app_role?.name || 'No role surfaced yet'
const linkedTenants = Array.isArray(linkedTenantSummary.rows) ? linkedTenantSummary.rows : []
const canViewOrganizations = hasPermission(currentUser, 'READ_ORGANIZATIONS')
const canViewTenants = hasPermission(currentUser, 'READ_TENANTS')
const tenantSummaryValue = !organizationId
? 'No workspace linked'
: !canViewTenants
? 'Restricted'
: isLoadingTenants
? 'Loading…'
: String(linkedTenantSummary.count || 0)
const tenantEmptyStateMessage = !organizationId
? 'No organization is attached to this account yet.'
: !canViewTenants
? 'Your current role does not include tenant-read access for this organization context.'
: 'No tenant link surfaced yet for your organization.'
return (
<CardBox className={className}>
@ -116,7 +128,7 @@ const MyOrgTenantSummary = ({ className = '' }: MyOrgTenantSummaryProps) => {
{ label: 'Email', value: currentUser?.email },
{
label: 'Linked tenants',
value: isLoadingTenants ? 'Loading…' : String(linkedTenantSummary.count || 0),
value: tenantSummaryValue,
},
]}
actions={
@ -179,7 +191,7 @@ const MyOrgTenantSummary = ({ className = '' }: MyOrgTenantSummaryProps) => {
</div>
) : (
<div className="rounded-xl border border-dashed border-gray-200 bg-gray-50 px-4 py-3 text-sm text-gray-600 dark:border-dark-700 dark:bg-dark-800 dark:text-gray-300">
No tenant link surfaced yet for your organization.
{tenantEmptyStateMessage}
</div>
)}
</div>

View File

@ -0,0 +1,44 @@
import Link from 'next/link';
import React from 'react';
import BaseButton from './BaseButton';
import { webPagesNavBar } from '../menuNavBar';
type Props = {
title?: string;
subtitle?: string;
};
export default function PublicSiteFooter({
title = 'Gracey Corporate Stay Portal',
subtitle = 'A cleaner public front door for bookings, guest operations, and billing.',
}: Props) {
const year = new Date().getFullYear();
return (
<footer className="border-t border-slate-200 bg-white">
<div className="mx-auto grid max-w-6xl gap-8 px-6 py-10 lg:grid-cols-[1.2fr_0.8fr] lg:px-10">
<div>
<div className="text-[11px] font-medium uppercase tracking-[0.28em] text-slate-500">{title}</div>
<p className="mt-3 max-w-xl text-sm leading-7 text-slate-600">{subtitle}</p>
<div className="mt-5 text-sm text-slate-500">© {year} {title}. All rights reserved.</div>
</div>
<div className="flex flex-col gap-6 lg:items-end">
<nav className="flex flex-wrap gap-4 text-sm text-slate-600 lg:justify-end">
{webPagesNavBar.map((item) => (
<Link key={item.href || item.label} href={item.href || '/'} className="transition-colors hover:text-slate-950">
{item.label}
</Link>
))}
</nav>
<div className="flex flex-wrap gap-3 lg:justify-end">
<BaseButton href="/login" color="whiteDark" label="Login" small />
<BaseButton href="/command-center" color="info" label="Open workspace" small />
</div>
</div>
</div>
</footer>
);
}

View File

@ -0,0 +1,41 @@
import Link from 'next/link';
import React from 'react';
import BaseButton from './BaseButton';
import { webPagesNavBar } from '../menuNavBar';
type Props = {
title?: string;
subtitle?: string;
};
export default function PublicSiteHeader({
title = 'Gracey Corporate Stay Portal',
subtitle = 'Corporate stay operations',
}: Props) {
return (
<header className="sticky top-0 z-30 border-b border-slate-200/80 bg-white/90 backdrop-blur">
<div className="mx-auto flex max-w-6xl items-center justify-between gap-4 px-6 py-4 lg:px-10">
<Link href="/" className="min-w-0">
<div className="text-[11px] font-medium uppercase tracking-[0.28em] text-slate-500">{subtitle}</div>
<div className="truncate text-base font-semibold text-slate-950 md:text-lg">{title}</div>
</Link>
<div className="hidden items-center gap-6 md:flex">
<nav className="flex items-center gap-5 text-sm text-slate-600">
{webPagesNavBar.map((item) => (
<Link key={item.href || item.label} href={item.href || '/'} className="transition-colors hover:text-slate-950">
{item.label}
</Link>
))}
</nav>
<div className="flex items-center gap-3">
<BaseButton href="/login" color="whiteDark" label="Login" small />
<BaseButton href="/command-center" color="info" label="Open workspace" small />
</div>
</div>
</div>
</header>
);
}

View File

@ -1,31 +1,95 @@
import React, {useEffect, useId, useState} from 'react';
import React, { useEffect, useId, useState } from 'react';
import { AsyncPaginate } from 'react-select-async-paginate';
import axios from 'axios';
const getOptionLabel = (item: any, showField?: string) => {
if (!item) {
return '';
}
if (showField && item[showField]) {
return item[showField];
}
return (
item.label ||
item.name ||
item.firstName ||
item.email ||
item.action ||
item.title ||
item.request_code ||
item.reservation_code ||
item.file_name ||
item.summary ||
item.reference ||
item.id ||
''
);
};
const toSelectOption = (item: any, showField?: string) => {
if (!item) {
return null;
}
const value = item.value || item.id;
if (!value) {
return null;
}
return {
value,
label: getOptionLabel(item, showField),
};
};
export const SelectFieldMany = ({ options, field, form, itemRef, showField }) => {
const [value, setValue] = useState([]);
const PAGE_SIZE = 100;
useEffect(() => {
if (field.value?.[0] && typeof field.value[0] !== 'string') {
form.setFieldValue(
field.name,
field.value.map((el) => el.id),
);
} else if (!field.value || field.value.length === 0) {
if (!Array.isArray(field?.value) || field.value.length === 0) {
setValue([]);
return;
}
}, [field.name, field.value, form]);
useEffect(() => {
if (options) {
setValue(options.map((el) => ({ value: el.id, label: el[showField] })));
form.setFieldValue(
field.name,
options.map((el) => ({ value: el.id, label: el[showField] })),
);
const nextValue = field.value
.map((item) => {
if (typeof item === 'string') {
const matchingOption = Array.isArray(options)
? options.find((option) => option?.id === item || option?.value === item)
: null;
return {
value: item,
label: getOptionLabel(matchingOption, showField) || item,
};
}
return toSelectOption(item, showField);
})
.filter(Boolean);
setValue(nextValue);
const normalizedIds = field.value
.map((item) => {
if (typeof item === 'string') {
return item;
}
return item?.id || item?.value || null;
})
.filter(Boolean);
const shouldNormalizeFieldValue = field.value.some((item) => typeof item !== 'string');
if (shouldNormalizeFieldValue) {
form.setFieldValue(field.name, normalizedIds);
}
}, [options]);
}, [field?.name, field?.value, form, options, showField]);
const mapResponseToValuesAndLabels = (data) => ({
value: data.id,
@ -33,10 +97,12 @@ export const SelectFieldMany = ({ options, field, form, itemRef, showField }) =>
});
const handleChange = (data: any) => {
setValue(data)
const nextValue = Array.isArray(data) ? data : [];
setValue(nextValue);
form.setFieldValue(
field.name,
data.map(el => (el?.value || null)),
field.name,
nextValue.map((item) => item?.value).filter(Boolean),
);
};
@ -46,22 +112,23 @@ export const SelectFieldMany = ({ options, field, form, itemRef, showField }) =>
return {
options: data.map(mapResponseToValuesAndLabels),
hasMore: data.length === PAGE_SIZE,
}
};
}
return (
<AsyncPaginate
classNames={{
control: () => 'px-1 py-2',
}}
classNamePrefix='react-select'
instanceId={useId()}
value={value}
isMulti
debounceTimeout={1000}
loadOptions={callApi}
onChange={handleChange}
defaultOptions
isClearable
/>
<AsyncPaginate
classNames={{
control: () => 'px-1 py-2',
}}
classNamePrefix='react-select'
instanceId={useId()}
value={value}
isMulti
debounceTimeout={1000}
loadOptions={callApi}
onChange={handleChange}
defaultOptions
isClearable
/>
);
};

View File

@ -46,8 +46,19 @@ const menuNavBar: MenuNavBarItem[] = [
},
]
export const webPagesNavBar = [
];
export const webPagesNavBar: MenuNavBarItem[] = [
{
label: 'Home',
href: '/',
},
{
label: 'Privacy',
href: '/privacy-policy',
},
{
label: 'Terms',
href: '/terms-of-use',
},
]
export default menuNavBar

View File

@ -5,47 +5,64 @@ import {
mdiFileDocument,
mdiHomeCity,
mdiRoomService,
mdiShieldLockOutline,
} from '@mdi/js';
import Head from 'next/head';
import React, { ReactElement } from 'react';
import BaseButton from '../components/BaseButton';
import BaseDivider from '../components/BaseDivider';
import BaseIcon from '../components/BaseIcon';
import CardBox from '../components/CardBox';
import PublicSiteFooter from '../components/PublicSiteFooter';
import PublicSiteHeader from '../components/PublicSiteHeader';
import LayoutGuest from '../layouts/Guest';
import { getPageTitle } from '../config';
const platformHighlights = [
const productAreas = [
{
title: 'Corporate accounts',
description: 'Manage negotiated rate plans, account history, billing rules, and repeat-booker relationships.',
icon: mdiShieldLockOutline,
},
{
title: 'Reservation control',
description: 'Move a stay from request to approval, inventory assignment, confirmation, arrival, and post-stay handling.',
title: 'Booking intake',
description: 'Receive requests with the right tenant, dates, approvals, and context before service delivery begins.',
icon: mdiCalendarCheck,
},
{
title: 'Concierge operations',
description: 'Track airport pickup, housekeeping, maintenance, extension, and custom guest service requests.',
title: 'Stay operations',
description: 'Coordinate readiness, guest service, and internal updates from one controlled operating view.',
icon: mdiRoomService,
},
{
title: 'Finance visibility',
description: 'Issue invoices, watch overdue exposure, and keep executive housing accounts commercially disciplined.',
title: 'Billing follow-through',
description: 'Keep invoicing and account visibility tied to the same reservation record from start to finish.',
icon: mdiFileDocument,
},
];
const workflowSteps = [
'Corporate booker submits a stay request',
'Approver reviews against policy and budget',
'Reservations team quotes and allocates inventory',
'Traveler receives confirmed stay details',
'Concierge and finance teams manage service and billing',
const servicePillars = [
'Tenant-aware workspace context for multi-organization teams',
'Reserved, premium visual tone without turning the page into a template',
'A direct path from public website to the real operating workspace',
];
const workflowMoments = [
{
step: '01',
title: 'Receive the brief',
description: 'Capture the request with account context, approvals, dates, and service notes intact.',
},
{
step: '02',
title: 'Deliver the stay',
description: 'Move into reservations, readiness, support, and day-to-day guest coordination with clarity.',
},
{
step: '03',
title: 'Close with confidence',
description: 'Finish with billing and account follow-through attached to the same operational source of truth.',
},
];
const highlights = [
{ label: 'Private operator view', value: 'Multi-tenant workspace' },
{ label: 'Service posture', value: 'Calm, controlled, premium' },
{ label: 'Execution model', value: 'Request to reservation to billing' },
];
export default function HomePage() {
@ -53,120 +70,167 @@ export default function HomePage() {
<>
<Head>
<title>{getPageTitle('Gracey Corporate Stay Portal')}</title>
<meta
name="description"
content="Manage booking requests, reservations, guest operations, and billing from one refined corporate stay workspace."
/>
</Head>
<main className="min-h-screen bg-[#f4f6f8] text-slate-950">
<section className="border-b border-slate-200 bg-[#0b1220] text-white">
<div className="mx-auto flex max-w-7xl flex-col gap-12 px-6 py-6 lg:px-10">
<div className="flex items-center justify-between gap-4">
<div>
<div className="text-xs uppercase tracking-[0.28em] text-slate-400">Gracey Corporate Stay Portal</div>
<div className="mt-2 text-sm text-slate-300">Executive software for serviced apartments, embassy housing, and long-stay corporate operations.</div>
<main className="min-h-screen bg-[#f7f4ee] text-slate-950">
<PublicSiteHeader />
<section className="relative overflow-hidden border-b border-[#d9cfbf] bg-[linear-gradient(135deg,rgba(15,23,42,0.96)_0%,rgba(30,41,59,0.92)_42%,rgba(88,65,38,0.88)_100%)] text-white">
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top_left,_rgba(255,255,255,0.12),_transparent_28%),radial-gradient(circle_at_bottom_right,_rgba(245,208,140,0.18),_transparent_26%)]" />
<div className="relative mx-auto grid max-w-6xl gap-14 px-6 py-18 lg:grid-cols-[1.1fr_0.9fr] lg:px-10 lg:py-24">
<div className="flex flex-col justify-center">
<div className="inline-flex w-fit items-center gap-2 rounded-full border border-white/15 bg-white/8 px-4 py-1.5 text-[11px] font-medium uppercase tracking-[0.32em] text-[#ead7b0]">
<BaseIcon path={mdiHomeCity} size={16} className="text-[#f5deb3]" />
Gracey private operations suite
</div>
<div className="flex flex-wrap gap-3">
<h1 className="mt-7 max-w-4xl text-4xl font-semibold leading-[1.02] text-white md:text-[64px]">
A more elegant operating layer for premium corporate stays.
</h1>
<p className="mt-6 max-w-2xl text-lg leading-8 text-slate-200">
Gracey gives booking, service, and billing teams one composed system for high-touch staysquiet in tone,
precise in execution, and built for the work behind the guest experience.
</p>
<div className="mt-8 flex flex-wrap gap-3">
<BaseButton href="/command-center" color="info" label="Open workspace" icon={mdiArrowTopRight} />
<BaseButton href="/login" color="whiteDark" label="Login" />
<BaseButton href="/command-center" color="info" label="Open workspace" />
</div>
<div className="mt-12 grid gap-4 sm:grid-cols-3">
{highlights.map((item) => (
<div key={item.label} className="border-l border-white/20 pl-4">
<div className="text-[11px] font-medium uppercase tracking-[0.24em] text-[#d7c19a]">{item.label}</div>
<div className="mt-2 text-sm leading-6 text-slate-100">{item.value}</div>
</div>
))}
</div>
</div>
<div className="grid gap-10 lg:grid-cols-[1.2fr_0.8fr] lg:items-end">
<div>
<div className="inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/5 px-3 py-1 text-xs uppercase tracking-[0.24em] text-slate-300">
<BaseIcon path={mdiHomeCity} className="text-slate-100" />
Luxury corporate stay operations
<CardBox className="border border-white/10 bg-white/8 text-white shadow-[0_40px_120px_-60px_rgba(15,23,42,0.95)] backdrop-blur-sm">
<div className="flex items-start justify-between gap-4">
<div>
<div className="text-[11px] font-medium uppercase tracking-[0.28em] text-[#d7c19a]">Operator brief</div>
<h2 className="mt-3 text-2xl font-semibold text-white">What the platform quietly keeps under control</h2>
</div>
<h1 className="mt-6 max-w-4xl text-4xl font-semibold leading-tight text-white md:text-6xl">
The control layer for premium furnished inventory, approvals, guest logistics, and invoice billing.
</h1>
<p className="mt-6 max-w-2xl text-base leading-8 text-slate-300">
Gracey helps serviced-apartment operators win repeat institutional accounts with faster quoting,
cleaner approval workflows, better arrival readiness, and a more executive operating posture.
</p>
<div className="mt-8 flex flex-wrap gap-3">
<BaseButton href="/command-center" color="info" label="Open command center" icon={mdiArrowTopRight} />
<BaseButton href="/booking_requests/booking_requests-list" color="whiteDark" label="Review booking workflow" />
<div className="rounded-full border border-[#d7c19a]/40 bg-[#d7c19a]/12 px-3 py-1 text-xs font-medium text-[#f2dfba]">
Refined workflow
</div>
</div>
<CardBox className="border border-white/10 bg-white/5 text-white shadow-[0_30px_80px_-40px_rgba(15,23,42,0.9)]">
<div className="grid gap-4 md:grid-cols-2">
<div className="rounded-2xl border border-white/10 bg-white/5 p-4">
<div className="text-xs uppercase tracking-[0.2em] text-slate-400">Use cases</div>
<div className="mt-2 text-2xl font-semibold text-white">Corporate teams, NGOs, embassies</div>
<div className="mt-2 text-sm leading-6 text-slate-300">Built for medium-stay housing that needs negotiated rates, approvals, and invoice-grade accountability.</div>
</div>
<div className="rounded-2xl border border-white/10 bg-white/5 p-4">
<div className="text-xs uppercase tracking-[0.2em] text-slate-400">Operational posture</div>
<div className="mt-2 text-2xl font-semibold text-white">Cold, executive, credible</div>
<div className="mt-2 text-sm leading-6 text-slate-300">No consumer fluff. Just the controls needed to run luxury serviced inventory for professional clients.</div>
<div className="mt-8 space-y-4">
{workflowMoments.map((item) => (
<div key={item.step} className="rounded-[28px] border border-white/10 bg-black/10 p-5">
<div className="text-xs font-semibold tracking-[0.24em] text-[#d7c19a]">{item.step}</div>
<h3 className="mt-2 text-lg font-semibold text-white">{item.title}</h3>
<p className="mt-2 text-sm leading-7 text-slate-200">{item.description}</p>
</div>
))}
</div>
<div className="mt-8 rounded-[28px] border border-white/10 bg-white/6 p-5">
<div className="flex items-start gap-3">
<BaseIcon path={mdiCheckDecagram} size={20} className="mt-0.5 text-[#f0d8ab]" />
<p className="text-sm leading-7 text-slate-100">
The public experience stays polished and restrained. The real depth appears only where it belongs:
inside the authenticated workspace.
</p>
</div>
</CardBox>
</div>
</div>
</CardBox>
</div>
</section>
<section className="mx-auto max-w-7xl px-6 py-16 lg:px-10">
<div className="grid gap-6 lg:grid-cols-4">
{platformHighlights.map((item) => (
<CardBox key={item.title} className="border border-slate-200 bg-white shadow-[0_20px_60px_-40px_rgba(15,23,42,0.45)]">
<div className="rounded-2xl bg-slate-950 p-3 text-white w-fit">
<BaseIcon path={item.icon} size={20} />
<section className="mx-auto max-w-6xl px-6 py-16 lg:px-10 lg:py-20">
<div className="grid gap-10 lg:grid-cols-[0.9fr_1.1fr] lg:items-end">
<div>
<div className="text-[11px] font-medium uppercase tracking-[0.28em] text-[#8b6b42]">Signature coverage</div>
<h2 className="mt-3 max-w-2xl text-3xl font-semibold leading-tight text-slate-950 md:text-5xl">
Designed for teams that want operational polish, not marketing noise.
</h2>
</div>
<p className="max-w-2xl text-base leading-8 text-slate-700 lg:justify-self-end">
Every section is meant to feel intentional: fewer blocks, better restraint, and a stronger sense that the
product supports a premium service model behind the scenes.
</p>
</div>
<div className="mt-10 grid gap-6 md:grid-cols-3">
{productAreas.map((item) => (
<div
key={item.title}
className="rounded-[32px] border border-[#d9cfbf] bg-[#fcfaf6] p-7 shadow-[0_28px_70px_-60px_rgba(15,23,42,0.45)]"
>
<div className="flex h-12 w-12 items-center justify-center rounded-2xl bg-[#efe5d4] text-[#5f4526]">
<BaseIcon path={item.icon} size={22} />
</div>
<h2 className="mt-5 text-2xl font-semibold text-slate-950">{item.title}</h2>
<p className="mt-3 text-sm leading-7 text-slate-600">{item.description}</p>
</CardBox>
<h3 className="mt-6 text-xl font-semibold text-slate-950">{item.title}</h3>
<p className="mt-3 text-sm leading-7 text-slate-700">{item.description}</p>
</div>
))}
</div>
</section>
<section className="mx-auto max-w-7xl px-6 pb-16 lg:px-10">
<div className="grid gap-8 lg:grid-cols-[0.95fr_1.05fr]">
<CardBox className="border border-slate-200 bg-white shadow-[0_20px_60px_-40px_rgba(15,23,42,0.45)]">
<div className="text-xs uppercase tracking-[0.24em] text-slate-500">First delivered workflow</div>
<h2 className="mt-3 text-3xl font-semibold text-slate-950">Corporate stay booking lifecycle</h2>
<p className="mt-4 text-sm leading-7 text-slate-600">
The initial slice connects the public brand front door to a live authenticated command center, then into the existing request, approval, reservation, concierge, and finance records.
<section className="border-y border-[#d9cfbf] bg-[#efe7db]">
<div className="mx-auto grid max-w-6xl gap-10 px-6 py-16 lg:grid-cols-[0.95fr_1.05fr] lg:px-10 lg:py-20">
<div>
<div className="text-[11px] font-medium uppercase tracking-[0.28em] text-[#8b6b42]">Service standard</div>
<h2 className="mt-3 text-3xl font-semibold leading-tight text-slate-950 md:text-4xl">
Luxury online should feel composed, selective, and credible.
</h2>
<p className="mt-4 text-base leading-8 text-slate-700">
So the page leads with restraint: rich tone, quiet confidence, and a product story grounded in actual
operations rather than inflated feature theater.
</p>
<BaseDivider />
<div className="space-y-4">
{workflowSteps.map((step, index) => (
<div key={step} className="flex gap-4 rounded-2xl border border-slate-200 bg-slate-50 p-4">
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-slate-950 text-sm font-semibold text-white">
{index + 1}
</div>
<div className="text-sm leading-7 text-slate-700">{step}</div>
</div>
))}
</div>
</CardBox>
</div>
<CardBox className="border border-slate-200 bg-[#0f172a] text-white shadow-[0_24px_70px_-38px_rgba(15,23,42,0.8)]">
<div className="text-xs uppercase tracking-[0.24em] text-slate-400">Immediate access</div>
<h2 className="mt-3 text-3xl font-semibold text-white">Choose your entry point</h2>
<p className="mt-4 text-sm leading-7 text-slate-300">
Enter the authenticated workspace to manage live operations, or go straight to the broader admin environment.
</p>
<div className="mt-8 grid gap-4 sm:grid-cols-2">
<div className="rounded-2xl border border-white/10 bg-white/5 p-5">
<div className="text-xs uppercase tracking-[0.2em] text-slate-400">Operations</div>
<div className="mt-2 text-2xl font-semibold text-white">Command center</div>
<div className="mt-2 text-sm leading-6 text-slate-300">Live view across bookings, reservations, service pressure, and invoice exposure.</div>
<div className="mt-5">
<BaseButton href="/command-center" color="info" label="Open workspace" />
</div>
<div className="grid gap-4">
{servicePillars.map((item) => (
<div key={item} className="flex items-start gap-4 rounded-[28px] border border-white/60 bg-white/70 p-5 backdrop-blur">
<BaseIcon path={mdiCheckDecagram} size={20} className="mt-1 text-[#8b6b42]" />
<p className="text-sm leading-7 text-slate-800">{item}</p>
</div>
<div className="rounded-2xl border border-white/10 bg-white/5 p-5">
<div className="text-xs uppercase tracking-[0.2em] text-slate-400">Administration</div>
<div className="mt-2 text-2xl font-semibold text-white">Role-based workspace</div>
<div className="mt-2 text-sm leading-6 text-slate-300">Land in one shared command center, then branch into the modules your role actually owns.</div>
<div className="mt-5">
<BaseButton href="/command-center" color="whiteDark" label="Open workspace" />
</div>
</div>
</div>
</CardBox>
))}
</div>
</div>
</section>
<section className="px-6 py-16 lg:px-10 lg:py-20">
<div className="mx-auto max-w-5xl rounded-[36px] border border-[#cfbc9b] bg-[linear-gradient(135deg,#111827_0%,#1f2937_58%,#6b4f2c_100%)] px-6 py-10 text-white shadow-[0_40px_120px_-55px_rgba(15,23,42,0.9)] lg:px-10 lg:py-12">
<div className="grid gap-8 lg:grid-cols-[1.1fr_0.9fr] lg:items-end">
<div>
<div className="text-[11px] font-medium uppercase tracking-[0.28em] text-[#dcc49f]">Private access</div>
<h2 className="mt-3 max-w-2xl text-3xl font-semibold leading-tight md:text-4xl">
Enter the workspace when the conversation turns into execution.
</h2>
<p className="mt-4 max-w-2xl text-sm leading-7 text-slate-200 md:text-base">
Review booking flow activity, open the operating workspace, and continue from live data instead of a
brochure-style homepage.
</p>
</div>
<div className="rounded-[28px] border border-white/10 bg-black/10 p-5">
<div className="text-[11px] font-medium uppercase tracking-[0.24em] text-[#d7c19a]">For internal teams</div>
<p className="mt-3 text-sm leading-7 text-slate-200">
Reservations, service operations, and billing remain connectedwithout losing the elevated feel the
brand should project externally.
</p>
</div>
</div>
<div className="mt-8 flex flex-wrap gap-3">
<BaseButton href="/command-center" color="info" label="Open workspace" />
<BaseButton href="/booking_requests/booking_requests-list" color="whiteDark" label="View booking flow" />
</div>
</div>
</section>
<PublicSiteFooter />
</main>
</>
);

View File

@ -2,6 +2,8 @@ import React, { useEffect, useState } from 'react';
import type { ReactElement } from 'react';
import Head from 'next/head';
import LayoutGuest from '../layouts/Guest';
import PublicSiteFooter from '../components/PublicSiteFooter';
import PublicSiteHeader from '../components/PublicSiteHeader';
import { getPageTitle } from '../config';
export default function PrivacyPolicy() {
@ -261,14 +263,16 @@ export default function PrivacyPolicy() {
};
return (
<div className='prose prose-slate mx-auto max-w-none'>
<div className='min-h-screen bg-slate-50 text-slate-950'>
<Head>
<title>{getPageTitle('Privacy Policy')}</title>
</Head>
<div className='flex justify-center'>
<div className='z-10 md:w-10/12 my-4 bg-white border border-pavitra-400 rounded'>
<div className='p-8 lg:px-12 lg:py-10'>
<PublicSiteHeader />
<div className='mx-auto flex max-w-6xl justify-center px-6 py-8 lg:px-10 lg:py-10'>
<div className='z-10 w-full rounded-2xl border border-slate-200 bg-white shadow-[0_18px_50px_-42px_rgba(15,23,42,0.35)]'>
<div className='prose prose-slate max-w-none p-8 lg:px-12 lg:py-10'>
<h1>Privacy Policy</h1>
<Introduction />
<Information />
@ -283,6 +287,8 @@ export default function PrivacyPolicy() {
</div>
</div>
</div>
<PublicSiteFooter />
</div>
);
}

View File

@ -390,6 +390,7 @@ const EditTenantsPage = () => {
itemRef={'organizations'}
options={initialValues.organizations}
component={SelectFieldMany}
showField={'name'}
></Field>
</FormField>
@ -400,6 +401,7 @@ const EditTenantsPage = () => {
itemRef={'properties'}
options={initialValues.properties}
component={SelectFieldMany}
showField={'name'}
></Field>
</FormField>

View File

@ -225,6 +225,7 @@ const TenantsNew = () => {
itemRef={'organizations'}
options={initialValues.organizations}
component={SelectFieldMany}
showField={'name'}
></Field>
</FormField>
@ -233,8 +234,24 @@ const TenantsNew = () => {
name="properties"
id="properties"
itemRef={'properties'}
options={[]}
options={initialValues.properties}
component={SelectFieldMany}
showField={'name'}
></Field>
</FormField>
<FormField
label="Audit logs"
labelFor="audit_logs"
help="Optional: attach audit log records that should stay owned by this tenant."
>
<Field
name="audit_logs"
id="audit_logs"
itemRef={'audit_logs'}
options={initialValues.audit_logs}
component={SelectFieldMany}
showField={'action'}
></Field>
</FormField>

View File

@ -2,6 +2,8 @@ import React, { useEffect, useState } from 'react';
import type { ReactElement } from 'react';
import Head from 'next/head';
import LayoutGuest from '../layouts/Guest';
import PublicSiteFooter from '../components/PublicSiteFooter';
import PublicSiteHeader from '../components/PublicSiteHeader';
import { getPageTitle } from '../config';
export default function PrivacyPolicy() {
@ -173,14 +175,16 @@ export default function PrivacyPolicy() {
};
return (
<div className='prose prose-slate mx-auto max-w-none'>
<div className='min-h-screen bg-slate-50 text-slate-950'>
<Head>
<title>{getPageTitle('Terms of Use')}</title>
</Head>
<div className='flex justify-center'>
<div className='z-10 md:w-10/12 my-4 bg-white border border-pavitra-400 rounded'>
<div className='p-8 lg:px-12 lg:py-10'>
<PublicSiteHeader />
<div className='mx-auto flex max-w-6xl justify-center px-6 py-8 lg:px-10 lg:py-10'>
<div className='z-10 w-full rounded-2xl border border-slate-200 bg-white shadow-[0_18px_50px_-42px_rgba(15,23,42,0.35)]'>
<div className='prose prose-slate max-w-none p-8 lg:px-12 lg:py-10'>
<h1>Terms of Use</h1>
<Information />
@ -197,6 +201,8 @@ export default function PrivacyPolicy() {
</div>
</div>
</div>
<PublicSiteFooter />
</div>
);
}