Skip to content

Fix/booking timezone static services#17

Closed
Tung-Quan wants to merge 2 commits into
mainfrom
fix/booking-timezone-static-services
Closed

Fix/booking timezone static services#17
Tung-Quan wants to merge 2 commits into
mainfrom
fix/booking-timezone-static-services

Conversation

@Tung-Quan
Copy link
Copy Markdown
Contributor

@Tung-Quan Tung-Quan commented May 12, 2026

Summary by CodeRabbit

  • Bug Fixes

    • Appointment scheduling and confirmation emails now correctly calculate and display times using the salon's configured timezone (America/Chicago) instead of a hardcoded timezone.
  • Changes

    • Services page now displays a static catalog of service offerings.
  • Configuration

    • Updated default application timezone to America/Chicago; ensure APP_TIMEZONE is set in deployment environment files.

Review Change Stack

@qodo-code-review
Copy link
Copy Markdown

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 12, 2026

📝 Walkthrough

Walkthrough

The PR introduces a timezone-aware appointment scheduling system and refactors the web Services page. A new timezone.util module provides configurable, timezone-aware date/time utilities (defaulting to America/Chicago). The appointment service and controller now use these utilities to parse incoming appointment times and format confirmation emails consistently. The web app's default timezone is updated to match, and the Services page is converted from dynamic API-fetched content to static definitions.

Changes

Timezone-Aware Appointment System

Layer / File(s) Summary
Timezone utility library
apps/api/src/utils/timezone.util.ts
Introduces timezone utilities: date/datetime parsers with validation, conversion to Date objects with offset handling, day-range calculation, and formatting helpers for dates, times, and weekdays—all configurable via APP_TIMEZONE environment variable (default: America/Chicago).
Timezone utility tests
apps/api/src/utils/_tests_/timezone.util.test.ts
Validates timezone utilities by verifying that parsing "2026-05-12T09:00" in Chicago timezone and formatting it back yields the expected local date and time.
Appointment service timezone integration
apps/api/src/services/appointment.service.ts
Refactors service to replace inline date/time logic with shared timezone utilities: isSameLocalDate now uses formatLocalDate, slot filtering uses getLocalClock() and getLocalDayRange(), and weekday lookups use formatLocalWeekday().
Appointment controller parsing and email formatting
apps/api/src/controllers/appointment.controller.ts
Updates reserveAppointment to parse scheduledAt via parseLocalDateTimeToDate with validation, reject invalid dates with 400 errors, and format confirmation email dates/times using formatLocalDate and formatLocalTime instead of hardcoded Asia/Bangkok timezone.
Test fixture alignment
apps/api/src/services/_tests_/appointment.service.test.ts
Adjusts expected appointment date in findConfirmedAppointmentsForCheckIn test from "2026-04-13" to "2026-04-12" to match timezone-aware behavior.
Environment and deployment configuration
AGENTS.md, deploy/lightsail/api.env.example, docs/deploy-lightsail.md
Documents APP_TIMEZONE setup, adds it to Lightsail environment example, and updates deployment guide to include timezone configuration in production setup.

Web App Refactoring

Layer / File(s) Summary
Web app default timezone update
apps/web/src/utils/dateStringHandler.ts
Updates DEFAULT_TIME_ZONE from Asia/Bangkok to America/Chicago, aligning the web app's default timezone behavior with the new system configuration.
Services page migration to static content
apps/web/src/pages/Services.tsx
Converts Services page from dynamic (fetching via getAllServices() with React state/effect) to static: defines hardcoded serviceCategories array with 6 categories (pedicure, manicure, nail enhancement, add-ons, waxing, kids) and removes loading/error UI while preserving conditional rendering of enhancement and beverage sections.
Service name rendering cleanup
apps/web/src/components/services/ServiceCategory.tsx
Removes conditional trailing colon logic from service name display; header now shows only item.name.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • Nail-Addison/nail-star#6: Related through Lightsail deployment configuration updates (deploy/lightsail/api.env.example, docs/deploy-lightsail.md).

Poem

🐰 Timezones tamed in Chicago's grace,
Where appointments find their place,
No more Bangkok in the code so bright,
Static services, clean and right,
From dynamic chaos to order true—
Hop, hop, hop!

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title covers multiple distinct changes: timezone handling for bookings and static services, but doesn't clearly highlight the primary change or main focus. Consider a more specific title like 'Implement timezone-aware appointment booking and static services' or break into separate concerns if possible.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/booking-timezone-static-services

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/api/src/services/_tests_/appointment.service.test.ts (1)

280-296: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Make timezone explicit in this suite to prevent env-coupled test behavior.

This expectation now encodes timezone semantics, so set process.env.APP_TIMEZONE = "America/Chicago" in suite setup (and restore after) to avoid cross-test/env drift.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/services/_tests_/appointment.service.test.ts` around lines 280 -
296, Tests for findConfirmedAppointmentsForCheckIn rely on a specific timezone;
set process.env.APP_TIMEZONE = "America/Chicago" in this describe block's setup
and restore the original value in teardown to avoid env-coupled failures. Use
Jest hooks (beforeAll/afterAll or beforeEach/afterEach) inside the
describe("findConfirmedAppointmentsForCheckIn", ...) to assign
process.env.APP_TIMEZONE, call
appointmentService.findConfirmedAppointmentsForCheckIn(...) as-is, and then
restore the previous process.env.APP_TIMEZONE value so other tests are
unaffected.
🧹 Nitpick comments (2)
apps/api/src/utils/_tests_/timezone.util.test.ts (1)

18-24: ⚡ Quick win

Add DST boundary assertions for conversion safety.

Given localDateTimeToDate/getLocalDayRange complexity, one standard-date case is thin. Add spring/fall DST boundary tests (23h/25h local day) to catch regressions early.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/utils/_tests_/timezone.util.test.ts` around lines 18 - 24, Add
tests that assert behavior across DST boundary days for parseLocalDateTimeToDate
and related helpers: create two new test cases (spring-forward day producing a
23-hour local day and fall-back day producing a 25-hour local day) that call
parseLocalDateTimeToDate/localDateTimeToDate and also use getLocalDayRange to
assert the day length (23 or 25 hours) and verify the parsed Date still formats
to the correct local date/time via formatLocalDate/formatLocalTime; reference
the functions parseLocalDateTimeToDate, localDateTimeToDate, and
getLocalDayRange so reviewers can find where to add the assertions.
apps/api/src/controllers/appointment.controller.ts (1)

103-108: ⚡ Quick win

Add timeZone option to Intl.DateTimeFormat instead of re-parsing the date string.

The code unnecessarily converts the date to a formatted string via formatLocalDate(), then parses it back into a Date object just to reformat it. This double-conversion is inefficient and couples the timezone handling indirectly.

Instead, directly format the date with the timezone option:

date: new Intl.DateTimeFormat("en-GB", {
    day: "2-digit",
    month: "2-digit",
    year: "numeric",
    timeZone: getAppTimeZone(),
}).format(scheduledAt),
time: formatLocalTime(scheduledAt),

This is simpler, avoids unnecessary parsing, and makes the timezone handling explicit.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/controllers/appointment.controller.ts` around lines 103 - 108,
The current code re-formats scheduledAt by calling formatLocalDate() and parsing
that string into a Date before using Intl.DateTimeFormat, which is inefficient
and implicit about timezone handling; update the date formatting to call
Intl.DateTimeFormat directly with scheduledAt and add the timeZone option (use
getAppTimeZone()) instead of re-parsing, and keep time formatted via
formatLocalTime(scheduledAt); specifically change the block that uses
formatLocalDate(scheduledAt) to call new Intl.DateTimeFormat("en-GB", { day:
"2-digit", month: "2-digit", year: "numeric", timeZone: getAppTimeZone()
}).format(scheduledAt) while leaving formatLocalTime(scheduledAt) intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/web/src/pages/Services.tsx`:
- Around line 24-93: serviceCategories no longer includes a category with title
"beverage", which makes the derived beverageCategory and the BeverageSection
render branch unreachable; either add a beverage entry to the static catalog
(e.g., include an object in serviceCategories with title: 'beverage' and
appropriate items built via serviceItem) or remove the beverageCategory
derivation and the BeverageSection render branch (references: serviceCategories,
beverageCategory, BeverageSection, and the string 'beverage') so the UI and data
model stay in sync.

---

Outside diff comments:
In `@apps/api/src/services/_tests_/appointment.service.test.ts`:
- Around line 280-296: Tests for findConfirmedAppointmentsForCheckIn rely on a
specific timezone; set process.env.APP_TIMEZONE = "America/Chicago" in this
describe block's setup and restore the original value in teardown to avoid
env-coupled failures. Use Jest hooks (beforeAll/afterAll or
beforeEach/afterEach) inside the describe("findConfirmedAppointmentsForCheckIn",
...) to assign process.env.APP_TIMEZONE, call
appointmentService.findConfirmedAppointmentsForCheckIn(...) as-is, and then
restore the previous process.env.APP_TIMEZONE value so other tests are
unaffected.

---

Nitpick comments:
In `@apps/api/src/controllers/appointment.controller.ts`:
- Around line 103-108: The current code re-formats scheduledAt by calling
formatLocalDate() and parsing that string into a Date before using
Intl.DateTimeFormat, which is inefficient and implicit about timezone handling;
update the date formatting to call Intl.DateTimeFormat directly with scheduledAt
and add the timeZone option (use getAppTimeZone()) instead of re-parsing, and
keep time formatted via formatLocalTime(scheduledAt); specifically change the
block that uses formatLocalDate(scheduledAt) to call new
Intl.DateTimeFormat("en-GB", { day: "2-digit", month: "2-digit", year:
"numeric", timeZone: getAppTimeZone() }).format(scheduledAt) while leaving
formatLocalTime(scheduledAt) intact.

In `@apps/api/src/utils/_tests_/timezone.util.test.ts`:
- Around line 18-24: Add tests that assert behavior across DST boundary days for
parseLocalDateTimeToDate and related helpers: create two new test cases
(spring-forward day producing a 23-hour local day and fall-back day producing a
25-hour local day) that call parseLocalDateTimeToDate/localDateTimeToDate and
also use getLocalDayRange to assert the day length (23 or 25 hours) and verify
the parsed Date still formats to the correct local date/time via
formatLocalDate/formatLocalTime; reference the functions
parseLocalDateTimeToDate, localDateTimeToDate, and getLocalDayRange so reviewers
can find where to add the assertions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 17a2d4f8-2081-4874-ac3b-ee08d0c343f1

📥 Commits

Reviewing files that changed from the base of the PR and between 19e6a64 and 6e3c152.

📒 Files selected for processing (11)
  • AGENTS.md
  • apps/api/src/controllers/appointment.controller.ts
  • apps/api/src/services/_tests_/appointment.service.test.ts
  • apps/api/src/services/appointment.service.ts
  • apps/api/src/utils/_tests_/timezone.util.test.ts
  • apps/api/src/utils/timezone.util.ts
  • apps/web/src/components/services/ServiceCategory.tsx
  • apps/web/src/pages/Services.tsx
  • apps/web/src/utils/dateStringHandler.ts
  • deploy/lightsail/api.env.example
  • docs/deploy-lightsail.md
💤 Files with no reviewable changes (1)
  • apps/web/src/components/services/ServiceCategory.tsx

Comment on lines +24 to +93
const serviceCategories: ServiceListCategory[] = [
{
title: 'Pedicure',
items: [
serviceItem('Day-by-Day pedicure', '$30'),
serviceItem('Hydra Glow pedicure', '$45'),
serviceItem('The Stella pedicure', '$65'),
serviceItem('Cosmic Veil pedicure', '$75'),
serviceItem('Seasonal pedicure', '$65'),
serviceItem('Star Dust pedicure', '$95'),
serviceItem('Additional Gel polish', '(+ $20)'),
],
},
{
title: 'Manicure',
items: [
serviceItem('Day-by-Day manicure', '$20'),
serviceItem('Hydra Glow manicure', '$35'),
serviceItem('The Stella manicure', '$50'),
],
},
{
title: 'Nail Enhancement',
items: [
serviceItem('Dipping Powder', '$40+'),
serviceItem('Acrylic Fullset', '$50+'),
serviceItem('Acrylic Fill', '$40+'),
serviceItem('Gel Manicure', '$40'),
serviceItem('Gel X', '$65+'),
serviceItem('Builder Gel', '$60'),
serviceItem('Tap Gel', '$60'),
],
},
{
title: 'Add-on services',
items: [
serviceItem('French Tip', '$15+'),
serviceItem('Length', '$5+'),
serviceItem('Shaping', '$5+'),
serviceItem('Trim/reshape', '$10+'),
serviceItem('Soak off', '$15+'),
serviceItem('Regular polish change', '$15'),
serviceItem('Gel polish change', '$30'),
],
},
{
title: 'Waxing services',
items: [
serviceItem('Eyebrows', '$12'),
serviceItem('Lip', '$10'),
serviceItem('Chin', '$10+'),
serviceItem('Sideburn', '$15+'),
serviceItem('Full Face', '$35+'),
serviceItem('Under arms', '$20+'),
serviceItem('Half Arms', '$30+'),
serviceItem('Full Arms', '$40+'),
serviceItem('Half Legs', '$40+'),
serviceItem('Full Legs', '$55+'),
],
},
{
title: 'Kids services',
items: [
serviceItem('Kids pedicure', '$20'),
serviceItem('Kids manicure', '$15'),
serviceItem('Kids hands polish change', '$8'),
serviceItem('Kids toe polish change', '$10'),
],
},
]
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add the missing beverage data or remove the unreachable section.

beverageCategory is still derived and rendered later, but this static catalog never defines any category whose title matches "beverage". That means the BeverageSection branch at Lines 137-145 is now dead and the page silently drops that section after this refactor.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/pages/Services.tsx` around lines 24 - 93, serviceCategories no
longer includes a category with title "beverage", which makes the derived
beverageCategory and the BeverageSection render branch unreachable; either add a
beverage entry to the static catalog (e.g., include an object in
serviceCategories with title: 'beverage' and appropriate items built via
serviceItem) or remove the beverageCategory derivation and the BeverageSection
render branch (references: serviceCategories, beverageCategory, BeverageSection,
and the string 'beverage') so the UI and data model stay in sync.

@Tung-Quan Tung-Quan closed this May 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant