Customer Portal & My Home — End-user view
A. Customer (Customer Portal)
1. Entering the portal: bootstrap by app mode
app_typedecides the host mode:aoadmin/onsite= Admin Mode; everything else (Customer Portal, AimyMe =aimyme) = customer mode.- Customer mode: fetch
GET /api/ConsumerAccount(no_a) +GET /api/ConsumerManage/CurrentUser; does not callGET /api/Manage/CurrentUser/GET /api/Employee/LinkOrg. - Identity and timezone/date-time hydrate from
ConsumerManage/CurrentUserin every mode (INV-USERTZAPI-002).
Real-world example: A customer without a staff profile opens the portal. → Avoid calling staff endpoints that cause a 403. → Bootstrap only calls
ConsumerAccount+ConsumerManage/CurrentUser; no 403, and the account hydrates as usual.
2. Landing: site cards
- Row order: name → phone (phone icon) → email (mail icon) → address (pin icon); a missing value → drop that row, the remaining rows keep their order.
- Icons reuse
@ant-design/iconsshared with the Booking UI (no dedicated icons).
Real-world example: Site A has phone + email but no address. → No empty row. → The card shows name, phone, email; no address row.
3. Booking UI: Program Details
- Below the site name (keeping the pin icon): a mail icon + site email row, then a phone icon + site mobile/phone row; no title text.
- Site without email or mobile → no corresponding row is rendered.
- Clicking the info icon → the existing site info modal keeps its content.
4. Timezone per session
- Single zone (
ServiceAreaId = Local): the time displays as the exact wall-clock source; no conversion by user/browser timezone (opening in another browser still shows the same time). - MultiZone: event times convert to the effective timezone from
User.TimezoneIdentifier; null → fallback to the browser-detected timezone. - Sidebar quick-select (left drawer
MenuHamburgerDrawer): a Select above Logout, pre-selected bytimezoneIdentifier(null → best browser match); changing it → auto-saves, no confirm button; disabled while saving. - Session popup: MultiZone + authenticated +
User.TimezoneIdentifiernull + not dismissed → the popup appears automatically; save/dismiss both setsessionStorage['tz-popup-dismissed']. - Guest/anonymous or unresolved auth → no popup ever, no session flag check/set.
- When
ianaTimezonein the store differs from the browser timezone → show a mismatch modal (MultiZone, authenticated, not dismissed only).
Real-world example: A user opens a MultiZone portal with no timezone and has never dismissed. → Pick the right timezone right on entry. → The popup shows a dropdown; selecting
Australia/Sydney (AEDT)→ save →dayjs.tz.setDefault()changes, and the popup does not reappear this session.
5. My Subscriptions
- The
My Subscriptionsmenu → route/{curl}/enrollments(the existing management route; no new route). - Requires login; lists only the signed-in account's enrollments; if the client sends an account identifier → backend ignores or rejects it.
- Compact row → expand: program/session/weekly pattern (7 letter tags Mon–Sun, selected days highlighted), add-ons, Subscription Summary, Booking Range + View Calendar.
- Only customer-safe actions: view details, refresh, cancel, terminate (when the backend policy allows); no Confirm, Manage Billing, admin note, or account filter.
- Admin-only fields are removed: admin notes, payment method identifier, invoice identifier (unless an invoice action requires it), internal booking-order id.
6. My Bookings: subscription section
- The "My Subscriptions" section sits below the term-booking list, only when the active tab has a matching status:
| Tab | Enrollment status shown |
|---|---|
| Pending | Submitted |
| Confirmed | Approved |
| Cancelled | Canceled |
| Waiting / Cart (Draft) | section not rendered |
- Collapsible card: header enrollment number + status tag; expand → attendee + subscription name, session block (name, label, time range, weekday tags), Booking Range (period, term label, Booking/Add-on/Total, Invoice Date, View Calendar), add-ons at the end.
- Valid booked periods:
IsActive = trueandStatusId != Canceled; cancelled periods do not appear in the list or calendar. - Switching tabs filters the already-fetched data; no re-fetch on tab change.
Real-world example: A customer opens My Bookings on the Confirmed tab. → Only confirmed bookings/subscriptions show. → The section shows status
Approved; the Waiting tab shows nothing; switching to Pending filters immediately, no network reload.
7. Share link
- The share button (from Admin Portal or Customer Portal) always builds a Customer Portal origin URL → the item's booking/program detail route.
- Keeps the needed params (program, term, site, category, TPS); does not include an admin prefix, permission/session artifacts (bearer-token query param), or admin-only routes.
- The link shown in the modal = the exact string copied to the clipboard.
Real-world example: Staff click share from the Admin Portal. → The customer must reach the booking/program without exposing admin. → The modal shows a Customer Portal origin URL, no token; copy → paste yields exactly that URL.
B. Staff (My Home)
My Home header
- Title on the left; the right menu renders per BusinessUnit:
| Menu | Show when |
|---|---|
| Calendar | BU has BusinessUnitPermission Staff Roster enabled |
| Timetable | BU has ≥ 1 Class Program Category |
| My Profile | always shown |
- An Enterprise-level profile (not Employee/Staff) → the My Home menu is not shown (legacy behavior), and they never hit the timezone prompt.
Real-world example: A BU only enables Staff Roster, with no Class Program Category. → No extra menus. → The header shows the title + Calendar + My Profile menus; Timetable is hidden because the BU has no Class category.
Local time (Calendar & Timetable)
ServiceAreaId = Local→ renders the exact wall-clock source, unchanged by user/browser timezone.ServiceAreaId = MultiZone→ converts byUser.TimezoneIdentifier(null → browser); the backend returns the source value, no user-based conversion.
Real-world example: A MultiZone enterprise, staff with no saved timezone opens the calendar in a browser at
Asia/Ho_Chi_Minh. → No wrong hour conversion without config. →User.TimezoneIdentifiernull → browser-timezone fallback; onceW. Australia Standard Timeis saved → converts by the saved ID.
First-time timezone prompt (staff)
- Shown when: MultiZone +
User.TimezoneIdentifiernull +sessionStorage['my-home-tz-prompted']falsy. - Modal is not dismissible — must pick a timezone and save; the dropdown lists every system timezone (IANA + abbreviation).
- Save →
POST /api/User/UpdateUserTimezoneIdentifier(Windows TZ ID) → setsmy-home-tz-prompted→ the modal closes and the page renders in the new timezone.
Real-world example: Staff new to My Home in a MultiZone enterprise who never set a timezone. → Must pick a timezone so the calendar renders correctly. → The modal shows a dropdown; it cannot be closed without a choice; after save
my-home-tz-prompted = 1, no repeat ask this session.
Summary
| Role | What they do |
|---|---|
| Customer | Views landing site cards, Program Details (contact), manages My Subscriptions / My Bookings, sets the timezone via sidebar or session popup, receives portal-origin share links. |
| Staff | Browses My Home (BU-based header), views Calendar/Timetable in local time, picks the timezone first-time via a mandatory prompt. |
| Admin Portal | Creates share links; runs staff-context bootstrap; every fetch toward Customer Portal still exposes only account-scoped, read-only data. |