Skip to main content

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 labelService
Tab accent#2563eb (blue)
Display order6 (between Sales at 5 and Customers at 10)
Foldersrc/modules/service/
Migration030_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_menu catalog (coded shop items like a1 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 every service_menu row falls under gst_only.
  • Parts lines — pulled from product_variants via 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.

StatusMeaning
Drop-offTicket exists, bike is in
Start WorkMechanic has started
Wait for PartsBlocked on incoming parts (typically linked to a Special Order)
Stop · CallBlocked on customer decision — call and confirm
ReadyWork complete, awaiting pickup
NotifyCustomer has been notified (SMS / email stub in v1; real wire deferred)
InvoicedTicket 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

PermissionWhat it gates
screen.serviceThe Service tab is visible at all
service.readToday's ledger + ticket detail
service.createStart a new ticket
service.editAdd / remove labour + parts; change assigned mechanic; edit issues + notes
service.transitionMove ticket status (Start Work / Ready / Notify / etc.)
service.invoiceRing pickup as a service_invoice sale
service.voidVoid 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_id on 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