Customer Portal & My Home — Core concepts
App mode
app_type defines the host context and decides which APIs bootstrap calls (INV-CPBOOT-001).
| Mode | app_type | Bootstrap |
|---|---|---|
| Admin Mode | aoadmin, onsite | Calls the staff context: GET /api/Manage/CurrentUser, then GET /api/Employee/LinkOrg if the response has employeeId and the user is not an admin. Does not fetch the caller account (GET /api/ConsumerAccount without _a). |
| Customer mode | Customer Portal, aimyme, everything else | Calls the caller account GET /api/ConsumerAccount (no _a) + GET /api/ConsumerManage/CurrentUser. Does not call staff endpoints. |
_a= account selector on theConsumerAccountcall: admins may still fetch the selected customer's account in Admin Mode; the rule only forbids fetching the admin's own account at bootstrap.- Identity + timezone hydrate from
CurrentUser(every mode) — staff-context gating must not break timezone hydration.
Real-world example: A page embedded in the Onsite APP (
app_type = onsite). → Staff still need the staff context. → Bootstrap callsManage/CurrentUser+Employee/LinkOrg, populatingcurrentUserTopi/currentUserLinkedSites; noConsumerAccountcall for the admin themselves.
ServiceAreaId — display-timezone mode
EnterpriseSetting.ServiceAreaId decides how Calendar/Timetable display the time (INV-ENTTZMODE-002). The backend returns the source value; the frontend handles the display conversion.
| Value | Code | How it displays |
|---|---|---|
Local | single zone | Keeps the wall-clock source; no user/browser timezone conversion; Syncfusion ScheduleComponent does not reinterpret through a user-local timezone. |
MultiZone | 1 | Converts to the effective timezone from User.TimezoneIdentifier; null → fallback to the browser-detected timezone. |
Real-world example: The same 09:00 event, users in another browser timezone open it at a
Localenterprise. → The time must not change with the browser. → Both browsers see 09:00 because no conversion is applied.
Timezone identifier
User.TimezoneIdentifier= the user's Windows TZ ID (timezoneIdentifier). The dropdown shows<IANA name> (<abbreviation>)(e.g.Australia/Sydney (AEDT)).- Options come from the frontend catalog
getSystemTimezoneOptions(); it does not useuseUserStore.systemTimeZoneshydrated from the API; the Select supports search/filter (showSearch). - Updated via
POST /api/User/UpdateUserTimezoneIdentifier(Windows TZ ID) orupdateUserTimezone(newWindowsId)(sidebar auto-save). - After reload, the saved value hydrates from
ConsumerManage/CurrentUser; no extraConsumerManage/GetUserTimezoneIdentifiercall needed.
Session flags (sessionStorage)
| Key | Surface | What it blocks this session |
|---|---|---|
my-home-tz-prompted | My Home (staff) | First-time timezone prompt — only set on save. |
tz-popup-dismissed | Customer Portal | Timezone session popup — set on save or dismiss. |
- The two keys are independent per surface; never shared.
- Guest/auth-unresolved: no check, no flag set.
Real-world example: A user dismisses the session popup on the portal. → The popup does not reappear in the same session. →
tz-popup-dismissed = '1'; but staff My Home still shows the first-time prompt because themy-home-tz-promptedflag is not set.
Enrollment and status
- Enrollment = the runtime subscription sign-up; statuses map to My Bookings tabs as
Submitted(Pending),Approved(Confirmed),Canceled(Cancelled). - EnrollmentPeriod = a booked period. Only active, non-cancelled periods go into Booking Range and the calendar.
- The cancel workflow leaves
StatusId = CanceledbutIsActive = true— filtering byIsActivealone is not enough, it must combineStatusId != Canceled.
Real-world example: An enrollment has a cancelled period (
StatusId = Canceled,IsActive = true). → The cancelled period must not show. → Booking Range and View Calendar both drop that period because of theIsActive && StatusId != Canceledfilter.
My Subscriptions — read model
- Single route:
/{curl}/enrollments(no new route); theMy Subscriptionsmenu points there. - Hard-scoped to the authenticated account: client filters are ignored/rejected; another account's enrollments are never returned.
- Row: site, subscription name + enrollment number, account/customer display (if customer-safe), attendee, start date, optional end date, status.
- Summary: the
Subscription Summaryheader uses the BU/portal theme accent (no hard-coded pink from the screenshot); the recurring price label + amount align right on desktop, readable on mobile. - Program/session/weekly pattern table: groups sessions by program; weekdays in Mon–Sun order, selected days highlighted.
- States: loading (no stale rows from another account), empty (with a browse/book subscription action), error (recoverable + retry).
- Read-only: load/expand creates or mutates no
TermBookingOrder,TermBooking,Enrollment,Billing,Invoice,Payment,CreditNote.
Customer-safe vs admin-only
| Customer-safe (shown/allowed) | Admin/Staff-only (hidden) |
|---|---|
| View details, refresh, cancel, terminate (when the backend policy allows per row status) | Confirm button |
| Subscription name, site name, price label + amount, description / customer-facing note | Manage Billing |
| Program/session/weekly pattern, add-ons | Edit admin note |
| Booking Range + View Calendar (read-only) | Account selection / cross-account filter |
| Admin notes, payment method identifier, internal booking-order id |
- Disabled/unavailable actions must not call mutation endpoints.
- Allowed actions must go through the backend policy; no buttons shown just for show.