Service module
The workshop work-order screen. Same sales-3zone chassis (Search · Ledger · Till) as Sales, adapted to service tickets: today's-service-orders ledger grouped by status on the left, the active service order (work-order) on the right. Drop-off → labour + parts → status lifecycle → pickup invoices as sale.
| Tab label | Service |
| Tab accent | #2563eb (blue) |
| Display order | 6 (between Sales at 5 and Customers at 10) |
| Folder | src/modules/service/ |
| Migration | 030_service.sql |
What it owns
src/modules/service/
index.ts — registers the module with the Kernel
view.ts — markup + styles for the till + status-grouped ledger + modals
client.ts — browser JS (ticket flow, labour + parts add, status transitions)
api.ts — server-side routes (/api/m/service/*)
What it does
The work-order. New Service Order → attach customer → pick a bike from the customer's Equipment section (or add a new one) → drop-off ticket opens on the right pane. The ticket carries the standard order fields (customer, bike, dates) plus the service-specific ones (drop-off date, promised date, reported issues, mechanic assigned, deposit).
Lines. Two kinds:
- Labour lines — picked from the
service_menucatalog (coded shop items likea1 BARE ESSENTIALS TUNE,b3 E-BIKE ADVANCED TUNE, …). Every menu row has a fixed price and default labour minutes; the row itself carries the tax category. Labour is GST-only in BC (no PST), so everyservice_menurow falls undergst_only. - Parts lines — pulled from
product_variantsvia the same search chassis that Sales uses. Adding a part decrements stock through the movement engine (core/movements.ts) once the ticket is invoiced, not when the line lands on the ticket.
Status lifecycle.
| Status | Meaning |
|---|---|
| Drop-off | Ticket exists, bike is in |
| Start Work | Mechanic has started |
| Wait for Parts | Blocked on incoming parts (typically linked to a Special Order) |
| Stop · Call | Blocked on customer decision — call and confirm |
| Ready | Work complete, awaiting pickup |
| Notify | Customer has been notified (SMS / email stub in v1; real wire deferred) |
| Invoiced | Ticket has been rung as a sale at pickup |
Transitions land on audit_events and, for Stop · Call and Notify, insert into service_ticket_messages with the notification channel + body. The real SMS / email wire is deferred to a later minor; the substrate captures the notification record with a stub channel for now.
Estimate / deposit / cancel. Standard sales-side controls: create an estimate before Start Work; take a deposit against a Wait-for-Parts ticket; cancel with a captured reason. Deposits ride the same /deposit path Sales uses.
Pickup invoices as sale. When the ticket is Ready and the customer arrives, Pickup rings the ticket as a service_invoice transaction (transactions.transaction_type='service_invoice', related_ticket_id set) that flows through the standard tender modal. Parts lines fire the movement engine; labour lines are cash-only in the accounting sense (no stock change).
The service_menu catalog
Coded labour items with fixed price + default labour minutes. Full 67-item catalog imports from the prior system's tuning menu; v1 seeds a representative starter set (a1 BARE ESSENTIALS TUNE, b3 E-BIKE ADVANCED TUNE, categorised by Tune-ups / Wheels / Brakes / …) so the module is testable without the full import.
The menu items live in one shared catalog per shop; every ticket picks from it. Edits to a menu row don't flow retroactively into past tickets (line snapshots the price + tax at add-time, same as Sales lines).
Permissions
| Permission | What it gates |
|---|---|
screen.service | The Service tab is visible at all |
service.read | Today's ledger + ticket detail |
service.create | Start a new ticket |
service.edit | Add / remove labour + parts; change assigned mechanic; edit issues + notes |
service.transition | Move ticket status (Start Work / Ready / Notify / etc.) |
service.invoice | Ring pickup as a service_invoice sale |
service.void | Void a ticket after invoicing |
Special orders → PO bucket → back to the ticket
A service ticket in Wait for Parts often has a linked special-order deposit (a Sales-side transaction with transaction_type='special_order'). As of v0.7.86 (migration 033), the special-order auto-posts a line to the Purchases Bucket the moment it's created; the Sales ledger row for that special order tracks the PO status live — Special order → Ordered → Partially received → Ready for pickup. One PO line links back to its originating special-order transaction; one PO can carry lines for several different customers' special orders at once.
How it composes with the rest
- Customers — the attached customer's Equipment section supplies the bike picker; the customer Till's ledger shows this ticket as one of the interactions after invoicing.
- Sales — the pickup flow uses the same tender modal, refund path, and audit shape as a Sales sale.
related_ticket_idon the resulting transaction links back. - Inventory — every parts line reads from
product_variants; the movement engine records the decrement at invoicing. - Purchases — the special-order → PO bucket link (migration 033) surfaces order progress on the Service ticket without the operator having to switch tabs.
- Audit chain — every ticket create, status transition, line add / remove, deposit, pickup / invoice, void lands in
audit_events.
See also
- Three strata — Kernel · Meta · Modules — the architectural shape
- Sales module — the tender + transaction substrate the pickup flow uses
- Inventory module — parts lines read variants from here
- Purchases module — special-order → Bucket → PO substrate (migration 033)
- Customers module — the Equipment section supplies the bike picker