Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
4f59c26
perf(nyu): cache NYU Identity API responses in Firestore for 7 days
rlho May 17, 2026
8317d35
chore(deps-dev): bump postcss
dependabot[bot] May 19, 2026
229b006
Merge pull request #1479 from ITPNYU/dependabot/npm_and_yarn/booking-…
rlho May 19, 2026
0d42d6a
chore(deps): bump the npm_and_yarn group across 2 directories with 2 …
dependabot[bot] May 23, 2026
51a95df
Merge pull request #1482 from ITPNYU/dependabot/npm_and_yarn/booking-…
rlho May 26, 2026
4112fa8
fix(a11y): improve form labels, keyboard access, and landmarks
rlho May 26, 2026
ea77acc
fix(a11y): address Copilot review feedback
rlho May 26, 2026
a8a3f7b
Merge pull request #1483 from ITPNYU/fix/accessibility-improvements
rlho May 26, 2026
8a06607
fix(nyuIdentity): address Copilot review — encode URL segment, dedupe…
rlho May 26, 2026
ea1fa47
chore(eslint): enable jsx-a11y recommended rules
rlho May 26, 2026
8c614d3
Merge pull request #1484 from ITPNYU/chore/eslint-jsx-a11y
rlho May 26, 2026
88ae597
Merge pull request #1475 from ITPNYU/perf/nyu-api-cache
rlho May 26, 2026
7a31140
updated README for .env location
Kaibanda May 27, 2026
d9383a8
chore(deps): bump axios
dependabot[bot] May 29, 2026
c8553fa
Merge pull request #1485 from Kaibanda/readme_update
rlho Jun 2, 2026
fac1083
Merge pull request #1487 from ITPNYU/dependabot/npm_and_yarn/booking-…
rlho Jun 2, 2026
557cc59
Refactor schema
pearmini May 13, 2026
fed5e1c
Fix ts errors
pearmini May 13, 2026
40da4b0
Update e2e tests
pearmini May 13, 2026
32c5c56
fix(schema): prevent legacy data loss and make coercion server-safe
rlho Jun 2, 2026
729bb3e
fix(e2e): restore VIP submit and ITP duration-limit validation
rlho Jun 2, 2026
9255fd6
fix(schema): preserve top-level time-sensitive warning; tidy review nits
rlho Jun 3, 2026
2c65279
Merge pull request #1470 from ITPNYU/refactor-schema
rlho Jun 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ cd booking-app/booking-app
npm install
```

4. 🔐 Obtain the `.env.local` file from a project administrator (Riho or Nima) and place it in the root directory of the project.
4. 🔐 Obtain the `.env` file from a project administrator (Riho or Nima) and place it in the `booking-app` directory.

### Running the Application

Expand All @@ -52,7 +52,7 @@ Once the application is running, you should be able to:
## 🔒 Environment Variables

This project relies on environment variables for secure configuration. Important notes:
- Request the `.env.local` file from project admin or another dev
- Request the `.env` file from project admin or another dev
- Never commit the `.env` file to version control
Comment on lines +55 to 56
- Keep the environment variables secure and confidential

Expand Down Expand Up @@ -106,7 +106,7 @@ In the `package.json` file, the following test scripts are defined:
3. View the results in the console. Fix any failing tests before proceeding with development or deployment.

#### Notes
- **Environment:** Ensure you have the **development** `.env.local` file properly configured for your testing environment.
- **Environment:** Ensure you have the **development** `.env` file properly configured for your testing environment.
- **Playwright Setup:** If running Playwright tests for the first time, install the required browsers:
Comment on lines 108 to 110
```bash
npx playwright install
Expand Down
4 changes: 2 additions & 2 deletions booking-app/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Follow these steps to run the application in your local environment.
```
3. Install the dependencies:
`npm install`
4. Obtain the `.env` file from a project administrator and place it in the root directory of the project.
4. Obtain the `.env` file from a project administrator and place it in the `booking-app` directory.

### Running the Application

Expand All @@ -31,7 +31,7 @@ The application should now be running on [http://localhost:3000](http://localhos

## Environment Variables

This project uses environment variables for configuration. Make sure you have received the `.env` file from a project administrator and placed it in the root directory before running the application.
This project uses environment variables for configuration. Make sure you have received the `.env` file from a project administrator and placed it in the `booking-app` directory before running the application.

## Deployment

Expand Down
28 changes: 15 additions & 13 deletions booking-app/app/[tenant]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,24 +27,24 @@ export async function generateMetadata({
const { tenant } = await params;
try {
const tenantSchema = await getCachedTenantSchema(tenant);
if (!tenantSchema?.name) {
if (!tenantSchema?.tenant?.name) {
return {
title: "NYU room booking",
description: "NYU space reservation",
};
}
const title = `${tenantSchema.name} Booking`;
const title = `${tenantSchema.tenant.name} Booking`;
const description =
tenantSchema.nameForPolicy?.trim() ||
tenantSchema.tenant.nameForPolicy?.trim() ||
`${title} — NYU space reservation`;
return {
title,
description,
...(tenantSchema.logo
...(tenantSchema.tenant.logo
? {
icons: {
icon: tenantSchema.logo,
apple: tenantSchema.logo,
icon: tenantSchema.tenant.logo,
apple: tenantSchema.tenant.logo,
},
}
: {}),
Expand All @@ -69,8 +69,8 @@ const Layout: React.FC<LayoutProps> = async ({ children, params }) => {
const tenantSchema = await getCachedTenantSchema(tenant);

console.log("Layout: Retrieved tenantSchema:", {
tenant: tenantSchema?.tenant,
name: tenantSchema?.name,
tenantId: tenantSchema?.tenantId,
name: tenantSchema?.tenant?.name,
resourcesCount: tenantSchema?.resources?.length || 0,
});

Expand All @@ -89,12 +89,12 @@ const Layout: React.FC<LayoutProps> = async ({ children, params }) => {
const serializedTenantSchema: SchemaContextType = {
...tenantSchema,
resources: resources ?? tenantSchema.resources,
tenant: tenantSchema.tenant || tenant,
tenantId: tenantSchema.tenantId || tenant,
};

console.log("Layout: Serialized tenantSchema:", {
tenant: serializedTenantSchema.tenant,
name: serializedTenantSchema.name,
tenantId: serializedTenantSchema.tenantId,
name: serializedTenantSchema.tenant.name,
resourcesCount: serializedTenantSchema.resources?.length || 0,
});

Expand All @@ -104,8 +104,10 @@ const Layout: React.FC<LayoutProps> = async ({ children, params }) => {
<SchemaDriftBanner />
<TenantEntitlementGuard>
<TenantSiteBanner />
<NavBar />
{children}
<header>
<NavBar />
</header>
<main>{children}</main>
</TenantEntitlementGuard>
</ClientProvider>
</SchemaProviderWrapper>
Expand Down
4 changes: 2 additions & 2 deletions booking-app/app/api/bookings/edit/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ async function sendEditNotificationEmails(
email,
startDate: bookingCalendarInfo?.startStr,
endDate: bookingCalendarInfo?.endStr,
headerMessage: emailConfig.emailMessages.firstApprovalRequest,
headerMessage: emailConfig.emailNotifications.requestedNeedsApproval,
requestNumber: existingContents.requestNumber,
origin: existingContents.origin || BookingOrigin.USER,
};
Expand Down Expand Up @@ -143,7 +143,7 @@ async function sendEditNotificationEmails(
await serverSendBookingDetailEmail({
calendarEventId,
targetEmail: email,
headerMessage: emailConfig.emailMessages.requestConfirmation,
headerMessage: emailConfig.emailNotifications.requestedUser,
status: BookingStatusLabel.REQUESTED,
replyTo: email,
tenant,
Expand Down
4 changes: 2 additions & 2 deletions booking-app/app/api/bookings/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -413,7 +413,7 @@ async function handleBookingApprovalEmails(
email,
startDate: bookingCalendarInfo?.startStr,
endDate: bookingCalendarInfo?.endStr,
headerMessage: emailConfig.emailMessages.firstApprovalRequest,
headerMessage: emailConfig.emailNotifications.requestedNeedsApproval,
requestNumber: sequentialId,
origin: formatOrigin(data.origin) ?? BookingOrigin.USER,
};
Expand Down Expand Up @@ -449,7 +449,7 @@ async function handleBookingApprovalEmails(
await serverSendBookingDetailEmail({
calendarEventId,
targetEmail: email,
headerMessage: emailConfig.emailMessages.requestConfirmation,
headerMessage: emailConfig.emailNotifications.requestedUser,
status: BookingStatusLabel.REQUESTED,
replyTo: email,
tenant,
Expand Down
4 changes: 2 additions & 2 deletions booking-app/app/api/bookingsDirect/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -620,8 +620,8 @@ export async function POST(request: NextRequest) {
const emailConfig = await getTenantEmailConfig(tenant);
const confirmationMessage =
type === "vip"
? emailConfig.emailMessages.vipConfirmation
: emailConfig.emailMessages.walkInConfirmation;
? emailConfig.emailNotifications.approvedVIP
: emailConfig.emailNotifications.approvedWalkIn;

const sendWalkInNofificationEmail = async (recipients: string[]) => {
const emailPromises = recipients.map(recipient =>
Expand Down
2 changes: 1 addition & 1 deletion booking-app/app/api/checkin-processing/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export async function POST(req: NextRequest) {
await serverSendBookingDetailEmail({
calendarEventId,
targetEmail: guestEmail,
headerMessage: emailConfig.emailMessages.checkinConfirmation,
headerMessage: emailConfig.emailNotifications.checkedIn,
status: BookingStatusLabel.CHECKED_IN,
tenant,
});
Expand Down
2 changes: 1 addition & 1 deletion booking-app/app/api/checkout-processing/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export async function POST(req: NextRequest) {
await serverSendBookingDetailEmail({
calendarEventId,
targetEmail: guestEmail,
headerMessage: emailConfig.emailMessages.checkoutConfirmation,
headerMessage: emailConfig.emailNotifications.checkedOut,
status: BookingStatusLabel.CHECKED_OUT,
tenant,
});
Expand Down
18 changes: 11 additions & 7 deletions booking-app/app/api/safety_training_form/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { getFormsClient, getLoggingClient } from "@/lib/googleClient";
import { serverGetDocumentById } from "@/lib/firebase/server/adminDb";
import { TableNames } from "@/components/src/policy";
import { extractGoogleFormId } from "@/components/src/utils/formUrlUtils";
import { coerceTenantSchema } from "@/lib/tenant/coerceTenantSchema";

export const dynamic = "force-dynamic";

Expand All @@ -19,31 +20,34 @@ export async function GET(request: NextRequest) {
}

// Get tenant schema (includes training form URL per resource and tenant-level fallback)
const schema = await serverGetDocumentById(
const raw = await serverGetDocumentById<Record<string, unknown>>(
TableNames.TENANT_SCHEMA,
tenant,
);
if (!schema) {
if (!raw) {
return NextResponse.json(
{ error: "Tenant schema not found" },
{ status: 404 },
);
}

const schema = coerceTenantSchema(raw, tenant);

// Resolve training form from schema: resource by resourceId, then tenant-level fallback
let formId: string | null = null;

if (resourceId) {
const resource = schema.resources?.find(
(r: any) => r.roomId?.toString() === resourceId.toString(),
(r) => r.roomId?.toString() === resourceId.toString(),
);
if (resource?.trainingFormUrl) {
formId = extractGoogleFormId(resource.trainingFormUrl);
const resourceForm = resource?.training?.formId;
if (resourceForm) {
formId = extractGoogleFormId(resourceForm);
}
}

if (!formId && (schema as any).safetyTrainingGoogleFormId) {
formId = extractGoogleFormId((schema as any).safetyTrainingGoogleFormId);
if (!formId && schema.training?.formId) {
formId = extractGoogleFormId(schema.training.formId);
}

// If no form found, return error
Expand Down
22 changes: 16 additions & 6 deletions booking-app/app/api/tenantSchema/[tenant]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
} from "@/lib/firebase/server/adminDb";
import { TableNames } from "@/components/src/policy";
import { applyEnvironmentCalendarIds } from "@/lib/utils/calendarEnvironment";
import { coerceTenantSchema } from "@/lib/tenant/coerceTenantSchema";
import { isValidTenant } from "@/components/src/constants/tenants";
import admin from "@/lib/firebase/server/firebaseAdmin";

Expand All @@ -17,20 +18,29 @@ export async function GET(
const { tenant } = await params;

// Fetch the specific schema document using tenant as document ID
const schema = await serverGetDocumentById(
const rawDoc = await serverGetDocumentById<Record<string, unknown>>(
TableNames.TENANT_SCHEMA,
tenant,
);

if (!schema) {
if (!rawDoc) {
return NextResponse.json(
{ error: `Schema not found for tenant: ${tenant}` },
{ status: 404 },
);
}

// Skip environment calendar ID rewriting when raw=1 is requested
// (used by schema editor to avoid data corruption on round-trip)
// Always coerce to the canonical (nested) shape. The schema editor needs
// the nested shape to render its fields, so it consumes this coerced
// payload and persists it on save — which is the intended lazy migration of
// a legacy document to the canonical shape (no data is lost; coercion maps
// every legacy field across). It is NOT a way to fetch the un-coerced
// document.
const schema = coerceTenantSchema(rawDoc, tenant);

// `raw=1` only skips environment-specific calendar ID rewriting, so the
// editor saves back the stored calendar IDs rather than env-substituted
// ones. It does not bypass coercion.
const raw = request.nextUrl.searchParams.get("raw") === "1";
if (!raw && schema.resources && Array.isArray(schema.resources)) {
schema.resources = applyEnvironmentCalendarIds(schema.resources);
Expand Down Expand Up @@ -86,8 +96,8 @@ export async function PUT(

const newSchema = await request.json();

// Enforce tenant field consistency with URL param
newSchema.tenant = tenant;
// Enforce tenant id consistency with URL param
newSchema.tenantId = tenant;

// Backup current schema before overwriting
const existingSchema = await serverGetDocumentById(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ export default function BookingActions(props: Props) {
value={reason}
setValue={setReason}
>
<IconButton color={"primary"}>
<IconButton color={"primary"} aria-label="Confirm decline">
<Check />
</IconButton>
</DeclineReasonDialog>
Expand All @@ -116,7 +116,10 @@ export default function BookingActions(props: Props) {
value={reason}
setValue={setReason}
>
<IconButton color={"primary"}>
<IconButton
color={"primary"}
aria-label={`Confirm ${selectedAction}`}
>
<Check />
</IconButton>
</DeclineReasonDialog>
Expand All @@ -136,6 +139,7 @@ export default function BookingActions(props: Props) {
<IconButton
disabled={selectedAction === Actions.PLACEHOLDER}
color={"primary"}
aria-label={`Confirm ${selectedAction}`}
>
<Check />
</IconButton>
Expand All @@ -150,6 +154,7 @@ export default function BookingActions(props: Props) {
onClick={() => {
handleDialogChoice(true);
}}
aria-label={`Confirm ${selectedAction}`}
>
<Check />
</IconButton>
Expand Down
Loading
Loading