Appointment App
Overview
Appointment is one of the Apps — optional modules a platform administrator enables or disables per organisation, rather than being governed by role-based permissions like the rest of the CMS.
The app lets a business run appointment booking through LINE, organised into three parts:
- Locations — the address, business hours for all seven days, and the timezone. Each location owns its own services and staff.
- Booking journeys — a drag-and-drop builder for the booking flow, running from service selection through staff selection, date and time, customer details, and a confirmation step, with a token that customers use to enter.
- Bookings — incoming appointments, viewable as a table or a weekly calendar, with status controls that follow a defined lifecycle.
CMS users are administrators who configure locations, services, and staff, design the booking journey, and manage appointment statuses. Customers enter through a journey's token in the customer-facing web app, and their details can be pre-filled from their LINE display name.
Compared with Loyalty and Bulletin — the more mature modules in the same group — this app is visibly thinner. Its service layer has no dedicated type definitions, there is no shared error translator, and every screen reads responses defensively in two shapes because the API's response envelope is not yet settled.
Business Flow
Access and app visibility
- The side menu queries the app registry on load and keeps only the enabled entries. If Appointment is present, a submenu item is added pointing straight at the locations tab. The parent "Apps" menu appears only when at least one app is enabled.
- Menu visibility does not depend on the user's role or permissions at all — only on the app registry. Unlike Bulletin, this app carries no menu badge.
- Every route under
/appssits behind a shared route guard that reads the app ID from the URL, checks it against the registry, redirects home when the app is disabled, and allows access when the registry call itself fails, since the server still protects the data and a temporary registry outage should not lock users out of an app they legitimately have. - Because the structure is registry-driven, newly added apps are governed automatically.
- The app layout only renders three tabs (locations, journeys, bookings); breadcrumbs and active menu state are set by each page individually.
Locations
- The locations screen loads every location into a client-paginated table with columns for the row number, the name (clickable into the detail page), the address, the phone number, the status, and the actions.
- The create/edit modal covers the name (required), the address (required), the phone number, the timezone (required, chosen from a fixed list of six), and the status.
- Business hours for all seven days are managed as state separate from the main form. Each day has an enable switch plus opening and closing time pickers, which are disabled when the switch is off.
- The defaults are Monday to Friday open, Saturday and Sunday closed, all with hours of 09:00 to 18:00. Clearing a time picker restores the default rather than leaving it empty.
- On save, the form values and the business hours are merged into a single payload. Form validation errors do not raise a toast, since the form already displays messages inline.
- Deleting a location requires confirmation.
Services and staff per location
- The location detail page reads the location ID from the URL, loads the location record so its name can fill the section header and breadcrumb, and loads that location's services and staff.
- The page holds two inner tabs, with the top-right button switching between "add service" and "add staff" depending on which tab is open.
- The services tab lists the row number, name, duration, price, capacity per slot, whether the service requires a staff member, and the status. The create/edit modal covers the name (required), duration in minutes (required), capacity per slot, price, a requires-staff toggle, and the status.
- The staff tab lists the row number, name, job title, and the services they cover, rendered as tags. The create/edit modal covers the name (required), the job title, and a multi-select of that location's services.
- Structurally worth noting: creating requires the location to be named in the path, while updating does not, because the two use different endpoint shapes.
Booking journeys
- The list screen loads journeys along with the location list, which both populates the form selector and resolves location names in the table.
- Columns cover the row number, name, location, auto-confirm state, token, status, and actions.
- The token renders as truncated code with a copy button, and only the raw token can be copied. This app does not assemble a customer-clickable URL, unlike Bulletin and Loyalty, which each have their own link builders — turning the token into a usable link has to happen elsewhere.
- The create/edit modal exposes only three fields: name (required), location (required), and auto-confirm. The journey's steps are edited on a separate screen.
- Each row offers three actions: open the step editor, open the basic-details modal, and delete.
Editing journey steps
- The screen reads the journey ID from the URL and loads its name, the reminder lead time in hours, and the no-show grace period.
- If the journey has no steps yet, a default set is generated — both for genuinely new journeys and when the load fails.
- If the journey is bound to a location, that location's services and staff are loaded in parallel so they can be shown as reference data on the step cards.
- There are five fixed step types: choose a service, choose a staff member, choose a date and time, enter customer details, and confirm. Each has its own colour so they remain visually distinguishable.
- The default step set is built from translated labels so that both the card headers and the freshly seeded configuration speak the same language the user is working in. The staff step is disabled by default, and the customer-details step arrives with three seeded fields: name (auto-filled from LINE), phone, and notes.
- Steps are reordered by dragging, with keyboard reordering also supported. The confirm step is always excluded from the draggable set and rendered afterwards as a fixed final step.
- Each type can appear only once, because the drag-and-drop identity is the step type itself — so a duplicate step of the same type cannot be added.
- Each step card has a drag handle, a numbered circle in the step's colour, the step name with a subtitle, an enable switch (hidden on the confirm step), and an expand control. Multiple cards can be expanded at once.
- Configuration forms differ by type:
- Service — title, subtitle, display mode (card grid or list), show price, show duration, plus the location's real service list shown for reference, or a warning that no services have been configured.
- Staff — title, subtitle, show avatar, show job title, allow any staff member, plus the real staff list or an equivalent warning.
- Date and time — title, subtitle, how many days ahead can be booked, and whether fully booked slots are shown.
- Customer details — title, subtitle, and a repeatable field manager where each row defines a key, a label, an input type, an auto-fill source, and whether the field is required.
- Confirm — title, the confirm button's label, and whether a summary is shown.
- Above the step list sit two journey-level fields: the reminder lead time in hours and the no-show grace period.
- Saving submits only the steps and those two journey-level fields — not the name, location, or auto-confirm setting, all three of which are edited from the modal on the list screen.
Bookings
- The screen loads the location list once and reloads bookings whenever a filter changes, sending only the parameters that actually have values.
- Two views are available, list and calendar, and calendar is the default.
- The filter bar offers preset date ranges (all, yesterday, today, tomorrow, this week, this month), a custom range picker, a location selector, a status selector, and a clear button. Changing the range start scrolls the calendar to that week automatically.
- Table pagination is client-side and resets to page one whenever a filter changes, otherwise narrowing a filter would leave the user staring at an empty final page.
- Bookings have five statuses: pending, confirmed, completed, cancelled, and no-show.
- Action buttons appear according to the current status. Pending bookings offer confirm and cancel; confirmed bookings offer complete, no-show, and cancel; the three terminal statuses offer nothing.
- Cancelling goes through a modal that asks for a reason, unlike the other buttons which act immediately — though the reason itself is not mandatory.
- Clicking a customer name in the table, or a block in the calendar, opens a two-column detail modal. The left side shows the customer, booking number, status, contact details, LINE user ID, and the values the customer actually entered during the details step. The right side shows the service, staff member, location, date, time slot, notes, and creation date.
Weekly calendar
- The calendar receives the bookings and the week start; the week navigation controls live on the parent page rather than inside the component.
- The time axis is hardcoded from 07:00 to 18:00 and does not read the location's business hours, so bookings outside that window fall outside the grid.
- Block height is calculated from the booking's real duration, with a minimum so it stays legible.
- Each block is colour-coded by status and reveals more or less information depending on its height: the time range and service name always show, while the customer name, staff member, and location appear progressively as the block grows taller.
- Today's column is highlighted in green with a current-time line drawn through it; weekends use a light grey background.
Key Screens & Components
Shared route guard (src/app/apps/layout.tsx) — verifies that the app in the path is enabled, with fail-open behaviour on error.
App layout (src/app/apps/appointment/layout.tsx) — renders the three tabs only; it does not set breadcrumbs.
Locations screen (src/app/apps/appointment/locations/page.tsx) — the table, the location modal, and the per-day business hours editor.
Location detail (.../locations/detail/page.tsx) — two inner tabs for services and staff with their management modals, reading the location ID from the query string.
Journeys screen (.../journeys/page.tsx) — the table, the basic-details modal, token copying, and the entry point to the step editor.
Step editor (.../journeys/edit/page.tsx) — the drag-and-drop journey builder with per-type configuration forms.
Bookings screen (.../bookings/page.tsx and .../bookings/WeeklyCalendar.tsx) — filters, view switching, lifecycle-aware status buttons, the cancellation and detail modals, and the weekly calendar itself.
Service (src/services/appointment.service.ts) — covers every operation across locations, services, staff, journeys, and bookings, though it remains a small file without detailed type declarations.
Dependencies
- App registry and platform administrator — decide whether the app appears at all; the CMS can only read the registry.
- Permissions — subjects are declared but have no backend mapping, so they serve only as menu and breadcrumb keys. Real access control comes from the registry plus the API-side guard.
- Customer web app and LIFF — where customers book using the journey token; each step's configuration is what that side renders.
- LINE profile — supplies the auto-filled name in the customer details step, and the detail modal displays the customer's LINE user ID and picture.
- Background notification system — the reminder lead time and no-show grace period are stored at the journey level so the backend can send reminders and mark no-shows. The CMS only configures them.
- Shared CMS components — the table scroll area, the section header, table page sizes, and the filter control styling.
- Internal architecture — like its sibling apps, every screen calls services directly and manages its own state rather than going through the shared request layer. This app has no image upload, so it does not depend on the content management service.
Known gaps
This section is included so readers understand the module's real state.
- The top-level services and staff pages are still empty placeholders with no API wiring; the real management lives in the tabs inside the location detail page.
- There is no customer URL builder, so journeys expose only a raw token for copying, and the copy button does not yet handle browsers that lack clipboard support or non-secure contexts.
- The calendar's 07:00 to 18:00 window is hardcoded rather than derived from the location's business hours, and the current-time line is positioned using a different scale factor from the one used to place blocks.
- The customer details field manager does not yet validate that keys are non-empty or unique.
- The step editor has no unsaved-changes check when navigating away.
Backend Details (CMS API)
The module lives in internal/modules/apps/ (in service_appointment.go), with every endpoint under /api/apps/appointment/.
Protection is uneven across endpoint groups (security note)
This is the most important thing to know about the app — the guard level differs by group, carried over verbatim from the previous system.
- Bookings — the best protected. They require the global
JwtAuthand theAppEnabledGuard, and declare per-action policies. - Journeys, services, and staff — sit on the public group behind only a login-level token check, and do not pass through the
AppEnabledGuard. An organisation that has never enabled this app can still call these endpoints with any account that can log in. - Locations — sit on the authenticated group but declare no policy at all.
In short, only the bookings group is dependably controlled; the configuration groups should be assumed reachable at a lower privilege level than expected, and they are an item to address in any system-wide permissions rework.
What happens when a booking's status changes
PUT /api/apps/appointment/bookings/:id/status does more than write a status — it also publishes a message to the booking_event_trigger RabbitMQ queue. The consequences:
- The Trigger Rule engine consumes that event and can dispatch a notification to the person who booked, such as a confirmation or a cancellation notice. The message content is therefore configured in the Trigger Rule screens, not in this app.
- If the queue is unhealthy or the worker is down, the status change still succeeds but the customer never hears about it, and the CMS surfaces no error.
- The published message shape is byte-for-byte pinned to the legacy format because the original consumer still reads it.
Behaviour shared by every CRUD group
- Scoping values (
lineOaId,organizationId, the current user, and the language) all come from the request context, never from the payload. - Read-one checks the ID together with the LINE OA. An ID belonging to another OA returns 404, indistinguishable from one that does not exist, so no information leaks about records elsewhere.
- List reads return a bare array rather than a wrapped envelope, excluding already-deleted rows.
- Deletion is a soft delete wired into the ORM's automatic mechanism, unlike several modules in this system where the predicate must be written by hand. Reads therefore filter deleted rows automatically with nothing to remember.
What the CMS cannot do
The customer's actual booking does not happen in the CMS API — it happens in the client-api through the LIFF pages where the customer picks a location, service, staff member, and time, then confirms. This back office configures the structure (locations, services, staff, journeys) and manages the bookings that result.
Tables and external systems
Tables booking, location, service, staff, and journey, together with line_oa_app and line_oa. The external system is RabbitMQ (the booking_event_trigger queue).